Переменные окружения

29 мая 2026 · ~12 мин чтения

концепция unix конфигурация секреты devops

Переменные окружения

Это пары «имя=значение», которые операционная система передаёт каждому запускаемому процессу. Через них программа узнаёт свои настройки и секреты, не зашитые в код.

История

Переменные окружения родились в Bell Labs в 1979 году, в седьмой версии Unix (Unix V7). Автор — Стивен Борн (Stephen Bourne), тот самый, чьей фамилией назван sh (Bourne shell). До этого, в Unix V6 (1975), у программ был только массив аргументов командной строки — argv. Этого хватало, пока программы были маленькими, но как только понадобилось передавать сразу десяток параметров (где живёт почта, какой язык, где терминал), стало очевидно: командная строка превращается в кашу из флагов.

Борн добавил в системный вызов execve() третий аргумент — envp (environment pointer). Это NULL-завершённый массив строк вида "KEY=VALUE". Идея простая: пусть каждая новая программа получает не только аргументы, но и «вторую руку» — набор именованных параметров, которые она может читать или игнорировать.

Дальше — этапы канонизации:

Сейчас, в 2026-м, это базовый словарь любого инженера: фраза «прокинь это через env» понятна и Go-программисту, и тому, кто настраивает GitHub Actions, и DevOps на Kubernetes.

Что это такое

Переменные окружения — это таблица «имя → значение», которую операционная система прикладывает к каждому процессу. Имена — строки латиницей в верхнем регистре по соглашению (DATABASE_URL, TELEGRAM_TOKEN), значения — произвольные строки. Просто строки, не числа, не словари, не списки — только текст. Это важно: в окружении нет типов.

Каждый процесс получает свою копию окружения при запуске. Когда программа запускает другую программу (через fork+exec в Unix), дочерний процесс наследует окружение родителя — но это именно копия. Если ребёнок изменит у себя PATH, родитель об этом никогда не узнает. Это одностороннее наследование, как сундук с приданым: что положили — то и взял с собой, обратно ничего не возвращается.

В системном смысле окружение живёт в памяти процесса. В C — это глобальная переменная extern char **environ, в Python — os.environ (словарь, имитирующий это поведение), в shell — то, что показывает команда env или printenv. На Linux окружение работающего процесса можно подсмотреть через файл /proc/<PID>/environ (если хватает прав). На macOS аналогичного публичного файла нет, но через ps -E можно увидеть env-переменные своих процессов.

Чем переменные окружения отличаются от похожего:

Аналогии из жизни

1. Рюкзак, который мама собрала в школу.
Утром мама положила в рюкзак тетради, бутерброд, ключи. В школе ты можешь достать оттуда что угодно, можешь даже что-то добавить. Когда возвращаешься домой — мама не узнает, что ты подложил себе фантики; её рюкзак (то, что осталось у неё) не меняется.

Где ломается: в реальности рюкзак физически один и переходит из рук в руки. В случае env — рюкзак копируется на пороге школы: мама и ты держите два разных одинаковых рюкзака. Менять можно только свой.

2. Доска объявлений в подъезде.
Управляющая компания вывесила объявления: режим работы лифта, телефон диспетчерской, тариф за воду. Любой жилец, выходя из квартиры, может прочитать.

Где ломается: доска одна на всех, видна всем. Env — это доска только для одной квартиры (процесса) и её детей. Соседский ребёнок (другой процесс) её не видит. И в отличие от настоящей доски, переменные окружения у каждого жильца свои, не общие.

3. Конверт с инструкциями от заказчика курьеру.
Курьеру выдают конверт: куда ехать, что забрать, какой код от подъезда. Курьер берёт его с собой, в дороге читает. Когда курьер передаёт заказ субподрядчику — он копирует конверт (или передаёт оригинал — но в случае env всегда копию).

Где ломается: в реальности заказчик может позвонить и поменять адрес. С env так нельзя: пока процесс запущен, родитель в его окружение не дотянется снаружи. Хочешь поменять — перезапусти.

Как это работает

Системный вызов execve(path, argv, envp) — главная точка входа в Unix. У него три аргумента:

  1. path — путь к исполняемому файлу.
  2. argv — массив аргументов («имя программы», «аргумент 1», ..., NULL).
  3. envp — массив окружения («KEY1=VALUE1», «KEY2=VALUE2», ..., NULL).

Когда ты в терминале набираешь python myscript.py --verbose:

  1. shell делает fork() — создаёт копию себя.
  2. В копии вызывает execve("/usr/bin/python", ["python", "myscript.py", "--verbose"], envp), где envp — это всё, что у shell было в export.
  3. Ядро заменяет память процесса на образ python, копируя туда argv и envp.
  4. Python запускается, читает os.environ — это и есть распарсенный envp.

В коде на C это выглядит так:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char *db = getenv("DATABASE_URL");
    if (!db) { fprintf(stderr, "DATABASE_URL not set\n"); return 1; }
    printf("connecting to %s\n", db);
    return 0;
}

В Python — то же самое одной строкой:

import os
db = os.environ["DATABASE_URL"]   # упадёт, если не задано
db = os.environ.get("DATABASE_URL", "sqlite:///fallback.db")  # с дефолтом

В shell:

echo $DATABASE_URL
echo "${DATABASE_URL:-sqlite:///fallback.db}"   # с дефолтом

Жизненный цикл переменной от загрузки системы до запуска программы:

  1. Ядро запускает первый процесс (init/launchd/systemd) с минимальным окружением — буквально пара переменных: PATH=/bin:/usr/bin, иногда HOME.
  2. Менеджер сессии при логине пользователя выставляет USER, HOME, SHELL, читает /etc/environment (на Linux) или launchd.conf (на старых macOS).
  3. Shell при старте читает свои конфиги: ~/.profile, ~/.bashrc, ~/.zshenv, ~/.zshrc. В них живут команды export VAR=value, добавляющие переменные в окружение текущего shell.
  4. Запуск программы из shell — программа наследует окружение shell.
  5. Программа может прочитать окружение через getenv()/os.environ, может добавить свои через setenv()/os.environ[...]=.... Эти добавления уйдут в её детей, но не вернутся к родителю.

Ключевая контринтуитивность

Окружение — это снимок, сделанный в момент запуска. Если ты поменял ~/.zshenv и не перезапустил процесс — он работает со старыми значениями. Любой systemd-сервис, launchd-агент, докер-контейнер должен быть перезапущен, чтобы увидеть новые env.

Команды важные на каждый день:

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Если коротко — все. Но есть знаковые применения:

Альтернативы и конкуренты

Файлы конфигурации (YAML/TOML/JSON/INI).
+ Структурированные данные: вложенные объекты, списки, типы.
+ Можно комментировать, версионировать в git (если без секретов).
− Секреты в файлах — антипаттерн (попадают в git, бэкапы, образы).
− Нужно решать, где файл лежит, кто его кладёт.

CLI-флаги (--db-url=...).
+ Явно и видно при запуске.
+ Хорошо для разовых вызовов CLI-утилит.
− Видны в ps aux любому пользователю на машине — категорически плохо для секретов.
− Сложно передавать много значений (флагов десятки).

Vault / AWS Secrets Manager / HashiCorp Vault / Doppler.
+ Централизованное хранилище, ротация, аудит доступа.
+ Можно дать сервису короткоживущий токен вместо вечного пароля.
− Дополнительная зависимость: если Vault недоступен — приложение не стартует.
− Сложность настройки, цена.

Аргументы конструктора / DI-контейнер.
+ Типы, IDE-автодополнение, проверки на этапе компиляции.
+ Легко тестировать (передаёшь mock в конструктор).
− Где-то на самом верху всё равно нужно прочитать значения из мира — обычно из env.

На практике это не «или–или»

Современная архитектура — слоёная: секреты в Vault, оттуда подтягиваются в env при старте, приложение читает os.environ и собирает оттуда объект конфигурации с типами через Pydantic/zod/joi. Env здесь — клей, а не единственная защита.

Когда НЕ стоит использовать

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера делал аудит секретов в дереве проектов: разные локальные агенты складывали API-токены в свои .env-файлы, и эти файлы синхронизировались через облачное хранилище — то есть пароли уезжали в облако вместе с обычными настройками. Унифицировал хранение секретов в одном месте вне облака (~/.zshenv), а .env агентов оставил пустыми или только с публичными параметрами. Понял, что приоритет источников у каждого агента свой — некоторые читают os.environ поверх .env, и это спасает.

Краткое резюме