Минимизация различий между development, staging и production
Минимизируйте различия между development и production
Dev/Prod Parity — десятый фактор 12-Factor App. Принцип гласит:
Среда разработки (development), тестирования (staging) и production должны быть максимально похожи по времени, персоналу и инструментам.
┌─────────────────────────────────────────────────────────────────┐
│ Идеальный паритет │
│ │
│ Development ────────▶ Staging ────────▶ Production │
│ (локально) (тестирование) (пользователи) │
│ │
│ Одинаковые: │
│ • База данных (версия, конфигурация) │
│ • Внешние сервисы (кэш, очереди) │
│ • Переменные окружения (структура) │
│ • Процесс развёртывания │
│ • Инструменты мониторинга │
└─────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Разрыв во времени между деплоем в dev и prod │
│ │
│ ❌ Плохо: │
│ Dev: деплой каждый день │
│ Prod: деплой раз в месяц (накопление изменений) │
│ │
│ ✅ Хорошо: │
│ Dev: деплой каждый день │
│ Prod: деплой каждый день (непрерывная доставка) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Разрыв в персонале между разработкой и эксплуатацией │
│ │
│ ❌ Плохо: │
│ Разработчики пишут код │
│ Операторы развёртывают и поддерживают │
│ (разработчики не видят проблем production) │
│ │
│ ✅ Хорошо: │
│ Разработчики участвуют в развёртывании │
│ On-call rotation для разработчиков │
│ (разработчики чувствуют pain production) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Разрыв в инструментах между средами │
│ │
│ ❌ Плохо: │
│ Dev: SQLite, локальные файлы, ручные скрипты │
│ Prod: PostgreSQL, S3, автоматизированный CI/CD │
│ │
│ ✅ Хорошо: │
│ Dev: PostgreSQL (Docker), S3 (MinIO), CI/CD пайплайн │
│ Prod: PostgreSQL, S3, CI/CD пайплайн │
└──────────────────────────────────────────────────────────────┘
# Development
# .env.dev
DATABASE_URL=sqlite:///dev.db # SQLite
# Production
# .env.prod
DATABASE_URL=postgresql://user:pass@prod-db:5432/app # PostgreSQLПроблемы:
# Development
# .env.dev
DATABASE_URL=postgresql://dev:dev@localhost:5432/dev_db
# Production
# .env.prod
DATABASE_URL=postgresql://prod:pass@prod-db:5432/app# docker-compose.yml для локальной разработки
version: '3.8'
services:
db:
image: postgres:15 # Та же версия, что в production
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: dev_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:# docker-compose.yml
services:
redis:
image: redis:7-alpine # Та же версия, что в production
ports:
- "6379:6379"
command: redis-server --appendonly yes # AOF persistence# Одинаковая конфигурация для всех сред
import os
REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')# docker-compose.yml
services:
rabbitmq:
image: rabbitmq:3-management # С веб-интерфейсом
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest# .env.example (шаблон для всех сред)
# Обязательные переменные
DATABASE_URL=postgresql://user:pass@host:5432/db
REDIS_URL=redis://host:6379/0
SECRET_KEY=change-me-in-production
# Опциональные с дефолтами
DEBUG=false
LOG_LEVEL=INFO
MAX_UPLOAD_SIZE=10485760
# Внешние сервисы
STRIPE_KEY=sk_test_...
SENTRY_DSN=https://...@sentry.io/...# .env.dev (development)
DATABASE_URL=postgresql://dev:dev@localhost:5432/dev_db
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=dev-secret-key-not-for-production
DEBUG=true
LOG_LEVEL=DEBUG
STRIPE_KEY=sk_test_...
SENTRY_DSN= # Пусто, не отправлять ошибки в Sentry
# .env.staging
DATABASE_URL=postgresql://staging:pass@staging-db:5432/app
REDIS_URL=redis://staging-redis:6379/0
SECRET_KEY=<staging-secret>
DEBUG=false
LOG_LEVEL=INFO
STRIPE_KEY=sk_test_...
SENTRY_DSN=https://...@sentry.io/...
# .env.prod (production)
DATABASE_URL=postgresql://prod:pass@prod-db:5432/app
REDIS_URL=redis://prod-redis:6379/0
SECRET_KEY=<production-secret>
DEBUG=false
LOG_LEVEL=WARNING
STRIPE_KEY=sk_live_...
SENTRY_DSN=https://...@sentry.io/...# Development
# Ручное развёртывание
git pull
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
# Production
# Автоматизированное развёртывание
# CI/CD пайплайн# .gitlab-ci.yml (единый пайплайн для всех сред)
stages:
- test
- build
- deploy
test:
stage: test
script:
- pytest
- flake8
build:
stage: build
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push registry/myapp:$CI_COMMIT_SHA
deploy-dev:
stage: deploy
script:
- kubectl set image deployment/myapp myapp=registry/myapp:$CI_COMMIT_SHA
environment:
name: development
when: manual # Ручное подтверждение
deploy-staging:
stage: deploy
script:
- kubectl set image deployment/myapp-staging myapp=registry/myapp:$CI_COMMIT_SHA
environment:
name: staging
when: manual
deploy-prod:
stage: deploy
script:
- kubectl set image deployment/myapp-prod myapp=registry/myapp:$CI_COMMIT_SHA
environment:
name: production
when: manual
needs:
- deploy-staging# Dockerfile
FROM python:3.12-slim as base
WORKDIR /app
# Установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование кода
COPY . .
# Сборка (если нужна)
RUN python manage.py collectstatic --noinput
# Runtime
FROM base as runtime
USER nobody
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]# docker-compose.yml для development
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- DEBUG=true
- DATABASE_URL=postgresql://dev:dev@db:5432/dev_db
- REDIS_URL=redis://redis:6379/0
volumes:
- .:/app # Hot reload
depends_on:
- db
- redis
db:
image: postgres:15
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: dev_db
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:# docker-compose.prod.yml
version: '3.8'
services:
web:
image: registry/myapp:latest # Готовый образ
ports:
- "5000:5000"
environment:
- DEBUG=false
- DATABASE_URL=postgresql://prod:pass@db:5432/app
- REDIS_URL=redis://redis:6379/0
restart: unless-stopped
depends_on:
- db
- redis
db:
image: postgres:15
environment:
POSTGRES_USER: prod
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
resources:
limits:
memory: 2G
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
deploy:
resources:
limits:
memory: 512M
volumes:
postgres_data:
redis_data:# manage.py
#!/usr/bin/env python
import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)# Одинаковые команды для всех сред
# Development
docker-compose exec web python manage.py migrate
# Staging
kubectl exec -it deployment/myapp-staging -- python manage.py migrate
# Production
kubectl exec -it deployment/myapp-prod -- python manage.py migrate# alembic/env.py
from alembic import context
from sqlalchemy import engine_from_config
import os
# Конфигурация из переменных окружения
config = context.config
config.set_main_option(
'sqlalchemy.url',
os.environ['DATABASE_URL']
)
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.'
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
run_migrations_online()# Применение миграций
# Development
alembic upgrade head
# Production (в CI/CD)
alembic upgrade head# docker-compose.yml (development с мониторингом)
services:
web:
build: .
environment:
- SENTRY_DSN=https://dev@ingest.sentry.io/...
- PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin# logging_config.py
import os
import logging
import json
class JSONFormatter(logging.Formatter):
"""JSON формат для структурированного логирования"""
def format(self, record):
log_entry = {
'timestamp': self.formatTime(record),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno
}
if record.exc_info:
log_entry['exception'] = self.formatException(record.exc_info)
return json.dumps(log_entry)
def setup_logging():
log_level = os.environ.get('LOG_LEVEL', 'INFO')
log_format = os.environ.get('LOG_FORMAT', 'text')
logger = logging.getLogger()
logger.setLevel(log_level)
handler = logging.StreamHandler()
if log_format == 'json':
handler.setFormatter(JSONFormatter())
else:
handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
logger.addHandler(handler)# Development
LOG_FORMAT=text
LOG_LEVEL=DEBUG
# Production
LOG_FORMAT=json
LOG_LEVEL=WARNING# ❌ Неправильно: mock Redis в development
class MockRedis:
def get(self, key):
return None
def set(self, key, value):
pass
# Development использует mock
# Production использует реальный Redis
# Ошибки кэширования проявляются только в productionРешение: Использовать реальный Redis в development через Docker.
# ❌ Неправильно: разные процессы
# Development
git pull && python manage.py migrate && python manage.py runserver
# Production
# 1. Создать бэкап БД
# 2. Остановить приложение
# 3. Применить миграции
# 4. Запустить приложение
# 5. Проверить логи
# 6. Отправить email командеРешение: Автоматизировать production deployment через CI/CD.
# ❌ Неправильно: hardcoded пути
if os.environ.get('ENV') == 'production':
STATIC_ROOT = '/var/www/static'
DATABASE = 'postgresql://prod-db/app'
else:
STATIC_ROOT = './static'
DATABASE = 'sqlite:///dev.db'Решение:
# ✅ Правильно: переменные окружения
STATIC_ROOT = os.environ['STATIC_ROOT']
DATABASE_URL = os.environ['DATABASE_URL']Задайте себе вопросы:
Development:
• SQLite вместо PostgreSQL
• Локальные файлы вместо S3
• Нет Redis (кэш отключён)
• Ручное развёртывание
• DEBUG=True всегда
• Нет мониторинга
Production:
• PostgreSQL кластер
• Amazon S3
• Redis кластер
• Автоматизированное развёртывание
• DEBUG=False
• Полный стек мониторинга
Результат:
• Ошибки БД проявляются в production
• Проблемы кэширования только в production
• Разработчики не понимают production issues
# docker-compose.yml — полная копия production локально
services:
web:
build: .
environment:
- DATABASE_URL=postgresql://dev:dev@db:5432/dev_db
- REDIS_URL=redis://redis:6379/0
- AWS_S3_ENDPOINT_URL=http://minio:9000 # S3-compatible
- DEBUG=true
- LOG_LEVEL=DEBUG
- SENTRY_DSN=https://dev@ingest.sentry.io/...
db:
image: postgres:15
# Та же версия, что в production
redis:
image: redis:7-alpine
# Та же версия, что в production
minio:
image: minio/minio
# S3-compatible storage локально
prometheus:
image: prom/prometheus
# Мониторинг как в production
grafana:
image: grafana/grafana
# Дашборды как в production# Одинаковый процесс развёртывания
# CI/CD пайплайн для всех сред
# 1. Тесты
pytest
# 2. Сборка образа
docker build -t myapp:$COMMIT_SHA .
# 3. Деплой
kubectl set image deployment/myapp myapp=myapp:$COMMIT_SHA
# 4. Миграции
kubectl exec deployment/myapp -- python manage.py migrate
# 5. Health check
curl https://myapp.example.com/healthРезультат:
Ключевой вывод: Минимизируйте различия между development, staging и production по времени, персоналу и инструментам. Используйте Docker для локального запуска тех же сервисов, что в production, и автоматизируйте процесс развёртывания для всех сред.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.