Инженерная практика CI/CD: непрерывная интеграция и непрерывная поставка
0%

CI/CD: непрерывная интеграция и непрерывная поставка

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.

Практический вывод: нельзя «внедрить 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.

Анатомия конвейера

Ядро главы. Конвейер — не «скрипт, который всё делает», а последовательность стадий с явными входами, выходами и правилами прохода.

  1. Checkout — получение исходников на фиксированном коммите. Никаких «последних версий чего-то».
  2. Восстановление кэша — зависимости, скачанные в прошлый раз. Ключ кэша строится из хэша lock-файла: изменился lock — кэш обновился. Про lock-файлы и воспроизводимость — Сборка и зависимости.
  3. Сборка — компиляция, транспиляция, генерация кода.
  4. Быстрые тесты — unit и лёгкие интеграционные, параллельно и шардированно.
  5. Упаковка артефакта — исполняемый файл, jar, wheel, контейнерный образ. Тегируется хэшем коммита, а не «latest».
  6. Медленные тесты — e2e, контрактные, нагрузочные, длинные сканы безопасности.
  7. Публикация артефакта в реестр или хранилище — теперь он неизменяем и адресуем.
  8. Деплой в стенд — тот же самый артефакт плюс конфигурация окружения.
  9. Smoke-проверка — минимальный набор: сервис поднялся, healthcheck зелёный, ключевой сценарий отвечает.
  10. Продвижение в прод — тот же артефакт, другая конфигурация.

Три принципа, которые отличают конвейер от набора скриптов:

Собрать один раз — продвигать тот же артефакт. Пересборка «для прода» означает, что вы тестировали не то, что выпускаете: другая версия компилятора, другой 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.

Источники

Что дальше

Окружения, конфигурация и секреты — что именно отличает staging от прода, как конфигурация попадает в приложение и почему секреты не живут в репозитории.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов