database-migrations.md 4.4 KB

Database: Миграции и Schema Management

Контекст: Миграции в backend/migrations/ используют golang-migrate. Запускаются в проде через sidecar контейнер migrate/migrate.

Суть

Миграции работают, но есть риски:

  1. Sidecar паттерн — нет гарантии порядка запуска перед backend
  2. Нет rollback стратегии в проде
  3. Нет проверки миграций в CI
  4. Down-миграции могут быть опасны в проде

Текущая схема (хорошо)

backend/migrations/
  000001_create_extensions.up.sql     -- pgcrypto, postgis
  000002_create_users.up.sql          -- users, refresh_tokens
  000003_create_tags_features.up.sql  -- справочники
  000004_create_places.up.sql         -- places, images, tags, features
  000005_create_services_reviews_bookings.up.sql
  000006_create_moderation_payments_visitors.up.sql

Каждая миграция имеет .down.sql — это правильно.

Проблема: Порядок запуска в docker-compose.prod.yml

# deploy/docker-compose.prod.yml:48-57
migrations:
  image: migrate/migrate:v4.17
  command:
    - -path=/migrations
    - -database "postgres://photoplaces:${DB_PASSWORD}@postgres:5432/photoplaces?sslmode=disable"
    - up
  depends_on:
    postgres:
      condition: service_healthy

backend:
  depends_on:
    migrations:
      condition: service_completed_successfully  -- Хорошо, но...

Риск: Если миграция падает, backend не стартует (хорошо), но нет retry логики. migrate/migrate не умеет ретраить подключение к БД.

Решение: Entrypoint скрипт с retry

# backend/Dockerfile — добавить entrypoint
FROM golang:1.22-alpine AS builder
# ... build stages ...

FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata postgresql-client
COPY --from=builder /photoplaces-api /photoplaces-api
COPY --from=builder /app/migrations /migrations
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/photoplaces-api"]
# docker-entrypoint.sh
#!/bin/sh
set -e

# Ждём БД с таймаутом
echo "Waiting for database..."
for i in $(seq 1 30); do
    if pg_isready -h postgres -U photoplaces -q; then
        break
    fi
    sleep 2
done

# Накатываем миграции
echo "Running migrations..."
migrate -path /migrations -database "$DATABASE_URL" up

# Стартуем приложение
exec /photoplaces-api

Плюсы: Миграции накатываются в том же контейнере, гарантированно перед стартом, с retry.

CI/CD: Проверка миграций

# .github/workflows/ci.yml
- name: Test migrations
  run: |
    docker compose -f docker-compose.yml up -d postgres
    sleep 5
    docker run --rm --network host \
      -v $(pwd)/backend/migrations:/migrations \
      migrate/migrate:v4.17 \
      -path=/migrations \
      -database "postgres://photoplaces:photoplaces_dev@localhost:5432/photoplaces?sslmode=disable" \
      up
    # Тест down/up
    migrate ... down 1
    migrate ... up

Best Practices для миграций

Правило Почему
Одна миграция = одно логическое изменение Легче откатить, понять историю
Никогда не редактировать закоммиченные миграции Нарушает воспроизводимость
Идемпотентность где возможно CREATE TABLE IF NOT EXISTS, DROP INDEX IF EXISTS
Тестировать down миграции В проде может понадобиться откат
Не делать DDL в транзакции PostGIS, CREATE INDEX CONCURRENTLY не работают в tx

Связанные заметки

  • [[deploy-production-readiness]] — entrypoint вместо sidecar
  • [[architecture-overview]]

Источник

Задача: Code review PhotoPlaces — миграции

Теги

#database #migrations #golang-migrate #docker #best-practice