автоматизировать изменения схемы БД через CI/CD и GitOps

Database DevOps: миграции схемы БД в CI/CD-конвейерах

14 минут

Когда релизы приложения и изменения схемы идут разными дорожками, продакшен ломается быстро. В статье — миграции как полноценные артефакты поставки: Flyway или Liquibase, безопасный expand-contract и GitOps-управление порядком выполнения.

Когда релизы приложения и изменения схемы расходятся

Выпуск кода и обновление схемы БД часто ведут разные команды, инструменты и расписания. Отсюда сбои, когда код ждёт колонки, которых нет в базе; сложное восстановление после деструктивного DDL; повторный запуск ручного SQL; изменения в обход review и автотестов; расхождение local, staging и production. В микросервисной среде с десятками независимых схем ручное управление миграциями не масштабируется.

Схема как версионированный артефакт поставки

Принцип простой: изменения схемы — часть конвейера доставки ПО, а не разовые окна DBA. Каждое изменение — упорядоченный файл миграции в Git, review как у кода приложения, применение runner-ом с записью в служебную таблицу истории (для Flyway это flyway_schema_history). Если используете Flyway, придерживайтесь его формата имён: V001__initial_schema.sql, V002__add_users_table.sql. Разовые psql-команды в production убирают гарантии порядка, повторяемости и трассируемости.

Порядок в CI/CD и развёртывание expand-contract

После unit-тестов и до выката новой версии приложения прогоняйте ожидающие миграции на тестовой БД. Проверяйте синтаксис, ограничения и безопасность данных, затем деплойте приложение и smoke-тесты на окружении, близком к production. Для zero-downtime используйте expand-contract: expand — добавить nullable колонки или таблицы при работающей версии N; migrate — заполнить данные, пока старый код игнорирует новые поля; contract — удалить устаревшие объекты только после стабилизации N+1. Свяжите схему с feature flags: код, использующий новые структуры, выключен, пока не применены и миграция, и бинарник.

Откаты: реальность зависит от инструмента

Стратегия отката зависит от инструмента миграций и профиля риска. В Liquibase rollback поддерживается как часть модели. В Flyway многие команды работают по forward-only схеме (особенно в Community): вместо «реверса любой ценой» выпускают корректирующую миграцию, а для деструктивных инцидентов опираются на backup или point-in-time recovery. Если используете Flyway Teams undo, это дополнительный контур безопасности, но не единственный. Feature-ветки по-прежнему несут миграции, а при merge runner применяет только pending-версии.

SQL · forward-миграция
-- V004__alter_orders_add_status_column.sql
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';
SQL · корректирующая follow-up миграция
-- V005__fix_orders_status_default.sql
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'new_pending';

Пример: Flyway в GitHub Actions и sync-wave Argo CD

Структура рядом с кодом сервиса: db/migrations для версионированного SQL, db/flyway.conf для подключения, workflow в CI, который validate и migrate на ephemeral PostgreSQL перед integration-тестами. В Kubernetes Argo CD может задать порядок через sync-wave, но Job миграции должен получать и конфиг, и SQL-файлы. sync-wave управляет только порядком среди отрендеренных манифестов и сам по себе не доставляет миграции в контейнер.

flyway.conf
flyway.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
flyway.user=${DB_USER}
flyway.password=${DB_PASSWORD}
flyway.locations=filesystem:./migrations
flyway.baselineOnMigrate=true
flyway.outOfOrder=false
flyway.validateOnMigrate=true
GitHub Actions · validate и migrate
name: Database Migrations
on:
  push:
    paths:
      - 'db/**'
      - 'src/**'
jobs:
  validate-migrations:
    runs-on: ubuntu-latest
    env:
      DB_HOST: localhost
      DB_PORT: 5432
      DB_NAME: testdb
      DB_USER: testuser
      DB_PASSWORD: testpassword
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpassword
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - name: Set up Flyway
        run: |
          curl -L https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/9.22.3/flyway-commandline-9.22.3.zip -o flyway.zip
          unzip flyway.zip
          sudo mv flyway-9.22.3 /opt/flyway
          sudo ln -s /opt/flyway/flyway /usr/local/bin/flyway
      - name: Validate migration files
        run: flyway -configFiles=db/flyway.conf validate
      - name: Run migrations against test database
        run: flyway -configFiles=db/flyway.conf migrate
      - name: Run application integration tests
        run: ./gradlew integrationTest
      - name: Inspect migration history
        run: flyway -configFiles=db/flyway.conf info
Argo CD · Application для path db
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: orders-db-migrations
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/orders-service
    targetRevision: main
    path: db
  destination:
    server: https://kubernetes.default.svc
    namespace: orders-db
  syncPolicy:
    automated:
      prune: false
      selfHeal: false
    syncOptions:
      - ApplyOutOfSyncOnly=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 60s
Kubernetes Job · Flyway migrate (sync-wave -1)
apiVersion: batch/v1
kind: Job
metadata:
  name: orders-migration-v004
  namespace: orders-db
  annotations:
    argocd.argoproj.io/sync-wave: "-1"
spec:
  ttlSecondsAfterFinished: 300
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: flyway
          image: flyway/flyway:9.22.3
          args:
            - migrate
            - -configFiles=/mnt/config/flyway.conf
            - -locations=filesystem:/flyway/sql
          env:
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: orders-db-credentials
                  key: host
            - name: DB_PORT
              value: "5432"
            - name: DB_NAME
              value: orders
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: orders-db-credentials
                  key: username
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: orders-db-credentials
                  key: password
          volumeMounts:
            - name: flyway-config
              mountPath: /mnt/config
            - name: flyway-sql
              mountPath: /flyway/sql
      volumes:
        - name: flyway-config
          configMap:
            name: flyway-config
        - name: flyway-sql
          configMap:
            name: orders-db-migrations-sql

Лучшие практики: additive-изменения и обратимость

Сначала additive-миграции: nullable колонка, backfill, затем NOT NULL. Не делайте DROP, пока все инстансы не перестали читать объект — expand-contract и feature flags отделяют рискованный DDL от выката фичи. Тестируйте на объёме, близком к production: пять секунд на сотне строк могут стать часами на десятках миллионов. Разделяйте DDL и DML: схема применяется быстро, backfill — батчами в фоне. В PostgreSQL поведение блокировок зависит от операции и версии: часть ALTER TABLE проходит как metadata-only, а часть всё ещё может брать ACCESS EXCLUSIVE. Перед рискованным DDL задавайте lock_timeout и планируйте тяжёлые операции на окна низкого трафика.

SQL · безопасное добавление колонки
-- Избегайте немедленного NOT NULL на существующих строках
ALTER TABLE orders ADD COLUMN priority VARCHAR(10) NOT NULL;  -- упадёт

ALTER TABLE orders ADD COLUMN priority VARCHAR(10);
UPDATE orders SET priority = 'medium' WHERE priority IS NULL;
ALTER TABLE orders ALTER COLUMN priority SET NOT NULL;
SQL · разделение DDL и DML
-- V005: изменение схемы (быстро)
ALTER TABLE orders ADD COLUMN shipped_at TIMESTAMP;

-- V006: миграция данных (можно async батчами)
UPDATE orders SET shipped_at = updated_at WHERE status = 'shipped';
SQL · ограничение lock timeout
SET lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN tracking_number VARCHAR(100);

Инструменты, паритет окружений и уверенность в масштабе

Используйте Flyway, Liquibase, goose или встроенные runner-ы фреймворка — не ручной psql. Не запускайте приложение, пока миграции не успешны: через init-контейнеры, startup-проверки или deployment gates. Dev, staging и production должны использовать одинаковую модель runner-ов и конфигурации; отличаются только credentials и endpoints. Каждая миграция проходит тот же code review: назначение, стратегия восстановления, влияние на производительность, обратная совместимость. Мониторьте длительность миграций в production — скачок часто сигнализирует о росте данных, блокировках или отсутствии индексов. Управление схемой — не отдельная дисциплина от DevOps: версионирование, автоматизация и операционная репетиция дают ту же уверенность, что и релизы приложения, и сохраняют аудит каждого ALTER.

Порядок миграций в Kubernetes естественно сочетается с декларативной поставкой из гайда по GitOps с Argo CD и Flux.

Если работа со схемой тормозит релизы, ищите трение вместе с диагностикой узких мест release-пайплайна.