open atlas
↑ К треку
CI/CD-пайплайны CICD · 07 · 01

Capstone: спроектируй полный CI/CD-пайплайн от и до

Production-пайплайн — это одна цепь. PR-проверки гейтят merge, артефакт собирается один раз с SBOM и подписанной provenance-аттестацией, затем тот же digest продвигается staging → canary → prod с откатом по метрикам. Пропусти звено — цепь рвётся.

CICD Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Релиз выглядел чисто: зелёный PR, зелёная сборка по тегу, staging здоров, один клик аппрува, прод задеплоился успешно. Через сорок минут p99-латентность утроилась, и бюджет ошибок на квартал сгорел за полдня. Разбор нашёл одну строку. Прод-джоб делал свой собственный docker build из тега вместо того, чтобы тянуть digest, который проверил staging, — транзитивная зависимость опубликовала новый патч в зазоре, поэтому прод гонял другой бинарник, не тот, что прошёл каждый гейт. Каждая стадия работала. Цепь — нет, потому что одна стадия пересобрала вместо того, чтобы продвинуть. А ещё не было автоматического отката, так что человеку пришлось заметить, словить пейдж и откатывать руками.

Весь пайплайн — одна цепь, а цепь прочна настолько, насколько прочно её пропущенное звено

К концу урока ты сможешь назвать каждое звено production-пайплайна, объяснить, что оно гарантирует, и мгновенно распознать, какое пропущенное звено стоит за любым инцидентом с релизом.

Каждый прошлый урок трека был одним звеном: пайплайны, тестирование в CI, стратегии доставки, окружения и аппрувы, автоматизация релизов, supply-chain provenance, масштабирование. Capstone-пайплайн — это те звенья, сваренные по порядку, и интегративный режим отказа всегда один: любое пропущенное звено молча ломает гарантию, которую цепь должна была дать целиком. Пересобираешь на каждой стадии — твоё обещание «протестированного артефакта» ложь. Деплоишь без пути отката — твой canary это просто медленный аутаж. Отгружаешь непроверенный артефакт — твои SBOM и тесты описывают не тот бинарник, что работает. Дисциплина сеньорского пайплайна не в добавлении стадий — в том, чтобы ни одна стадия тихо не подменяла гарантию на более слабую.

Есть три несущих инварианта. (1) PR-проверки — это гейт merge’а — быстрая обратная связь первой, required status checks блокируют merge, поэтому main всегда релизопригодна. (2) Сборка один раз — на merge или теге артефакт собирается ровно один раз, давая один неизменяемый digest, и с этого момента ничто не пересобирается; он только продвигается. (3) Продвигай тот же digest — staging, canary и production деплоят идентичный image@sha256:…, поэтому единственная переменная между окружениями — конфигурация, никогда не бинарник. Держи эти три — и большинство сюрпризов дня релиза становятся невозможны по построению.

Стадия 1 — PR-проверки: падай быстро, потом доказывай, потом блокируй merge

У PR-гейта есть форма: самые дешёвые, самые вероятные на падение проверки первыми. Lint и typecheck бегут за секунды и ловят большинство ошибок, поэтому идут первыми и закорачивают дорогую работу. Дальше affected-only сборка и тесты — в монорепо ты тестируешь то, что трогает diff, а не весь мир (тут окупается урок про масштабирование). Настрой их как required status checks на ветке, чтобы красная проверка физически блокировала кнопку merge; проверка, которая только репортит, — это совет, не гейт. PR-стадия никогда не деплоит и не подписывает — её единственная задача держать main в режиме «merge-пригодна значит релизопригодна».

name: ci
on:
  pull_request:
permissions:
  contents: read          # least privilege: PR-проверкам больше ничего не нужно
jobs:
  fast-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29  # v4 запинено по SHA
      - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a  # v4 запинено по SHA
        with: { node-version: '22', cache: 'npm' }
      - run: npm ci
      - run: npm run lint && npm run typecheck   # секунды; fail fast
      - run: npx nx affected -t build test       # affected-only, не весь мир

Стадия 2 — Сборка один раз: один digest, SBOM, подписанная provenance

Это разворотная точка всего пайплайна. На merge в main (или на релизном теге) ты собираешь образ один раз и пушишь его по digest. В том же ране ты генерируешь SBOM (что внутри), производишь build-provenance-аттестацию (как, где и из какого коммита он собран) и подписываешь их keyless-cosign через Sigstore — OIDC-токен GitHub является подписывающей личностью, поэтому нет долгоживущего ключа, который можно утечь. Это ровно та цепочка, что зарабатывает SLSA (Supply-chain Levels for Software Artifacts — фреймворк уровней доверия к цепочке поставок) Build L2 (а L3 — с изолированным эфемерным билдером на reusable workflow): хостовый, закалённый билдер автоматически генерирует и подписывает provenance. Два пункта без компромиссов для сеньорского workflow: пинни сторонние actions по полному SHA коммита (сдвинутый тег — это supply-chain-бэкдор) и выдавай least-privilege permissions: на каждый джоб, добавляя id-token: write только тому джобу, которому реально нужен OIDC, и attestations: write только там, где аттестуешь.

  build-once:
    needs: fast-gate
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write        # push в GHCR
      id-token: write         # OIDC для keyless-подписи
      attestations: write     # provenance-аттестация
    outputs:
      digest: ${{ steps.push.outputs.digest }}
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29  # v4
      - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcd730a4f6c4f  # v6
        id: push
        with:
          push: true
          tags: ghcr.io/acme/api:${{ github.sha }}
      - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be  # v1
        with:
          subject-name: ghcr.io/acme/api
          subject-digest: ${{ steps.push.outputs.digest }}
          push-to-registry: true
      - uses: anchore/sbom-action@d94f46e13c6c62f59525ac9a1e147a99dc0b9bf5  # SPDX SBOM
        with: { image: ghcr.io/acme/api@${{ steps.push.outputs.digest }} }

Стадии 3–4 — Релиз, затем продвижение того же digest с гейтами и откатом

Релиз — это бухгалтерия поверх проверенного артефакта: выводи версию из conventional commits (semver), тегай и публикуй подписанный образ в реестр. Дальше деплой — это чистое продвижение одного digest. Staging авто-деплоит api@${{ digest }} и гоняет smoke-тесты. Production — отдельное GitHub-окружение с правилами защиты: required reviewers и настройка «без само-аппрува» превращают деплой в осознанный, аудируемый человеческий гейт. За гейтом ты не переключаешь 100% трафика; ты делаешь canary: маршрутизируешь маленький срез, даёшь автоматическому анализу несколько минут следить за error rate и латентностью против базовой линии, и расширяешь только если метрический гейт прошёл. Если срабатывает нарушение SLO (Service Level Objective — целевой показатель уровня сервиса, например p99-латентность ≤ 200 мс), раскатка останавливается и автоматически откатывается к предыдущему digest — то самое звено, которого не хватало в пайплайне из истории. Тот же неизменяемый digest течёт через каждую стадию; меняется только конфиг.

СтадияЦельЧто сюда вставляетсяПровал, если пропустить
PR-проверкиДержать main релизопригоднойБыстрый гейт lint/typecheck, affected-only тесты, required status checksСломанный код мёржится; каждая поздняя стадия наследует гниль
Сборка один разОдин неизменяемый digestSBOM, provenance-аттестация, cosign keyless, SHA-пиннинг actions, OIDCПересборка на стадии → прод гонит другой бинарник, не тот, что тестировали
РелизВерсионированный, опубликованный артефактConventional commits → semver, тег, push подписанного образаНет трассируемой версии; не сказать, что задеплоено, и не откатиться вперёд чисто
Поэтапный деплойБезопасно продвинуть тот же digestАппрув окружения, smoke-тесты, canary + метрический гейт, авто-откатНет пути отката → плохой canary это просто медленный, более полный аутаж
  deploy:
    needs: build-once
    uses: ./.github/workflows/promote.yml
    with:
      digest: ${{ needs.build-once.outputs.digest }}   # продвигаем, не пересобираем
  staging:
    needs: build-once
    environment: staging                                # авто-деплой
    runs-on: ubuntu-latest
    steps:
      - run: deploy ghcr.io/acme/api@${{ needs.build-once.outputs.digest }}
      - run: ./smoke-tests.sh
  production:
    needs: staging
    environment: production    # правило защиты: required reviewers, без само-аппрува
    runs-on: ubuntu-latest
    steps:
      - run: deploy-canary ghcr.io/acme/api@${{ needs.build-once.outputs.digest }} --weight 5
      - run: analyze-slo --window 5m || { rollback; exit 1; }   # метрический гейт → авто-откат
      - run: promote-to-full --weight 100
Почему это работает

Зачем проверять артефакт на деплое, если ты уже его подписал? Потому что подписать на сборке и не проверить подпись перед деплоем оставляет зазор нараспашку — любой, кто может писать в реестр или подменить тег, может подсунуть образ. Прод-джоб должен cosign verify-attestation (или gh attestation verify) ровно этот digest против ожидаемой workflow-личности до деплоя. Provenance, которую ты генерируешь, но никогда не проверяешь, — это декорация; provenance, которую ты проверяешь на гейте, — это контроль.

Выбери лучший вариант

Как production должен получить артефакт, который staging уже проверил?

Викторина

Staging прошёл все гейты, но production утроил латентность. Прод-джоб делал `docker build` из тега. Что сломалось?

Викторина

На build-once-джобе ты добавляешь `id-token: write` и пиннишь actions по SHA. Зачем именно оба?

Расставь шаги по порядку

Расставь стадии полного пайплайна от PR до полностью раскатанного прод-релиза:

  1. 1 PR-проверки — быстрый гейт lint/typecheck, affected-only тесты, required status checks блокируют merge
  2. 2 Сборка один раз — один digest + SBOM + подписанная provenance-аттестация (OIDC, SHA-пиннинг actions)
  3. 3 Релиз — semver из conventional commits, тег, публикация подписанного образа
  4. 4 Staging — авто-деплой того же digest, прогон smoke-тестов
  5. 5 Прод-гейт — ручной аппрув через правила защиты окружения
  6. 6 Canary + откат — раскатка малого среза под метрическим гейтом; авто-откат при нарушении SLO
Вспомните перед уходом
  1. 01
    Пройди весь пайплайн от PR до production и назови, что вставляется на каждой стадии и что ломается, если её пропустить.
  2. 02
    Почему «собрать раз, продвигать тот же digest» — несущий инвариант, и как он делает историю невозможной?
Итог

Production-CI/CD-пайплайн — это единая цепь, чья гарантия выживает, только если не пропущено ни одно звено. PR-проверки идут первыми как гейт merge’а: быстрый проход lint/typecheck падает дёшево, affected-only сборка и тесты покрывают то, что трогает diff, а required status checks физически блокируют merge, чтобы main оставалась релизопригодной. На merge или теге артефакт собирается ровно один раз в один неизменяемый digest, с SBOM для того, что внутри, build-provenance-аттестацией для того, как и где он собран, и keyless-cosign-подписями через OIDC-личность GitHub — actions запинены по SHA коммита, а permissions держатся least-privilege на каждый джоб. Релиз — это бухгалтерия поверх проверенного артефакта: semver из conventional commits, тег, публикация подписанного образа. Дальше деплой — чистое продвижение того одного digest — staging авто-деплоит и smoke-тестит его, production сидит за правилом защиты окружения с required reviewers и без само-аппрува, а за гейтом canary маршрутизирует малый срез под метрическим гейтом, который авто-откатывается при нарушении SLO. Интегративный режим отказа всегда пропущенное звено: пересобери на стадии — и прод гонит непротестированный бинарник, задеплой без отката — и плохой canary это медленный аутаж, отгрузи непроверенный артефакт — и твой SBOM описывает что-то другое. Собери раз, проверь и продвигай тот же digest — это и есть весь пайплайн. Теперь, когда встретишь инцидент с релизом, первый вопрос — какое звено было пропущено: пересборка вместо продвижения, отсутствие отката или непроверенный артефакт — в девяти случаях из десяти ответ там.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.