開発で通ったものが本番で通らない本当の理由
一言でいうと
環境ごとに再ビルドすると、環境ごとに違うものが出てきます。一度ビルドしたアーティファクトをそのままプロモーションすることが、この問題の唯一の根本的な解決策です。
なぜ必要なのか
パイプラインがこのような形になっている現場は、非常に多くあります。
dev 브랜치 → 빌드 → dev 배포
stage 브랜치 → 빌드 → stage 배포
main 브랜치 → 빌드 → prod 배포
3回ビルドしています。ソースが同じなので結果も同じだろうと考えますが、そうではありません。ビルドの間に、次のようなものが変わります。
- ベースイメージのタグが動きました(
python:3.12が昨日と違います) npm installが、間接依存関係の新しいパッチを取得しました- aptリポジトリのパッケージのバージョンが上がりました
- ビルドマシンのツールチェーンのバージョンが違います
そのため、devで通ったテストは、prodのイメージでは意味を持ちません。テストしたものとデプロイしたものが別物だからです。
ビルドは1回、プロモーションは何度でも
소스 커밋 → 빌드 1회 → 아티팩트(불변) ─┬→ dev 배포 → 테스트
├→ stage 배포 → 검증
└→ prod 배포
核心は、アーティファクトが不変であることです。devで検証したそのバイト列がprodに行きます。そうすれば「環境のせいで違う」という変数が消え、残る違いは設定とデータだけです。それは私たちが制御できます。
プロモーションは再ビルドではなく、参照を切り替える作業です。
# gitops/prod/kustomization.yaml
images:
- name: registry/backend
newTag: v0820-1500 # ← 이 한 줄을 고치는 커밋이 배포다
LabHub自体がこの方式で動いています。イメージを1回ビルドし、devオーバーレイのタグを上げて検証し、通ったらprodオーバーレイの同じタグに上げます。デプロイの記録がそのままgitログです。
設定はアーティファクトの外へ
同じアーティファクトを複数の環境で使うには、環境ごとに変わるものを外へ出す必要があります。
| 項目 | イメージの中 | イメージの外 |
|---|---|---|
| アプリケーションコード | ✅ | |
| ランタイム・ライブラリ | ✅ | |
| DBのアドレス、外部APIのURL | ✅ 環境変数/ConfigMap | |
| 認証情報 | ✅ Secret | |
| ログレベル、フィーチャーフラグ | ✅ 設定 |
イメージの中にapplication-prod.ymlをビルドして入れた瞬間、そのイメージはprod専用になり、プロモーションのモデルが壊れます。
何で識別するのか
アーティファクトには、たどり直せる名前が必要です。
- コミットSHA: 最も正確です。
backend:a1b2c3d。どのソースから出たのかが一目でわかります。 - セマンティックバージョン: 人が読みやすいです。リリースに付けます。
- ダイジェスト:
@sha256:...。タグは付け替えられますが、ダイジェストは絶対に変わりません。本当に確実にしたい場所(prod)では、これを使います。
latestはプロモーションのモデルでは使ってはいけません。いつの時点のlatestなのかがわからず、ロールバックする対象を特定できないからです。
プロモーションの間に置くゲート
빌드 → [단위 테스트] → dev → [통합 테스트] → stage → [수동 승인] → prod
各矢印の前の角括弧がゲートです。ゲートは通過/ブロックを終了コードで伝えなければなりません。ログに「失敗」と出力してexit 0を返すと、パイプラインはそのまま通り過ぎます。実際によくあるバグです。
ロールバックはプロモーションの逆方向
プロモーションがタグを上げるコミットなら、ロールバックは以前のタグに戻すコミットです。再ビルドも、緊急パッチも必要ありません。以前のアーティファクトは、レジストリにそのまま残っているからです。
これを可能にするには、アーティファクトを削除してはいけません。保持ポリシーを作るときに「直近のN個」だけを残すと、それより古いバージョンには戻れません。
プロモーションが実際にずれる場所
「一度ビルドして、あとはプロモーションだけを行う」という原則は単純ですが、守ろうとすると3か所から漏れます。
タグは動きます。myapp:v1.2.3をプロモーションしたあとで、誰かが同じタグで再度プッシュすると、昨日検証したものと今日デプロイされるものが別物になります。ダイジェストでデプロイし、タグは人が読むためのラベルとしてだけ使います。レジストリにタグ不変の設定があれば、それも一緒に有効にします。
docker buildx imagetools inspect myapp:v1.2.3 --format '{{.Manifest.Digest}}'
環境ごとのビルドが、静かによみがえります。--build-arg ENV=prodのようなものが1つでも入ると、その瞬間、開発環境でテストしたイメージと本番環境のイメージが別のものになります。違いは実行時の設定だけに置く必要があります。ビルド引数に環境名が見えたら、それがサインです。
設定がイメージの中に入り込みます。便宜のために既定の設定ファイルをイメージに入れておくと、その値がいつか本番環境で使われます。設定は外から注入し、ない場合は起動に失敗するようにしておくほうが安全です。既定値で静かに動き続けるのが最も悪い状態です。
ゲートはプロモーションの間に置き、同じものを2回測りません。ユニットテストはビルドで1回、統合テストは開発環境へのプロモーションのあとで1回、負荷テストはステージングへのプロモーションのあとで1回行います。本番環境へのプロモーションの前にユニットテストを再実行するパイプラインがよくありますが、それは時間を使うだけで、新しいことは何も教えてくれません。
元に戻す作業は、古いダイジェストを再度プロモーションすることでなければなりません。元に戻すために再ビルドすると、その間に変わった依存関係のせいで、戻したものが以前とは別物になります。そのため、古いアーティファクトは削除せず、最低でも数世代分は保管します。
何がどこにあるかを記録します。各環境に今どのダイジェストがあるのかが1画面で見えないと、障害のときに「本番に何が動いているのか」を突き止めるのに時間を使います。
現場での姿
- 「ステージングでは動いたのに」→ 再ビルドして、別のものが出てしまいました。
- ロールバックしようとしたら以前のイメージがない → 保持ポリシーが短すぎます。
- prodだけ別の設定ファイルがイメージに入っている → そもそもプロモーションが不可能です。
次の確認で見ること
続くラボで、この文章の主張を手で確認します。コミットからリリース名を付け、開発環境に相当するリポジトリにアーティファクトを1回だけ作り、タグではなくダイジェストで指定してステージングと本番環境へ移し、タグが別の内容を指すようになった瞬間を捕まえ、どの環境にどのダイジェストがあるのかを1つのファイルに残します。ラボのあとのクイズでは、ビルドとプロモーションの違い、不変のダイジェスト、外部の設定と保持ポリシーがロールバックの可否に与える影響を判断します。