Почему порядок развёртывания важнее вашего пайплайна

У вас готова новая версия приложения. Пайплайн зелёный. Команда смотрит. Вы нажимаете «деплой». Через несколько минут в логах появляются ошибки. Пользователи сообщают, что не могут завершить покупку. Администраторы базы данных говорят, что изменение схемы было применено после запуска приложения, а не до него.

Этот сценарий не редкость. Он происходит потому, что развёртывание редко сводится к одному приложению, делающему что-то одно. Речь о том, как это приложение соединяется со всем остальным, от чего оно зависит. Понимание этих связей — и связанных с ними рисков — это то, что отличает гладкое развёртывание от инцидента на продакшене.

Зависимости — это не только код

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

Самая распространённая зависимость — это база данных. Ваше приложение хранит данные пользователей в PostgreSQL или MySQL. Когда вы развёртываете новую версию, она читает и записывает данные иначе, чем старая. Возможно, старая версия хранила адрес пользователя в одном столбце, а новая разбивает его на улицу, город и почтовый индекс. Если новая версия запустится до обновления схемы базы данных, она не сможет прочитать существующие данные. Это ломающее изменение — старая и новая версии несовместимы друг с другом.

Зависимости также включают API других команд или внешних сервисов. Представьте, что ваше приложение вызывает платёжный API. Новая версия ожидает ответ в другом JSON-формате. Если этот API ещё не обновлён, ваше приложение получит данные, которые не сможет разобрать, и транзакции завершатся ошибкой. Проблема в том, что вы не всегда контролируете, когда этот API меняется. У другой команды может быть свой график развёртывания, и они могут не знать, что ваше приложение зависит от определённого формата ответа.

Сторонние библиотеки и пакеты — это тоже зависимости. Когда вы обновляете библиотеку с версии 1.0 до 2.0, функции могут быть переименованы или их сигнатуры изменены. Если ваше приложение всё ещё вызывает старое имя функции, код выдаст ошибку во время выполнения. Вот почему зрелые команды чётко отслеживают свои зависимости — в файлах вроде requirements.txt, package.json или go.mod — и тестируют новые версии библиотек перед использованием их на продакшене.

Скрытый риск: порядок развёртывания

Зависимости напрямую влияют на порядок, в котором вы развёртываете компоненты. Если приложение A зависит от базы данных, требующей изменения схемы, вы должны сначала обновить базу данных, а затем развернуть приложение A. Если приложение B вызывает API приложения C, вы должны сначала развернуть приложение C, а затем приложение B. Этот порядок важен, потому что если вы его перевернёте, только что развёрнутое приложение будет искать данные или сервисы, которые ещё недоступны. Результат — ошибки, неудачные запросы или полностью сломанное приложение.

Следующая диаграмма последовательности показывает правильный порядок развёртывания и что происходит, когда он нарушен:

sequenceDiagram participant DB as Database participant API as Backend API participant FE as Frontend participant Err as Error Log Note over DB,FE: Правильный порядок DB->>DB: Обновление схемы API->>API: Развёртывание новой версии FE->>FE: Развёртывание новой версии Note over FE,Err: Все сервисы совместимы Note over DB,FE: Неправильный порядок (обратный) FE->>API: Запрос данных API->>DB: Запрос к новой схеме DB-->>API: Схема не найдена API-->>FE: Ответ с ошибкой FE->>Err: Логирование сбоя

Риск растёт с количеством зависимостей. Чем больше сервисов должны быть синхронизированы, тем выше вероятность, что один из них не синхронизирован. Команда, которая хорошо справляется с этим, составляет карту всех зависимостей перед развёртыванием, подтверждает правильный порядок и готовит план отката на случай, если что-то пойдёт не так. Они также используют такие техники, как обратная совместимость — делая новую версию всё ещё работоспособной со старой — чтобы снизить риск ломающих изменений.

Ломающие изменения не всегда очевидны

Ломающее изменение не обязательно должно быть драматичным. Оно может быть незаметным. Например, новая версия вашего приложения может начать отправлять дополнительное поле в запросе к внутреннему API. Принимающий сервис игнорирует неизвестные поля, поэтому всё кажется нормальным. Но через несколько недель этот принимающий сервис обновляется, и теперь он ожидает, что это дополнительное поле присутствует. Старая версия вашего приложения, которая всё ещё работает в некоторых средах, перестаёт функционировать. Поломка отсрочена, а первопричину трудно отследить.

Вот почему составление карты зависимостей — это не разовое действие. Её нужно обновлять по мере развития системы. Каждый раз, когда добавляется новая зависимость или изменяется существующая, порядок развёртывания и профиль риска тоже меняются.

Как снизить риск зависимостей

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

Во-первых, документируйте свои зависимости. Это не означает написание длинного документа, который никто не читает. Это означает наличие чёткой, машиночитаемой записи того, от чего зависит ваше приложение и что зависит от него. Такие инструменты, как графы зависимостей, каталоги сервисов или даже простой README в вашем репозитории, могут помочь.

Во-вторых, тестируйте интеграцию, а не только юнит-тесты. Юнит-тесты, которые имитируют каждый внешний сервис, не выявят проблем, вызванных реальным поведением зависимостей. Интеграционные тесты, которые запускаются против реальных баз данных, API или очередей сообщений, выявят проблемы до того, как они попадут на продакшен.

В-третьих, используйте функциональные флаги или версионированные API для поддержания обратной совместимости. Если ваша новая версия всё ещё может обслуживать запросы от старых клиентов, у вас будет больше гибкости в порядке развёртывания. Вы можете сначала развернуть новую версию, проверить, что она работает, а затем обновить зависимые сервисы.

В-четвёртых, практикуйте откат. Точно знайте, что нужно сделать, если развёртывание не удалось из-за несоответствия зависимостей. Можете ли вы откатить изменение схемы базы данных? Можете ли вы указать приложению на старую версию API? Наличие проверенного плана отката снижает давление во время развёртывания.

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

#!/bin/bash
# deploy.sh - Обеспечение правильного порядка развёртывания

set -e  # Выход при любой ошибке

echo "Шаг 1: Развёртывание схемы базы данных"
./deploy_database.sh || { echo "Развёртывание базы данных не удалось. Прерывание."; exit 1; }

echo "Шаг 2: Развёртывание бэкенд API"
./deploy_api.sh || { echo "Развёртывание API не удалось. Откат базы данных..."; ./rollback_database.sh; exit 1; }

echo "Шаг 3: Развёртывание фронтенда"
./deploy_frontend.sh || { echo "Развёртывание фронтенда не удалось. Откат API и базы данных..."; ./rollback_api.sh; ./rollback_database.sh; exit 1; }

echo "Все развёртывания успешно завершены."

Практический чек-лист перед следующим развёртыванием

Перед развёртыванием пройдитесь по этому короткому чек-листу:

  • Перечислили ли вы каждую зависимость, которую использует ваше приложение (база данных, API, библиотека)?
  • Знаете ли вы правильный порядок развёртывания для каждой зависимости?
  • Тестировали ли вы новую версию против актуальных версий этих зависимостей?
  • Есть ли план отката для каждой зависимости, которая может сломаться?
  • Сообщили ли вы порядок развёртывания каждой команде, владеющей зависимостью?

Этот чек-лист не является исчерпывающим, но он охватывает наиболее распространённые точки отказа. Если вы можете ответить «да» на все пять вопросов, вы в гораздо лучшем положении, чем большинство команд.

Вывод

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