Чому додатки Vite React ігнорують змінні середовища Kubernetes під час виконання
Vite вбудовує змінні VITE_ під час збірки, тому зміни середовища Kubernetes pod ніколи не потрапляють до 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_*. Він створює лише статичні ресурси; вставка даних відбувається пізніше у точці входу:
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. Точка входу читає ці значення під час запуску та робить підмножину VITE_ доступною для браузера.
Після цієї зміни оновлення середовища розгортання (наприклад, налаштування VITE_API_URL на новий HTTPS-оригін з використанням kubectl set env) призводить до перезапуску поду, генерації нового файлу env-config.js, і під час наступного завантаження сторінки використовується оновлений кінцевий пункт. Не потрібно перебудовувати зображення чи запускати цикл Jenkins — одне зображення, багато середовищ, що є звичайною причиною використання контейнерів.
Те, що справді мене розлютило
Складною частиною рідко є виправлення проблеми за двадцять хвилин. Болісною є тиша: немає помилок у консолі та попереджень про будову, які б вказували на те, що рядок було заморожено ще три рази під час будови. Інтерфейс виглядає так, ніби Kubernetes проігноровано, хоча клієнт навіть не дізнався про його існування під час роботи.
Якщо сервіс на Vite + React переходить на Kubernetes, а CI все ще включає значення VITE_*, очікуючи подальших змін у конфігурації подів, пропустіть цей цикл відладки. Не вбудовуйте конфігурацію клієнта, специфічну для середовища, у образ. Вставляйте її під час запуску контейнера та дозвольте браузеру завантажити конфігурацію перед виконанням вашого пакету.