Манифесты приложения ревьюятся в PR построчно. Terraform-план читают перед apply. А ALTER TABLE в проде чаще всего живёт в файле V47__add_column.sql, который никто не диффит против реального состояния базы — просто верят, что он применился именно так, как задуман. Atlas закрывает этот разрыв: он умеет описать схему декларативно и сам посчитать diff, вместо того чтобы вы вручную вели цепочку пронумерованных миграций.
Содержание
Открыть содержание
- Почему миграции — это версионный контроль, а не diff
- Что меняется на практике
- Atlas Kubernetes Operator поверх CloudNativePG
- Откат и совместимость с blue/green
- Сравнение: версионные миграции против декларативного diff
- Что нужно, чтобы подключить Atlas к уже работающему CloudNativePG
- Как проверить, что всё работает
- Итог
Почему миграции — это версионный контроль, а не diff
Классический инструмент миграций (Flyway, golang-migrate, Django ORM) работает так: вы пишете up-скрипт с изменением и down-скрипт для отката, даёте файлу номер, и на выкладке движок применяет все ещё не применённые номера по порядку. Это версионный контроль в буквальном смысле — история команд, а не состояние.
Проблема появляется не в одном окружении, а между ними. В stage кто-то накатил миграцию руками, чтобы разблокировать тестирование, забыл закоммитить файл — и stage тихо разъехался с тем, что должно получиться в проде после того же набора номеров. Диагностировать это можно только сравнением реальных схем через pg_dump --schema-only, а не чтением истории миграций: история говорит, что должно было произойти, а не что произошло на самом деле.
Декларативный подход убирает этот класс дрейфа целиком. Вы описываете желаемое состояние схемы — таблицы, колонки, индексы, внешние ключи — в HCL или в чистом SQL-файле. Atlas подключается к реальной базе, строит её актуальный граф схемы и сам вычисляет минимальный набор DDL-операций, чтобы превратить текущее состояние в желаемое. Не важно, что происходило с базой раньше вручную, — план строится от факта, а не от истории.
Что меняется на практике
- Diff, а не миграция.
atlas schema diffпечатает конкретныеALTER TABLE, которые нужны, — какterraform plan, только для DDL. atlas schema apply --auto-approve=falseкак gate в CI. Команда строит план, показывает его в выводе пайплайна и ждёт подтверждения перед применением — PR со схемой ревьюится так же, как PR с манифестом.- Проверка на destructive-изменения. Если diff требует дропнуть колонку или таблицу, Atlas по умолчанию отказывается и требует явного
--allow-destructive— случайно потерять данные из-за неаккуратногоDROP COLUMNв желаемой схеме становится сложнее, а не проще. AtlasSchemaкак Kubernetes-ресурс. Через Atlas Kubernetes Operator схема становится таким же декларативным CR, какClusterу CloudNativePG илиKustomizationу Flux, — reconcile-цикл сам приводит базу к описанному состоянию.
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 прямо в кластере, без похода в отдельную систему миграций.
Сама схема одной таблицы в 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
- Секрет с connection string уже есть — CloudNativePG сам создаёт
<cluster>-appс URI на сервис-rw, отдельно ничего заводить не нужно. - Установить Atlas Operator в кластер (Helm-чарт
atlasgo/atlas-operator) — он ставит CRDAtlasSchemaи контроллер, который следит за ними. - Описать схему в HCL для нужных таблиц — начните с одной, экспортировав текущее состояние командой
atlas schema inspect -u "$DATABASE_URL" --format hclвместо того, чтобы писать её с нуля. - Включить
policy.lint.destructive.error: trueвAtlasSchemaс самого начала, а не после первого инцидента с потерянной колонкой. - Добавить gate в CI —
atlas 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 уже применил к манифестам, только доехавший наконец до базы данных.