コードからビルドへ:なぜあなたのラップトップはコンパイルに適した場所ではないのか
新しい機能を書き終えたところです。あなたのマシンではテストが通り、go build や npm run build を実行しても問題なく動作します。デプロイの準備は整いました。
しかし、同じコードをサーバーにプッシュするとビルドが失敗します。あるいは、ラップトップではビルドが成功しても、ライブラリのバージョン不一致が原因で本番環境でクラッシュします。コードは同じなのに、環境が異なるのです。
これこそが、多くのチームがソフトウェア開発は単にコードを書くことだけではないと気づく瞬間です。それは、コードをどこでも確実に実行できるものに変換することなのです。
ビルド時に実際に何が起きているのか
人間が書いたコードは、サーバーが直接実行できるものではありません。クラスやメソッドを持つJavaファイル、パッケージ構造を持つGoファイル、型アノテーションを持つTypeScriptファイル — これらは人間が読めるように書かれています。サーバーが必要とするものはまったく別物です。
この変換プロセスは言語によって異なります。GoやRustの場合、コンパイラは単一のバイナリファイルを生成します。Javaの場合は、Java仮想マシン上で動作するバイトコードを生成します。TypeScriptやモダンなJavaScriptの場合は、コードがトランスパイルされ、しばしば圧縮されてコンパクトなファイルになります。この変換ステップをコンパイルと呼びます。
しかし、コンパイルは話の一部に過ぎません。モダンなアプリケーションが単一のファイルであることはほとんどありません。サードパーティのライブラリ、設定ファイル、CSSアセット、画像、そして場合によってはビューをレンダリングするためのテンプレートを取り込みます。これらのすべての部品を収集し、チェックし、一貫性のある構造に整理する必要があります。そのプロセスがビルドです。
すべてが収集されコンパイルされたら、結果をポータブルなものにパッケージ化する必要があります。パッケージ形式はアプリケーションの種類によって異なります:
以下の図は、ラップトップとCIサーバーでのビルドプロセスを対比し、環境の違いがどのように障害ポイントを生み出すかを示しています。
- JavaアプリケーションはJARまたはWARファイルを生成します
- Goアプリケーションは単一の実行可能バイナリを生成します
- Node.jsアプリケーションはすべての依存関係とアセットを含むフォルダを生成します
- モバイルアプリはAndroid用のAPKファイルまたはiOS用のIPAファイルを生成します
この最終的なパッケージ化された結果をアーティファクトと呼びます。それは、コードの完全で実行可能なバージョンです。
なぜラップトップでのビルドが悪いアイデアなのか
自分のマシンでビルドするのは魅力的です。高速で、完全に制御でき、問題をすぐにデバッグできます。しかし、このアプローチには根本的な問題があります:再現性です。
あなたのラップトップには、特定のオペレーティングシステム、何ヶ月も何年もかけてインストールされた特定のバージョンのライブラリ、そして自分でも覚えていないかもしれないローカル設定があります。ビルドサーバー、同僚のマシン、本番サーバー — それらはすべて異なる環境を持っています。ラップトップで動作するビルドが、ライブラリの欠落、コンパイラのバージョンの違い、またはあなたのマシンにしか存在しない環境変数が原因で、他の場所では失敗する可能性があります。
具体的な例を示します。同じ go build コマンドでも、2つの異なるマシンでは異なる動作をするバイナリが生成されることがあります:
# macOSのラップトップの場合:
go build -o myapp .
file myapp
# 出力: myapp: Mach-O 64-bit executable x86_64
ls -lh myapp
# 出力: -rwxr-xr-x 1 user staff 12M Mar 15 10:23 myapp
# CIサーバー(Linux)の場合:
go build -o myapp .
file myapp
# 出力: myapp: ELF 64-bit LSB executable, x86-64, dynamically linked
ls -lh myapp
# 出力: -rwxr-xr-x 1 root root 18M Mar 15 10:23 myapp
バイナリ形式(Mach-O vs. ELF)、サイズ(12MB vs. 18MB)、リンク方法が異なります。ラップトップでビルドしてバイナリをLinuxサーバーにコピーしても、単純に実行できません。毎回同じ環境を使用するCIサーバーは、この不一致を排除します。
これが自動ビルドシステムが存在する理由です。それらは、制御された一貫性のある環境で毎回ビルドプロセスを実行します。同じ手順、同じツール、同じ依存関係。ビルドサーバーでビルドが成功すれば、デプロイ時にも動作するという確信が持てます。
自動ビルドは問題を早期に発見することもできます。ビルドシステムは、必要なライブラリがすべて利用可能かどうか、コンパイルエラーがないかどうか、ファイル構造が正しいかどうかをチェックします。何か問題があればビルドは即座に失敗し、開発者は何を修正すべきか明確なレポートを受け取ります。
アーティファクトとは実際には何か
アーティファクトはビルドプロセスの最終出力です。それは、サーバーに移動して配置できるものです。それ以上の変換は必要ありません。サーバーはそれを受け取り、適切な場所に配置し、実行するだけで済みます。
これは、ミールキットと家庭料理の違いに似ています。家庭で料理をする場合、生の食材があり、それを切り、味付けし、調理します。結果は完成した料理です。アーティファクトはその完成した料理です。何かを切ったり、味を調整したりする必要はありません。温めて提供するだけです。
アーティファクトは可能な限り自己完結型であるべきです。Goアプリケーションの場合、必要なものをすべて含む単一のバイナリを意味します。Javaアプリケーションの場合、必要なライブラリをすべて含むJARファイルを意味します。Node.jsアプリケーションの場合、すべての依存関係がバンドルされたフォルダを意味します。
この自己完結性は、「自分のマシンでは動く」問題を排除するために重要です。ビルド環境でビルドされテストされたアーティファクトは、本番環境にデプロイされるものとまったく同じものです。ビルドとデプロイの間で何も変わりません。
実際のビルドパイプライン
典型的な自動ビルドプロセスは以下の手順に従います:
- チェックアウト: ビルドシステムがリポジトリから最新のコードをプルします。
- 依存関係の解決: 必要なすべてのライブラリとパッケージをダウンロードします。
- コンパイル: ソースコードを実行可能な形式に変換します。
- テスト: コンパイルされたコードに対して単体テストと統合テストを実行します。
- パッケージ化: すべてを最終的なアーティファクトにまとめます。
- アーティファクトの保存: デプロイシステムがアクセスできる中央リポジトリにアーティファクトを保存します。
各ステップは自動化され、ログに記録されます。いずれかのステップが失敗すると、ビルド全体が失敗し、チームに通知されます。部分的なビルドや、プロセス途中での手動修正はありません。
何が問題になる可能性があるか
自動ビルドでも、問題が発生する可能性があります。チームが直面する最も一般的な問題は次のとおりです:
依存関係の欠落: 開発中に利用可能だったライブラリがビルド環境で利用できない。これは通常、依存関係のバージョンが固定されていない場合や、ビルド環境にそれらをダウンロードするためのネットワークアクセスがない場合に発生します。
環境固有のコード: macOSでは動作するがLinuxでは動作しないコード。これは、ファイルパスの処理、システムコール、環境変数の使用でよく見られます。
ビルドツールのバージョン不一致: ビルドサーバーが、開発者がローカルで使用しているものとは異なるバージョンのコンパイラやビルドツールを実行している。これにより、出力に微妙な違いが生じる可能性があります。
リソースの枯渇: 特に大規模なアプリケーションのビルドや広範なテストスイートの実行時に、ビルドプロセスがメモリやディスク容量を使い果たす。
一貫性のないアーティファクトの命名: バージョン番号やタイムスタンプのないアーティファクトは、どのバージョンがどこで実行されているかを把握することを不可能にします。
ビルドプロセスのための実践的なチェックリスト
ビルドパイプラインを設定する前に、このチェックリストを確認してください:
- ビルドは毎回クリーンな環境で実行されますか?
- すべての依存関係のバージョンは明示的に定義され、ロックされていますか?
- ビルドはコンパイルエラーで即座に失敗しますか?
- テストはビルドの一部として実行され、別途実行されることはありませんか?
- アーティファクトには一意の識別子でバージョンが付けられていますか?
- アーティファクトは中央のアクセス可能なリポジトリに保存されていますか?
- ビルドはいつでもゼロから再現できますか?
まとめ
コードのビルドは、開発者の便利さのためのタスクではありません。それは、コードがデプロイ可能な製品になる瞬間です。一貫性のある環境、明確な手順、バージョン管理されたアーティファクトを使用してそのプロセスを自動化することで、デプロイメント障害の最も一般的な原因である「ラップトップで動作するものとサーバーで動作するものの違い」を排除できます。
信頼性の高いビルドプロセスが確立できたら、次の課題は、デプロイ時に利用できるようにそれらのアーティファクトをどこに保管するかです。ここでアーティファクトの保存と管理が重要になります。