第8回でインフラのコードが管理できるようになりました。次に決めるのは、その上に載るアプリケーションをどう届けるかです。
本記事も、第7回で残した課題から始めます。Backup for GKE はコンテナイメージをバックアップしません。バックアップされるのは「ワークロードを記述し、コンテナイメージを参照するKubernetesリソース」だけです。参照先のイメージがレジストリから削除されていると、復元してもワークロードは正常に復元されません。
この記事の主題は、パイプラインの作り方ではありません。どのタイミングで何を検証し、その検証がいつ有効でなくなるかを設計することです。「スキャンを入れました」で終わる設計書は、半年後には守っていないのと同じ状態になります。
目次
イメージの保持期間を先に決める
レジストリの設計は、費用の話から始めがちです。古いイメージが溜まり、ストレージ費用が増え、消したくなる。その順序で始めると、復元できないバックアップを作ります。
バックアップ世代から逆算する
手順は4つです。
- 第7回で決めたバックアップの保持世代を確認する
- その期間内に参照されうるイメージを特定する
- その期間はイメージを削除しない保持ポリシーを置く
- 残りに対して、削除ポリシーを設計する
30日前のバックアップから復元する可能性があるなら、30日前のイメージがレジストリに残っている必要があります。これは復元可能性の要件であって、費用の都合で短縮できるものではありません。
第7回のテンプレートに「レジストリの保持期間 vs バックアップ保持世代/矛盾していないか」という記入欄を置いたのは、この突き合わせのためです。矛盾したまま運用すると、「30日前のバックアップは残っているが、それが参照するイメージは20日前に削除済み」という状態が生まれます。そして気づくのは、復元しようとしたときです。
クリーンアップポリシーの構造
Artifact Registry のクリーンアップポリシーは3種類あります。
| 種類 | 内容 |
|---|---|
| 条件付き削除 | 指定した条件に合致するアーティファクトを削除する |
| 条件付き保持 | 指定した条件に合致するアーティファクトを保持する |
| 最新バージョンの保持 | keepCount で指定した数の最新バージョンを保持する |
条件に使えるパラメータは、tagState(tagged / untagged / any)、olderThan と newerThan(30d のように秒・分・時・日で指定)、tagPrefixes、versionNamePrefixes、packageNamePrefixes、そして keepCount です。
そして、設計に使える性質があります。
複数のポリシーで削除条件と保持条件の両方に合致する場合、アーティファクトは保持されます。
つまり削除ポリシーの上に保持ポリシーを重ねれば、消えてはいけないものを守れます。前節の逆算は、この性質で実装します。「古いものは消す」という削除ポリシーを書いたうえで、「バックアップ世代の期間内のものは保持する」という保持ポリシーを重ねる。消さない側が勝つ、という優先順位が保証されているので、この重ね方が安全です。
運用上の注意が2つあります。
1つ目、ポリシーはバックグラウンドジョブとして定期的に実行され、変更の反映まで約1日かかります。設定してすぐ結果を確認できるものではありません。ドライランがあるので、必ず先に実行し、監査ログで対象を確認してください。
2つ目、親マニフェストから参照されているイメージは削除されません。マルチアーキテクチャのイメージなどでは、個別のイメージが条件に合致していても消えない挙動があります。「消したのに減らない」と見えることがあります。
「タグがない」は「使われていない」ではない
削除対象を決めるとき、untagged のイメージから手を付けるのが自然に見えます。タグが付いていないなら、誰も参照していないだろう、と。
この推論は成り立ちません。
次章で述べるとおり、本記事はデプロイをダイジェストで固定することを推奨します。ダイジェストで参照している場合、タグが付いていなくても稼働中です。タグを整理する運用を入れていれば、稼働中のイメージからタグが外れることもあります。
「タグがない=使われていない」という誤解が、稼働中のイメージを消す事故につながります。ダイジェスト参照を採用する場合、untagged を無条件に削除対象としないでください。

レジストリを環境ごとに分けるか
保持期間と並んで決めるのが、レジストリのリポジトリ構成です。環境ごとに分けるか、1つを共有するか。
この判断は、後半で扱うPromotionの設計と直結します。
| 構成 | Promotionの実装 | 代償 |
|---|---|---|
| 1つのレジストリを共有 | 同じダイジェストを、環境ごとのマニフェストから参照するだけ。コピーが発生しない | 本番が参照するレジストリに、検証前のイメージも入る。アクセス制御はレジストリ単位では分けられない |
| 環境ごとに分ける | 昇格のたびにイメージをコピーする操作が入る | コピー時にダイジェストが保たれるかを確認する必要がある。転送の時間と費用も発生する |
判断基準を渡します。本番のレジストリへ書き込める主体を、検証前のイメージを作る主体と分ける必要があるか。必要ならレジストリを分けます。必要でなければ、共有のほうが構成は単純です。
ここでも第2回の運開分離が効きます。「本番に入るものを誰が置けるか」という問いは、レジストリの構成にも現れます。第5回でNamespaceの書き込み権限を設計し、第8回でstateの書き込み権限を設計したのと同じ問いです。
分ける場合、コピー後もダイジェストが同一であることを確認してください。ダイジェストが変わる方式でコピーすると、「同一のイメージを昇格させている」という前提が崩れます。本記事の後半の設計が、そこで無効になります。
タグは動く、ダイジェストは動かない
第5回で「latestタグのイメージを拒否する」というルールを作りました。それだけでは足りません。
既定でタグは上書きできる
Artifact Registry のドキュメントは、こう書いています。
「If you tag an image with a tag that’s already in use, you will move the tag from the original version to the newly tagged version」
既に使われているタグを別のイメージに付けると、タグが元のバージョンから新しいバージョンへ移動します。これは既定の挙動です。
ここから導かれる結論があります。v1.2.3 のようなバージョンタグでも、指す先は変えられます。「latestを使っていないから再現性がある」は成り立ちません。
事故の現れ方が、この問題の厄介さを示しています。Stgで検証した v1.2.3 と、1週間後にProdへ出した v1.2.3 が、別のイメージになる。マニフェストは同一、Gitの履歴も同一です。差分からは追えません。
機械が参照するのはダイジェストにする
対処は明快です。デプロイのマニフェストにはダイジェスト(SHA256)を書きます。
タグは人間が読むためのラベルとして残してかまいません。役割を分けます。人間が読むのはタグ、機械が参照するのはダイジェスト。ダイジェストはイメージの内容から決まるため、指す先が変わることはありません。
環境間で昇格させるのもダイジェストです。この点は後半で扱います。
なお、マニフェストのどこにダイジェストを書き、それをどう更新するかは第10回のテンプレート設計の領分です。本記事は「ダイジェストで固定する」という方針までを決めます。
レジストリ側でタグの上書きを禁止する設定があるかは、採用するレジストリで確認してください。設定の有無にかかわらず、ダイジェストで固定するという設計は変わりません。上書きを禁止できたとしても、それは事故を防ぐ二重化であって、参照方式の設計を置き換えるものではありません。
タグには何を入れるか
タグを人間が読むためのラベルと位置づけたなら、次の問いは「人間は何を知りたいか」です。
本番で問題が起きたとき、運用担当者が最初に知りたいのは「これはどのコミットから作られたのか」です。バージョン番号だけでは、そこからコードへ辿れません。
したがって、タグの命名規則にはソースへ辿れる情報を含めてください。Gitのコミットハッシュを含める、あるいはイメージのラベルとしてコミットハッシュを埋め込む、といった方法があります。
これは第16回のインシデント対応にも効きます。障害の一次対応で「動いているイメージからコードを特定する」までに何分かかるかは、この設計で決まります。ダイジェストだけが記録されていて、そこからコミットに辿る手段がない状態は、深夜の調査を長引かせます。
設計書には、ダイジェストからコミットへ辿る経路を書いてください。タグの命名規則でも、イメージのラベルでも、外部の対応表でもかまいません。経路が存在することが要件です。
スキャンは「押した時」だけでは足りない
脆弱性スキャンには、あまり語られない性質があります。検証結果には有効期限があります。
30日でメタデータが古くなる
Artifact Analysis の挙動を整理します。
| タイミング | 挙動 |
|---|---|
| プッシュ時 | 新しいイメージが Artifact Registry にプッシュされるたびに自動的にスキャンが開始される。対象はOSパッケージと複数の言語パッケージ(Go、Java、Python、Node.js など) |
| その後 | イメージやパッケージが過去30日以内にプルされている限り、スキャンと更新が継続される |
| 30日超 | メタデータが古い場合、結果は古くなる |
| 90日超 | メタデータはアーカイブされる |
| 再開 | イメージを再度プルすることで再スキャンできる。更新に最大24時間かかる |
ここに逆転があります。
本番で安定稼働しているイメージほど、プルされません。デプロイし直す理由がないからです。そして更新は「過去30日以内にプルされている限り」継続します。
つまり、最も長く本番で動いているイメージが、最も古い情報で評価されているという状態が生まれます。1年間動き続けているコンポーネントの脆弱性情報は、11か月前で止まっている可能性があります。
「ビルド時にスキャンしている」という対策は、時間とともに有効でなくなります。導入した事実は変わりませんが、守っている状態ではなくなります。

設計としての対処
対処は3つです。
- 稼働中のイメージを定期的にプルする仕組みを作る(更新を継続させる)
- 定期的に再デプロイする運用にする(変更がなくても出し直す)
- スキャン結果の鮮度を監視し、古くなったものを検知する
そして設計書に書くべき項目があります。「スキャン結果が何日以内のものであることを要求するか」という鮮度の要件です。
第5回で「Critical脆弱性は即ブロック」のような深刻度のルールを決めました。鮮度の要件は、それと対になるものです。「Criticalがゼロであること」という条件は、「いつ時点でゼロか」が定まって初めて意味を持ちます。
境界を明示します。どの深刻度でブロックするかというルールは第5回の管轄です。本記事が扱うのは、いつ検証し、その結果がいつまで有効かという経路と期間の設計です。
ゲートを置く3つの位置
| 位置 | 性質 | 役割 |
|---|---|---|
| ビルド時 | 速い。ただしその時点の情報 | 開発者へのフィードバック。直すコストが最も安い段階で知らせる |
| デプロイ時 | 実際に出る直前。ここで止まると開発が止まる | 本番への流入を止める最後の関門 |
| 稼働中の継続検知 | 実態に最も近い。ただし止められない | 既に入っているものの再評価 |
3つとも役割が違うので、どれか1つを選ぶ問題ではありません。「スキャンはビルド時にやっているから十分」という判断は、稼働中の再評価が抜けています。逆に継続検知だけでは、開発者は問題を知るのが遅すぎます。
設計書には、3箇所それぞれについて「実施するか」と「誰に通知するか」を書いてください。通知先が決まっていない検知は、ログに出るだけで終わります。
検知した後に何をするかまで決める
ここまでは検知の設計です。検知しただけでは、脆弱性は消えません。
稼働中のイメージに新しい脆弱性が見つかったとき、多くの場合アプリケーションのコードは変わっていません。問題はベースイメージや依存パッケージにあります。つまり必要なのは、コードの修正ではなくリビルドです。
ところが、多くのパイプラインはコードの変更をトリガーにしています。コードが変わらない限りビルドは走りません。脆弱性が見つかっても、誰かが手動でパイプラインを起動しない限り、直りません。
したがって、設計書にリビルドのトリガーを書いてください。判断の入力は3つです。
- 定期的なリビルド。コードが変わらなくても、決めた周期で作り直す。ベースイメージの更新が自動的に取り込まれる
- ベースイメージの更新を検知したリビルド。依存元が更新されたときに走らせる
- 脆弱性の検知をトリガーにしたリビルド。特定の深刻度以上が検出されたときに走らせる
そしてリビルドには副作用があります。コードが変わっていないのにイメージが変わるということは、テストを通し直す必要があるということです。「脆弱性を直すために作り直したイメージで、別の不具合が出る」という事態は起こりえます。
したがってリビルドしたイメージも、通常の昇格フローを通します。緊急だからといって Prod へ直接出す経路を作ると、そこが恒久的な抜け道になります。緊急時の扱いは第16回のインシデント対応で設計してください。
署名は第5回の宣言に対する証明
第5回で Binary Authorization のポリシーを設計しました。判定モードには「すべて許可」「すべて拒否」「構成証明を要求」があり、強制モードには「ブロックして監査ログに記録」と「ドライランで監査ログのみ」があります。
構成証明を要求するモードを選んだなら、その証明を作る主体が要ります。それが本記事の担当です。
署名するのはパイプライン
署名はパイプラインが行います。人間が手元で署名できる状態にしないでください。
理由は単純です。手元で署名できるなら、検証を通っていないイメージにも署名できます。署名の意味は「このイメージは定められた経路を通った」という証明であり、経路を通らずに署名できるなら証明になりません。
鍵と権限は他の回の管轄にある
署名を設計に入れるとき、2つの依存が生まれます。
- 署名鍵は、第5回のシークレット管理の対象です。どこに保管し、誰が読めるかは第5回で設計した枠組みに従います
- 署名を実行できる主体は、第8回で設計したCI/CDの権限の一部です。パイプラインが持つ権限の一覧に、署名の権限が加わります
「署名を導入した」だけでは、鍵を誰でも使える状態が残ります。設計書には、署名鍵の保管場所と、署名を実行できる主体を明記してください。第5回で「シークレットを読める人の一覧を作る」と決めたのと同じ作業を、署名鍵についても行います。
そして第5回で決めたとおり、ドライランから始めてください。拒否されるはずだったイメージの一覧を作り、それが想定どおりであることを確認してから本適用に切り替えます。いきなりブロックにすると、自組織が持ち込む常駐ワークロード(監視エージェント、EDR、ログ収集)が止まります。
署名は何に対して行うか
1点、設計上の確認があります。署名の対象はダイジェストです。
前章で「タグは動く」と確認しました。タグに対して署名しても、タグが別のイメージへ移動した時点で、署名は別のものを指すことになります。署名がイメージの同一性を保証するのは、対象がダイジェストである場合だけです。
この点は、本記事の3つの設計が同じ方向を向いていることを示しています。デプロイの参照方式、昇格の単位、署名の対象。すべてダイジェストです。どれか1つでもタグにすると、そこが再現性の穴になります。
そして検証が行われる位置も確認してください。Binary Authorization の検証は、Podの作成が要求された時点(アドミッション)で行われます。つまり第5回で設計した層2の仕組みの一部です。本記事で作るのは証明であり、それを検証して止めるのは第5回で設計した層です。作る側と止める側が別の回にあることを、設計書でも意識してください。
Promotionはビルドし直さない
環境間でアプリケーションを進めるとき、原則が1つあります。
ビルドは1回だけ
Dev、Stg、Prod と進むとき、ビルドするのは1回だけです。以降は同じダイジェストを、参照先の環境へ移すだけにします。
環境ごとにビルドすると何が起きるか。ベースイメージが更新されていれば中身が変わります。依存パッケージの解決結果も、実行した時刻によって変わります。同じDockerfileから、違うイメージができます。
ここで第3回の決定に戻ります。第3回で「StgはProdのミラー環境」と決め、「ミラーの定義は構成の同一性であって規模の同一性ではない」と確認しました。
イメージが違えば、その構成の同一性が崩れます。Stgで検証したという事実が、Prodについて何も保証しなくなります。
事故は分かりにくい形で現れます。Stgでは通ったテストがProdで落ちる。しかしコードは同じです。違うのはビルドした時刻だけで、その差分はどのリポジトリにも記録されていません。
どのダイジェストがどの環境にいるかを記録する
昇格を設計するとき、もう1つ決めることがあります。いま各環境で動いているのがどのダイジェストかを、どこで確認できるようにするかです。
クラスタに問い合わせれば分かる、という答えでは足りません。クラスタが落ちているときにも確認できる必要があります。第7回で「クラスタ構成はバックアップされない」と確認したのと同じ理由です。
GitOpsを採用しているなら、リポジトリの状態がそのまま記録になります。マニフェストにダイジェストが書かれ、それがGitの履歴に残る。これが「どのコミットが適用されたか」で追跡できる状態の実体です。第8回で述べた「誰が適用したかではなく、どのコミットが適用されたか」という話が、ここで具体化します。
ロールバックの単位を決める
昇格の設計は、戻す設計とセットです。
イメージを同一ダイジェストで昇格させているなら、戻す操作は「前のダイジェストに戻す」だけです。ビルドし直す必要はなく、そのイメージは既にレジストリに存在します。
ここで第1章の保持期間が効いてきます。戻したい先のイメージが、クリーンアップポリシーで削除されていたら戻せません。保持期間の設計は、バックアップの復元だけでなく、ロールバックの可否も決めています。
設計書には「何世代前まで戻せるか」を書いてください。そしてその世代数が、レジストリの保持ポリシーで守られているかを確認してください。2つの数字が独立に決められていると、戻せないロールバック手順ができます。
なお、本番移行時のトラフィック切り替えや、致命的な障害時の旧環境への切り戻し手順は第13回の管轄です。本記事が扱うのは、平時のデプロイを1つ前の状態に戻す操作までです。
環境の差は設定で表現する
イメージを同一にすると、環境ごとの違いをどこかで表現する必要が出ます。接続先、レプリカ数、リソース値。これらは設定で表現します。
境界を明示します。その設定をどう構造化するか、開発者にどこまで公開するかは第10回の管轄です。本記事は「イメージは同一、環境の差は設定」という方針までを決めます。
承認ルールを決める
環境ごとに、昇格に承認が要るかどうかを決めます。Dev から Stg は自動、Stg から Prod は承認ありが現実的な出発点です。
承認者は、第2回のRACIで決めた責任の所在に従います。承認者を「誰でもよい」にすると、承認は形式になります。そして形式になった承認は、監査で「承認プロセスがある」と説明できても、実質的には何も止めていません。
ビルドとデプロイを分ける
最後に、権限の設計です。第8回でやったことと同じ構造が、ここにも現れます。
第8回では「人間はplanまで、applyはパイプラインだけ」という分離を Cloud IAM で実装しました。本記事では「ビルドは誰でも起動できる、デプロイは承認を経た経路だけ」という形で、同じ分離が現れます。
連載を通じて、同じ運開分離を4つの場所に実装してきたことになります。第2回のRACI(タスクの分離)、第5回のKubernetes RBAC(本番の書き込み権限)、第8回のCloud IAM(planとapply)、そして本記事(ビルドとデプロイ)。主体と仕組みが違うだけで、構造は同じです。
同期ポリシーについても決めます。環境ごとに自動同期にするか手動にするか。ここで1つ、誤解しやすい点があります。
本番を自動同期にすると、承認はマージの時点に前倒しされます。「自動化したから承認が不要になる」ではありません。承認の位置が変わるだけです。この前提を共有していないと、「自動同期なので誰も承認していません」という状態が生まれ、監査で問題になります。
なお、選定したCDツールの構成そのものは、ツールに依存します。ツールの選定理由は第6回、マニフェストの構造は第10回です。本記事は同期ポリシーの決め方までを扱います。
同じコミットから同じイメージが出るか
最後に、ビルド側にも1つ確認事項があります。
同じコミットから2回ビルドしたとき、同じイメージができますか。
多くの場合、できません。ベースイメージをタグで参照していれば中身が変わり、依存パッケージをバージョン範囲で指定していれば解決結果が変わり、タイムスタンプが埋め込まれればダイジェストが変わります。
完全な再現性を目指す必要はありません。ただし、再現できないという事実は認識してください。そのうえで設計に反映します。
- ベースイメージもダイジェストで参照する。タグ参照だと、いつ変わったか分かりません
- 「壊れたから作り直す」を復旧手段にしない。作り直したものは別のイメージです。レジストリに残っているものを使います
2番目が、本記事の第1章に戻ります。レジストリの保持期間が、事実上の復旧可能性そのものです。ビルドし直せば戻せるという前提は、成り立ちません。
【実務テンプレート】CI/CDパイプライン・サプライチェーン設計書
第2回から第8回の成果物が入力になります。
セクション1: レジストリの保持期間
第7回で決めたバックアップの保持世代: ____日
その期間のイメージを保持するポリシー: 条件______________________________
削除ポリシーの条件: tagState[______] / olderThan[______] / keepCount[______]
ドライランの実施日と結果: ____/____/____ / 削除対象____件 / 想定どおりか[はい / いいえ]
※ 反映まで約1日かかることを承知しているか: [はい]
レジストリのリポジトリ構成: [1つを共有 / 環境ごとに分ける]
分ける場合、コピー後もダイジェストが同一であることを確認したか: [確認済 / 未確認]
本番レジストリへ書き込める主体: ______________________________
ロールバックで戻れる世代数: ____世代
→ その世代が保持ポリシーで守られているか: [確認済 / 未確認]
セクション2: タグとダイジェスト
デプロイ時の参照方式: [ダイジェスト / タグ]
タグの命名規則: ______________________________
レジストリ側のタグ上書き禁止設定: [利用する / 利用できない / 未確認]untagged を削除対象にするか: [する / しない]
→ するなら、ダイジェスト参照との整合を確認したか: [確認済 / 未確認]
セクション3: スキャン
ビルド時: [実施 / 不実施] / 通知先______
デプロイ時: [実施 / 不実施] / 通知先______ / ブロックするか[する / しない]
稼働中の継続検知: [実施 / 不実施] / 通知先______
スキャン結果の鮮度の要件: ____日以内
稼働中イメージを定期的にプルする仕組み: [あり(周期____) / なし]
※ 深刻度によるブロックのルールは第5回で定義済みのものを参照する
リビルドのトリガー(検知した後に何をするか)
□ 定期的なリビルド(周期____)
□ ベースイメージの更新を検知したリビルド
□ 脆弱性の検知をトリガーにしたリビルド(深刻度____以上)
リビルドしたイメージも通常の昇格フローを通すか: [はい / いいえ]
→ いいえの場合、その経路が恒久的な抜け道にならないか: ______
ベースイメージの参照方式: [ダイジェスト / タグ]
セクション4: 署名
Binary Authorization の判定モード(第5回で決定済み): ______
強制モード: [ドライラン / ブロック] / 本適用への切替予定日____/____/____
署名を実行する主体: ______________________________
署名鍵の保管場所(第5回のシークレット管理に従う): ______
署名鍵を読める主体の一覧: ______________________________
人間が手元で署名できない構成になっているか: [確認済 / 未確認]
セクション5: Promotion
ビルド回数: ____回(1回であること)
昇格の単位: [ダイジェスト / タグ / 再ビルド]
Dev → Stg: [自動 / 承認あり] / 承認者______
Stg → Prod: [自動 / 承認あり] / 承認者______
→ 承認者は第2回のRACIの責任者と一致しているか: [はい / いいえ]
環境ごとの差分をどこで表現するか: ______(構造の設計は第10回)
セクション6: 同期ポリシーと権限
環境ごとの同期: Dev[自動 / 手動] / Stg[自動 / 手動] / Prod[自動 / 手動]
自動にした環境について、承認をどこに置いたか: ______________________________
ビルドを起動できる主体: ______
デプロイを実行できる主体: ______
→ 人間が直接デプロイできない構成になっているか: [確認済 / 未確認]
セクション7: 見直し
レジストリの保持期間と第7回のバックアップ世代の突き合わせ: 周期____ヶ月 / 前回____/____/____
スキャン結果の鮮度が要件を満たしているかの確認: 周期____
ドライランの再実施(ポリシー変更時): [実施する / しない]
次回予告
届ける経路が決まりました。次に決めるのは、開発者に何を書かせるかです。
次回の第10回「マニフェスト抽象化・IDP設計書」では、開発者に提供する values.yaml のインターフェース仕様、共通Helm ChartとKustomize Overlayのディレクトリ構成、IDPのサービスカタログ設計、そしてKubernetesマニフェストリポジトリのディレクトリ構成を扱います。
本記事で「イメージは同一、環境の差は設定で表現する」と決めました。その設定をどう構造化し、開発者にどこまで公開するかが第10回です。加えて、第8回で保留した「インフラコードとマニフェストを同じリポジトリに置くか」も、第10回で両方の要件が揃って初めて判断できます。
