Административные задачи как одноразовые процессы
Запускайте административные задачи как одноразовые процессы
Admin Processes — двенадцатый фактор 12-Factor App. Принцип гласит:
Административные задачи (миграции БД, консольные команды, скрипты) должны запускаться как одноразовые процессы в том же окружении, что и приложение.**
┌─────────────────────────────────────────────────────────────────┐
│ Административные процессы │
│ │
│ ┌─────────────────┐ │
│ │ Долгосрочные │ Веб-процессы, воркеры │
│ │ процессы │ (работают постоянно) │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ Одноразовые │ Миграции, консоль, скрипты │
│ │ процессы │ (запускаются по необходимости) │
│ └─────────────────┘ │
│ │
│ Оба типа используют одинаковое окружение и код │
└─────────────────────────────────────────────────────────────────┘
# Применение миграций
python manage.py migrate
alembic upgrade head
rails db:migrate# Django shell
python manage.py shell
# Создание суперпользователя
python manage.py createsuperuser
# Сборка статики
python manage.py collectstatic# Импорт данных
python scripts/import_data.py
# Очистка кэша
python scripts/clear_cache.py
# Отчёт за период
python scripts/generate_report.py --from 2024-01-01 --to 2024-01-31# Бэкап базы данных
pg_dump $DATABASE_URL > backup.sql
# Восстановление из бэкапа
psql $DATABASE_URL < backup.sql
# Очистка старых данных
python scripts/cleanup_old_data.py --days 365# Миграции локально
docker-compose exec web python manage.py migrate
# Консоль Django
docker-compose exec web python manage.py shell
# Скрипт
docker-compose exec web python scripts/import_data.py# Миграции в Kubernetes
kubectl exec -it deployment/myapp -- python manage.py migrate
# Консоль в поде
kubectl exec -it deployment/myapp -- python manage.py shell
# Запуск скрипта
kubectl exec -it deployment/myapp -- python scripts/cleanup.py# Миграции
docker run --rm \
--env-file .env.prod \
--network app-network \
myapp:latest \
python manage.py migrate
# Консоль
docker run -it --rm \
--env-file .env.prod \
--network app-network \
myapp:latest \
python manage.py shell
# Скрипт
docker run --rm \
--env-file .env.prod \
myapp:latest \
python scripts/generate_report.py# .gitlab-ci.yml
stages:
- test
- build
- migrate
- deploy
migrate:
stage: migrate
image: registry/myapp:$CI_COMMIT_SHA
script:
- python manage.py migrate
environment:
name: production
when: manual # Ручное подтверждение перед деплоем
only:
- main
deploy:
stage: deploy
image: bitnami/kubectl
script:
- kubectl set image deployment/myapp myapp=registry/myapp:$CI_COMMIT_SHA
environment:
name: production
needs:
- migrate# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
- name: Run migrations
run: |
docker run --rm \
-e DATABASE_URL=${{ secrets.DATABASE_URL }} \
registry/myapp:${{ github.sha }} \
python manage.py migrate
deploy:
runs-on: ubuntu-latest
environment: production
needs: migrate
steps:
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/myapp myapp=registry/myapp:${{ github.sha }}# migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
template:
spec:
containers:
- name: migration
image: registry/myapp:latest
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
restartPolicy: OnFailure
backoffLimit: 3 # Количество повторных попыток# Обновление deployment после миграции
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
initContainers:
- name: wait-for-migration
image: busybox
command: ['sh', '-c', 'until [ -f /migration-done ]; do sleep 1; done']
volumeMounts:
- name: migration-status
mountPath: /migration-status
containers:
- name: web
image: registry/myapp:latest# cleanup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: data-cleanup
spec:
schedule: "0 2 * * *" # Каждый день в 2:00
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanup
image: registry/myapp:latest
command: ["python", "scripts/cleanup_old_data.py", "--days", "365"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
restartPolicy: OnFailure# management/commands/create_admin.py
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
User = get_user_model()
class Command(BaseCommand):
help = 'Создаёт администратора'
def add_arguments(self, parser):
parser.add_argument('--email', type=str, required=True)
parser.add_argument('--password', type=str, required=True)
def handle(self, *args, **options):
email = options['email']
password = options['password']
if User.objects.filter(email=email).exists():
self.stdout.write(
self.style.WARNING(f'User {email} already exists')
)
return
User.objects.create_superuser(email=email, password=password)
self.stdout.write(
self.style.SUCCESS(f'Successfully created user {email}')
)# Использование
python manage.py create_admin --email admin@example.com --password secret123# management/commands/import_data.py
import csv
from django.core.management.base import BaseCommand
from myapp.models import Product
class Command(BaseCommand):
help = 'Импортирует продукты из CSV'
def add_arguments(self, parser):
parser.add_argument('csv_file', type=str)
parser.add_argument('--dry-run', action='store_true')
def handle(self, *args, **options):
csv_file = options['csv_file']
dry_run = options['dry_run']
with open(csv_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
if dry_run:
self.stdout.write(f"Would create: {row['name']}")
else:
Product.objects.create(
name=row['name'],
price=row['price'],
sku=row['sku']
)
self.stdout.write(
self.style.SUCCESS(f"Created: {row['name']}")
)# Сухой запуск
python manage.py import_data products.csv --dry-run
# Реальный импорт
python manage.py import_data products.csv# management/commands/delete_user.py
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
import os
User = get_user_model()
class Command(BaseCommand):
help = 'Удаляет пользователя (требует подтверждения)'
def add_arguments(self, parser):
parser.add_argument('user_id', type=int)
parser.add_argument('--force', action='store_true')
def handle(self, *args, **options):
# Проверка переменной окружения для production
if os.environ.get('ENV') == 'production' and not options['force']:
raise CommandError(
'Production delete requires --force flag. '
'Are you sure?'
)
user_id = options['user_id']
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise CommandError(f'User {user_id} does not exist')
self.stdout.write(f'Deleting user: {user.email}')
user.delete()
self.stdout.write(
self.style.SUCCESS('User deleted successfully')
)# management/commands/update_config.py
import logging
from django.core.management.base import BaseCommand
logger = logging.getLogger('audit')
class Command(BaseCommand):
help = 'Обновляет конфигурацию'
def handle(self, *args, **options):
logger.info(
"Config update started",
extra={
'event_type': 'config_update',
'user': os.environ.get('USER', 'unknown'),
}
)
# Логика обновления
update_config()
logger.info(
"Config update completed",
extra={
'event_type': 'config_update',
'status': 'success',
}
)# ❌ Неправильно: админ-эндпоинты в веб-приложении
@app.route('/admin/migrate')
def run_migrations():
# Миграции через HTTP!
subprocess.run(['python', 'manage.py', 'migrate'])
return 'Migrations complete'
@app.route('/admin/shell')
def web_shell():
# Веб-консоль!
code = request.args.get('code')
return eval(code) # 😱Проблемы:
# ❌ Неправильно: ручные команды в production
# 1. SSH на сервер
ssh admin@prod-server
# 2. Переход в директорию
cd /var/www/myapp
# 3. Активация виртуального окружения
source venv/bin/activate
# 4. Запуск миграций
python manage.py migrate
# 5. Перезапуск приложения
sudo systemctl restart myappРешение: Автоматизировать через CI/CD.
# ❌ Неправильно: разные переменные окружения
# Development
DATABASE_URL=sqlite:///dev.db
python manage.py migrate
# Production
DATABASE_URL=postgresql://prod-db/app
python manage.py migrate # Может не работать из-за различийРешение: Использовать одинаковое окружение.
Задайте себе вопросы:
# Ручное развёртывание
# 1. SSH на сервер
ssh admin@prod-server
# 2. Остановка приложения
sudo systemctl stop myapp
# 3. Бэкап БД
pg_dump myapp > backup_$(date +%Y%m%d).sql
# 4. Применение миграций
python manage.py migrate
# 5. Запуск приложения
sudo systemctl start myapp
# 6. Проверка
curl https://myapp.example.com/healthПроблемы:
# .gitlab-ci.yml
stages:
- test
- build
- migrate
- deploy
migrate:
stage: migrate
image: registry/myapp:$CI_COMMIT_SHA
script:
- python manage.py migrate
environment:
name: production
when: manual
deploy:
stage: deploy
image: bitnami/kubectl
script:
- kubectl rollout restart deployment/myapp
- kubectl rollout status deployment/myapp
environment:
name: production
needs:
- migrate# cleanup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-cleanup
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanup
image: registry/myapp:latest
command: ["python", "scripts/cleanup.py"]
restartPolicy: OnFailureРезультат:
Ключевой вывод: Административные задачи (миграции, консоль, скрипты) должны запускаться как одноразовые процессы в том же окружении, что и основное приложение. Это обеспечивает переносимость, аудируемость и автоматизацию через CI/CD.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.