DevOps

Автоматизация схемы базы данных: миграции PostgreSQL в CI/CD пайплайне с Flyway и GitHub Actions

Ruslan Ismailov Опубликовано 14 мин чтения
А

Введение: почему ручные миграции опасны в 2026 году

Ещё несколько лет назад «запустить скрипт вручную перед деплоем» было нормой для многих команд. Сегодня в 2026 году это — источник инцидентов. Команды работают быстрее, деплои происходят по несколько раз в день, инфраструктура воспроизводится автоматически. Ручное применение SQL-скриптов к базе данных в такой среде означает:

  • Риск человеческой ошибки: применён не тот скрипт, не к той базе, не в том порядке.
  • Отсутствие аудита: непонятно, кто и когда изменил схему.
  • Рассинхронизация между окружениями: staging и production живут по-разному.
  • Невозможность автоматического rollback при падении деплоя.
  • Проблемы при горизонтальном масштабировании и работе нескольких команд над одной базой.

Решение — интегрировать управление схемой PostgreSQL непосредственно в CI/CD пайплайн. Инструмент Flyway в связке с GitHub Actions позволяет сделать миграции воспроизводимыми, версионированными и безопасными. В этой статье разберём полный цикл: от установки до production-ready конфигурации.

Flyway vs Liquibase vs встроенные миграции фреймворков

Прежде чем погружаться в Flyway, стоит понять, почему именно он, а не альтернативы.

Flyway

  • Простая модель: SQL-файлы с именами по конвенции, таблица истории миграций flyway_schema_history.
  • Поддержка PostgreSQL, MySQL, Oracle и других СУБД из коробки.
  • Минимальная конфигурация для старта, богатые возможности для enterprise.
  • Интегрируется в Maven, Gradle, Docker, CLI и GitHub Actions.
  • Платная Flyway Teams/Enterprise добавляет dry-run, undo-миграции и больше функций отката.

Liquibase

  • Более гибкий формат: XML, YAML, JSON или SQL для описания изменений.
  • Поддерживает генерацию diff между схемами.
  • Сложнее в освоении, больше конфигурации.
  • Хорошо подходит для команд, которым нужна база-нейтральная абстракция.

Встроенные миграции фреймворков

  • Django migrations, Laravel migrations, Alembic (SQLAlchemy) — удобны внутри своей экосистемы.
  • Привязаны к языку и фреймворку, сложнее использовать в polyglot-системах.
  • Нет прямой интеграции с CI/CD без дополнительных оберток.

Для команд, работающих с PostgreSQL в production и имеющих CI/CD на GitHub Actions, Flyway — оптимальный выбор: минимум зависимостей, предсказуемое поведение, нативная поддержка Docker.

Установка и базовая настройка Flyway для PostgreSQL

Flyway можно запустить несколькими способами. Наиболее универсальный для CI/CD — официальный Docker-образ flyway/flyway.

Структура проекта

Рекомендуемая структура директорий в репозитории:

project-root/
├── db/
│   ├── migrations/
│   │   ├── V1__init_schema.sql
│   │   ├── V2__add_users_table.sql
│   │   └── V3__add_index_on_email.sql
│   └── flyway.conf
├── docker-compose.yml
└── .github/
    └── workflows/
        └── deploy.yml

Файл конфигурации flyway.conf

Базовый конфиг для подключения к PostgreSQL:

flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=${DB_USER}
flyway.password=${DB_PASSWORD}
flyway.schemas=public
flyway.locations=filesystem:./db/migrations
flyway.baselineOnMigrate=false
flyway.validateOnMigrate=true
flyway.outOfOrder=false

Значения ${DB_USER} и ${DB_PASSWORD} Flyway подставляет из переменных окружения — это критично для безопасности в CI.

Написание версионированных SQL-миграций: naming convention и best practices

Соглашение об именовании файлов

Flyway строго следует паттерну имён файлов:

  • V{version}__{description}.sql — версионированные миграции (применяются один раз).
  • R__{description}.sql — повторяемые миграции (views, stored procedures, triggers).
  • U{version}__{description}.sql — undo-миграции (только в платной версии).

Примеры корректных имён:

V1__init_schema.sql
V2__add_users_table.sql
V2_1__add_users_email_index.sql
V3__add_orders_table.sql
R__refresh_analytics_view.sql

Пример реальных миграций

Инициализация схемы (V1__init_schema.sql):

-- V1__init_schema.sql
CREATE TABLE IF NOT EXISTS schema_meta (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Создание таблицы пользователей (V2__add_users_table.sql):

-- V2__add_users_table.sql
CREATE TABLE users (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email       VARCHAR(255) NOT NULL,
    username    VARCHAR(100) NOT NULL,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_users_email ON users(email);

Добавление внешнего ключа (V3__add_orders_table.sql):

-- V3__add_orders_table.sql
CREATE TABLE orders (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    total       NUMERIC(12, 2) NOT NULL DEFAULT 0,
    status      VARCHAR(50) NOT NULL DEFAULT 'pending',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status  ON orders(status);

Best practices написания миграций

  • Каждая миграция должна быть атомарной: одно логическое изменение на файл.
  • Используйте IF NOT EXISTS / IF EXISTS для идемпотентности там, где это применимо.
  • Никогда не изменяйте уже применённые миграционные файлы — Flyway проверяет контрольные суммы.
  • Избегайте DROP TABLE без резервной копии или отдельного этапа деплоя.
  • Для изменения структуры больших таблиц используйте ALTER TABLE ... ADD COLUMN с дефолтными значениями вместо пересоздания.
  • Документируйте назначение каждой миграции в комментарии в начале файла.

Интеграция Flyway в GitHub Actions: запуск миграций перед деплоем

Ключевой принцип: миграции должны выполняться до деплоя нового кода приложения. Это гарантирует, что новый код всегда видит актуальную схему.

Полный workflow GitHub Actions

# .github/workflows/deploy.yml
name: Deploy with DB Migrations

on:
  push:
    branches:
      - main

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  migrate:
    name: Run PostgreSQL Migrations
    runs-on: ubuntu-22.04
    environment: production

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run Flyway migrations
        uses: docker://flyway/flyway:10-alpine
        with:
          args: migrate
        env:
          FLYWAY_URL: jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }}
          FLYWAY_USER: ${{ secrets.DB_USER }}
          FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }}
          FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations
          FLYWAY_VALIDATE_ON_MIGRATE: "true"
          FLYWAY_OUT_OF_ORDER: "false"
          FLYWAY_BASELINE_ON_MIGRATE: "false"

      - name: Verify migration status
        uses: docker://flyway/flyway:10-alpine
        with:
          args: info
        env:
          FLYWAY_URL: jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }}
          FLYWAY_USER: ${{ secrets.DB_USER }}
          FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }}
          FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations

  deploy:
    name: Deploy Application
    runs-on: ubuntu-22.04
    needs: migrate
    environment: production

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          echo "Deploying application after successful migrations..."
          # Ваш деплой-скрипт: kubectl apply, docker stack deploy и т.д.

Запуск тестов миграций в PR

Для pull request пайплайна полезно запускать миграции против временной тестовой базы:

# .github/workflows/pr-migration-test.yml
name: Test Migrations on PR

on:
  pull_request:
    paths:
      - 'db/migrations/**'

jobs:
  test-migrations:
    runs-on: ubuntu-22.04

    services:
      postgres:
        image: postgres:16-alpine
        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:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run Flyway migrate on test DB
        uses: docker://flyway/flyway:10-alpine
        with:
          args: migrate
        env:
          FLYWAY_URL: jdbc:postgresql://localhost:5432/testdb
          FLYWAY_USER: testuser
          FLYWAY_PASSWORD: testpassword
          FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations

      - name: Validate migration info
        uses: docker://flyway/flyway:10-alpine
        with:
          args: validate
        env:
          FLYWAY_URL: jdbc:postgresql://localhost:5432/testdb
          FLYWAY_USER: testuser
          FLYWAY_PASSWORD: testpassword
          FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations

Стратегии отката (rollback) и baseline для существующих баз

Проблема отката в Flyway

Flyway Community Edition не поддерживает автоматические undo-миграции. Это осознанное дизайнерское решение: откат схемы базы данных — опасная операция, особенно если данные уже были записаны по новой схеме.

Стратегия «forward-only»

Рекомендуемый подход для большинства production-систем: никогда не откатывать схему назад, а исправлять ошибки новой миграцией вперёд.

  • Если V5 добавила столбец с ошибкой — пишем V6, которая исправляет это.
  • Если V5 удалила нужный столбец — V6 воссоздаёт его.

Blue-green деплой как стратегия безопасности

При blue-green деплое миграция выполняется до переключения трафика. Если миграция упала — переключение не происходит, старая версия продолжает работать с нетронутой схемой.

Baseline для существующей базы данных

Если вы подключаете Flyway к уже существующей production-базе, используйте команду baseline:

# Установить baseline на версию 1 для существующей БД
flyway -url=jdbc:postgresql://localhost:5432/mydb \
       -user=myuser \
       -password=mypassword \
       -baselineVersion=1 \
       -baselineDescription="Initial baseline" \
       baseline

После этого Flyway пометит текущее состояние как версию 1 и начнёт применять только новые миграции, начиная с V2. В конфиге установите flyway.baselineOnMigrate=false — baseline нужно выполнить только один раз вручную.

Работа с миграциями в Docker-окружении

Docker Compose для локальной разработки

Следующий docker-compose.yml поднимает PostgreSQL и автоматически запускает Flyway при старте:

version: '3.9'

services:
  postgres:
    image: postgres:16-alpine
    container_name: app_postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: appsecret
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5

  flyway:
    image: flyway/flyway:10-alpine
    container_name: app_flyway
    command: migrate
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      FLYWAY_URL: jdbc:postgresql://postgres:5432/appdb
      FLYWAY_USER: appuser
      FLYWAY_PASSWORD: appsecret
      FLYWAY_LOCATIONS: filesystem:/flyway/sql
      FLYWAY_VALIDATE_ON_MIGRATE: "true"
    volumes:
      - ./db/migrations:/flyway/sql
    restart: on-failure

volumes:
  postgres_data:

Запуск: docker compose up flyway — применит все pending-миграции к локальной базе.

Flyway в Dockerfile для production

Альтернативный подход — включить Flyway в init-контейнер Kubernetes или отдельный Job:

FROM flyway/flyway:10-alpine

COPY db/migrations /flyway/sql

# CMD задаётся при запуске контейнера через args

Безопасность: управление credentials и секретами в пайплайне

Безопасность credentials базы данных в CI/CD — критический аспект. Утечка пароля от production PostgreSQL может привести к катастрофическим последствиям.

GitHub Actions Secrets

Никогда не храните учётные данные в репозитории или в переменных окружения без шифрования. Используйте GitHub Secrets:

  • Перейдите в Settings → Secrets and variables → Actions в вашем репозитории.
  • Создайте секреты: DB_HOST, DB_NAME, DB_USER, DB_PASSWORD.
  • Используйте environment в workflow для разделения секретов по окружениям (staging, production).

Использование HashiCorp Vault или AWS Secrets Manager

Для enterprise-окружений рекомендуется динамическое получение credentials из Vault:

# Шаг получения credentials из Vault
- name: Import secrets from Vault
  uses: hashicorp/vault-action@v3
  with:
    url: ${{ secrets.VAULT_ADDR }}
    token: ${{ secrets.VAULT_TOKEN }}
    secrets: |
      secret/data/production/postgres username | DB_USER ;
      secret/data/production/postgres password | DB_PASSWORD

- name: Run Flyway migrations
  uses: docker://flyway/flyway:10-alpine
  with:
    args: migrate
  env:
    FLYWAY_URL: jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }}
    FLYWAY_USER: ${{ env.DB_USER }}
    FLYWAY_PASSWORD: ${{ env.DB_PASSWORD }}
    FLYWAY_LOCATIONS: filesystem:/github/workspace/db/migrations

Принцип минимальных привилегий

Создайте отдельного пользователя PostgreSQL для Flyway с минимально необходимыми правами:

-- Создание пользователя для миграций
CREATE USER flyway_runner WITH PASSWORD 'strong_random_password';

-- Права только на нужную базу и схему
GRANT CONNECT ON DATABASE appdb TO flyway_runner;
GRANT USAGE, CREATE ON SCHEMA public TO flyway_runner;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO flyway_runner;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO flyway_runner;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT ALL ON TABLES TO flyway_runner;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT ALL ON SEQUENCES TO flyway_runner;

Ротация паролей и аудит

  • Регулярно ротируйте пароли сервисных аккаунтов (минимум раз в квартал).
  • Включите логирование подключений к PostgreSQL через log_connections = on.
  • Мониторьте таблицу flyway_schema_history на предмет неожиданных изменений.
  • Используйте SSL-соединение: добавьте ?sslmode=require к JDBC URL.

Заключение и чеклист

Автоматизация миграций PostgreSQL через Flyway в CI/CD пайплайне на GitHub Actions — это не усложнение, а фундамент надёжного управления схемой базы данных. Правильно настроенный пайплайн гарантирует, что схема всегда синхронизирована с кодом, изменения версионированы и аудитируемы, а человеческий фактор исключён из критического процесса.

Ваша база данных — это не конфигурационный файл. Она требует такого же версионного контроля и автоматизации, как и исходный код приложения.

Чеклист для внедрения

  1. Создана директория db/migrations/ с версионированными SQL-файлами.
  2. Настроен flyway.conf без жёстко заданных credentials.
  3. Создан workflow GitHub Actions: миграции выполняются в job migrate перед job deploy.
  4. PR-пайплайн тестирует миграции против ephemeral PostgreSQL-сервиса.
  5. Все credentials хранятся в GitHub Secrets с разделением по окружениям.
  6. Создан отдельный PostgreSQL-пользователь для Flyway с минимальными правами.
  7. JDBC URL использует sslmode=require для production.
  8. Настроен Docker Compose для локального воспроизведения пайплайна миграций.
  9. Зафиксирована стратегия forward-only для изменений схемы.
  10. Команда ознакомлена с naming convention для миграционных файлов.

Такой подход масштабируется от небольшого стартапа до высоконагруженных production-систем, обеспечивая предсказуемость и безопасность на каждом этапе жизненного цикла приложения.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →