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 оставляют там, где выкат нужно согласовывать: регулируемые отрасли, мобильные приложения с проверкой в магазине, редкие окна обновления.
Этапы развертывания после сборки
- Публикация артефакта. Сборка (образ контейнера, архив, пакет) получает версию и кладется в хранилище. Дальше по всем средам едет один и тот же артефакт — его не пересобирают под production.
- Выкат на тестовый стенд (staging). Среда, близкая к боевой по конфигурации.
- Проверки на стенде. Дымовые (smoke) тесты: приложение стартует и отвечает; затем приемочные сценарии.
- Согласование — только в режиме delivery.
- Выкат в production по выбранной стратегии.
- Проверка после выката. Проверка работоспособности (health check), доля ошибок, время ответа.
- Решение: оставить версию или откатить.
Пример: выкат с проверкой и откатом
Самая простая схема без контейнеров и оркестратора: каждая версия лежит в своем каталоге, а ссылка 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.
Как часто выкатывать?
Общей нормы нет. Чем меньше изменений в одном выкате, тем проще найти причину сбоя и тем дешевле откат, поэтому команды стремятся к частым небольшим выкатам.



