プロダクション向けの再ビルドが思ったより危険な理由
ステージングでビルドが通り、テストもパスした。チームは出荷の準備ができている。そんなとき、誰かがこう言う。「同じタグをチェックアウトして、再ビルドして本番にデプロイすればいいじゃないか。古いアーティファクトを保管しておく必要はない。」
一見効率的に聞こえる。アーティファクトストレージを管理する必要もないし、古いビルドを探し回る必要もない。ただビルドを再実行して出荷するだけだ。しかし、この一見実用的なショートカットには、デリバリーパイプラインで達成しようとしていることを静かに損なう2つの問題が潜んでいる。すなわち、トレーサビリティの喪失と再現性の喪失である。
トレーサビリティの問題
プロダクション向けに再ビルドすると、本番環境で動作しているアーティファクトは、ステージングでテストを通過したアーティファクトと同じものではなくなる。同じソースコードからビルドしたが、ビルドプロセスによって新しいIDを持つ新しいアーティファクトが作成される。チェックサムもビルドタイムスタンプも異なり、埋め込まれたメタデータも異なる可能性がある。
本番環境で何か問題が発生した場合、ステージングの結果を指して「この正確なアーティファクトがテストされた」とは言えなくなる。2回目のビルドが同一の結果を生成したと信じるしかない。検証なしでは、その信頼は盲目的なものだ。
以下のフローチャートは、2つの経路を対比している。
トレーサビリティは書類上の問題ではない。「現在本番環境で正確に何が動いていて、それがテストしたものと一致することをどうやって確認するのか」というシンプルな問いに答えられるかどうかの問題だ。再ビルドすると、その問いに自信を持って答えるのが難しくなる。
再現性の問題
まったく同じコミットをチェックアウトしても、ビルドプロセスが常に同じ出力を生成するとは限らない。2回のビルドの間で結果を変える要因はいくつもある。
依存関係のドリフト。 月曜日の最初のビルドでは、オープンソースライブラリのバージョン1.2.3をプルした。水曜日までにメンテナーがバージョン1.2.4をリリースした。2回目のビルドは新しいバージョンを自動的にプルする。ソースコードは変わっていないが、アプリケーションの動作は変わるかもしれない。マイナーパッチが、依存していたバグを修正したり、リグレッションを引き起こしたりする可能性がある。また、セキュリティ脆弱性を静かに取り込むこともあり得る。
ベースイメージの更新。 ビルドでlatestタグや、パッチが適用される特定のマイナーバージョンが付いたDockerベースイメージを使用している場合、2回目のビルドでは異なる基盤OSレイヤーが使われる可能性がある。アプリケーションコードは同じでも、ランタイム環境がその下で変わってしまう。
ビルド環境の違い。 ステージングをビルドしたCIランナーは、わずかに異なるJDKバージョン、Node.jsランタイム、システムライブラリを持っていたかもしれない。ビルドキャッシュが実行間に期限切れになり、完全な再コンパイルが発生して異なるバイトコードが生成される可能性もある。これらの違いが開発中に表面化することはめったにないが、本番環境で微妙な障害を引き起こす可能性がある。
タイムスタンプとメタデータ。 多くのビルドツールは、タイムスタンプ、ビルド番号、コミットハッシュをアーティファクトに埋め込む。機能コードが同一であっても、アーティファクトのメタデータは異なる。これにより、本番環境の動作を特定のビルドと関連付ける必要がある場合、デバッグが難しくなる。
現実世界への影響
これらの問題は理論上の話ではない。チームが本番環境向けに再ビルドを出荷したところ、自動更新された依存関係がセキュリティ上の欠陥をもたらした事例がある。また、ライブラリのパッチレベルの更新によって関数の動作が変わり、アプリケーションの振る舞いが変わったケースもある。さらに、再ビルドによって開発環境では問題なく動作したが、ビルド環境のコンパイラバージョンが異なるために本番環境で失敗したアーティファクトが生成されたこともある。
最悪なのは、これらの問題が検出しにくいことだ。パイプラインはグリーンを示している。コミットハッシュは一致している。誰かが本番環境で何かおかしいと気づくまではすべて順調に見え、その時点では根本原因の追跡はフォレンジック調査になる。
原則:ビルドは1回、プロモーションは複数回
これが「ビルドは1回、プロモーションは複数回」という原則が存在する理由だ。アーティファクトを1回ビルドし、検証し、その後、そのまったく同じアーティファクトを開発環境、ステージング環境、本番環境の各環境にプロモーションする。再ビルドはしない。セカンドチャンスはない。
このアプローチでは、本番環境のアーティファクトは、すべてのテストに合格したアーティファクトと物理的に同一である。チェックサムが一致する。埋め込まれたメタデータが一致する。何がテストされ、何が出荷されたかを正確に把握できる。すべてのアーティファクトには、そのコミット、ビルド構成、タイムスタンプにリンクされた一意のIDがあるため、トレーサビリティは組み込まれている。再現性は問題にならない。なぜなら、何かを再現しているのではなく、同じバイナリを使用しているからだ。
再ビルドが避けられない場合
再ビルドが必要な正当なケースもある。古いアーティファクトが本番インフラストラクチャのアップグレードと互換性がない場合がある。ベースイメージの重要なセキュリティパッチが再ビルドを強制する場合もある。コンプライアンス要件により、本番アーティファクトを特定のハードニングされた環境からビルドする必要がある場合もある。
こうした状況は存在するが、例外であってデフォルトであってはならない。チームが本番環境向けに再ビルドすることを決定するたびに、リスクを明示的に認識すべきだ。すなわち、トレーサビリティを壊していること、そしてビルドが再現可能であることに賭けていることだ。決定を文書化し、出力を元のビルドと照合して検証し、将来の再ビルドを防ぐためのインシデントレビューの機会として扱うこと。
本番環境向け再ビルド前の実用的チェックリスト
- 再ビルドされたアーティファクトが、テスト済みのアーティファクトと機能的に同一であることを検証できるか?
- 推移的依存関係を含むすべての依存関係のバージョンを固定しているか?
- ビルド環境(コンパイラ、ランタイム、システムライブラリ)は元のビルドと同一か?
- ベースイメージはタグではなく、特定のダイジェストに固定されているか?
- 元のアーティファクトのプロモーションが不可能な理由を文書化しているか?
- チームはこれを標準的なプラクティスではなく例外として扱うことに同意しているか?
まとめ
本番環境向けの再ビルドは、デリバリーパイプラインに不確実性をもたらす。本番環境で動作しているアーティファクトがテストに合格したものと同じであると自信を持って言えなくなる。アーティファクトストレージの節約や利便性は、何か問題が発生したときのデバッグコストに見合うことはほとんどない。ビルドは1回。その同じアーティファクトをあらゆる場所にプロモーションすること。再ビルドは、明確な正当化を必要とする例外とし、本番環境へのデフォルトの経路としてはならない。