Олег Марков
Конфигурация окружения environment-config в современных приложениях
Введение
Конфигурация окружения (environment-config) — это способ описать поведение приложения с помощью настроек, которые можно менять без изменения кода.
Вы выносите в конфигурацию то, что зависит от среды запуска: адреса баз данных, ключи API, режим логирования, фичи для тестового стенда, параметры производительности. Код остается одинаковым, а ведет себя по‑разному в зависимости от окружения.
Смотрите, я кратко сформулирую идею:
- код описывает логику;
- конфигурация окружения описывает контекст выполнения;
- переключение окружения (dev, test, prod) должно происходить без перекомпиляции и без правок исходников.
Давайте разберемся, что такое environment-config на практике, какие есть подходы, как их комбинировать и как организовать конфиги так, чтобы проект не превратился в набор хаотичных .env и JSON‑файлов.
Базовые принципы конфигурации окружения
Конфигурация vs константы в коде
Иногда соблазнительно "зашить" параметры прямо в код:
// ПЛОХО - настройки захардкожены в коде
const dbURL = "postgres://user:pass@localhost:5432/app"
const logLevel = "debug"
Проблема в том, что:
- для каждого окружения нужно менять код и пересобирать бинарь;
- вы рискуете закоммитить чувствительные данные в репозиторий;
- сложно управлять разными конфигурациями для dev/test/prod.
Гораздо лучше вынести значения в конфигурацию окружения:
// ХОРОШО - значения читаем из окружения
dbURL := os.Getenv("APP_DB_URL") // URL базы данных
logLevel := os.Getenv("APP_LOG_LEVEL") // Уровень логирования
Так вы можете запустить один и тот же бинарь:
- локально с
APP_DB_URL=postgres://localhost/...; - на тесте с
APP_DB_URL=postgres://test-db/...; - в проде с
APP_DB_URL=postgres://prod-db/....
Ключевые требования к environment-config
Хорошо организованная конфигурация окружения:
- Повторяема — вы можете быстро развернуть новое окружение (например, стенд для QA) по тем же правилам.
- Явна — понятно, какие переменные влияют на поведение приложения.
- Проверяется — при старте приложения можно валидировать конфиг и падать с понятной ошибкой, если чего-то не хватает.
- Безопасна — секреты не хранятся в Git, доступ к ним ограничен, логи не светят пароли.
- Удобна в разработке — локально легко запускаться без сложных танцев с настройкой окружения.
Основные подходы к конфигурации окружения
Переменные окружения как основной источник
Переменные окружения — базовый элемент environment-config. Они передаются процессу операционной системой и доступны из кода.
В Go их обычно читают так:
package config
import (
"log"
"os"
)
type Config struct {
DBURL string // строка подключения к БД
LogLevel string // уровень логирования
}
// Load загружает конфигурацию из переменных окружения
func Load() (*Config, error) {
cfg := &Config{}
// Здесь я читаю значение обязательной переменной
dbURL := os.Getenv("APP_DB_URL")
if dbURL == "" {
// Возвращаем ошибку - невозможно работать без строки подключения
return nil, fmt.Errorf("APP_DB_URL is required")
}
cfg.DBURL = dbURL
// А здесь задаю значение по умолчанию
logLevel := os.Getenv("APP_LOG_LEVEL")
if logLevel == "" {
log.Println("APP_LOG_LEVEL not set, using 'info' by default")
logLevel = "info"
}
cfg.LogLevel = logLevel
return cfg, nil
}
Давайте разберемся, что здесь важно:
- Для обязательных параметров вы валидируете наличие и возвращаете ошибку.
- Для необязательных задаете разумные значения по умолчанию.
Проблемы "чистых" переменных окружения
Если опираться только на окружающие переменные, довольно быстро возникают сложности:
- В проекте может быть десятки или сотни переменных окружения.
- Их трудно документировать и поддерживать в актуальном состоянии.
- Для локального запуска нужно каждый раз экспортировать кучу значений.
Поэтому часто переменные окружения комбинируют с конфигурационными файлами.
Конфигурационные файлы (.env, YAML, JSON)
Частая практика — хранить значения по умолчанию (или удобные значения для разработки) в файлах.
Самые популярные варианты:
.env— простые пары ключ‑значение;config.yml— структурированные настройки;config.jsonилиconfig.toml— тоже структурированные форматы.
Смотрите, я покажу, как может выглядеть .env:
# .env
APP_DB_URL=postgres://user:pass@localhost:5432/app
APP_LOG_LEVEL=debug
APP_HTTP_PORT=8080
И YAML‑конфиг:
# config.dev.yml
server:
port: 8080
read_timeout: 5s
db:
url: postgres://user:pass@localhost:5432/app
max_open_conns: 10
log:
level: debug
Часто делают гибрид:
- файл
.envили YAML определяет значения по умолчанию; - переменные окружения перекрывают эти значения при необходимости (например, в CI или проде).
Приоритеты источников конфигурации
Чтобы не запутаться, заранее определите приоритет источников конфигурации. Например:
- Значения по умолчанию в коде (hard defaults).
- Файл конфигурации (например,
config.dev.yml). - Переменные окружения (самый высокий приоритет).
Давайте посмотрим, как это может выглядеть на практике в Go с YAML‑конфигом:
type Config struct {
Server struct {
Port int `yaml:"port"`
} `yaml:"server"`
DB struct {
URL string `yaml:"url"`
} `yaml:"db"`
Log struct {
Level string `yaml:"level"`
} `yaml:"log"`
}
// loadFromFile загружает конфиг из YAML
func loadFromFile(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
// Здесь мы парсим YAML по структуре Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// overrideFromEnv перекрывает значения из файла переменными окружения
func overrideFromEnv(cfg *Config) {
// Если переменная установлена - переписываем значение
if v := os.Getenv("APP_SERVER_PORT"); v != "" {
// Здесь преобразуем строку в число
if port, err := strconv.Atoi(v); err == nil {
cfg.Server.Port = port
}
}
if v := os.Getenv("APP_DB_URL"); v != "" {
cfg.DB.URL = v
}
if v := os.Getenv("APP_LOG_LEVEL"); v != "" {
cfg.Log.Level = v
}
}
Теперь вы видите, как можно:
- описать структуру конфигурации один раз;
- подставлять значения из файла;
- при необходимости переопределять их через переменные окружения.
Организация конфигурации для разных окружений
Стратегия "один бинарь много окружений"
Один из здоровых принципов — собирать один и тот же бинарь для всех окружений, а поведение менять только через конфигурацию.
Типичный набор окружений:
local— локальная разработка;dev— стенд для команды разработки;stageилиqa— пред‑прод;prod— боевая среда.
Есть несколько популярных стратегий.
Стратегия 1: Один конфиг с профилями
Вы держите один config.yml, внутри которого несколько профилей:
# config.yml
local:
db:
url: postgres://local-user:local-pass@localhost:5432/app
log:
level: debug
dev:
db:
url: postgres://dev-user:dev-pass@dev-db:5432/app
log:
level: info
prod:
db:
url: postgres://prod-user:prod-pass@prod-db:5432/app
log:
level: warn
При старте приложения вы выбираете профиль по переменной окружения, например APP_ENV.
// LoadWithProfile загружает конфиг с учетом профиля окружения
func LoadWithProfile(path string, env string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Здесь описываем "обертку" для профилей
var raw map[string]Config
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, err
}
cfg, ok := raw[env]
if !ok {
return nil, fmt.Errorf("profile %s not found in config", env)
}
overrideFromEnv(&cfg)
return &cfg, nil
}
Плюсы:
- все профили в одном файле, удобно сравнивать;
- легко добавить новое окружение.
Минусы:
- риск утечки прод‑настроек (особенно секретов) в репозиторий;
- файл разрастается.
Стратегия 2: Отдельный файл на окружение
Вы держите несколько файлов:
config.local.ymlconfig.dev.ymlconfig.prod.yml
А файл выбираете по переменной APP_ENV:
func ConfigPathFromEnv(env string) string {
switch env {
case "local":
return "config.local.yml"
case "dev":
return "/etc/myapp/config.dev.yml"
case "prod":
return "/etc/myapp/config.prod.yml"
default:
// Значение по умолчанию для локальной разработки
return "config.local.yml"
}
}
Такой подход часто используют совместно с секрет‑менеджерами, где часть значений конфигурации хранится отдельно от репозитория.
Валидация конфигурации
Зачем валидировать конфиг при старте
Если конфигурация неверная, лучше узнать об этом как можно раньше — сразу при запуске, а не через час логов с ошибками подключения.
Примеры типичных проблем:
- не указаны обязательные переменные (
APP_DB_URL,APP_JWT_SECRET); - порт сервиса за пределами диапазона 1–65535;
timeoutменьше нуля;log_levelимеет невозможное значение.
Давайте сделаем простой валидатор:
type Config struct {
Server struct {
Port int `yaml:"port"`
} `yaml:"server"`
DB struct {
URL string `yaml:"url"`
} `yaml:"db"`
Log struct {
Level string `yaml:"level"`
} `yaml:"log"`
}
// Validate проверяет корректность конфигурации
func (c *Config) Validate() error {
// Проверяем порт
if c.Server.Port <= 0 || c.Server.Port > 65535 {
return fmt.Errorf("server.port must be between 1 and 65535, got %d", c.Server.Port)
}
// Проверяем наличие URL базы данных
if strings.TrimSpace(c.DB.URL) == "" {
return fmt.Errorf("db.url is required")
}
// Проверяем уровень логирования
switch c.Log.Level {
case "debug", "info", "warn", "error":
// допустимые значения
default:
return fmt.Errorf("log.level must be one of debug info warn error, got %s", c.Log.Level)
}
return nil
}
При старте приложения вы делаете:
cfg, err := LoadWithProfile("config.yml", os.Getenv("APP_ENV"))
if err != nil {
// Здесь выведем ошибку и завершим процесс
log.Fatalf("failed to load config: %v", err)
}
if err := cfg.Validate(); err != nil {
// Здесь тоже завершаем процесс - некорректная конфигурация
log.Fatalf("invalid config: %v", err)
}
Как видите, приложение не перейдет в "полурабочее" состояние с кривыми настройками.
Секреты и безопасное хранение конфигурации
Что считать секретом
Секреты — это значения, утечка которых нанесет прямой ущерб:
- пароли к базам данных;
- API‑ключи к платежным системам, внешним сервисам;
- приватные ключи JWT;
- токены доступа к S3/облакам.
Не стоит хранить секреты:
- в репозитории;
- в логах;
- в артефактах сборки, если к ним есть лишний доступ.
Подходы к хранению секретов
Смотрите, здесь несколько реалистичных вариантов:
Переменные окружения
Простые и привычные, но их нужно аккуратно прокидывать через CI/CD и не логировать.Секрет‑менеджеры
Например, HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager.
В этом случае приложение:- при старте читает конфиг (без секретов);
- затем обращается к секрет‑менеджеру за токенами/паролями.
Менеджеры секретов в Kubernetes
Secret‑объекты в Kubernetes монтируются в контейнер как файл или переменные окружения.
Минимальный безопасный вариант для небольших проектов:
- значения по умолчанию (безопасные) — в конфиге;
- чувствительные данные — только в переменных окружения;
- в
.env.exampleхраните только "заглушки" без реальных секретов.
Пример:
# .env.example
APP_DB_URL=postgres://USER:PASSWORD@HOST:PORT/DBNAME
APP_JWT_SECRET=changeme
.env.example можно и нужно коммитить, а .env с реальными значениями — нет.
Конфигурация окружения и 12‑factor app
Манифест 12‑factor app предлагает простой принцип: "конфигурация должна храниться в окружении".
Это означает:
- никакой разницы в коде между dev/test/production;
- все различия описываются переменными окружения или тем, что поверх них;
- зависимости от внешних сервисов описываются URL/ключами в конфиге.
На практике это часто выглядит так:
- все основные настройки — через переменные окружения;
- файлы конфигурации используются только как слой "удобства" для локальной разработки;
- деплой (Docker, Kubernetes, Heroku и т.п.) отвечает за установку переменных окружения.
Конфигурация окружения в Docker и Kubernetes
Docker
В Docker переменные окружения можно задавать:
- через аргумент
-eдляdocker run; - через файл с переменными (
--env-file); - через
docker-compose.yml.
Пример docker-compose.yml:
version: "3.8"
services:
app:
image: myapp:latest
env_file:
- .env # Здесь мы подключаем локальный файл с переменными
ports:
- "8080:8080"
В .env вы описываете:
APP_ENV=dev
APP_DB_URL=postgres://user:pass@db:5432/app
APP_LOG_LEVEL=debug
Приложение при этом читает обычные переменные окружения, код мы не меняем.
Kubernetes
В Kubernetes переменные окружения задают в манифестах Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
template:
spec:
containers:
- name: myapp
image: myorg/myapp:latest
env:
- name: APP_ENV
value: prod
- name: APP_LOG_LEVEL
value: info
- name: APP_DB_URL
valueFrom:
secretKeyRef:
name: myapp-db-secret
key: url
Обратите внимание:
APP_ENVиAPP_LOG_LEVELзадаются прямо в манифесте;APP_DB_URLберется изSecret‑объектаmyapp-db-secret.
Так вы разделяете:
- обычные настройки (могут быть в Git);
- чувствительные данные (живут в Kubernetes Secrets или в внешнем секрет‑менеджере).
Структурирование конфигурации внутри приложения
Модульность конфигурации
Вместо одной гигантской структуры лучше разделить конфиг на логические блоки:
type ServerConfig struct {
Port int `yaml:"port"`
ReadTimeout time.Duration `yaml:"read_timeout"`
WriteTimeout time.Duration `yaml:"write_timeout"`
}
type DBConfig struct {
URL string `yaml:"url"`
MaxOpenConns int `yaml:"max_open_conns"`
}
type LogConfig struct {
Level string `yaml:"level"`
}
type Config struct {
Server ServerConfig `yaml:"server"`
DB DBConfig `yaml:"db"`
Log LogConfig `yaml:"log"`
}
Теперь давайте посмотрим, как использовать это в коде:
func NewServer(cfg ServerConfig, handler http.Handler) *http.Server {
// Здесь создаем HTTP сервер с параметрами из конфигурации
return &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: handler,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
}
}
Такой подход дает:
- более явные зависимости;
- удобство тестирования (можно создать
ServerConfigруками); - меньшую связанность модулей.
Примеры организации environment-config в разных языках
Пример на Node.js с dotenv
В экосистеме Node.js часто используют пакет dotenv, который загружает переменные из .env файла.
// config.js
require('dotenv').config() // Здесь мы загружаем переменные из .env
// Здесь описываем объект конфигурации
const config = {
env: process.env.APP_ENV || 'local',
server: {
port: parseInt(process.env.APP_PORT || '3000', 10),
},
db: {
url: process.env.APP_DB_URL, // Обязательное значение
},
log: {
level: process.env.APP_LOG_LEVEL || 'info',
},
}
// Простая валидация обязательных полей
if (!config.db.url) {
throw new Error('APP_DB_URL is required')
}
module.exports = config
В index.js вы можете написать:
const config = require('./config')
// Здесь запускаем сервер, используя порт из конфигурации
app.listen(config.server.port, () => {
console.log(`Server running on port ${config.server.port}`)
})
Пример на Python с pydantic / environs
В Python популярно описывать конфигурацию через модели.
from pydantic import BaseSettings, AnyHttpUrl
class Settings(BaseSettings):
app_env: str = "local" # Значение по умолчанию
app_port: int = 8000 # Значение по умолчанию
app_db_url: AnyHttpUrl # Обязательное поле без значения по умолчанию
app_log_level: str = "info"
class Config:
env_file = ".env" # Здесь указываем файл с переменными
env_prefix = "APP_" # Все переменные начинаются с APP_
# Здесь создаем глобальный объект настроек
settings = Settings()
Покажу вам, как это выглядит в использовании:
from fastapi import FastAPI
from config import settings
app = FastAPI()
@app.get("/health")
def health():
# Здесь для примера вернем часть конфигурации
return {
"env": settings.app_env,
"log_level": settings.app_log_level,
}
Если обязательная переменная APP_DB_URL не установлена, pydantic выбросит ошибку при создании Settings.
Типичные ошибки при работе с environment-config
Смешивание конфигурации и кода
Частая ошибка — часть настроек выносится в конфиг, а часть остается в коде "временно". Со временем это "временно" превращается в хаос.
Рекомендуемый подход:
- все, что может меняться между окружениями — в конфиг;
- все, что фундаментально для логики и не зависит от среды — в коде.
Секреты в репозитории
Еще одна типичная проблема — файл config.prod.yml с реальными паролями оказывается в Git.
Чтобы этого избежать:
- добавьте секретные файлы в
.gitignore; - используйте
.env.exampleс "пустыми" значениями; - заранее договоритесь в команде, где и как хранятся реальные секреты.
Отсутствие прозрачной документации
Разработчики часто спрашивают: "какие у нас вообще есть переменные окружения?"
Чтобы это не превращалось в устные легенды:
- заведите таблицу конфигурации (в README или отдельном файле);
- генерируйте документацию из структуры
Configи комментариев; - или хотя бы поддерживайте
.env.exampleв актуальном состоянии.
Пример простой текстовой документации:
APP_ENV окружение приложения - local dev stage prod
APP_DB_URL строка подключения к базе данных - обязательно
APP_LOG_LEVEL уровень логирования - debug info warn error - по умолчанию info
APP_HTTP_PORT порт HTTP сервера - по умолчанию 8080
Заключение
Конфигурация окружения — это не просто "пара переменных", а полноценный слой архитектуры приложения.
Хорошо продуманная environment-config дает вам:
- один бинарь на все окружения;
- предсказуемое поведение при деплое;
- явные зависимости от внешних сервисов;
- безопасную работу с секретами;
- удобную локальную разработку.
Если обобщить практические рекомендации:
- Определите, что именно должно настраиваться через конфигурацию, а что остается жестко в коде.
- Выберите формат конфигурации (переменные окружения + файл, YAML/JSON/TOML — не принципиально, важна последовательность).
- Введите единый приоритет источников значений (значения по умолчанию → файл → переменные окружения).
- Реализуйте валидацию конфигурации при старте и явно падайте при ошибках.
- Отделите секреты от обычных настроек и не храните их в репозитории.
- Документируйте переменные окружения и профили окружений.
Теперь, когда общая картинка понятна, можно постепенно усложнять подход: добавлять секрет‑менеджеры, динамически обновляемые конфиги, переиспользуемые модули конфигурации между сервисами и так далее. Важно, что базовые принципы остаются теми же — код описывает логику, конфигурация описывает контекст.
Частозадаваемые технические вопросы и ответы
1. Как подгружать конфиг динамически без рестарта приложения
Используйте файловый вотчер (например, fsnotify в Go) или механизм перезагрузки конфигурации фреймворка.
Алгоритм:
- Запускаете вотчер на конфигурационный файл.
- При изменении файла перечитываете его и валидируете.
- Обновляете конфиг в памяти атомарно (через мьютекс или
atomic.Value). - Компоненты приложения берут конфиг из общего хранилища при каждом использовании или при событии обновления.
Важно иметь fallback — если новый конфиг невалиден, продолжаете работать со старым.
2. Как разделить конфиг между несколькими микросервисами чтобы не дублировать
Подход:
- Выделите общий модуль конфигурации (например, общую библиотеку или пакет).
- В нем опишите общие структуры (например, для логирования, трассировки, подключения к шине событий).
- В каждом сервисе расширяйте конфиг своими полями.
- Общий модуль можно версионировать отдельно и обновлять по мере необходимости.
Так вы избегаете копирований и расхождений форматов.
3. Как тестировать код который зависит от конфигурации окружения
Лучше не полагаться на реальные переменные окружения в тестах:
- Оборачивайте конфигурацию в структуры и интерфейсы.
- В тестах создавайте
Configявно, без чтения из окружения. - Логику загрузки из окружения тестируйте отдельно, модифицируя
os.Environили аналоги, а после теста восстанавливайте состояние.
Так тесты станут предсказуемыми и независимыми от машины разработчика.
4. Что делать если один параметр зависит от другого (например таймауты)
Лучше:
- Хранить значения в человекочитаемом виде (например, строки с суффиксами
5s,500ms). - При загрузке конфига парсить значения в типизированные поля.
- Валидацию зависимостей делать в одном месте — в методе
Validate, где можно сравнить разные значения между собой и выдать осмысленную ошибку.
Это защищает от противоречивых настроек.
5. Как безопасно логировать конфиг при старте приложения
Иногда нужно увидеть, с какими настройками запущено приложение. Делайте так:
- Явно помечайте поля конфига как "секретные" (например, через отдельные структуры или теги).
- Пишите функцию "redact", которая перед логированием заменяет секреты на маски (
******). - Логируйте только "очищенную" версию конфига.
- Для особо чувствительных сред (prod) можно вообще отключить логирование конфигурации флагом.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев