プログレッシブデリバリー入門 — デプロイ・トラフィック切替・リリース

プログレッシブデリバリーを「デプロイ・トラフィック切替・リリース」の3つの営みに分解して整理する入門。B/G・canary・promotion gate・bake・feature flag の語彙と、それぞれが何を守るのか。実測値(失敗 0/1679 vs 69/2880、巻き戻し32秒と14秒)を証拠に、よくある誤解も正す。

はじめに

新しいバージョンを本番に出す、という一つの行為に見えるものは、実際には3つの別々の営みでできています。

  1. デプロイ: 新しいバイナリを本番環境に配置する
  2. トラフィック切替: 実ユーザーのリクエストをどの版に、どれだけ向けるか
  3. リリース: 機能をユーザーの目に触れさせるか

一括リリースとは、この3つを同時に、全量で、一発でやることです。うまくいけば速い。失敗すると全ユーザーが同時に影響を受け、戻す手段は「もう一度一括で置き直す」しかない。

プログレッシブデリバリーは、この3つを分解して、それぞれを段階的・可逆的にする考え方です。この記事はその全体地図と語彙を整理します。個々の数字は検証環境(Go + ECS + MySQL/Aurora)で実測したものです。

リリース

トラフィック切替

デプロイ

デプロイ

新版を配置(まだ誰も見ない)

promotion gate

テストトラフィックで検証

canary

n% だけ向ける

全量切替

bake

旧版を温存して監視

feature flag

機能の公開を独立に制御

デプロイの流れ: 変更が本番に配置されるまで

3つの営みのうち最初のデプロイは、語彙より手順そのものが要になる。検証リポジトリ greenfield で回しているフローを、 開発の流れから順に図で追う。数字はすべて実測で、出典は各節に書く。

開発の流れ: PR から署名済みイメージまで

デプロイの前に、変更がどう main に入り、どう配布物になるかを追う。 greenfield は複数のサービスを1つのリポジトリで持つので、 「変更したサービスだけが検査され、配布される」ことが開発の流れの要になる。

ghcrimage-releasemainCI(PR)リポジトリ開発者ghcrimage-releasemainCI(PR)リポジトリ開発者issue に着手宣言、ブランチを切る1契約を先に切る(OpenAPI、実装前は 0.x)2実装、mise run check(CI と同じ検査を手元で)3PR4affected: go.mod の replace から依存グラフを作り変更の影響を受けるモジュールだけ選ぶ5選ばれたモジュールごとに build / vet / test / govulncheck(GOWORK=off)6横断の検査: cross-db-fk、api-breaking、trivy、frontend、keycloak、authz、eventual7check(全ジョブの結果をまとめ、1つでも失敗なら赤)8マージ(squash)9workflow_dispatch(service = photo | gear | authz | all)10build & push(ARM64)11cosign keyless 署名(GitHub OIDC。鍵を持たない)12SBOM(syft、SPDX)を attestation として紐付け。空の SBOM は失敗13build provenance(どの workflow がどの commit から作ったか)14

図から読み取れないこと。

デプロイの3段: expand → release → healthcheck

デプロイは3段で、順序を守らせるのはスクリプトではなくジョブグラフである。 dev/scripts/deploy.sh <service> migrate|release|healthcheck は1段ずつしか実行せず、 deploy.yml の needs が順序を強制する。3段を1つのスクリプトにまとめると 「migrate が落ちたのに続きが走らないこと」の保証がスクリプトの制御フロー、 つまり読む人の信頼になる。ジョブグラフに置けば、保証は CI の実行機構そのものになる。

成功

失敗

成功

失敗

成功

失敗

後続のリリースで明示的に

deploy.yml

workflow_dispatch: service を1つ選ぶ

concurrency: deploy-service

migrate

expand だけ適用

(カラム追加・インデックス追加)

release

新しいイメージをビルドして差し替える

ここで止まる

release は走らない

healthcheck

/healthz が 200 を返すまで待つ

止まる

旧版が動いたまま

デプロイ完了

contract は含まない

止まる

新版は起動したが確認できていない

migrate contract

旧カラムを落とす

図から読み取れないこと。

トラフィック切替の語彙

トラフィック制御の部品は4つ覚えれば足ります。

語彙何か何を守るか
Blue/Green新旧2系統を並走させ、ロードバランサの向き先を一斉に掛け替える切替そのものの無停止(実測: 切替をまたぐ503リクエストで非200ゼロ)
canaryまず n% だけ新版に向けて観測し、問題なければ全量へ壊れた版の被害を「全量」から「n%」に縮める
promotion gate本番トラフィックを向ける前に、テスト用の経路で新版を検証するテストで再現できる壊れ方の失敗ゼロ
bake全量切替の後も旧版を落とさず併走させる監視時間即座に戻れる状態の温存(戻すのはLBの向き先変更だけ)

大事なのは、これらが代替関係ではなく直列だということです。同じ「壊れた版」(ヘルスチェックは正常・書き込みだけ 500 を返す)を流して測った失敗数がそれを示します。

構成失敗したリクエスト
promotion gate あり0 / 1,679(ゲートが本番前に捕捉、103秒で自動ロールバック)
ゲートなし、カナリア20% + アラーム69 / 2,880(2.4%)(検知窓の間に流れた分は失敗する)

この対比が入門で一番伝えたい数字です。カナリアは失敗を消しません。 カナリア中に新版へ流れたリクエストは、新版が壊れていれば失敗します。カナリアの価値は「全量事故を n% の事故に縮める」ことであり、失敗をゼロにするのはその手前のpromotion gateの仕事です。

流れ: ECS ネイティブ B/G の中で何が起きるか

ECS の B/G デプロイは、デプロイ1回の中に promotion gate・canary・bake・rollback が 全部入っている。1回のデプロイが辿る状態を追う。

PRODUCTION_TRAFFIC_SHIFT

taskdef を改版してデプロイ開始

TEST_TRAFFIC_SHIFT

POST_TEST_TRAFFIC_SHIFT フック

FAILED(書き込みが 500)

アラームが鳴らなければ全量

ALB 5xx アラームが ALARM

stopDeployment(人が判断)

bake 完了、blue を終了

ROLLBACK_SUCCESSFUL(103 秒、本番で失敗したリクエスト 0)

ROLLBACK_SUCCESSFUL(4分50秒、失敗したリクエスト 2.4%)

ROLLBACK_SUCCESSFUL(32 秒)

SUCCEEDED

全量が green

green を起動(本番トラフィックはまだ blue)

テストリスナー(別ポート)に green を向ける

Lambda がテスト経路で /healthz と書き込み(create 201)を検証

Rollback1

CANARY なら n% を green へ(canary bake 1分)

BLUE_GREEN なら 100/0 → 0/100 を一度に

Rollback2

bake 2分。blue は落とさず温存

Rollback3

Done

同じ「壊れた版」(ヘルスチェックは正常、書き込みだけ 500)を流して測った結果を 並べる。

止めた場所仕組み本番で失敗したリクエスト戻るまで
promotion gateフック Lambda がテスト経路で書き込みまで検証0 / 1,679103 秒(自動)
カナリア窓20% を流し、ALB 5xx アラームで自動ロールバック69 / 2,880(2.4%)4分50秒(自動)
bake 中人が stopDeployment を1回発行0 / 2,399(健全な版)32 秒

図から読み取れないこと。

リリースの語彙: フィーチャーフラグ

デプロイと切り離して機能の公開を制御するのがフィーチャーフラグです。コードはデプロイ済みだが、フラグが無効ならリリースされていない — この分離により、機能の公開・撤回がデプロイなしでできます。

実測では、フラグ反転がユーザーに届くまで 14秒・再起動なしでした。デプロイの巻き戻し(後述の stopDeployment、32秒)と並ぶ、もう1つの巻き戻し手段です。

ここでの誤解も正しておきます。「フラグの方が速いなら、事故対応は全部フラグでいいのでは」— なりません。フラグで隠せるのは、コードが正しく動く前提での見せ方だけです。壊れたバイナリはフラグの後ろでも壊れ続けます(ログとリソースを汚しながら)。バイナリの問題はデプロイの巻き戻しで、見せ方の問題はフラグで戻します。2つの手段を独立に保つから、どちらも迷わず使えます。

事象対処実測
バイナリが壊れていたデプロイの巻き戻し(stopDeployment)32秒で本番復帰
機能の見せ方を引っ込めたいフラグ反転14秒で全ユーザーへ
フラグ基盤自体が死んだ何もしないコードに宣言した既定値で稼働継続

3行目も設計の一部です。フラグの既定値は「フラグ基盤が死んだときの安全側」を宣言する場所で、これを決めてあればフラグ基盤の障害がサービス停止に化けません。

流れ: feature flag → backfill → contract

デプロイもトラフィック切替も終わって新版が全量で動いていても、機能はまだ リリースされていなくてよい。リリースはフラグで、デプロイとは別の時刻に、別の 手段で行う。カラム改名を例に、リリースの流れを DB の変更と一緒に追う。 これは greenfield の rename-drill.sh が負荷をかけたまま自動で回す手順である。

PR 2: contract

旧カラムへの書き込みを止める

フラグと旧経路を削除

旧カラムを落とす

DROP COLUMN ALGORITHM=INSTANT

稼働中(負荷をかけたまま)

backfill

batch 50、pause 100ms

feature flag 1%

50%

100%

OFF に戻す

巻き戻せることを確認

100%

columncheck

新旧カラムの一致を検算

PR 1: expand

新カラムを追加

ALGORITHM=INSTANT

書き込みは新旧両方へ

読みはフラグで切る

既定 OFF = 旧カラム

図から読み取れないこと。

N-1 互換性 — DB は徐々に切り替えられない

ここまでのトラフィック切替の話はすべて、N-1 互換性という前提の上に立っています。新旧1世代差のバイナリが同時に正しく動けること — B/G もカナリアも bake も、新旧のバイナリが同時に動く時間を作るからです。トラフィックは 20% と 80% に割れますが、データベースのスキーマは1つしかありません。新旧両方のバイナリが、同じスキーマで正しく動く必要がある。

つまりアプリ層のプログレッシブデリバリーは、DB 層に「新旧互換なスキーマ変更」という別種の規律を要求します。トラフィックのように割合で割る話ではなく、時間で分ける話(互換を保って先に広げ、旧版が消えてから縮める)になる — この非対称性は1本の記事に値するので、アプリと DB でプログレッシブは別物として続編に分けました。

まとめ

続編: アプリと DB でプログレッシブは別物(各層の仕組みと実測)、選び方、そして選ばせない運用(組み合わせの運用)

全体の流れ

ここまでの節で追ったフローを1枚に並べる。縦に4つの営みが並び、左から右へ時間が進む。

リリース

トラフィック切替

デプロイ

開発

issue に着手宣言

契約を先に切る

OpenAPI 0.x

実装 + mise run check

PR → CI

変更モジュールだけ

マージ → image

署名 + SBOM

migrate

expand だけ

release

新タスク起動

healthcheck

promotion gate

テスト経路で書き込み検証

canary n%

全量切替

bake

旧版を温存

feature flag 1% → 50% → 100%

OFF で巻き戻せるか

旧経路の削除

contract

旧カラムを落とす

4つの営みはそれぞれ独立した巻き戻しの手段を持つ。デプロイは stopDeployment、トラフィックはターゲットグループの向き先、 リリースはフラグの反転で、どれも他を動かさずに戻せる。

失敗したときの対処: 何が壊れたかで決まる

4つの営みを分けた最大の利点は、何が壊れたかによって対処が決まることである。

CI が赤

デプロイ直後に

書き込みが失敗

カナリア中に 5xx

bake 中に異常に気づいた

機能の見せ方を引っ込めたい

フラグ基盤が死んだ

バイナリが壊れている

何が壊れた?

マージしない。

直すのはブランチ

promotion gate が止める。

人は何もしない(103 秒で戻る)

アラームが戻す。

失敗は n% に閉じる

stopDeployment

32 秒で旧版へ

フラグを OFF

14 秒、再起動なし

何もしない。

既定値で稼働継続

フラグでは隠せない。

デプロイを巻き戻す

最後の2行が分離の意味を言っている。フラグで隠せるのはコードが正しく動く前提での 見せ方だけで、壊れたバイナリはフラグの後ろでも壊れ続ける。バイナリの問題は デプロイの巻き戻しで、見せ方の問題はフラグで戻す。2つの手段が独立しているから、 どちらも迷わず使える。

← Back to all posts