Wyjaśnienie zmiennych środowiskowych: .env Pliki, wpisy tajne i konfiguracja zrobione prawidłowo
Zmienne środowiskowe pozwalają na uruchomienie tego samego kodu na laptopie, w CI i w środowisku produkcyjnym z różnymi ustawieniami. Oto jak się zachowują w rzeczywistości i jakie niespodzianki natrafiliśmy podczas ich testowania.
- Zmienne środowiskowe to ustawienia klucz-wartość, które proces otrzymuje od swojego otoczenia.
- Trzymają konfigurację, taką jak porty i adresy URL baz danych, poza kodem.
- Wartości to zawsze ciągi znaków, więc „fałsz” jest prawdą, chyba że ją przeanalizujesz.
- Nigdy nie udostępniaj plików
.envzawierających prawdziwe sekrety i sprawdzaj wymagane ustawienia po uruchomieniu aplikacji.
Twoja aplikacja potrzebuje adresu URL bazy danych. Na Twoim laptopie jest to lokalna baza danych, w CI jednorazowa, a na produkcji klaster zarządzany. Zakodowanie dowolnego z nich oznacza zmianę kodu dla każdego środowiska. Zmienne środowiskowe rozwiązują ten problem. Kod brzmi DATABASE_URL, a każde środowisko dostarcza własną wartość.
Pomysł ten jest kluczowy dla metody aplikacji Twelve-Factor, która mówi, że konfiguracja różniąca się w zależności od wdrożenia należy do środowiska. Przetestowaliśmy poniższe zachowanie w Pythonie 3.13 w systemie Windows. Wszystkie pokazane dane wyjściowe pochodzą z tego przebiegu.
Co to są zmienne środowiskowe?
Każdy działający proces ma zestaw nazwanych wartości tekstowych zwanych jego środowiskiem. Znane przykłady to PATH, które mówią powłoce, gdzie znaleźć programy, oraz HOME lub USERPROFILE. Twoja własna aplikacja może korzystać z tego samego mechanizmu dla ustawień takich jak APP_PORT, LOG_LEVEL lub DATABASE_URL.
Ustawianie zmiennych środowiskowych
| Powłoka | Na tę sesję | Na jedno polecenie |
|---|---|---|
| Bash / zsz | eksportuj APP_PORT=8080 |
APP_PORT=8080 aplikacja Pythona.py |
| PowerShell | $env:APP_PORT = "8080" |
Ustaw, uruchom, a następnie usuń |
| Polecenie Windows | ustaw APP_PORT=8080 |
Nie wbudowany |
Trwają one tylko dla bieżącej sesji terminala. Aby uczynić je trwałymi, dodaj je do profilu powłoki, takiego jak ~/.bashrc lub, w systemie Windows, użyj ustawień środowiska systemowego lub setx. W przypadku aplikacji zwykle lepiej jest dostarczyć je na etapie wdrażania, niż ustawiać je w całym systemie.
Odczytywanie ich w kodzie
import os
port = os.environ.get("APP_PORT", "8000") # default if missing
print(port) # 8000
db = os.environ["DATABASE_URL_MISSING_DEMO"]
# KeyError: 'DATABASE_URL_MISSING_DEMO'
Użyj .get() z wartością domyślną dla ustawień opcjonalnych. W przypadku ustawień, bez których aplikacja nie może działać, użyj nawiasów kwadratowych, które podnoszą KeyError. Inne języki są podobne: process.env.APP_PORT w Node.js, System.getenv("APP_PORT") w Javie i os.Getenv("APP_PORT") w Go.
Wszystko jest sznurkiem
To przyciąga ludzi w każdym języku. Ustawiliśmy port, a następnie dodaliśmy do niego „1”:
os.environ["APP_PORT"] = "8080"
port = os.environ["APP_PORT"]
print(type(port).__name__, repr(port), port + "1")
# str '8080' 80801
os.environ["APP_PORT"] = 8080
# TypeError: str expected, not int
Wartość wróciła jako tekst, a dodanie spowodowało 80801, a nie 8081. Konwertuj jawnie, jak w int(os.environ["APP_PORT"]). Wartości logiczne są gorsze:
os.environ["DEBUG"] = "false"
print(bool(os.environ["DEBUG"])) # True
Każdy niepusty ciąg znaków jest prawdziwy w Pythonie, więc "false" stało się True. Tryb debugowania zostałby włączony w środowisku produkcyjnym. Celowo analizuj wartości logiczne:
def env_bool(name, default=False):
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
env_bool("DEBUG") # False
env_bool("NOT_SET", default=True) # True
W jaki sposób procesy je dziedziczą
Proces potomny otrzymuje kopię środowiska swojego rodzica. Rozpoczęliśmy procesy potomne w Pythonie, aby zobaczyć to:
child sees: 8080 inherited # default: full copy of the parent's environment
child sees: 9000 None # given a custom env: only what we passed
parent sees child var: None # a child's changes never flow back
Następują trzy lekcje praktyczne. Eksportowane zmienne są widoczne dla każdego uruchomionego programu, w tym dla skryptów, których nie napisałeś, co ma znaczenie dla tajemnic. Przekazanie niestandardowego środowiska zastępuje całość, więc uwzględnij wszystko, czego potrzebuje dziecko. Skrypt nie może zmienić zmiennych powłoki nadrzędnej, dlatego narzędzia proszą o source plik zamiast go uruchomić.
pliki .env
Ręczne wpisywanie wielu zmiennych jest uciążliwe, dlatego większość projektów przechowuje lokalne wartości rozwojowe w pliku .env:
# local development only
APP_PORT=8080
DATABASE_URL="postgres://dev:devonly@localhost:5432/shop"
FEATURE_SEARCH=true
Biblioteki takie jak python-dotenv i dotenv dla Node.js ładują ten plik podczas uruchamiania. Node.js 20.6 i nowsze wersje mogą również ładować go natywnie za pomocą --env-file. Aby zobaczyć pomysł, napisaliśmy mały parser, który zamienił powyższy plik na:
{'APP_PORT': '8080',
'DATABASE_URL': 'postgres://dev:devonly@localhost:5432/shop',
'FEATURE_SEARCH': 'true'}
Wykorzystaj w praktyce prawdziwą bibliotekę. Obsługuje cytowanie, znaki ucieczki, komentarze i wartości wieloliniowe, które nasza 10-liniowa wersja ignoruje. Domyślnie większość programów ładujących dotenv nie nadpisuje zmienne, które są już ustawione, więc wartości rzeczywistego środowiska wygrywają z plikiem. Zwykle tego właśnie chcesz.
Wskazówka: Dodaj .env do .gitignore przed pierwszym zatwierdzeniem i zamiast tego zatwierdź .env.example z każdą nazwą zmiennej i bezpiecznymi wartościami zastępczymi. Nowi programiści kopiują go i wypełniają własne wartości.
Sprawdź konfigurację przy uruchomieniu
Brakująca zmienna powinna natychmiast zatrzymać aplikację i dać jasny komunikat, a nie powodować mylącego błędu godzinę później. Podczas ładowania sprawdziliśmy wymagane ustawienia:
REQUIRED = ["DATABASE_URL", "SECRET_KEY"]
def check_required(env):
missing = [k for k in REQUIRED if not env.get(k)]
if missing:
raise SystemExit(f"Missing required settings: {', '.join(missing)}")
# Missing required settings: SECRET_KEY
Wpisane biblioteki konfiguracyjne idą dalej. Ustawienia Pydantic w Pythonie, zod lub envalid w Node.js oraz właściwości konfiguracyjne Springa w Javie konwertują typy i zgłaszają każdy problem na raz.
Zmienne środowiskowe i sekrety
Zmienne środowiskowe są powszechnym sposobem przekazywania do aplikacji sekretów, takich jak klucze API, i są znacznie lepsze niż kodowanie ich na stałe. Mają słabe strony, o których warto wiedzieć:
- Jak pokazał nasz test, procesy potomne dziedziczą je.
- Mogą pojawiać się w raportach o awariach, stronach debugowania i dziennikach, które zrzucają środowisko.
- W niektórych systemach inne procesy uruchamiane przez tego samego użytkownika mogą je odczytać.
W przypadku produkcji wiele zespołów przechowuje sekrety w dedykowanym menedżerze, takim jak HashiCorp Vault, AWS Secrets Manager, Azure Key Vault lub Google Secret Manager, i wprowadza je w czasie wykonywania. Niektórzy wolą montować sekrety w postaci plików zamiast zmiennych. Tak czy inaczej, nigdy nie drukuj konfiguracji w dziennikach i zmieniaj wszelkie sekrety, które kiedykolwiek zostały przekazane Gitowi, nawet po usunięciu ich z historii.
W Dockerze i CI
# Docker (not run for this article)
docker run -e APP_PORT=8080 --env-file .env.production myapp
# GitHub Actions (not run for this article)
env:
APP_PORT: 8080
API_KEY: ${{ secrets.API_KEY }}
Platformy CI przechowują zaszyfrowane sekrety i maskują je w dziennikach. Unikaj umieszczania sekretów w obrazach Dockera za pomocą ENV w pliku Dockerfile, ponieważ pozostają one czytelne w warstwach obrazu. Pełne konfiguracje znajdziesz w naszym samouczku Docker Compose i przewodniku CI/CD dotyczącym akcji GitHub Actions.
Najlepsze praktyki
- Używaj wyraźnych, wielkich nazw z przedrostkiem, na przykład
SHOP_DB_URL. - Przeczytaj całą konfigurację w jednym module, przekonwertuj tam typy i sprawdź przy uruchomieniu.
- Podaj bezpieczne ustawienia domyślne tylko dla ustawień niewrażliwych.
- Trzymaj
.envz dala od Git i utrzymuj.env.example. - Użyj menedżera wpisów tajnych w środowisku produkcyjnym dla poufnych wartości.
- Dokumentuj każdą zmienną, jej typ i przykładową wartość.
Debugowanie problemów ze zmiennymi środowiskowymi
Jeśli aplikacja zachowuje się tak, jakby brakowało jakiegoś ustawienia, wykonaj następujące kontrole w podanej kolejności:
- Czy jest ustawione w dobrym miejscu? Zmienna wyeksportowana w jednej zakładce terminala nie istnieje w innej, lub w usłudze uruchomionej przez system.
- Czy aplikacja została uruchomiona ponownie? Działający proces zachowuje środowisko, w którym został uruchomiony. Późniejsza zmiana zmiennej nie przyniesie efektu aż do ponownego uruchomienia.
- Czy nazwa jest dokładna? W nazwach w systemie Linux i macOS rozróżniana jest wielkość liter, więc
App_PortiAPP_PORTto różne zmienne. Windows traktuje je tak samo. - Czy plik .env zastępuje go, czy jest ignorowany? Sprawdź, czy moduł ładujący zastępuje istniejące wartości i który plik faktycznie czyta.
- Czy występują przypadkowe cudzysłowy lub spacje? Wartość skopiowana ze spacją na końcu lub dodatkowymi cudzysłowami może nie zostać przeanalizowana lub dopasować w sposób trudny do wykrycia.
Aby zobaczyć, co naprawdę otrzymał proces, wypisz nazwy zmiennych, nigdy wartości sekretów. W systemach Linux i macOS printenv wyświetla listę bieżącego środowiska powłoki, a w PowerShell Get-ChildItem Env: robi to samo. Wewnątrz kontenera, przejście od env do docker exec pokazuje widok kontenera, który często różni się od widoku twojej własnej powłoki.
Ostatnia wskazówka: podczas uruchamiania zapisz jedną linię zawierającą listę ustawień opcjonalnych, które korzystają z wartości domyślnych. To nic nie kosztuje, a odpowiada na większość pytań: „dlaczego to robi?” pytania od razu.
Podsumowanie
Zmienne środowiskowe są proste, ale skrywają kilka pułapek: wszystko jest ciągiem znaków, dzieci je dziedziczą i tajemnice mogą przez nie wyciekać. Traktuj je jako dane wejściowe do swojej aplikacji. Przeczytaj je w jednym miejscu, przekonwertuj i zweryfikuj je wcześniej, a także zachowaj prawdziwe tajemnice przed zatwierdzonymi plikami.
Najczęstsze pytania
Co to jest zmienna środowiskowa?
Jest to nazwana wartość tekstowa, którą proces otrzymuje od swojego otoczenia, np. APP_PORT=8080. Aplikacje używają ich do ustawień, które zmieniają się w zależności od środowiska.
Co to jest plik .env?
Plik tekstowy zawierający wiersze KEY=wartość, ładowany podczas uruchamiania przez bibliotekę taką jak python-dotenv. Służy głównie do rozwoju lokalnego i nie należy go zdradzać z prawdziwymi tajemnicami.
Dlaczego moja zmienna środowiskowa jest ciągiem znaków?
Systemy operacyjne przechowują wartości środowiska jako tekst. Konwertuj liczby za pomocą int() i jawnie analizuj wartości logiczne, ponieważ ciąg „false” liczy się jako prawdziwy.
Czy zmienne środowiskowe są bezpieczne dla sekretów?
Są lepsze niż kodowanie na stałe, ale procesy potomne dziedziczą je i dzienniki mogą je ujawnić. Systemy produkcyjne często korzystają z dedykowanego menedżera sekretów.
Czy skrypt może zmienić zmienne powłoki nadrzędnej?
Nie. Proces potomny otrzymuje kopię środowiska, więc wprowadzone w nim zmiany nie są cofane. Aby ustawić zmienne w bieżącej powłoce, zamiast tego pobierz plik źródłowy.
Ucz się dalej
Potrzebujesz pomocy przy testowaniu, WordPressie lub SEO?
Pomagamy zespołom dostarczać lepsze oprogramowanie i rozwijać ich witryny. Powiedz nam, nad czym pracujesz, a my zaproponujemy jasny następny krok.
Więcej poradników na testowanie, sztuczna inteligencja i nowoczesny rozwój
Podobał Ci się ten artykuł? Wybierz temat i przejdź bezpośrednio do naszych najnowszych tutoriali.
Wyjaśnienie halucynacji AI: dlaczego modele językowe tworzą rzeczy i jak je zmniejszyć
Wyjaśniono halucynacje AI: czym są, dlaczego powstają w modelach językowych, typowe typy, jak je zredukować za pomocą uziemień i podpowiedzi oraz pr…
Przeczytaj artykuł →DevOpsWyjaśnienie zmiennych środowiskowych: pliki .env, sekrety i konfiguracja wykonana prawidłowo
Zmienne środowiskowe wyjaśnione na przykładach Pythona, które przeprowadziliśmy: ich odczytywanie i ustawianie, dlaczego każda wartość jest ciągiem …
Przeczytaj artykuł →Tworzenie oprogramowaniaWyjaśnienie ograniczenia szybkości interfejsu API: algorytmy, nagłówki i obsługa błędów 429
Ograniczanie szybkości API wyjaśnione za pomocą przeprowadzonej przez nas symulacji: zasobnik tokenów, stałe okno, przesuwane okno, 429 odpowiedzi, …
Przeczytaj artykuł →Automatyzacja testówWyśmiewanie w Pythonie: unittest.mock, patch i błędy, które sprawiają, że testy kłamią
Wyśmiewanie w Pythonie za pomocą unittest.mock, przetestowane: Mock i zwracana wartość, efekt uboczny, łatanie tam, gdzie używana jest nazwa, specyf…
Przeczytaj artykuł →Testowanie oprogramowaniaWyjaśnienie zasięgu kodu: pokrycie linii i oddziału oraz co naprawdę oznacza 100%
Wyjaśnienie pokrycia kodu za pomocą prawdziwego pliku cover.py: pokrycie linii, gałęzi i warunków, przeczytanie raportu, dlaczego 100% pokrycia może…
Przeczytaj artykuł →Bazy danych i SQLDELETE vs TRUNCATE vs DROP w SQL: różnice, wycofywanie i kiedy używać każdego z nich
DELETE vs TRUNCATE vs DROP wyjaśnione za pomocą przetestowanego SQL: co każdy usuwa, wycofywanie, wyzwalacze, resetowanie tożsamości, prędkość i czy…
Przeczytaj artykuł →Bazy danych i SQLWyjaśnienie widoków SQL: tworzenie, używanie i unikanie pułapek wraz z przykładami
Widoki SQL wyjaśnione za pomocą zapytań, które uruchomiliśmy: tworzenie widoku, wyniki na żywo, ukrywanie kolumn, wydajność, widoki z możliwością ak…
Przeczytaj artykuł →Tworzenie oprogramowaniaInterfejs TypeScript a typ: rzeczywiste różnice, testowane za pomocą kompilatora
Interfejs TypeScript a typ, sprawdzony z prawdziwym kompilatorem: co oba mogą zrobić, co tylko jeden, łączenie deklaracji, rozszerzenia a przecięcia…
Przeczytaj artykuł →Tworzenie oprogramowaniaPython *args i **kwargs wyjaśnione: elastyczne funkcje z przejrzystymi przykładami
Python *args i **kwargs wyjaśnione za pomocą uruchomionego przez nas kodu: pakowanie i rozpakowywanie, kolejność parametrów, argumenty zawierające t…
Przeczytaj artykuł →Tworzenie oprogramowaniaFunkcje Lambda w Pythonie: składnia, rzeczywiste zastosowania i kiedy ich nie używać
Funkcje lambda w Pythonie wyjaśnione za pomocą uruchomionego przez nas kodu: składnia, sortowanie za pomocą klucza, mapa i filtr, domknięcia, pułapk…
Przeczytaj artykuł →Sztuczna inteligencjaKim są agenci AI? Jak one działają, dzięki prostej pętli agenta, którą możesz uruchomić
Kim są agenci AI? Prosty przewodnik po pętli agenta, narzędziach, pamięci i poręczach, z małą pętlą w Pythonie, którą uruchomiliśmy, prawdziwymi zag…
Przeczytaj artykuł →DevOpsMonitorowanie a obserwowalność: prawdziwa różnica z logami, wskaźnikami i śladami
Wyjaśnienie monitorowania a obserwowalności: znane i nieznane awarie, dzienniki, metryki i ślady, opis incydentu przy kasie, złote sygnały, OpenTele…
Przeczytaj artykuł →DevOpsWoluminy platformy Docker a mocowania powiązań: utrwalanie danych kontenera we właściwy sposób
Wyjaśniono woluminy Dockera a montowanie powiązań: dlaczego dane kontenera znikają, nazwane woluminy, montowanie powiązań, tmpfs, przykłady tworzeni…
Przeczytaj artykuł →Automatyzacja testówTDD vs BDD: kluczowe różnice, jedna funkcja zbudowana w obie strony i kiedy używać każdej z nich
TDD vs BDD wyjaśnione za pomocą jednej funkcji rabatowej zbudowanej w obie strony: refaktoryzacja czerwono-zielona z uruchomionym przez nas kodem, s…
Przeczytaj artykuł →Tworzenie oprogramowaniaGit merge vs rebase: różnice, przykłady i kiedy czego używać
Git merge vs rebase wyjaśnione na prawdziwym wyniku logu, z tabelą porównawczą, złotą zasadą rebase i bezpiecznym procesem pracy zespołu.
Przeczytaj artykuł →Tworzenie oprogramowaniaPython list comprehension: składnia, przykłady i kiedy jej unikać
Python list comprehension wyjaśnione: składnia, filtrowanie, if-else, zagnieżdżone pętle, zmierzone czasy i sytuacje, w których zwykła pętla for jes…
Przeczytaj artykuł →Bazy danych i SQLSQL JOIN – wyjaśnienie z przykładami (Inner, Left, Right, Full)
Zrozum złączenia SQL krok po kroku na przykładowych tabelach i zapytaniach: INNER, LEFT, RIGHT, FULL, CROSS i SELF, plus typowe błędy i wskazówki wy…
Przeczytaj artykuł →