Inicio / Artículos

¿Por qué las aplicaciones Vite React ignoran las variables de entorno de Kubernetes en tiempo de ejecución?

Vite incluye las variables VITE_ durante el proceso de compilación, por lo que los cambios en el entorno del pod de Kubernetes nunca llegan a la aplicación React. En su lugar, se inyecta window.env desde el punto de entrada del contenedor.

Editor: REACTAPP.TOP5 min de lectura1057 palabras

Fuentes: Why Your Vite + React App Ignores Kubernetes Env Vars (And How I Fixed It)

Los equipos de frontend que ya conocen los servidores Node suelen asumir que las variables de entorno del pod se comportan de la misma manera una vez que un paquete React llega a Kubernetes. Una SPA construida con Vite rompe esa suposición de forma silenciosa pero costosa: los valores que se establecen en el Deployment nunca llegan al navegador porque nunca se consultaron en el momento de la solicitud.

La configuración

Imagínese una aplicación React estructurada con Vite. La configuración utiliza el prefijo habitual VITE_: VITE_API_URL, VITE_APP_NAME, opciones para activar funcionalidades y otros parámetros similares.

El proceso de lanzamiento se realiza a través de Jenkins. La pipeline construye una imagen Docker y, durante esa fase de compilación, proporciona valores de entorno para que aparezcan “dentro” de la imagen. Más tarde, esa misma imagen se ejecuta en un pod de Kubernetes donde el Deployment también declara variables de entorno, con la expectativa de que el proceso en ejecución las lea de la misma manera que lo haría Express.

Eso nunca ocurre con el cliente de Vite. Al actualizar un valor en la configuración de despliegue, reiniciar el pod y volver a cargar el navegador, sigue mostrándose la cadena anterior. Al inspeccionar el pod con kubectl exec … -- env se demuestra que la variable existe en el entorno del contenedor. El JavaScript estático simplemente no la consulta.

Por qué ocurre esto (la versión sencilla)

Si se viene de un entorno con Node o Express, es fácil pasar por alto esta explicación: Vite resuelve las variables de entorno al compilar el proyecto, no cuando el usuario carga la página.

Cada vez que el código fuente haga referencia a import.meta.env.VITE_API_URL, la fase de compilación reemplazará esa expresión por una cadena literal. Una vez finalizada la ejecución de npm run build, ya no habrá búsquedas en tiempo de ejecución; solo texto fijo incrustado en el paquete, lo que se asemeja más a una operación de buscar y reemplazar única que al uso de process.env en un servidor.

Por lo tanto, el flujo es el siguiente:

  1. Jenkins construye la imagen y Vite incrusta los valores de tipo VITE_* que estuvieron presentes en esa fase de compilación.
  2. La imagen se sube y se programa para su ejecución.
  3. El pod se inicia con variables más recientes o específicas del entorno.
  4. Esos valores del pod llegan demasiado tarde. El navegador descarga recursos estáticos (a menudo desde nginx) cuyo JavaScript ya contiene cadenas de datos anteriores.

Docker y Kubernetes tratan las variables de entorno como un asunto relacionado con el tiempo de ejecución, por lo que la misma imagen puede utilizarse tanto en entornos de pruebas como en producción. Vite, en cambio, considera import.meta.env como algo relacionado con el tiempo de compilación. Utilizar mecanismos del tiempo de ejecución con un contrato definido en el tiempo de compilación provoca errores silenciosos: sin rastro de la pila, sin advertencias, solo una configuración obsoleta.

Ese conflicto entre modelos mentales es la causa de todo el incidente. La plataforma no está “ignorando” a Kubernetes; el cliente nunca tuvo la forma de observar el entorno del pod.

¿Cuál es entonces la solución real?

Una vez que se comprende el problema de sincronización, la solución consiste en dejar de incluir cadenas específicas del entorno dentro de la imagen. Se debe generar una única versión limpia de la imagen y luego inyectar los valores cuando se inicia el contenedor, antes de que el navegador ejecute el paquete de la aplicación.

Un patrón práctico sería el siguiente:

1. Envíe un pequeño punto de entrada que se ejecute al iniciar el contenedor, recoja todas las variables con prefijo VITE_ del entorno de proceso y escriba un archivo sencillo env-config.js que asigne window.__env__:

#!/bin/sh
echo "window.__env__ = {" > /usr/share/nginx/html/env-config.js
env | grep '^VITE_' | while IFS='=' read -r key value; do
  echo "  \"$key\": \"$value\"," >> /usr/share/nginx/html/env-config.js
done
echo "};" >> /usr/share/nginx/html/env-config.js
exec nginx -g "daemon off;"

El filtrado con el prefijo VITE_ constituye la barrera de seguridad. Nombres como DB_PASSWORD o secretos internos del clúster permanecen fuera del archivo y nunca llegan al navegador.

2. Cargue ese script desde index.html antes que el paquete de la aplicación para que window.__env__ esté disponible antes de que se ejecute el código de React.

3. Prefiera utilizar window.__env__ en la configuración de la app, recayendo en import.meta.env para el desarrollo local:

const getEnv = (key) => window.__env__?.[key] || import.meta.env[key];
export const config = {
  apiUrl: getEnv("VITE_API_URL"),
};

4. Mantenga la etapa de construcción del Dockerfile libre de datos sensibles o específicos del entorno como VITE_*. Solo genera recursos estáticos; la inyección ocurre más tarde en el punto de entrada:

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build        # no sensitive vars baked in here anymore
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]   # runs at pod startup, injects env, THEN starts nginx

El generador ya no necesita VITE_API_URL ni elementos similares. Las diferencias entre entornos solo se manifiestan después de que la imagen ya está en ejecución.

5. Declare las variables de entorno en el Kubernetes Deployment de la forma habitual. No se requiere nada específico de Vite en el lado del clúster:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:latest
          ports:
            - containerPort: 80
          env:
            # only VITE_ vars actually reach the browser
            - name: VITE_API_URL
              value: "https://api.my-cluster.com"
            - name: VITE_APP_NAME
              value: "MyApp"
            # non-VITE_ vars stay inside the pod, never exposed to the frontend
            - name: NODE_ENV
              value: "production"
          # for real secrets, pull from a Kubernetes secret instead of a plain value:
          # - name: VITE_API_KEY
          #   valueFrom:
          #     secretKeyRef:
          #       name: my-secret
          #       key: api-key
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080

El bloque env: es estándar de Kubernetes. El punto de entrada lee esos valores al iniciar y expone el subconjunto VITE_ al navegador.

Después de este cambio, al actualizar el entorno de despliegue (por ejemplo, al dirigir VITE_API_URL a un nuevo origen HTTPS con kubectl set env), se reinicia el pod, se regenera env-config.js, y la siguiente carga de página utiliza el endpoint modificado. Sin reconstrucción de imágenes, sin ciclo en Jenkins: una sola imagen, muchos entornos, que es la razón habitual para usar contenedores.

Lo que realmente me molestó

La parte difícil rara vez es la solución que lleva veinte minutos. Lo doloroso es el silencio: no hay errores en la consola ni advertencias de compilación que indiquen que la cadena fue congelada hace tres compilaciones. La interfaz parece indicar que Kubernetes fue ignorado, cuando el cliente nunca supo que Kubernetes existía en tiempo de ejecución.

Si un servicio basado en Vite + React se está migrando a Kubernetes y el proceso de CI sigue incluyendo valores VITE_* con la expectativa de sobrescribirlos posteriormente en los pods, omite ese bucle de depuración. No incluyas configuraciones del cliente específicas del entorno dentro de la imagen. Inyecta dichas configuraciones al iniciar el contenedor y permite que el navegador cargue la configuración antes de que se ejecute tu paquete.