Почему приложения Vite React игнорируют переменные окружения Kubernetes во время выполнения
Vite встраивает переменные VITE_ во время сборки, поэтому изменения среды Kubernetes-пода никогда не доходят до React-приложения. Вместо этого используйте window.env, загружаемый из точки входа контейнера.
Источники: 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 на сервере.
Следовательно, процесс выглядит следующим образом:
- Jenkins создает образ, а Vite встраивает все значения типа
VITE_*, имевшиеся на этом этапе сборки. - Образ загружается и планируется к использованию.
- Под запускается с более новыми или специфичными для среды переменными.
- Эти значения под поступают слишком поздно. Браузер загружает статические ресурсы (часто из 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_* с ожиданием последующих изменений в подах, пропустите этот цикл отладки. Не встраивайте конфигурацию клиента, специфичную для окружения, в образ. Внедряйте её при запуске контейнера и позвольте браузеру загрузить конфигурацию до выполнения вашего пакета кода.