Главная / Статьи

Почему приложения Vite React игнорируют переменные окружения Kubernetes во время выполнения

Vite встраивает переменные VITE_ во время сборки, поэтому изменения среды Kubernetes-пода никогда не доходят до React-приложения. Вместо этого используйте window.env, загружаемый из точки входа контейнера.

Издатель: REACTAPP.TOP5 мин чтения1057 слов

Источники: Why Your Vite + React App Ignores Kubernetes Env Vars (And How I Fixed It)

Команды фронтенда, уже знакомые с серверами Node, часто предполагают, что переменные окружения подов будут вести себя аналогично, когда бандл React попадает в Kubernetes. SPA, собранная с использованием Vite, нарушает это предположение тихим, но дорогостоящим способом: значения, заданные в Deployment, никогда не доходят до браузера, поскольку они никогда не запрашиваются в момент запроса.

Настройка

Представьте приложение на React, созданное с использованием Vite. Для конфигурации используется обычный префикс VITE_ — VITE_API_URL, VITE_APP_NAME, параметры включения/выключения функций и подобные настройки.

Процесс выпуска проходит через Jenkins. Пайплайн собирает образ Docker, и во время этой сборки задаются значения окружения, чтобы они «находились внутри» образа. Позже этот же образ запускается в поде Kubernetes, где Deployment также объявляет переменные окружения, рассчитывая, что запущенный процесс будет читать их так же, как это делал бы Express.

У клиента Vite это никогда не происходит. При обновлении значения ключа в настройках развертывания, перезапуске пода и обновлении браузера по-прежнему отображается прежняя строка. Проверка пода с помощью команды kubectl exec … -- env подтверждает, что переменная существует в среде контейнера. Однако статический JavaScript просто не обращается к ней.

Почему это происходит (простая версия)

У тех, кто работал с Node или Express, этот недостающий момент легко упускается из виду: Vite обрабатывает переменные окружения во время компиляции проекта, а не когда пользователь загружает страницу.

Во всех местах, где в исходном коде используется import.meta.env.VITE_API_URL, процесс сборки заменяет это выражение на литеральную строку. После завершения команды npm run build поиск значений во время выполнения программы больше не происходит — в бандле остается только зафиксированный текст, что больше похоже на однократную замену, чем на использование process.env на сервере.

Следовательно, процесс выглядит следующим образом:

  1. Jenkins создает образ, а Vite встраивает все значения типа VITE_*, имевшиеся на этом этапе сборки.
  2. Образ загружается и планируется к использованию.
  3. Под запускается с более новыми или специфичными для среды переменными.
  4. Эти значения под поступают слишком поздно. Браузер загружает статические ресурсы (часто из nginx), в JavaScript которых уже содержатся строки вчерашней даты.

Docker и Kubernetes рассматривают переменные окружения как проблему, связанную с временем выполнения, поэтому один и тот же образ может использоваться как на этапе разработки, так и в производственной среде. Vite же рассматривает import.meta.env как элемент, связанный с временем сборки. Использование механизмов времени выполнения вместо схемы, предусмотренной для времени сборки, приводит к беззвучному сбою: нет трассировки стека, нет предупреждений, только устаревшая конфигурация.

Именно это противоречие в подходах и стало причиной всего инцидента. Платформа не «игнорирует» Kubernetes; у клиента просто никогда не было способа отслеживать среду пода.

Каково же реальное решение?

Как только становится ясна проблема с временем, решение заключается в том, чтобы перестать встраивать в образ строки, специфичные для конкретной среды. Необходимо создать одну чистую версию образа, а затем вводить значения при запуске контейнера — до того, как браузер начнет выполнять пакет приложения.

Практический подход выглядит следующим образом:

1. Отправьте небольшой входной модуль, который запускается при старте контейнера, собирает все переменные с префиксом VITE_ из окружения процесса и создает обычный файл env-config.js, в котором задаётся значение 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;"

Фильтрация по префиксу VITE_ служит границей безопасности. Имена вроде DB_PASSWORD или внутренние секреты кластера не попадают в этот файл и никогда не доходят до браузера.

2. Загрузите этот скрипт из index.html раньше, чем пакет приложения, чтобы переменная window.__env__ уже существовала до запуска кода React.

3. В настройках приложения предпочитайте использование window.__env__, а для локальной разработки — import.meta.env:

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

4. Обеспечьте, чтобы на этапе сборки Dockerfile отсутствовали конфиденциальные данные или параметры типа VITE_*, связанные с конкретной средой. На этом этапе генерируются только статические ресурсы; вставка таких данных происходит позже, в функции entrypoint:

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

Для сборки больше не требуется параметр VITE_API_URL или аналогичные. Различия между средами проявляются только после того, как образ уже запущен.

5. Объявляйте переменные окружения в Kubernetes Deployment обычным способом. Со стороны кластера не требуется ничего специфического для Vite:

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

Блок env: является стандартным элементом Kubernetes. Функция entrypoint считывает эти значения при запуске и предоставляет подмножество параметров VITE_ браузеру.

После этого изменения обновление среды развертывания (например, указание VITE_API_URL на новый HTTPS-оригин с помощью kubectl set env) приводит к перезапуску пода, повторной генерации файла env-config.js, и при следующем загрузке страницы используется обновленный конечный пункт. Нет необходимости в пересборке изображений, нет циклов Jenkins — одно изображение, множество сред, что и является основной причиной использования контейнеров.

То, что действительно меня раздражало

Самой сложной частью редко бывает решение проблемы за двадцать минут. Наиболее неприятным является молчание: нет ошибок в консоли и никаких предупреждений о том, что строка была заморожена ещё три раза в процессе сборки. Интерфейс создает впечатление, что Kubernetes игнорируется, хотя клиент даже не узнал о существовании Kubernetes во время выполнения.

Если сервис на Vite + React переходит на Kubernetes, а CI по-прежнему включает значения VITE_* с ожиданием последующих изменений в подах, пропустите этот цикл отладки. Не встраивайте конфигурацию клиента, специфичную для окружения, в образ. Внедряйте её при запуске контейнера и позвольте браузеру загрузить конфигурацию до выполнения вашего пакета кода.