Хранение конфигурации в переменных окружения, разделение кода и конфига
Храните конфигурацию в переменных окружения
Config — третий фактор 12-Factor App. Принцип гласит:
Конфигурация (пароли, домены, настройки) должна храниться в переменных окружения, а не в коде приложения.
# ❌ Неправильно: конфигурация в коде
DATABASE_URL = 'postgres://admin:password123@prod-db.example.com:5432/myapp'
SECRET_KEY = 'super-secret-key-12345'
DEBUG = False
# ✅ Правильно: конфигурация из переменных окружения
import os
DATABASE_URL = os.environ['DATABASE_URL']
SECRET_KEY = os.environ['SECRET_KEY']
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'Конфигурация — это всё, что может измениться между развёртываниями без изменения кода:
| Тип | Примеры |
|---|---|
| Учётные данные | Пароли БД, API-ключи, секретные токены |
| Хосты и порты | DATABASE_URL, REDIS_HOST, SMTP_PORT |
| Флаги функций | ENABLE_NEW_UI, DEBUG_MODE, MAINTENANCE_MODE |
| Внешние сервисы | SENTRY_DSN, STRIPE_KEY, AWS_BUCKET |
| Настройки приложения | MAX_UPLOAD_SIZE, CACHE_TTL, LOG_LEVEL |
# config.py
# ❌ Пароль БД в коде
DB_PASSWORD = 'SuperSecret123!'
# ❌ API-ключ в коде
STRIPE_API_KEY = 'sk_live_abc123xyz'
# ❌ Внутренний домен в коде
INTERNAL_API = 'http://192.168.1.100:8080'Проблемы:
# config.py
import os
# ✅ Пароль из переменной окружения
DB_PASSWORD = os.environ['DB_PASSWORD']
# ✅ API-ключ из переменной окружения
STRIPE_API_KEY = os.environ['STRIPE_API_KEY']
# ✅ Домен из переменной окружения
INTERNAL_API = os.environ['INTERNAL_API']# Установка переменных окружения
export DB_PASSWORD='SuperSecret123!'
export STRIPE_API_KEY='sk_live_abc123xyz'
export INTERNAL_API='http://192.168.1.100:8080'
python app.pyПреимущества:
# Установка переменной
export DATABASE_URL=postgres://user:pass@localhost:5432/db
# Чтение переменной
echo $DATABASE_URL
# Запуск приложения с переменной
DATABASE_URL=postgres://... python app.pyimport os
# Обязательная переменная (выбросит ошибку, если не установлена)
database_url = os.environ['DATABASE_URL']
# Необязательная переменная со значением по умолчанию
debug_mode = os.environ.get('DEBUG', 'False')
log_level = os.environ.get('LOG_LEVEL', 'INFO')// Обязательная переменная
const databaseUrl = process.env.DATABASE_URL;
// Необязательная со значением по умолчанию
const debugMode = process.env.DEBUG || 'false';
const logLevel = process.env.LOG_LEVEL || 'info';# Обязательная переменная
database_url = ENV['DATABASE_URL']
# Необязательная со значением по умолчанию
debug_mode = ENV.fetch('DEBUG', 'false')
log_level = ENV.fetch('LOG_LEVEL', 'info')import "os"
// Обязательная переменная
databaseURL := os.Getenv("DATABASE_URL")
// Необязательная со значением по умолчанию
debugMode := os.Getenv("DEBUG")
if debugMode == "" {
debugMode = "false"
}export DATABASE_URL=postgres://user:pass@host:5432/db
export REDIS_URL=redis://localhost:6379/0
export SMTP_HOST=smtp.example.comexport PORT=5000
export MAX_CONNECTIONS=100
export CACHE_TTL=3600
export RATE_LIMIT=1000# Преобразование в число
port = int(os.environ.get('PORT', 5000))
max_connections = int(os.environ.get('MAX_CONNECTIONS', 100))export DEBUG=true
export ENABLE_FEATURE_X=false
export MAINTENANCE_MODE=1# Преобразование в boolean
def parse_bool(value, default=False):
if value is None:
return default
return value.lower() in ('true', '1', 'yes', 'on')
debug = parse_bool(os.environ.get('DEBUG'), default=False)export ALLOWED_HOSTS=example.com,www.example.com,api.example.com
export CORS_ORIGINS=https://app.com,https://admin.com# Преобразование в список
allowed_hosts = os.environ.get('ALLOWED_HOSTS', '').split(',')
cors_origins = os.environ.get('CORS_ORIGINS', '').split(',')# ❌ Утомительно вводить каждый раз
export DATABASE_URL=postgres://localhost/myapp
export REDIS_URL=redis://localhost:6379
export DEBUG=true
python app.py# .env (локальный файл, не коммитить в Git!)
DATABASE_URL=postgres://localhost/myapp
REDIS_URL=redis://localhost:6379
DEBUG=true
SECRET_KEY=dev-secret-key# Автоматическая загрузка .env
from dotenv import load_dotenv
load_dotenv() # Загружает переменные из .env
import os
database_url = os.environ['DATABASE_URL']# .env.example (можно коммитить в Git)
# Шаблон для разработчиков
DATABASE_URL=postgres://localhost/myapp
REDIS_URL=redis://localhost:6379
DEBUG=true
SECRET_KEY=change-me-in-production
API_KEY=your-api-key-hereРазработчики копируют:
cp .env.example .env
# Редактируют .env своими значениями# .gitignore
.env
.env.local
.env.*.local# .env.dev
DATABASE_URL=postgres://localhost:5432/dev_db
DEBUG=true
LOG_LEVEL=DEBUG
SECRET_KEY=dev-secret-key
EMAIL_BACKEND=console# Переменные окружения на staging-сервере
DATABASE_URL=postgres://staging-db:5432/app
DEBUG=false
LOG_LEVEL=INFO
SECRET_KEY=<staging-secret>
EMAIL_BACKEND=smtp
SMTP_HOST=smtp.staging.example.com# Переменные окружения в production
DATABASE_URL=postgres://prod-db-cluster:5432/app
DEBUG=false
LOG_LEVEL=WARNING
SECRET_KEY=<production-secret>
EMAIL_BACKEND=smtp
SMTP_HOST=smtp.example.com
SENTRY_DSN=https://...@sentry.io/...ps aux и /proc/[pid]/environimport hvac
client = hvac.Client(url='http://vault:8200', token='my-token')
secret = client.secrets.kv.v2.read_secret_version('myapp/database')
db_password = secret['data']['data']['password']import boto3
client = boto3.client('secretsmanager')
secret = client.get_secret_value(SecretId='myapp/database')
db_password = secret['SecretString']from azure.keyvault.secrets import SecretClient
client = SecretClient(vault_url="https://myvault.vault.azure.net/", credential)
db_password = client.get_secret("database-password").value# Установка Doppler CLI
doppler setup
# Запуск приложения с секретами из Doppler
doppler run -- python app.py# Dockerfile
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Переменные не указываются в Dockerfile!
# Они передаются при запуске
CMD ["gunicorn", "app:app"]# Запуск с переменными окружения
docker run -d \
-e DATABASE_URL=postgres://user:pass@db:5432/app \
-e SECRET_KEY=my-secret-key \
-e DEBUG=false \
-p 5000:5000 \
myapp# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- DATABASE_URL=postgres://db:5432/app
- REDIS_URL=redis://redis:6379
- DEBUG=true
env_file:
- .env.local # Дополнительные переменные из файла
depends_on:
- db
- redis
db:
image: postgres:15
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
redis:
image: redis:7# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
DEBUG: "false"
LOG_LEVEL: "INFO"
MAX_CONNECTIONS: "100"# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-secret
type: Opaque
stringData:
DATABASE_URL: postgres://user:pass@db:5432/app
SECRET_KEY: super-secret-key# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: web
image: myapp:latest
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secret# ❌ Неправильно
class Config:
DATABASE_URL = 'postgres://admin:password@prod-db/app'
SECRET_KEY = 'hardcoded-secret'
DEBUG = FalseПроблема: секреты в коде, невозможно менять без изменения кода.
# ❌ Неправильно: чтение конфига из БД
config = db.query("SELECT * FROM config WHERE key = 'database_url'")Проблема: курица и яйцо — для подключения к БД нужна конфигурация БД.
# ❌ Неправильно: секреты в файле
import json
with open('config.json') as f:
config = json.load(f)
db_password = config['database']['password']Проблема: файл с секретами может попасть в Git, сложно управлять для разных сред.
# ❌ Неправильно: пароль виден в ps aux
python app.py --db-password SuperSecret123Проблема: аргументы командной строки видны всем пользователям системы через ps.
import os
from typing import Optional
class Config:
def __init__(self):
# Обязательные переменные
self.database_url = self._require_env('DATABASE_URL')
self.secret_key = self._require_env('SECRET_KEY')
# Необязательные с дефолтами
self.debug = self._get_bool('DEBUG', default=False)
self.log_level = os.environ.get('LOG_LEVEL', 'INFO')
self.port = int(os.environ.get('PORT', 5000))
def _require_env(self, name: str) -> str:
value = os.environ.get(name)
if not value:
raise ValueError(f"Required environment variable {name} is not set")
return value
def _get_bool(self, name: str, default: bool = False) -> bool:
value = os.environ.get(name, str(default))
return value.lower() in ('true', '1', 'yes', 'on')
# Использование
config = Config() # Выбросит ошибку, если нет обязательных переменныхfrom pydantic import BaseSettings, Field, validator
class Settings(BaseSettings):
database_url: str = Field(..., env='DATABASE_URL')
secret_key: str = Field(..., env='SECRET_KEY')
debug: bool = Field(default=False, env='DEBUG')
log_level: str = Field(default='INFO', env='LOG_LEVEL')
port: int = Field(default=5000, env='PORT')
@validator('log_level')
def validate_log_level(cls, v):
allowed = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if v.upper() not in allowed:
raise ValueError(f'log_level must be one of {allowed}')
return v.upper()
class Config:
env_file = '.env'
env_file_encoding = 'utf-8'
# Использование
settings = Settings() # Автоматическая валидацияЗадайте себе вопросы:
.env.example для документации переменных?.env в .gitignore?# config.py
DATABASE = {
'host': 'prod-db.example.com',
'user': 'admin',
'password': 'SuperSecret123!', # ❌
'database': 'myapp'
}
STRIPE_KEY = 'sk_live_abc123' # ❌Проблемы:
# config.py
import os
DATABASE = {
'host': os.environ['DB_HOST'],
'user': os.environ['DB_USER'],
'password': os.environ['DB_PASSWORD'],
'database': os.environ['DB_NAME']
}
STRIPE_KEY = os.environ['STRIPE_KEY']# Production
export DB_HOST=prod-db.example.com
export DB_USER=admin
export DB_PASSWORD=<secret>
export STRIPE_KEY=<secret>
# Development
export DB_HOST=localhost
export DB_USER=dev
export DB_PASSWORD=dev
export STRIPE_KEY=sk_test_...Результат:
Ключевой вывод: Конфигурация должна храниться в переменных окружения, а не в коде. Это обеспечивает безопасность, переносимость и возможность развёртывания одного артефакта в разных средах без изменения кода.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.