CI/CD: что это и как устроено развертывание приложения

CI/CD: что это и как устроено развертывание приложения Полезное

CI/CD — это практика, при которой каждое изменение кода автоматически собирается и тестируется (CI, continuous integration), а готовая сборка автоматически доводится до серверов (CD). За буквами CD стоят два разных режима: continuous delivery и continuous deployment. Ниже разобрана именно вторая половина: что происходит со сборкой после тестов, как выкатывать без простоя и как откатываться.

Пример из статьи проверен 30.09.2026 в Ubuntu 24.04 (bash 5.2.21, GNU coreutils 9.4).

Четыре термина, которые путают

  • Continuous integration (CI) — изменения часто вливаются в общую ветку, каждое проходит сборку и автотесты. Результат — проверенный артефакт. Подробно эта часть разобрана в статье про непрерывную интеграцию.
  • Continuous delivery — артефакт в любой момент готов к выкату и уже проверен на тестовом стенде, но в production его отправляет человек: кнопкой или согласованием.
  • Continuous deployment — той же кнопки нет: сборка, прошедшая все проверки, попадает в production сама.
  • Release (релиз) — момент, когда новую функцию видят пользователи. Он не обязан совпадать с выкатом: код можно выкатить выключенным и включить позже флагом (feature flag).

Отличие delivery от deployment — один шаг. Во фрагменте конфигурации GitLab CI/CD его задает строка when: manual:

stages:
  - staging
  - production

deploy_staging:
  stage: staging
  environment: staging
  script: ./deploy.sh deploy "$CI_COMMIT_TAG" build/
  rules:
    - if: $CI_COMMIT_TAG

deploy_production:
  stage: production
  environment: production
  script: ./deploy.sh deploy "$CI_COMMIT_TAG" build/
  rules:
    - if: $CI_COMMIT_TAG
      when: manual

Пока строка есть, задание ждет нажатия в интерфейсе — это continuous delivery. Убрали ее — и каждый тег уходит в production без участия человека: continuous deployment. Это фрагмент, а не полный пайплайн: заданий сборки и тестов в нем нет.

Задания разнесены по двум этапам не случайно. Задания одного этапа GitLab запускает параллельно, а следующий этап начинается только после успеха предыдущего. Если оба выката поставить в один этап и убрать when: manual, production обновится одновременно со стендом, не дожидаясь его проверки. Еще одна деталь: скрипт из примера ниже принимает версию только вида 1.2.3, поэтому тег v1.2.3 он отклонит.

Выбор режима зависит не от «зрелости» команды вообще, а от того, чему вы доверяете. Deployment требует автотестов, которым верят, и быстрого отката. Delivery оставляют там, где выкат нужно согласовывать: регулируемые отрасли, мобильные приложения с проверкой в магазине, редкие окна обновления.

Этапы развертывания после сборки

  1. Публикация артефакта. Сборка (образ контейнера, архив, пакет) получает версию и кладется в хранилище. Дальше по всем средам едет один и тот же артефакт — его не пересобирают под production.
  2. Выкат на тестовый стенд (staging). Среда, близкая к боевой по конфигурации.
  3. Проверки на стенде. Дымовые (smoke) тесты: приложение стартует и отвечает; затем приемочные сценарии.
  4. Согласование — только в режиме delivery.
  5. Выкат в production по выбранной стратегии.
  6. Проверка после выката. Проверка работоспособности (health check), доля ошибок, время ответа.
  7. Решение: оставить версию или откатить.

Пример: выкат с проверкой и откатом

Самая простая схема без контейнеров и оркестратора: каждая версия лежит в своем каталоге, а ссылка current указывает на рабочую. Выкат — это перевод ссылки, откат — перевод обратно. Сохраните скрипт как deploy.sh:

#!/usr/bin/env bash
# deploy.sh - учебный выкат: каталоги релизов и атомарное переключение ссылки current
set -euo pipefail

APP_ROOT="${APP_ROOT:?задайте APP_ROOT - каталог песочницы}"
RELEASES="$APP_ROOT/releases"
CURRENT="$APP_ROOT/current"

current_version() {
  if [ -L "$CURRENT" ]; then basename "$(readlink "$CURRENT")"; else echo "нет"; fi
}

switch_to() {   # rename(2) заменяет ссылку одним шагом: состояния "current отсутствует" нет
  ln -s "releases/$1" "$APP_ROOT/current.new"
  mv -T "$APP_ROOT/current.new" "$CURRENT"
}

fail() { echo "отказ: $1" >&2; exit "${2:-2}"; }

case "${1:-}" in
  deploy)
    version="${2:?версия, например 1.0.0}"
    artifact="${3:?каталог с артефактом сборки}"
    case "$version" in ""|*[!0-9.]*) fail "версия должна быть вида 1.2.3" ;; esac
    echo "$version" | grep -Eqx '[0-9]+\.[0-9]+\.[0-9]+' || fail "версия должна быть вида 1.2.3"
    [ -x "$artifact/healthcheck" ] || fail "в артефакте нет исполняемого healthcheck"
    [ ! -e "$RELEASES/$version" ] || fail "релиз $version уже есть, версии не перезаписываются"
    mkdir -p "$RELEASES"
    cp -R "$artifact" "$RELEASES/$version"
    "$RELEASES/$version/healthcheck" \
      || fail "проверка релиза $version не прошла, current остался на $(current_version)" 1
    previous="$(current_version)"
    switch_to "$version"
    [ "$previous" = "нет" ] || echo "$previous" > "$APP_ROOT/previous"
    echo "выкат: $previous -> $version"
    ;;
  rollback)
    [ -s "$APP_ROOT/previous" ] || fail "предыдущий релиз не записан" 1
    target="$(cat "$APP_ROOT/previous")"
    "$RELEASES/$target/healthcheck" || fail "релиз $target не проходит проверку" 1
    bad="$(current_version)"
    switch_to "$target"
    rm "$APP_ROOT/previous"
    echo "откат: $bad -> $target"
    ;;
  *) fail "использование: deploy.sh deploy <версия> <каталог> | deploy.sh rollback" ;;
esac

Проверочный сценарий работает только во временных каталогах из mktemp, ваши файлы он не трогает. Три «сборки» — это каталоги с программой app и проверкой healthcheck; у версии 1.2.0 проверка намеренно сломана. Сохраните как demo.sh рядом и запустите bash demo.sh:

export APP_ROOT="$(mktemp -d /tmp/cd-demo.XXXXXX)"
build="$(mktemp -d /tmp/cd-build.XXXXXX)"

make_artifact() {   # $1 - версия, $2 - код возврата проверки
  mkdir "$build/$1"
  printf '#!/bin/sh\necho "приложение %s"\n' "$1" > "$build/$1/app"
  printf '#!/bin/sh\nexit %s\n' "$2" > "$build/$1/healthcheck"
  chmod +x "$build/$1/app" "$build/$1/healthcheck"
}
make_artifact 1.0.0 0
make_artifact 1.1.0 0
make_artifact 1.2.0 1   # сломанный релиз: проверка возвращает 1

bash deploy.sh deploy 1.0.0 "$build/1.0.0"
bash deploy.sh deploy 1.1.0 "$build/1.1.0"
bash deploy.sh deploy 1.2.0 "$build/1.2.0" || echo "код возврата: $?"
"$APP_ROOT/current/app"
bash deploy.sh rollback
"$APP_ROOT/current/app"
bash deploy.sh rollback || echo "код возврата: $?"
bash deploy.sh deploy ../etc "$build/1.0.0" || echo "код возврата: $?"

Вывод:

выкат: нет -> 1.0.0
выкат: 1.0.0 -> 1.1.0
отказ: проверка релиза 1.2.0 не прошла, current остался на 1.1.0
код возврата: 1
приложение 1.1.0
откат: 1.1.0 -> 1.0.0
приложение 1.0.0
отказ: предыдущий релиз не записан
код возврата: 1
отказ: версия должна быть вида 1.2.3
код возврата: 2

Что здесь видно:

  • Сломанная версия 1.2.0 до пользователей не дошла: проверка идет до переключения, рабочей осталась 1.1.0.
  • Откат — это возврат к записанному состоянию. Скрипт запоминает предыдущую версию в момент выката и не гадает, какая была «до».
  • Второй откат подряд отклонен: возвращаться некуда, и скрипт говорит об этом, а не переключает наугад.
  • Версия ../etc отклонена: иначе имя версии превратилось бы в путь за пределы каталога релизов.

Границы примера. Команда mv -T есть в GNU coreutils (Linux); в macOS у штатного mv этого ключа нет. Проверка здесь — просто код возврата, а в реальном сервисе это HTTP-запрос к запущенному процессу, и после переключения процесс еще нужно перезапустить или перечитать. Каталог релиза, не прошедшего проверку, остается в releases, поэтому исправление выпускают под новым номером версии. Скрипт хранит один шаг отката и не защищен от двух одновременных запусков — в production нужна блокировка и история выкатов.

Стратегии выката: rolling, blue-green, canary

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

Стратегия Как идет выкат Что с простоем Откат Цена
Recreate Остановить старую версию, запустить новую Простой есть Повторный выкат старой версии Минимум ресурсов
Rolling Экземпляры заменяются по одному или группами Обычно без простоя Обратная прокатка, занимает время Небольшой запас мощности
Blue-green Две полные среды, трафик переводят целиком Обычно без простоя Вернуть трафик на прежнюю среду Двойные ресурсы на время выката
Canary Новая версия получает малую долю трафика, доля растет по метрикам Обычно без простоя Убрать долю трафика Нужны балансировка по долям и метрики

Пример со ссылкой current ближе всего к blue-green: обе версии лежат рядом, переключение мгновенное. В Kubernetes у объекта Deployment по умолчанию действует стратегия RollingUpdate, откат к прошлой ревизии выполняет kubectl rollout undo. Blue-green и canary в Deployment не встроены: их собирают переключением сервисов, входным балансировщиком или отдельными контроллерами.

У rolling и canary есть общее условие: какое-то время работают обе версии. Значит, новая версия обязана быть совместима со старой по формату данных и API.

Откат и база данных

Откат кода не откатывает данные. Если новая версия удалила столбец или поменяла формат, старая после возврата может не запуститься. Поэтому изменения схемы разносят на шаги: сначала добавить новое так, чтобы работали обе версии, выкатить код, и только отдельным поздним выкатом убрать старое.

Второй путь — не возвращаться, а быстро выпустить исправление (roll forward). Он годится, когда пайплайн проходит за минуты и причина понятна. Если причина неясна, сначала возвращают рабочую версию, потом разбираются.

Безопасность развертывания

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

Риск Что делают
Утечка ключей выката Секреты хранят в защищенных переменных CI-системы или хранилище секретов, не в репозитории и не в аргументах команд
Выкат из чужой ветки Задания production запускаются только из защищенных веток и тегов, среда production ограничена по ролям
Подмена артефакта Артефакт неизменяем, выкатывается по версии или контрольной сумме, а не по плавающей метке вроде latest
Избыточные права Учетная запись выката умеет только выкатывать это приложение, без прав администратора
Неизвестно, кто что выкатил Журнал выкатов: версия, автор, время, результат проверки

Проверки безопасности самого кода и зависимостей (статический анализ, поиск секретов, сканирование образов) ставят раньше, на этапе CI: до выката доходит уже проверенный артефакт.

Если выкат не получился

  • Сайт отдает ошибки сразу после выката — откатить, потом читать журнал приложения. Частая причина — различие конфигурации staging и production.
  • На стенде работает, в production нет — сравнить переменные окружения и версии зависимых сервисов; убедиться, что выкатывался тот же артефакт.
  • Откат не помог — смотреть миграции базы и кеши: старая версия могла получить данные нового формата.
  • Часть пользователей видит старую версию — rolling еще не завершен или запросы кешируются на балансировщике или CDN.

Выводы

  • CI дает проверенный артефакт, CD доводит его до серверов; continuous delivery оставляет последний шаг человеку, continuous deployment — нет.
  • После сборки идут: публикация артефакта, выкат на стенд, проверки, выкат в production, проверка после выката, решение об откате.
  • Rolling экономит ресурсы, blue-green дает самый быстрый откат, canary ограничивает число пользователей, затронутых ошибкой.
  • Откат нужно готовить заранее: записанная предыдущая версия, проверка перед переключением, совместимые изменения схемы базы.
  • Доступ к production у пайплайна — отдельная зона риска: секреты, права и происхождение артефакта контролируют явно.

Где применяется / связь с практикой

Освойте тему на практике

Пайплайн выката — повседневная работа DevOps-инженера и часть работы любого бэкенд-разработчика: от скрипта с каталогами релизов команды переходят к контейнерам, Kubernetes и описанию инфраструктуры кодом. На курсе «DevOps практики и инструменты» эти темы проходят на практических заданиях. Познакомиться с форматом занятий можно на открытых уроках Otus.

FAQ

Нужен ли Kubernetes, чтобы настроить CD?
Нет. Автоматический выкат возможен на одном сервере скриптом или средствами CI-системы; оркестратор нужен, когда экземпляров много и требуется постепенная замена.

Что такое GitOps?
Подход, при котором желаемое состояние среды описано в Git-репозитории, а агент внутри кластера сам приводит среду к этому описанию. Пайплайн при этом не получает прямого доступа к production.

Как часто выкатывать?
Общей нормы нет. Чем меньше изменений в одном выкате, тем проще найти причину сбоя и тем дешевле откат, поэтому команды стремятся к частым небольшим выкатам.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)