Перейти к содержимому
Hogin Hogin
Назад

Схема БД как код: Atlas поверх CloudNativePG вместо ручных миграций

7 мин чтения

Манифесты приложения ревьюятся в PR построчно. Terraform-план читают перед apply. А ALTER TABLE в проде чаще всего живёт в файле V47__add_column.sql, который никто не диффит против реального состояния базы — просто верят, что он применился именно так, как задуман. Atlas закрывает этот разрыв: он умеет описать схему декларативно и сам посчитать diff, вместо того чтобы вы вручную вели цепочку пронумерованных миграций.

Содержание

Открыть содержание

Почему миграции — это версионный контроль, а не diff

Классический инструмент миграций (Flyway, golang-migrate, Django ORM) работает так: вы пишете up-скрипт с изменением и down-скрипт для отката, даёте файлу номер, и на выкладке движок применяет все ещё не применённые номера по порядку. Это версионный контроль в буквальном смысле — история команд, а не состояние.

Проблема появляется не в одном окружении, а между ними. В stage кто-то накатил миграцию руками, чтобы разблокировать тестирование, забыл закоммитить файл — и stage тихо разъехался с тем, что должно получиться в проде после того же набора номеров. Диагностировать это можно только сравнением реальных схем через pg_dump --schema-only, а не чтением истории миграций: история говорит, что должно было произойти, а не что произошло на самом деле.

Версионные миграции копят историю команд и расходятся между окружениями; декларативный diff всегда сравнивает с текущим состоянием

Декларативный подход убирает этот класс дрейфа целиком. Вы описываете желаемое состояние схемы — таблицы, колонки, индексы, внешние ключи — в HCL или в чистом SQL-файле. Atlas подключается к реальной базе, строит её актуальный граф схемы и сам вычисляет минимальный набор DDL-операций, чтобы превратить текущее состояние в желаемое. Не важно, что происходило с базой раньше вручную, — план строится от факта, а не от истории.

Что меняется на практике

CI gate: план изменений схемы виден в PR и требует подтверждения перед apply, как terraform plan для DDL

Atlas Kubernetes Operator поверх CloudNativePG

Если у вас уже поднят CloudNativePG-кластер (разбирали его в отдельной статье), Atlas Operator подключается к нему как обычный клиент — через Secret с connection string, который CNPG и так генерирует для сервиса -rw. Дальше желаемая схема описывается ресурсом AtlasSchema, который ссылается на файл HCL (или инлайн-схему) и на секрет с адресом кластера:

apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasSchema
metadata:
  name: orders-schema
  namespace: prod
spec:
  urlFrom:
    secretKeyRef:
      key: uri
      name: pg-app          # Secret, который создаёт CloudNativePG
  schema:
    configMapKeyRef:
      key: schema.hcl
      name: orders-schema-hcl
  policy:
    lint:
      destructive:
        error: true          # без allow оператор не дропнет колонку/таблицу

Оператор реагирует на изменение ConfigMap со схемой точно так же, как Flux реагирует на изменение манифеста: считает diff, применяет его к базе за сервисом -rw и обновляет статус ресурса — kubectl get atlasschema показывает текущее состояние reconcile прямо в кластере, без похода в отдельную систему миграций.

AtlasSchema CRD читает желаемую схему из ConfigMap, Atlas Operator считает diff и применяет его к CloudNativePG через сервис -rw

Сама схема одной таблицы в HCL выглядит компактно — это не SQL-миграция, а описание конечного состояния:

table "orders" {
  schema = schema.public
  column "id" {
    type = bigint
    identity {}
  }
  column "customer_id" {
    type = bigint
  }
  column "status" {
    type    = varchar(32)
    default = "pending"
  }
  primary_key {
    columns = [column.id]
  }
  index "orders_customer_id_idx" {
    columns = [column.customer_id]
  }
}

Добавили колонку в этот файл — diff покажет ADD COLUMN. Удалили — Atlas остановится на destructive-проверке и потребует явного подтверждения, что дроп колонки — не опечатка.

Откат и совместимость с blue/green

У версионных миграций откат — это отдельный down-скрипт, который вы пишете заранее и который часто оказывается не протестирован до момента, когда он реально понадобился под давлением инцидента. У Atlas отката в этом смысле нет — есть только применение схемы предыдущей версии тем же механизмом diff: вы возвращаете HCL-файл к прошлому коммиту, Atlas считает обратный diff и применяет его. Откат — не отдельный артефакт, а тот же самый инструмент, применённый к другому желаемому состоянию.

Это же свойство делает Atlas удобным при blue/green-деплое приложения. Классическое правило совместимости остаётся в силе: схема должна работать одновременно со старой и новой версией кода, пока идёт переключение трафика — новую колонку сначала добавляют nullable или с дефолтом, а NOT NULL/DROP откладывают на следующий релиз, когда старая версия приложения уже выключена. Atlas не отменяет эту дисциплину, но встроенный destructive-gate страхует от самого частого способа её нарушить — забытого DROP в диффе, который прилетел бы вместе с не связанным изменением.

Сравнение: версионные миграции против декларативного diff

АспектВерсионные миграции (Flyway, golang-migrate)Atlas (декларативный)
Источник истиныцепочка up/down-файловжелаемое состояние схемы в HCL/SQL
Дрейф между окружениямиобнаруживается постфактумсчитается diff-ом от реального состояния
Ревью изменениятекст SQL-файла в PRплан DDL, как terraform plan
Откатотдельный down-скрипт, часто не протестировантот же diff-механизм на прошлое состояние
Защита от DROPзависит от дисциплины автораявный --allow-destructive/policy-gate
В Kubernetesнет нативного ресурсаAtlasSchema CRD с reconcile-циклом

Что нужно, чтобы подключить Atlas к уже работающему CloudNativePG

  1. Секрет с connection string уже есть — CloudNativePG сам создаёт <cluster>-app с URI на сервис -rw, отдельно ничего заводить не нужно.
  2. Установить Atlas Operator в кластер (Helm-чарт atlasgo/atlas-operator) — он ставит CRD AtlasSchema и контроллер, который следит за ними.
  3. Описать схему в HCL для нужных таблиц — начните с одной, экспортировав текущее состояние командой atlas schema inspect -u "$DATABASE_URL" --format hcl вместо того, чтобы писать её с нуля.
  4. Включить policy.lint.destructive.error: true в AtlasSchema с самого начала, а не после первого инцидента с потерянной колонкой.
  5. Добавить gate в CIatlas schema apply -u "$DATABASE_URL" --to file://schema.hcl --auto-approve=false --dry-run в pipeline перед мёрджем PR со схемой, чтобы план был виден ревьюеру до применения.

Как проверить, что всё работает

Запустите diff между желаемой схемой и реальной базой без применения — это безопасно на любом окружении, включая прод:

atlas schema diff \
  --from "postgres://user:[email protected]:5432/orders?sslmode=require" \
  --to file://schema.hcl

Если diff пустой — база уже соответствует описанной схеме. Дальше проверьте destructive-gate намеренно: удалите колонку из HCL, прогоните atlas schema apply --auto-approve=false и убедитесь, что команда останавливается на предупреждении, а не применяет DROP COLUMN молча. И в кластере — kubectl get atlasschema -n prod должен показывать Ready со временем последнего успешного reconcile, а не зависшее Reconciling.

Итог

Atlas не заменяет обычные миграции целиком — для сложных data-миграций (перекладка данных между колонками, бэкфилл) вам всё ещё нужен процедурный скрипт. Но для структуры схемы он закрывает конкретный и давно назревший разрыв: последний слой инфраструктуры, который у большинства команд не проходит через git-diff review. Diff вместо истории команд, gate в CI перед мёрджем, явная защита от DROP и AtlasSchema как ресурс кластера — это тот же принцип, что GitOps уже применил к манифестам, только доехавший наконец до базы данных.


Поделиться:

Следующая статья
Guardrails для AI SRE-агентов: как разрешать автоматизацию, не теряя контроль