環境に実際に送られるもの(そしてそれが重要な理由)
こんな状況を想像してみてください。チームはスプリントを終えたばかりで、全員が疲れ切っています。しかし、今夜中にリリースを出さなければなりません。ある開発者が自分のラップトップでアプリケーションをビルドし、ローカルでテストして、ステージングに成果物をプッシュします。ステージングのテストはパスし、自信は高まっています。
そこで誰かが言います。「念のため、プロダクション用に再ビルドしよう。最新の設定修正も含めておくよ。」
プロダクション用のビルドはステージング用とは異なります。タイムスタンプが違い、コンパイル結果も異なります。もしかすると、わずかに異なるバージョンのライブラリが取り込まれているかもしれません。
1時間後、プロダクションがダウンします。そして、あなたは答えられない質問を抱えることになります。「バグの原因は、異なるコードなのか、それとも環境設定なのか?」確実に知る手段を失ってしまったのです。
この問題を解決するのが、不変の成果物(イミュータブル・アーティファクト)です。しかしその前に、成果物とは実際に何なのかを説明しましょう。
サーバーで実行されるのはソースコードではない
開発者がコードを書くと、ソースコードが生成されます。そのソースコードは原材料です。人間が読めるテキストであり、サーバーが実行できるように変換する必要があります。
この変換プロセスをビルドと呼びます。そしてビルドの出力を成果物(アーティファクト)と呼びます。
成果物の形は、何をビルドするかによって異なります。
- Javaアプリケーション →
.jarまたは.warファイル - Pythonアプリケーション → wheelファイルまたはパッケージ化されたフォルダ
- Node.jsアプリケーション → ミニファイされたファイルを含む
distフォルダ - Goアプリケーション → 単一のバイナリ実行ファイル
どの場合でも、成果物はサーバーに配置して実行する準備が整ったファイルの集まりです。コンパイルも依存関係の解決も不要で、実行するだけです。
ビルドパイプラインが成果物を生成する
現代のソフトウェアデリバリーでは、ビルドプロセスは自動的に実行されます。開発者がコードをリポジトリにプッシュすると、CIパイプラインが起動します。コードをコンパイルし、テストを実行し、成果物を生成します。その成果物は、ステージング、プロダクション、そしてその間のすべての環境に送られます。
正しいアプローチとリスクのあるアンチパターンを視覚的に比較した図です。
ここで、ほとんどのチームは選択を迫られます。そして、ほとんどのチームは気づかないうちに間違った選択をしています。
環境ごとに再ビルドする問題点
よくあるパターンは、ステージング用にビルドしてテストし、その後プロダクション用に再ビルドするというものです。その論理は一見もっともに聞こえます。「プロダクションには最新のビルドを届けたい。」
しかし、このパターンは隠れたリスクを生み出します。ビルドごとにわずかな違いが生じます。コンパイラの出力が異なる可能性があります。依存関係が微妙に異なるバージョンに解決されるかもしれません。ビルドのタイムスタンプも変わります。ファイルの書き込み順序さえ異なることがあります。
プロダクションで障害が発生したとき、変数は成果物と環境の2つになります。どちらが原因か特定できません。コード変更が原因なのか、それともステージングにはなかったプロダクション環境の何かが原因なのか。
確実性を失ってしまったのです。そして確実性こそ、インシデント対応において最も価値のあるものです。
不変の成果物が確実性を取り戻す
不変の成果物とは、ビルド後に決して変更されない成果物のことです。ビルドが生成したら、その成果物は凍結されます。修正も再ビルドも手動編集もありません。
同じ成果物(同じハッシュ、同じサイズ、同じファイル)が、そのバージョンを実行するすべての環境に送られます。
これにより強力な保証が得られます。ステージングでテストに合格した成果物は、環境が同様に設定されていれば、プロダクションでも同じように動作します。プロダクションで障害が発生した場合、問題が成果物にあるのではないとわかります。問題は設定、データ、または環境そのものにあるのです。
同じ成果物がどこにでもデプロイされていることを確認する簡単な方法です。
# ビルドマシンでビルド完了後
sha256sum myapp-v1.2.3.jar
# 出力: a1b2c3d4e5f6... myapp-v1.2.3.jar
# ステージングサーバーでデプロイ後
sha256sum /opt/myapp/myapp-v1.2.3.jar
# 出力: a1b2c3d4e5f6... /opt/myapp/myapp-v1.2.3.jar
# プロダクションサーバーでデプロイ後
sha256sum /opt/myapp/myapp-v1.2.3.jar
# 出力: a1b2c3d4e5f6... /opt/myapp/myapp-v1.2.3.jar
チェックサムが一致すれば、まったく同じ成果物がすべての環境にデプロイされています。
これで変数が1つ減りました。デバッグがより速く、より安全になります。
不変の成果物でロールバックが簡単に
不変の成果物があれば、ロールバックは簡単です。新しいバージョンで問題が発生しても、古いバージョンを再ビルドする必要はありません。正しいコミットを見つけてパイプラインを再実行する必要もありません。すでに存在する成果物をデプロイするだけです。
その成果物は数週間前や数ヶ月前にビルドされたものです。テスト済みで、以前プロダクションで動作した実績があります。何をするか正確にわかっています。ストレージから取得してデプロイするだけです。
不変の成果物がない場合、ロールバックは古いコードの再ビルドを意味します。その再ビルドは、元々実行されていたものとは異なる成果物を生成する可能性があります。現在の形では一度もテストされていないものをデプロイすることになります。それはギャンブルです。
成果物はどこに保存するのか
成果物には、中央集約型で安全かつ信頼性の高いストレージ場所が必要です。これを成果物リポジトリ(アーティファクト・リポジトリ)と呼びます。一般的な選択肢は以下の通りです。
- 汎用成果物ストレージ: Nexus や Artifactory
- コンテナイメージ: Docker Registry
- 生の成果物ファイル: S3バケット や Azure Blob Storage
- パッケージレジストリ: npm, PyPI, Maven Central など
各成果物には、バージョン番号、コミットハッシュ、ビルドタイムスタンプ、関連タグなどのメタデータを付与する必要があります。このメタデータにより、任意の成果物をソースコードとビルドパイプラインに遡って追跡できます。
成果物リポジトリは、どこで何が実行されているかに関する単一の真実源(シングル・ソース・オブ・トゥルース)となります。「今プロダクションで動いているバージョンは何か?」と聞かれたら、サーバーではなく成果物を確認します。
環境に送られるもの
ビルドが完了した後、環境に送られるのは成果物だけです。ソースコードではありません。再ビルドされたバージョンでもありません。手動で編集されたファイルでもありません。
すべての環境に対して1つの成果物。一貫性があり、追跡可能で、不変です。
この原則は、Javaマイクロサービス、Pythonデータパイプライン、Node.jsフロントエンド、Go CLIツールなど、どのようなものをデプロイする場合でも適用されます。パッケージ形式は変わりますが、考え方は同じです。「一度ビルドし、どこにでもデプロイする。」
チーム向けクイックチェックリスト
成果物戦略を設定または見直す際に、確認すべき項目をいくつか挙げます。
- すべてのビルドが、一意の識別子(コミットハッシュ、ビルド番号、セマンティックバージョン)を持つバージョン管理された成果物を生成していること
- 同じ成果物が、再ビルドされることなくすべての環境にプロモートされていること
- 成果物が中央リポジトリに保存されており、開発者のラップトップやビルドサーバー上にないこと
- 古い成果物が、少なくともロールバック期間中は保持されていること
- 各成果物にメタデータ(コミットハッシュ、ビルドタイムスタンプ、トリガー理由)が付与されていること
まとめ
次回チームがリリースを準備するとき、1つの質問をしてください。「ステージングで実行されている成果物は、プロダクションで実行されるファイルとまったく同じですか?」答えが「いいえ」の場合、測定不能なリスクを持ち込んでいることになります。他のことを心配する前に、まずそれを修正しましょう。
一度ビルドし、同じ成果物をどこにでもデプロイし、不変に保つ。この1つのプラクティスが、ほとんどの監視ツールよりも多くのプロダクションインシデントを排除します。