CI/CD: непрерывная интеграция и непрерывная поставка
Задача, которую решает CI: интеграционный ад
До того как непрерывная интеграция стала нормой, типичный проект выглядел так. Восемь разработчиков две-три недели работают каждый в своей ветке, потом наступает «неделя интеграции»: восемь наборов изменений сливают в одну кодовую базу. Конфликты слияния — самая безобидная часть: их хотя бы видно. Хуже семантические конфликты, когда код собирается, но ведёт себя не так: двое по-разному переименовали один класс, третий поменял смысл флага, четвёртый обновил библиотеку до мажорной версии. Сборка на этой неделе — событие, которое планируют, и человек, который её «чинит», работает на полную ставку.
Ключевое наблюдение: стоимость интеграции растёт нелинейно от времени, которое ветки живут раздельно. Два дня расхождения — десять минут разбирательства, три недели — три дня и потерянные изменения. Значит, лекарство не «лучше сливать», а «сливать чаще, пока сливать дёшево».
Мартин Фаулер формулирует непрерывную интеграцию так: участники команды интегрируют свою работу как минимум раз в день, и каждая интеграция проверяется автоматической сборкой, включая тесты (martinfowler.com/articles/continuousIntegration.html). Обратите внимание, чего в определении нет: слов «Jenkins», «GitHub Actions», «пайплайн». CI — это в первую очередь дисциплина работы с ветками и только во вторую — сервер сборки. Команда с идеальным пайплайном и трёхнедельными фича-ветками не практикует CI, она просто запускает тесты.
Эта глава — про CI/CD как инженерную практику и про устройство конвейера по существу, без привязки
к вендору. За конкретными платформами и их синтаксисом идите в трек devops:
01-ci-fundamentals,
02-jenkins,
03-modern-ci-platforms.
Три термина, которые постоянно путают
Continuous integration (CI) — все интегрируются в общую ветку минимум раз в день, каждая интеграция автоматически собирается и проверяется. Ответ на вопрос «работает ли код вместе».
Continuous delivery (CD) — каждое изменение, прошедшее конвейер, готово к выпуску в любой момент; выпуск запускается решением человека (одна кнопка, а не полдня работы). Ответ на вопрос «можем ли мы выпустить прямо сейчас».
Continuous deployment — то же самое, но человеческого шага нет: прошёл конвейер — уехал в прод. Ответ на вопрос «выпускаем ли мы автоматически».
Разница между вторым и третьим — ровно одна кнопка, и она не про технику, а про регуляторные требования, договоры с клиентами и зрелость мониторинга. Первоисточник обеих практик — книга Джеза Хамбла и Дэйва Фарли Continuous Delivery (2010), continuousdelivery.com.
и проверен"] end subgraph CDel["Continuous delivery: готово к релизу в любой момент"] D["Деплой в staging"] --> E["Приёмочные и медленные тесты"] E --> F{"Кнопка релиза"} end subgraph CDep["Continuous deployment: без ручного шага"] G["Прод"] end C --> D F -->|"нажимает человек"| G E -.->|"кнопки нет"| G
Практический вывод: нельзя «внедрить CD», не имея CI. Конвейер, который раз в две недели выкатывает трёхнедельную ветку, — это автоматизированный релизный процесс, а не continuous delivery.
Как CI меняет работу разработчика
Это главное, что теряется, когда CI сводят к настройке раннеров. Меняются не инструменты, а привычки.
- Мелкие изменения в общую ветку минимум раз в день. Не «коммит в свою ветку», а именно интеграция. Ветка живёт часы, максимум день-полтора.
- Зелёный
main— правило, а не пожелание. Сломанныйmainблокирует всех: никто не может отличить свою поломку от чужой. - Сломал сборку — чинишь первым делом. Не «после обеда», не «допишу фичу и посмотрю». Практика, которая делает это возможным: откат мержа как нормальная, не стыдная операция. Откатить и спокойно разобраться дешевле, чем чинить в спешке на глазах у команды.
- Перед push — прогон быстрых проверок локально. Это уважение к чужому времени и к очереди раннеров.
- Разбираться в падении обязан автор изменения, а не «дежурный по CI».
Отсюда прямая связь с двумя другими главами трека: Контроль версий как инженерная практика — про стратегии ветвления, короткоживущие ветки и trunk-based development; Качество в потоке — про то, какие именно ворота стоят в этом конвейере и сколько они стоят.
Незавершённая работа в общей ветке: ветки против флагов
Возникает честный вопрос: если я интегрируюсь каждый день, а фича готова через три недели, что
попадает в main? Есть ровно два ответа, и у каждого своя цена.
Длинная фича-ветка. Код живёт отдельно, main чист. Платим конфликтами слияния, которые растут
нелинейно, отсутствием обратной связи (ветка не проверяется на интеграцию с чужими изменениями)
и болезненным «днём слияния». Работает для небольших фич на 1–3 дня и для команд, где области
изменений почти не пересекаются.
Фича-флаг. Незавершённый код едет в main и в прод, но выключен. Платим комбинаторикой
(два флага — четыре состояния, десять — тысяча; тестировать все невозможно), риском случайно включить
недоделанное и техдолгом самих флагов, если их не удалять. Работает для крупных изменений,
для постепенной раскатки и для A/B-экспериментов.
// Флаг объявлен в одном месте: тип не даёт использовать несуществующий ключ,
// а реестр рядом хранит владельца и дату, после которой флаг обязан исчезнуть.
type FlagKey = "checkout-v2" | "recommendations-ml";
export const FLAG_REGISTRY: Record<FlagKey, { owner: string; removeBy: string }> = {
"checkout-v2": { owner: "payments", removeBy: "2026-09-01" },
"recommendations-ml": { owner: "growth", removeBy: "2026-08-15" },
};
interface FlagClient {
isEnabled(key: FlagKey, ctx: { userId: string }): Promise<boolean>;
}
// Одна точка ветвления, а не десять проверок, размазанных по слоям.
export async function checkout(order: Order, flags: FlagClient): Promise<Receipt> {
const useV2 = await flags.isEnabled("checkout-v2", { userId: order.userId });
return useV2 ? checkoutV2(order) : checkoutV1(order);
}
Правило удаления — часть работы, а не благое пожелание. Рабочая схема: у каждого флага есть
владелец и дата; тест в CI падает, если сегодняшняя дата больше removeBy; после полной раскатки
(100 % и неделя без инцидентов) заводится задача «убрать флаг», которая делается тремя шагами:
инвертировать значение по умолчанию → удалить ветку checkoutV1 вместе с её тестами → удалить сам
ключ из реестра. Флаги, живущие годами, — это if-и, о смысле которых никто уже не помнит,
и второй, невидимый набор конфигураций прода.
Инструменты управления флагами сегодня: LaunchDarkly (коммерческий лидер), Unleash и Flagsmith (open source, можно поднять у себя), а также OpenFeature — спецификация и SDK под эгидой CNCF, которая даёт единый API поверх любого провайдера, чтобы не привязываться к вендору. Флаги как механизм выката подробнее — в devops/04-cd-and-release-strategies.
Анатомия конвейера
Ядро главы. Конвейер — не «скрипт, который всё делает», а последовательность стадий с явными входами, выходами и правилами прохода.
- Checkout — получение исходников на фиксированном коммите. Никаких «последних версий чего-то».
- Восстановление кэша — зависимости, скачанные в прошлый раз. Ключ кэша строится из хэша lock-файла: изменился lock — кэш обновился. Про lock-файлы и воспроизводимость — Сборка и зависимости.
- Сборка — компиляция, транспиляция, генерация кода.
- Быстрые тесты — unit и лёгкие интеграционные, параллельно и шардированно.
- Упаковка артефакта — исполняемый файл, jar, wheel, контейнерный образ. Тегируется хэшем коммита, а не «latest».
- Медленные тесты — e2e, контрактные, нагрузочные, длинные сканы безопасности.
- Публикация артефакта в реестр или хранилище — теперь он неизменяем и адресуем.
- Деплой в стенд — тот же самый артефакт плюс конфигурация окружения.
- Smoke-проверка — минимальный набор: сервис поднялся, healthcheck зелёный, ключевой сценарий отвечает.
- Продвижение в прод — тот же артефакт, другая конфигурация.
Три принципа, которые отличают конвейер от набора скриптов:
Собрать один раз — продвигать тот же артефакт. Пересборка «для прода» означает, что вы тестировали не то, что выпускаете: другая версия компилятора, другой transitive-зависимости, другой таймстемп. Артефакт создаётся ровно один раз и дальше только перемещается между окружениями. Всё, что отличается между окружениями, — это конфигурация, а не бинарник (Окружения, конфигурация и секреты).
Конвейер как код в репозитории. Описание стадий лежит рядом с кодом, версионируется, проходит ревью и меняется в том же PR, что и код, которому оно нужно. Конвейер, настроенный кликами в UI, — это неповторяемая инфраструктура, о состоянии которой знает один человек.
Падать рано и быстро. Стадии упорядочены по возрастанию стоимости: линт до тестов, unit до e2e. Любая красная стадия останавливает конвейер — нет смысла тратить 30 минут на e2e, если не собралось.
Жизненный цикл самого артефакта удобно держать в голове как конечный автомат: он проясняет, что именно значит «продвижение» и куда ведёт откат.
Конвейер как код: рабочий пример
Ниже реальный, а не псевдо-конфиг для GitHub Actions: кэш, матрица версий, разделение на быстрые и медленные джобы, сборка образа один раз.
name: ci
on:
push:
branches: [main]
pull_request:
concurrency: # новый push в тот же PR отменяет прошлый прогон
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE: ${{ github.repository }}
jobs:
fast: # быстрые ворота: только они блокируют merge
runs-on: ubuntu-24.04
timeout-minutes: 10 # бюджет времени — часть контракта конвейера
strategy:
fail-fast: false # хотим видеть все падения матрицы разом
matrix:
python: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip # ключ кэша строится из хэша lock-файла
- run: pip install -e ".[dev]"
- run: ruff check . && ruff format --check .
- run: mypy src
- run: pytest -m "not slow" -n auto --junitxml=report.xml
- uses: actions/upload-artifact@v4
if: always() # отчёт нужен особенно тогда, когда всё красное
with:
name: junit-${{ matrix.python }}
path: report.xml
image: # артефакт собирается ОДИН раз и на всю жизнь
needs: fast
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
id-token: write # OIDC вместо долгоживущих секретов
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} # тег = коммит
cache-from: type=gha
cache-to: type=gha,mode=max
slow: # медленное не задерживает обратную связь на PR
needs: image
if: github.event_name == 'push' # только после merge в main
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- run: ./scripts/e2e.sh ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
Что здесь принципиально, помимо синтаксиса: тег образа равен SHA коммита (артефакт адресуем
и неизменяем); needs задаёт порядок и позволяет медленному не блокировать быстрое; timeout-minutes
превращает бюджет времени в проверяемое обязательство; concurrency экономит раннеры; права джобы
выданы минимально необходимые. Разбор конкретных платформ (GitLab CI, Jenkins, Buildkite) —
в devops/03-modern-ci-platforms.
Непрерывное тестирование — больше, чем автоматизация тестов
Автоматизировать тесты и встроить их в конвейер — разные вещи. Автоматизация даёт скрипт, который можно запустить; непрерывное тестирование означает, что результат каждого изменения известен автоматически и влияет на решение о продвижении.
Отсюда естественное разделение: то, что не требует развёрнутого окружения, принадлежит CI (unit, контрактные, статический анализ, аудит зависимостей); то, что требует поднятой системы, принадлежит CD и выполняется после развёртывания в окружение (e2e, тестирование производительности, динамическое сканирование безопасности, проверки миграций на копии реальных данных). Ключевое техническое требование к любому такому тесту одинаково: он должен запускаться из командной строки и возвращать однозначный код выхода — тогда его можно поставить в конвейер. Подробно — testing/13-tests-in-ci и testing/09-performance-testing.
Результаты конвейера должны быть видимыми: дашборд состояния веток и стендов, уведомление
в канал команды при падении main, ссылка со сборки на изменения и задачи, которые в неё вошли.
Уведомление о падении, которое приходит всем и всегда, через месяц игнорируется всеми — оповещать
надо адресно (автора изменения и владельца сервиса) и только о том, что требует действия. Это тот же
принцип, что в sre/05-alerting.
Раннеры и окружение сборки
Сборочный агент — полноценная часть продакшена, и относиться к нему надо соответственно.
Эфемерность. Раннер создаётся под задачу и уничтожается после. Долгоживущий агент накапливает состояние: остатки кэшей, установленные вручную пакеты, чужие временные файлы — и сборка начинает зависеть от того, на какую машину попала. Классический симптом: «на раннере №3 не собирается».
Изоляция. Джобы разных репозиториев и разных PR не должны делить файловую систему, сеть и переменные окружения. Особый случай — PR из форков: их код не заслуживает доступа к секретам.
Доступ к секретам. Долгоживущие токены в переменных CI — плохая практика; современный подход — короткоживущие учётные данные через OIDC, выдаваемые под конкретную джобу с конкретными правами. Секреты маскируются в логах, но не полагайтесь на маскирование как на защиту: значение, склеенное из частей или закодированное в base64, маскировщик не поймает. Подробнее — security/12-secrets-management.
Раннер как цель атаки. У сборочной системы есть доступ к исходникам, к реестру артефактов и часто к продакшену — это идеальная точка для атаки на цепочку поставок. Отсюда подпись артефактов, SBOM, закрепление версий действий/плагинов по хэшу, а не по тегу, и запрет произвольных скриптов из зависимостей. См. devops/17-security-in-pipeline и security/13-supply-chain.
Стратегии выката: обзор
Конвейер доводит артефакт до прода, но как именно он туда попадает — отдельное инженерное решение.
- Rolling — экземпляры обновляются порциями. Просто, но какое-то время в проде работают две версии сразу: контракты и схема БД должны это выдерживать.
- Blue-green — два полных окружения, переключение трафика целиком. Мгновенный откат, платим двойными ресурсами и сложностью с состоянием.
- Canary — 1–5 % трафика на новую версию, автоматическое решение по метрикам ошибок и задержек. Лучшее соотношение риска и стоимости, требует зрелого мониторинга.
- Feature flags — выкат кода и включение функции разделены во времени; самый гибкий вариант и единственный, позволяющий включать функцию для конкретных пользователей.
Общее для всех: откат — первоклассная операция, а не аварийная процедура. Он должен быть однокнопочным, отрепетированным и не требовать пересборки. Если откат возможен только «выкатить предыдущий коммит и подождать 40 минут», у вас нет отката. Глубина — в devops/04-cd-and-release-strategies и sre/13-release-safety.
CI/CD с контейнерами, Kubernetes и бессерверными вычислениями
Контейнеры сделали для CI/CD одну важную вещь: стандартизировали артефакт. До них «упаковка» означала jar плюс скрипты плюс инструкция; после — один образ с зафиксированным окружением, который одинаково запускается на ноутбуке, стенде и в проде. Реестр образов стал естественным хранилищем артефактов с адресацией по хэшу (devops/05-containers-and-registries).
Kubernetes добавил декларативность: вы описываете желаемое состояние, а контроллер приводит систему
к нему. Это меняет смысл стадии «деплой» — она превращается в «обнови описание желаемого состояния».
Отсюда логичное продолжение — GitOps: желаемое состояние всех окружений лежит в Git, а агент
в кластере (Argo CD, Flux) непрерывно сверяет фактическое состояние с описанным и устраняет
расхождение. Плюсы прямо следуют из свойств Git: полная история изменений инфраструктуры, ревью
через PR, откат через git revert, отсутствие людей с прямым доступом к кластеру. Подробно —
devops/06-kubernetes и
devops/08-helm-and-gitops.
Бессерверные вычисления меняют другое: артефактом становится не образ и не машина, а функция и её конфигурация, а масштабирование и инфраструктура уезжают к провайдеру. Конвейер при этом упрощается (нет стадии «поднять окружение»), но появляются свои сложности: холодный старт, версионирование функций и алиасов для канареечного выката, тестирование интеграций с управляемыми сервисами. См. architecture-patterns/10-serverless-and-edge.
Измерение: четыре метрики DORA и их пределы
Программа DORA (позже — исследование, обобщённое в книге Accelerate Форсгрен, Хамбла и Кима) выделила четыре показателя, по которым различаются высоко- и низкоэффективные команды:
| Метрика | Что означает | Как на неё влияет CI/CD |
|---|---|---|
| Частота развёртывания | как часто изменения доезжают до пользователей | автоматизация убирает стоимость одного релиза |
| Lead time для изменений | от коммита до работы в проде | конвейер сокращает ожидание и ручные шаги |
| Время восстановления (MTTR) | как быстро чинят сбой | быстрый откат и повторный выкат |
| Доля неудачных изменений | какая часть релизов ломает прод | ворота ловят регрессии до пользователей |
Ключевое наблюдение исследования: первые две метрики (скорость) и вторые две (стабильность) не противоречат друг другу — команды, которые выпускают чаще, ломают реже. Это контринтуитивно и разрушает аргумент «давайте релизиться реже, будет надёжнее»: реже релизы — крупнее изменения — выше риск каждого и дороже отладка.
Пределы этих метрик стоит знать. Они измеряют поток поставки, а не ценность: команда может идеально быстро выпускать никому не нужные функции. Они легко геймятся (дробите развёртывания — частота растёт). И они молчат о качестве кода, техдолге и удовлетворённости пользователей. DORA — хороший индикатор здоровья инженерного процесса и плохой единственный KPI. Разбор — project-management/07-dora-and-engineering-metrics.
CI/CD исторически выросла из DevOps именно как способ снять противоречие между разработкой, которая хочет менять систему часто, и эксплуатацией, которой нужна стабильность. Автоматизация и одинаковый для всех окружений процесс дают обеим сторонам то, что они просят.
Типичные ошибки
- Пересборка артефакта для каждого окружения. Вы выпускаете не то, что тестировали. Собирайте один раз, продвигайте тот же артефакт, различия выносите в конфигурацию.
- Секреты в логах сборки. Достаточно одного
echo $TOKENилиset -xперед вызовом с ключом. Лечится маскированием, короткоживущими токенами через OIDC и запретом на печать окружения. - Конвейер, который нельзя воспроизвести локально. Отладка превращается в игру с ходом в восемь минут. Все шаги должны запускаться одной командой на ноутбуке, желательно в контейнере.
- Ручные шаги «только Вася знает». Недокументированный шаг — это не процесс, а зависимость от человека; он же становится узким местом в ночь инцидента.
- Красный
mainкак норма. Если сломанная основная ветка перестала быть событием, вы потеряли главный сигнал: никто больше не отличает свою поломку от чужой. - «По пятницам не деплоим» как решение. Табу на день недели лечит симптом. Настоящая проблема — ненадёжный откат: если вернуть предыдущую версию можно за минуту и это отрепетировано, день недели перестаёт иметь значение.
- Конвейер, настроенный кликами в UI. Не версионируется, не ревьюится, не восстанавливается. Конвейер — код, лежит в репозитории.
- Один гигантский конвейер на 40 минут для всего. Разделите на быстрый обязательный контур и медленный фоновый, иначе платить будет каждый PR.
Мини-итог
- CI — это дисциплина: интеграция в общую ветку минимум раз в день с автоматической проверкой. Сервер сборки вторичен по отношению к привычке сливать часто и мелко.
- Continuous delivery — «можем выпустить в любой момент», continuous deployment — «выпускаем автоматически». Разница ровно в одном человеческом шаге.
- Незавершённую работу в общей ветке прячут либо ветками (платим конфликтами), либо флагами (платим комбинаторикой и техдолгом); у флага обязаны быть владелец и дата удаления.
- Артефакт собирается один раз, тегируется хэшем коммита и продвигается между окружениями без пересборки; различия окружений — только конфигурация.
- Конвейер — код в репозитории; стадии упорядочены по стоимости и падают рано.
- Часть тестов принадлежит CI, часть — CD: то, что требует развёрнутой системы, идёт после деплоя.
- Раннер эфемерен, изолирован и не хранит долгоживущих секретов — это часть периметра безопасности.
- Откат — первоклассная однокнопочная операция; без него любые стратегии выката декоративны.
- Метрики DORA хорошо показывают здоровье потока поставки и плохо работают как единственный KPI.
Источники
- Martin Fowler. Continuous Integration
- Jez Humble, David Farley. Continuous Delivery (2010), continuousdelivery.com
- Nicole Forsgren, Jez Humble, Gene Kim. Accelerate (2018) — исследование DORA
- OpenFeature — открытый стандарт API для фича-флагов (CNCF)
- Документация GitHub Actions и GitLab CI/CD
Что дальше
Окружения, конфигурация и секреты — что именно отличает staging от прода, как конфигурация попадает в приложение и почему секреты не живут в репозитории.