Wyjaśnienie zmiennych środowiskowych: pliki .env, sekrety i konfiguracja wykonana prawidłowo

  1. Strona główna
  2. Blog
  3. DevOps

DevOps

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.

8 min czytania
Wyjaśnienie zmiennych środowiskowych: pliki .env, sekrety i konfiguracja wykonane prawidłowo

Najważniejsze

  • 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 .env zawierają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 .env z 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:

  1. Czy jest ustawione w dobrym miejscu? Zmienna wyeksportowana w jednej zakładce terminala nie istnieje w innej, lub w usłudze uruchomionej przez system.
  2. 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.
  3. Czy nazwa jest dokładna? W nazwach w systemie Linux i macOS rozróżniana jest wielkość liter, więc App_Port i APP_PORT to różne zmienne. Windows traktuje je tak samo.
  4. 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.
  5. 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

Pracuj z Stack Quora

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.

Czytaj dalej

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.

Sztuczna inteligencja

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ł →
DevOps

Wyjaś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 oprogramowania

Wyjaś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ów

Wyś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 oprogramowania

Wyjaś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 SQL

DELETE 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 SQL

Wyjaś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 oprogramowania

Interfejs 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 oprogramowania

Python *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 oprogramowania

Funkcje 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 inteligencja

Kim 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ł →
DevOps

Monitorowanie 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ł →
DevOps

Woluminy 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ów

TDD 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 oprogramowania

Git 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 oprogramowania

Python 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 SQL

SQL 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ł →