RTP DesportoCorridas de Fórmula 1 mais curtas a partir de 2027וואלההאם רע"ם ועוצמה יהודית ייפסלו? דיונים בבקשות לפסילה לכנסת ה־26The Jerusalem PostMotive behind Ontario synagogue shooting attack not yet determined by Canadian policePunchVIDEO: NSCDC busts illegal fruit juice factory in Lagos, arrests twoDaily MaverickWHAT’S COOKING: A pair of meaty recipes to plan for Braai DayBollywood HungamaVeteran actor Radha files complaint against sister Ambika and brother over alleged Rs 50 crores cheque fraudInquirerDepEd asked: Why reiterate Pride Month memo when it’s always voluntary?UOLFuracão Polo atinge categoria 4 no Pacífico mexicanoColliderApple TV’s Sensational Spy Series Is on a 630-Day Streaming StreakEgypt IndependentEgypt stresses protection of workers in Lebanon, stronger labor cooperationThe Sydney Morning HeraldLachie Neale faces tribunal in bid to play in AFL grand finalBlickMit viel Gold versehen: Messi präsentiert Spezialtrikot für Argentinien-Abschied
The Daily Newsstand · Free, Always
Tuesday, September 22, 2026

Миграции от ИИ агента: почему я бы не пускал их в прод без отдельного firewall

Translate

Coding agent отлично справляется с тем, что раньше раздражало своей механичностью: протянуть поле через модель и сериализатор, обновить тесты, поправить клиент, сгенерировать миграцию. Последний пункт выглядит особенно безобидно. ORM сама умеет строить DDL, агент всего лишь запускает знакомую команду, diff небольшой, тесты зелёные.

Но миграция — странный вид кода. Она может состоять из пяти строк и при этом иметь больший blast radius, чем изменение на тысячу строк в бизнес-логике. Ошибка в Python обычно ломает конкретный путь выполнения. Неудачный ALTER TABLE способен поставить в очередь запросы ко всей таблице, съесть пул соединений и превратить локальную правку схемы в отказ сервиса.

С появлением coding agents эта асимметрия стала заметнее. Генерировать изменения схемы стало почти бесплатно, а стоимость проверки не уменьшилась. Поэтому я бы рассматривал миграцию, созданную агентом, не как обычный файл в diff, а как привилегированный артефакт — примерно как изменение Terraform, Kubernetes RBAC или CI-секрета. Агент может его подготовить, но право пройти в production должно определяться отдельным набором проверок.

Почему обычного code review здесь мало

Представим типичную задачу в Django: добавить индекс для часто используемого фильтра.

class Order(models.Model):
    provider_reference = models.CharField(max_length=128)

Агент видит медленный запрос и вполне логично предлагает:

class Order(models.Model):
    provider_reference = models.CharField(max_length=128, db_index=True)

После makemigrations получается обычный AddIndex. На тестовой базе всё проходит мгновенно. На production-таблице с десятками или сотнями миллионов строк цена операции уже другая.

Документация PostgreSQL прямо различает обычный CREATE INDEX и CREATE INDEX CONCURRENTLY: обычное построение блокирует записи в таблицу до завершения, тогда как CONCURRENTLY позволяет продолжать INSERT, UPDATE и DELETE, хотя выполняет больше работы и имеет собственные ограничения. PostgreSQL: CREATE INDEX

В Django для PostgreSQL поэтому существует отдельная операция AddIndexConcurrently. Она не просто меняет синтаксис. Она меняет эксплуатационный профиль миграции.

from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models

class Migration(migrations.Migration):
    atomic = False

    operations = [
        AddIndexConcurrently(
            model_name="order",
            index=models.Index(
                fields=["provider_reference"],
                name="order_provider_ref_idx",
            ),
        ),
    ]

Здесь важная деталь: CREATE INDEX CONCURRENTLY нельзя выполнять внутри обычного transaction block. То есть исправление одного риска сразу меняет свойства самой миграции. Она становится нетранзакционной, а значит, при падении посередине нужно понимать, какое состояние останется в базе и можно ли безопасно запустить её повторно.

Именно поэтому вопрос «валиден ли этот Python-файл?» почти бесполезен. Нужен другой: «что эта миграция сделает с живой базой, пока рядом идут реальные запросы?»

Агент видит схему. Production видит конкуренцию

На локальной базе обычно нет того, что делает DDL опасным: длинных транзакций, аналитических запросов, сотен соединений, репликации, фоновых задач и постоянного потока записи.

PostgreSQL использует разные режимы блокировок. ACCESS EXCLUSIVE конфликтует со всеми остальными режимами, а многие формы ALTER TABLE могут брать именно его. Это не означает, что любой ALTER TABLE обязательно положит production. Но означает, что безопасность операции определяется не размером SQL и не временем её выполнения на staging, а тем, с какими блокировками и с каким текущим трафиком она столкнётся. PostgreSQL: Explicit Locking

Особенно неприятен сценарий, когда сама DDL-команда выполняется быстро, но сначала ждёт другую транзакцию. Пока она ждёт, за ней могут начать копиться запросы, конфликтующие уже с её ожидающей блокировкой. Внешне всё выглядит странно: миграция «ещё ничего не сделала», а latency приложения уже растёт.

Coding agent почти всегда работает со статическим снимком репозитория. Даже если он прекрасно знает PostgreSQL, из кода он не узнает:

  • сколько строк сейчас в таблице;

  • какой реальный RPS приходится на неё;

  • есть ли длинные транзакции в момент deploy;

  • сколько свободных соединений остаётся в пуле;

  • какой replication lag допустим;

  • можно ли откатить конкретную операцию без ещё одной тяжёлой DDL.

Это не недостаток модели. Эти данные просто находятся вне репозитория.

Поэтому миграции нужен свой firewall

Я называю firewall не отдельный продукт, а границу в CI, через которую миграция проходит по более строгим правилам, чем обычный код.

Главная идея — агент не должен сам оценивать риск собственного изменения. Он может объяснить миграцию, но решение о допуске лучше строить на независимых и по возможности детерминированных сигналах.

Это совпадает с более общей моделью безопасной работы coding agents. OpenAI описывает внутреннее использование Codex через sandbox, approval policy, ограниченный сетевой доступ и отдельную остановку на действиях с повышенным риском. Running Codex safely at OpenAI

Для базы такой границей становится не shell-команда, а класс изменения схемы.

Слой 1. Запрещаем опасные операции автоматически

Самый дешёвый контроль — статический анализ миграции до запуска.

Для Django можно проверять операции из Migration.operations. Для Alembic — разбирать сгенерированный SQL или operation tree. На первом этапе даже не нужен сложный анализатор. Достаточно списка конструкций, которые нельзя молча пропускать.

DANGEROUS = {
    "RunSQL",
    "RemoveField",
    "DeleteModel",
    "AlterField",
}

for operation in migration.operations:
    if operation.__class__.__name__ in DANGEROUS:
        fail("migration requires manual review")

В реальном проекте правило должно быть точнее. AlterField может быть безопасным или очень дорогим в зависимости от изменения типа. RunSQL может содержать безобидный COMMENT ON или UPDATE всей таблицы. Но даже грубый deny-list полезен: он меняет default с «пропустить, если никто не заметил» на «остановить, пока риск не классифицирован».

Я бы отдельно помечал как минимум удаление колонок и таблиц, изменение типа колонки, добавление NOT NULL к существующим данным, обычное создание индекса на большой таблице, создание или валидацию ограничений, RunSQL и RunPython, массовые изменения данных внутри schema migration и операции, делающие миграцию atomic = False.

Не потому, что всё это запрещено. Потому, что размер diff ничего не говорит о цене ошибки.

Слой 2. Смотрим не только migration file, но и SQL

ORM скрывает детали, а именно они нас интересуют. Поэтому CI должен уметь показать SQL, который реально уйдёт в базу.

Для Django это можно сделать через sqlmigrate:

python manage.py sqlmigrate orders 0127

После этого проверять уже не абстрактный AddField, а конкретные ALTER TABLE, CREATE INDEX, DROP CONSTRAINT.

Полезно сохранять этот SQL как артефакт CI рядом с risk report. Тогда reviewer видит не только то, что хотел выразить автор миграции, но и то, что выполнит PostgreSQL.

CREATE INDEX                 -> HIGH
CREATE INDEX CONCURRENTLY    -> MEDIUM
DROP TABLE                   -> BLOCK
ALTER TABLE ... TYPE         -> HIGH
ADD CONSTRAINT ... NOT VALID -> MEDIUM
VALIDATE CONSTRAINT          -> MEDIUM

Уровни условные. Важен сам переход от бинарного «тест прошёл» к явному описанию эксплуатационного риска.

Слой 3. Прогоняем миграцию на копии схемы

Следующая проблема — миграция может быть синтаксически корректной, но не соответствовать реальному состоянию базы.

Хороший минимум — поднимать в CI временный PostgreSQL, восстанавливать актуальную схему и прогонять весь путь migrate -> rollback -> migrate, если rollback вообще поддерживается.

Но пустая база всё ещё слишком добрая. Для data migration полезнее shadow database с реалистичным объёмом синтетических данных. Не обязательно копировать production. Цель — поймать операции, сложность которых растёт вместе с таблицей.

migrations.RunPython(fill_normalized_email)

а внутри:

for user in User.objects.all():
    user.normalized_email = user.email.lower()
    user.save()

На тысяче строк это выглядит нормально. На десятках миллионов — это уже не миграция, а отдельная batch-задача с вопросами к размеру транзакций, нагрузке на WAL, autovacuum, репликации и возможности продолжить после падения.

Firewall должен ловить не только «опасный SQL», но и ошибку масштаба.

Слой 4. Expand — migrate — contract вместо красивого one-shot

Агент любит завершённые изменения. Человек тоже. Если поле переименовывается, естественно хочется получить одну миграцию RENAME COLUMN old TO new и сразу обновить код.

Для rolling deploy это часто неправильная форма изменения: часть инстансов ещё работает со старой версией приложения, часть уже с новой.

1. expand
   добавить new_column

2. deploy
   новый код умеет работать с обеими схемами

3. migrate
   заполнить new_column батчами

4. deploy
   переключить чтение на new_column

5. contract
   удалить old_column отдельным релизом

Это менее красиво в diff, зато сохраняет совместимость между версиями приложения во время выкладки.

И здесь coding agent как раз полезен: он отлично умеет механически протянуть временную совместимость, dual-write, backfill command и последующий cleanup. Но решение разбить изменение на несколько релизов должно появиться до генерации кода.

Слой 5. lock_timeout важнее уверенности агента

Даже проверенная миграция может встретить production в неудачный момент. Поэтому последняя линия защиты должна находиться в самой базе.

Для DDL разумно задавать небольшой lock_timeout, чтобы миграция упала, а не ждала блокировку бесконечно, удерживая очередь за собой.

SET lock_timeout = '2s';
ALTER TABLE orders ...;

Конкретное значение зависит от системы. Смысл в другом: отказ миграции обычно дешевле, чем отказ пользовательского трафика.

Для долгих операций нужен ещё statement_timeout, но смешивать эти два понятия не стоит. lock_timeout ограничивает ожидание блокировки, statement_timeout — общее время выполнения statement. Иногда индекс нормально строится двадцать минут, но ждать двадцать минут возможности начать его строить — уже плохой сигнал.

А что делать с NOT NULL и constraints

Допустим, агент меняет поле:

status = models.CharField(max_length=32, null=False)

Если колонка уже существует и содержит NULL, сначала нужно заполнить данные. Даже если NULL нет, проверка ограничения на большой таблице может быть нежелательна в критический момент.

PostgreSQL позволяет добавить некоторые ограничения как NOT VALID, а затем отдельно выполнить VALIDATE CONSTRAINT. Документация отмечает, что VALIDATE CONSTRAINT использует более мягкий SHARE UPDATE EXCLUSIVE lock. PostgreSQL: ALTER TABLE

Django для PostgreSQL предоставляет AddConstraintNotValid и ValidateConstraint, то есть сам фреймворк уже содержит примитивы для разделения опасного изменения на этапы. Django PostgreSQL migration operations

Именно такие конструкции стоит записывать не в промпт агенту, а в исполняемую policy проекта. Промпт можно забыть. CI — сложнее.

Как выглядел бы минимальный pipeline

Я бы начал без собственной платформы и без LLM-reviewer в CI.

migration-check:
  script:
    - python manage.py makemigrations --check
    - python tools/check_migration_policy.py
    - python manage.py sqlmigrate orders 0127 > migration.sql
    - python tools/check_ddl.py migration.sql
    - pytest tests/migrations
  artifacts:
    paths:
      - migration.sql
      - migration-risk.json

migration-risk.json может быть совсем простым:

{
  "risk": "high",
  "reasons": [
    "CREATE INDEX without CONCURRENTLY",
    "table orders is in large-table allowlist"
  ],
  "requires_manual_approval": true
}

Дальше policy можно постепенно связывать с реальными метаданными: размером таблиц, статистикой запросов, допустимым окном deploy, наличием реплик. Один и тот же AddIndex на таблице из 20 тысяч строк и на таблице из 200 миллионов строк не должен получать одинаковый вердикт.

Где здесь место самому AI

Парадоксально, но я бы не ставил второй LLM последним арбитром первой.

AI-review полезен для объяснения: найти потенциально опасную операцию, предложить expand/contract, подсказать CONCURRENTLY, собрать вопросы для reviewer. Но доказательство лучше оставлять инструментам, которые дают воспроизводимый результат: парсеру миграций, PostgreSQL, тестам, метрикам и policy engine.

AI:      предлагает изменение и объясняет риск
CI:      проверяет формальные правила
DB:      показывает реальное поведение DDL
human:   принимает решение там, где нужен контекст production

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

Самая полезная метрика — не число сгенерированных миграций

Если команда хочет понять, помогает ли агент в этой части разработки, я бы не измерял количество созданных файлов или сэкономленные минуты на makemigrations.

Полезнее смотреть на другое: сколько миграций CI остановил до deploy, сколько потребовали ручного изменения стратегии, сколько раз deploy был безопасно прерван по lock_timeout, сколько schema changes прошли без деградации latency и replication lag.

Ускорить генерацию миграции легко. Сделать так, чтобы её можно было скучно и предсказуемо применить на живой базе, — всё ещё инженерная работа.

Вместо вывода

Coding agents меняют стоимость производства кода, но не физику PostgreSQL. Блокировки не становятся мягче от того, что ALTER TABLE написал хороший агент. Большая таблица не уменьшается от зелёного unit-теста. А rolling deploy не начинает атомарно обновлять все инстансы только потому, что diff выглядит логично.

Поэтому миграции — хороший пример места, где автономию стоит ограничивать не инструкцией «будь осторожен», а архитектурой процесса.

Пусть агент пишет миграцию. Пусть предлагает оптимальный вариант. Пусть генерирует backfill и тесты. Но между его diff и production должна стоять граница, которая понимает DDL, блокировки и масштаб данных.

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.