すべてのビルドに一意のIDが必要な理由
ビルドを実行した。出力ディレクトリにJARファイルが現れる。あるいはZIPアーカイブやコンパイル済みバイナリかもしれない。今週行った他のビルドと何ら変わりはない。それをサーバーにコピーし、デプロイして、次に進む。
3日後、誰かが本番環境でバグを報告する。実際に動いているのはどのアーティファクトなのかを特定する必要がある。サーバーを確認すると、app-1.0.0.jar というファイルが見つかる。しかし、これが火曜日のビルドなのか木曜日のビルドなのか、まったく見当がつかない。どちらも 1.0.0 というラベルが付いていた。どちらも同じリポジトリから来ている。しかし何かがおかしい。問題をソースコードまでさかのぼって追跡できない。
これこそ、アーティファクトのIDが欠如していることが、実際の運用上の頭痛の種になる瞬間である。
単なるバージョン番号の問題点
バージョン番号は、最も基本的な識別方法である。大まかに何を扱っているかの感覚を与えてくれる。1.0.0、2.3.1、3.0.0-beta -- これらのラベルは、人間が進捗状況や互換性を理解するのに役立つ。
しかし、バージョン番号だけでは実際の運用ですぐに破綻する。
今日、バージョン 1.0.0 をビルドしたとする。明日、同じソースコードから同じビルドを実行する。すると、別の 1.0.0 ができる。これで、同じラベルを持つ異なる2つのファイルが存在することになる。どちらが本番環境にあるのか?どちらがテストされたのか?バグを再現する必要がある場合、どちらの 1.0.0 を使うべきか?
厄介な真実は、同一のソースコードからの2つのビルドが、異なるアーティファクトを生成する可能性があるということだ。パッケージマネージャーで依存関係のバージョンが変わっているかもしれない。ビルドツール自体が更新されているかもしれない。ビルド環境(OSパッチ、ライブラリバージョン、さらにはタイムゾーン)が微妙な違いを引き起こす可能性がある。同じバージョン番号、異なるアーティファクト。
優れたアーティファクトIDの条件
有用なアーティファクトIDは、一意で、トレーサブルで、永続的である必要がある。次の3つの質問に答えられるべきである。
- どのコードが使われたか?
- いつビルドされたか?
- どのビルド実行で生成されたか?
最も一般的なアプローチは、3つの情報を1つの識別子に組み合わせることである。
ビルドID
パイプライン実行ごとに連番が割り当てられる。Jenkinsではビルド番号、GitLab CIではパイプラインID、GitHub Actionsではラン番号と呼ばれる。名前は何であれ、プロジェクト内で一意の単調増加する整数である。ビルド142は常にビルド143とは異なる。
しかし、ビルドIDだけではどのコードがビルドされたかはわからない。さらに情報が必要である。
コミットハッシュ
GitのすべてのコミットにはSHAハッシュがある。これは、その時点のソースコードの正確な状態を一意に識別する長い16進数の文字列である。ビルドIDとコミットハッシュを組み合わせると、強力な情報が得られる。「このアーティファクトはビルド142からのもので、コミット a3f2c9e を使用した」。
何か問題が発生した場合、その正確なコミットをチェックアウトして、コンパイルされたコードを確認できる。推測や「これが使っていたバージョンだったと思う」は不要になる。
タイムスタンプ
チームによっては、IDにタイムスタンプを追加することもある。特に異なる環境間でアーティファクトを比較する場合に、ビルドが正確にいつ行われたかを知るのに役立つ。しかし、タイムスタンプだけでは一意ではない。異なるパイプラインの2つのビルドが同じ秒に開始する可能性がある。
ビルドID、コミットハッシュ、タイムスタンプの組み合わせにより、堅牢で人間が読めるIDが得られる。142-a3f2c9e-20250321T143022 のようなものだ。見た目は良くないが、曖昧さはない。
CIパイプラインでそのIDを構築する方法は次のとおりである。
BUILD_ID="${CI_PIPELINE_ID:-142}"
COMMIT_HASH="${CI_COMMIT_SHA:-a3f2c9e}"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
ARTIFACT_NAME="myapp-${BUILD_ID}-${COMMIT_HASH}-${TIMESTAMP}.jar"
echo "Building ${ARTIFACT_NAME}"
# ... ビルド手順 ...
cp target/app.jar "dist/${ARTIFACT_NAME}"
不変アーティファクトのルール
アーティファクトにIDが付与されたら、そのIDは決して変更してはならない。これが不変性の原則である。
不変アーティファクトとは、次のことを意味する。
- 既存のアーティファクトを上書きしない。
- 異なるファイルに同じIDを再利用しない。
- ビルド後にアーティファクトを変更しない。
再ビルドが必要な場合は、新しいIDを取得する。古いアーティファクトは元のラベルのまま、その場所に残る。これは無駄に思えるかもしれないが、トレーサビリティの基盤である。
不変性がなければ、アーティファクトストレージは上書きされたファイルと失われた履歴の混乱した状態になる。「これがステージングでテストされたアーティファクトだ」と確信を持って言えなくなる。なぜなら、誰かが同じ名前で新しいバージョンに置き換えているかもしれないからだ。
不変性があれば、すべてのアーティファクトをライフサイクルを通じて追跡できる。どのアーティファクトがステージングに送られ、どのアーティファクトが本番に送られ、どのアーティファクトがまだテスト待ちかを正確に把握できる。環境間でアーティファクトを比較し、それらが同一であることを確認できる。
アーティファクトはどこに保存されるのか
IDだけでは十分ではない。ビルドマシンを超えて存続するように、アーティファクトを保存する場所も必要である。
アーティファクトがCIサーバーやノートPCのフォルダに置かれているだけでは、ディスクがクリーンアップされたり、マシンが交換されたり、容量が不足したりすると消えてしまう。開発者、テスター、運用チーム、デプロイパイプラインなど、必要なすべての人がアクセスできる集中型ストレージシステムが必要である。
このストレージはレジストリと呼ばれる。単純なファイルサーバーでも、NexusやArtifactoryのような専用のアーティファクトリポジトリでも、Dockerイメージ用のコンテナレジストリのようなクラウドネイティブなソリューションでも構わない。重要なのは、ビルドされたすべてのアーティファクトの単一の信頼できる情報源であることだ。
一意で不変のIDと信頼性の高いレジストリを組み合わせることで、生成するすべてのソフトウェアの管理チェーンが作成される。実行中の任意のインスタンスを、その正確なビルド、ソースコード、および作成された条件までさかのぼって追跡できる。
実践的なチェックリスト
- すべてのビルドは、ビルドID、コミットハッシュ、タイムスタンプを組み合わせた一意の識別子を生成する。
- アーティファクトは、同じIDで上書きされたり置き換えられたりしない。
- アーティファクトストレージは集中管理され、必要なすべてのチームがアクセスできる。
- デプロイされた任意のアーティファクトを、その正確なソースコードコミットまで追跡できる。
- デプロイプロセスは、各環境で実行されているアーティファクトIDを記録する。
まとめ
一意のIDがないビルドは、責任の所在を不明確にする。本番の問題を効果的にデバッグできず、何がどこで実行されているかを検証できず、デプロイプロセスを信頼できない。ビルドID、コミットハッシュ、タイムスタンプの組み合わせにより、生成するすべてのアーティファクトを識別するためのシンプルで信頼性の高い方法が得られる。それを不変にし、レジストリに保存すれば、ソフトウェアのどのバージョンが実際に実行されているかを推測する必要はなくなる。