Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP5 min czytania1057 słów

Ź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:

  1. Jenkins tworzy obraz, a Vite włącza wszystkie wartości typu VITE_* obecne w tym kroku budowania.
  2. Obraz jest wysyłany i planowany do uruchomienia.
  3. Pod rozpoczyna działanie z nowszymi lub specyficznymi dla środowiska zmiennymi.
  4. 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.