Dlaczego aplikacje Vite React ignorują zmienne środowiskowe Kubernetes w czasie wykonywania
Vite wstawia zmienne VITE_ w trakcie budowania, dzięki czemu zmiany środowiska poda Kubernetes nigdy nie docierają do aplikacji React. Zamiast tego należy wprowadzić wartość window.env z punktu wejścia kontenera.
Źródła: Why Your Vite + React App Ignores Kubernetes Env Vars (And How I Fixed It)
Zespoły zajmujące się frontendem, które już znają serwery Node, często zakładają, że zmienne środowiskowe w podach zachowują się tak samo po umieszczeniu pliku React bundle w Kubernetes. SPA zbudowane przy użyciu Vite łamie to założenie w cichy, ale kosztowny sposób: wartości ustawione w Deployment nigdy nie trafiają do przeglądarki, ponieważ nigdy nie były sprawdzane w momencie żądania.
Ustawienia
Załóżmy aplikację React zbudowaną przy użyciu Vite. Konfiguracja wykorzystuje standardowe przedrostki VITE_ – VITE_API_URL, VITE_APP_NAME, opcje włączania funkcji oraz podobne parametry.
Proces wydawania aplikacji przebiega przez Jenkins. Pipeline buduje obraz Dockera i podczas tej operacji dostarcza wartości środowiskowe, aby pojawiły się „wewnątrz” obrazu. Ten sam obraz jest później uruchamiany w podzie Kubernetes, gdzie Deployment również deklaruje zmienne środowiskowe, zakładając, że bieżący proces odczyta je w taki sam sposób jak Express.
Takie rzeczy nigdy się nie zdarzają w przypadku klienta Vite. Aktualizacja wartości klucza w konfiguracji deployment, ponowne uruchomienie poda oraz odświeżenie przeglądarki nadal pokazują poprzednią wartość. Sprawdzenie poda za pomocą kubectl exec … -- env potwierdza, że zmienna istnieje w środowisku kontenera. Statyczny JavaScript po prostu jej nie wykorzystuje.
Dlaczego tak się dzieje (wersja uproszczona)
Dla osób przyzwyczajonych do Node lub Express brak wyjaśnienia może zostać przeoczony: Vite rozwiązuje zmienne środowiskowe podczas kompilacji projektu, a nie w momencie, gdy użytkownik ładuje stronę.
Gdziekolwiek kod źródłowy odwołuje się do import.meta.env.VITE_API_URL, proces budowania zastępuje to wyrażenie literowym łańcuchem tekstu. Po zakończeniu wykonywania polecenia npm run build nie ma już możliwości wyszukiwania w czasie wykonywania – jest tylko zamrożony tekst wpleciony w plik, co przypomina bardziej jednorazowe zastąpienie niż korzystanie z process.env na serwerze.
Zatem proces wygląda w następujący sposób:
- Jenkins tworzy obraz, a Vite włącza wszystkie wartości typu
VITE_*obecne w tym kroku budowania. - Obraz jest wysyłany i planowany do uruchomienia.
- Pod rozpoczyna działanie z nowszymi lub specyficznymi dla środowiska zmiennymi.
- Wartości tych podów przychodzą zbyt późno. Przeglądarka ściąga pliki statyczne (często z nginx), których JavaScript zawiera już tekst z wczoraj.
Docker i Kubernetes traktują zmienne środowiskowe jako kwestię w czasie wykonywania, dzięki czemu ta sama obraz może być używana zarówno w środowisku testowym, jak i produkcyjnym. Vite natomiast traktuje import.meta.env jako kwestię w czasie budowy. Wykorzystywanie mechanizmów z czasu wykonywania wobec kontraktu określonego na etapie budowy skutkuje bezgłośnym awarią: brak śladu wywołań, brak ostrzeżenia, tylko przestarzała konfiguracja.
To zderzenie różnych modeli myślenia stanowi sedno całego incydentu. Platforma nie „ignoruje” Kubernetesa; klient nigdy nie miał możliwości obserwacji środowiska poziomu poda.
Jaka więc jest rzeczywista poprawka?
Gdy problem z terminologią stanie się jasny, rozwiązaniem jest przestanie włączania do obrazu ciągów znaków specyficznych dla środowiska. Należy wykonać jedną czystą budowę, a następnie wprowadzić wartości w momencie uruchamiania kontenera — zanim przeglądarka wykona plik aplikacji.
Praktyczny wzorzec wygląda w ten sposób:
1. Wysyłaj mały punkt wejścia, który uruchamia się przy starcie kontenera, zbiera wszystkie zmienne z prefiksem VITE_ z środowiska procesu i tworzy prosty plik env-config.js, który przypisuje wartości do 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;"
Filtrowanie według prefiksu VITE_ stanowi granicę bezpieczeństwa. Nazwy takie jak DB_PASSWORD czy sekrety używane wewnątrz klastra nie trafiają do tego pliku i nigdy nie docierają do przeglądarki.
2. Ładuj ten skrypt z index.html przed plikiem z aplikacją, aby window.__env__ istniał już przed uruchomieniem kodu React.
3. W konfiguracji aplikacji preferuj window.__env__, a w przypadku rozwoju lokalnego używaj import.meta.env:
const getEnv = (key) => window.__env__?.[key] || import.meta.env[key];
export const config = {
apiUrl: getEnv("VITE_API_URL"),
};
4. Upewnij się, że etap budowania Dockerfile nie zawiera wrażliwych danych ani specyficznych dla środowiska wartości typu VITE_*. Tworzy on jedynie pliki statyczne; wstrzykiwanie danych następuje później, w punkcie wejścia:
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
Budzer nie potrzebuje już VITE_API_URL ani podobnych wartości. Różnice pomiędzy środowiskami pojawiają się dopiero wtedy, gdy obraz jest już uruchomiony.
5. Zadeklaruj zmienne środowiskowe w pliku Kubernetes Deployment w zwykły sposób. Po stronie klastra nie są wymagane żadne elementy specyficzne dla 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
Blok env: jest standardowym elementem Kubernetes. Punkt wejścia odczytuje te wartości podczas uruchamiania i udostępnia podzbiór VITE_ przeglądarce.
Po tej zmianie aktualizacja środowiska Deployment (na przykład ustawienie VITE_API_URL na nowy adres HTTPS za pomocą kubectl set env) restartuje pod, regeneruje plik env-config.js, a przy następnym załadowaniu strony używany jest już poprawiony punkt końcowy. Nie ma konieczności ponownego budowania obrazu ani cyklu w Jenkins – jeden obraz, wiele środowisk, co jest typowym powodem stosowania kontenerów.
To, co naprawdę mnie zdenerwowało
Trudna część rzadko polega na naprawie trwającej dwadzieścia minut. Najbardziej irytujące jest milczenie: brak błędów w konsoli i brak ostrzeżeń podczas budowania wskazujących, że ciąg znaków został zamarznięty trzy razy wcześniej. Interfejs wygląda tak, jakby Kubernetes został zignorowany, mimo że klient w czasie działania aplikacji nigdy nie dowiedział się o jego istnieniu.
Jeśli usługa oparta na Vite + React przenosi się na Kubernetes, a proces CI nadal zawiera wartości VITE_* zakładające późniejsze modyfikacje w kontenerach, pomij tego cyklu debugowania. Nie włączaj konfiguracji klienta specyficznej dla środowiska do obrazu. Wstrzykuj ją przy uruchamianiu kontenera i pozwól przeglądarce naładować konfigurację przed uruchomieniem twojego pliku bundle.