コードから実行可能パッケージへ:デプロイ前に行われること

新しい機能を書き終えたとしよう。コードはローカルでコンパイルが通り、テストもパスし、自信満々だ。ところが、プッシュしてデプロイしようとすると、何かが壊れる。サーバーがライブラリがないと文句を言う。別のOSバージョンでコンパイルされたバイナリがクラッシュする。デプロイスクリプトが正しいアーティファクトを見つけられない。

この状況は、ほとんどのチームが認めるよりもずっと頻繁に発生している。「自分のマシンでは動く」コードと「本番で動く」コードの間には、見落とされがちな多くのステップが存在する。コードを書いてからデプロイ可能なパッケージができるまでの間に何が起こるかを理解することは、ソフトウェアデリバリーに関わるすべてのエンジニアにとって不可欠だ。

ビルドとは、実際のところ何か?

開発者がコードを書いた時点では、そのコードはまだサーバー上で実行できる状態ではない。実行可能なものに変換する必要がある。この変換プロセスをビルドと呼ぶ。ビルドの出力をアーティファクトと呼ぶ。

アーティファクトには、一般的に2つの形態がある。

  • バイナリファイル: オペレーティングシステムが直接実行できる単一の実行可能ファイル。コンパイルされたGoプログラムやJavaのJARファイルを想像してほしい。
  • コンテナイメージ: バイナリに加えて、実行に必要なすべてのもの(ライブラリ、設定ファイル、環境変数、ランタイム自体)を含むパッケージ。コンテナイメージは、DockerやKubernetesのような環境にデプロイする際に使用される。

バイナリとコンテナイメージのどちらを選ぶかは、デプロイインフラストラクチャ次第だ。コンテナは環境間での一貫性が高く、バイナリは軽量で起動が速い。どちらにせよ、目標は同じだ。信頼性高くデプロイできる自己完結型のパッケージを生成することである。

ビルドの4つのステージ

すべてのバックエンドサービスは、言語やフレームワークに関係なく、これらのステージを経る。一つずつ見ていこう。

以下の図は、ソースコードから保存されたアーティファクトに至るまでの完全なビルドパイプラインを示している。

flowchart TD A[ソースコード] --> B[コンパイル] B --> C[依存関係のバンドル] C --> D[アーティファクト作成] D --> E{バイナリ or コンテナ?} E --> F[バイナリファイル] E --> G[コンテナイメージ] F --> H[アーティファクトストレージ] G --> H H --> I[レジストリ / リポジトリ]

ステージ1: コンパイル

コードがGo、Java、Rust、C++のようなコンパイル言語で書かれている場合、実行前にマシンコードまたはバイトコードにコンパイルする必要がある。Python、Node.js、Rubyのような言語は、ランタイムでインタプリタ処理されるため、このステップはスキップされる。

ここで重要なのは環境の一貫性だ。開発者のノートPCでコンパイルしてから本番サーバーにバイナリをコピーすると、互換性の問題が発生するリスクがある。本番サーバーは、異なるOSバージョン、異なるシステムライブラリ、または異なるCPUアーキテクチャを持つ可能性がある。常に、本番ターゲットに可能な限り近い環境でコンパイルすること。

インタプリタ言語の場合、コンパイルは不要だが、ランタイム環境を準備する必要がある。それが次のステージにつながる。

ステージ2: 依存関係のバンドル

バックエンドアプリケーションは、ほとんどの場合、自身のコードだけで動作することはない。データベースアクセス、HTTP処理、認証、ロギングなど、さまざまな目的のために外部ライブラリに依存している。これらの依存関係を収集し、コードと一緒にパッケージ化する必要がある。

正確なメカニズムは言語エコシステムによって異なる。

  • Python: pip install を実行し、すべてのパッケージを特定のフォルダまたは仮想環境に収集する。
  • Node.js: npm install を実行し、node_modules ディレクトリが完全であることを確認する。
  • Java: 依存関係はビルドプロセス中にJARまたはWARファイル内にバンドルされる。
  • Go: 依存関係はバイナリに直接コンパイルされるため、個別のバンドルは不要。

原則は単純だ。アーティファクトがサーバー上で実行されるとき、必要なものはすべてその中に含まれているべきである。デプロイ中に誰も手動で依存関係をインストールする必要があってはならない。それは、不整合と失敗のレシピだ。

ステージ3: アーティファクトの作成

次に、最終パッケージを生成する必要がある。チームがコンテナを使用している場合、Dockerfileを使ってDockerイメージをビルドすることを意味する。イメージには、コンパイルされたバイナリまたはアプリケーションコード、すべての依存関係、基本設定、およびアプリケーションを起動するためのコマンドが含まれる。

以下は、Goアプリケーションをコンパイルし、最小限のコンテナイメージを生成するマルチステージDockerfileの実用的な例である。

# Stage 1: コンパイルと依存関係のバンドル
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

# Stage 2: 最終アーティファクトの作成
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

チームがコンテナを使用しない場合、ビルド出力はサーバーに直接コピーできるバイナリファイルまたは圧縮フォルダになる。バイナリはよりシンプルでデプロイが高速だが、コンテナイメージほどの分離性や移植性はない。

いずれにせよ、アーティファクトはイミュータブルであるべきだ。一度ビルドされたら、変更してはならない。変更がある場合は、新しいビルドと新しいアーティファクトを意味する。これにより、テストしたものがそのままデプロイされることが保証される。

ステージ4: アーティファクトの保存

完成したアーティファクトには、デプロイプロセスがアクセスできる恒久的な保存場所が必要だ。この保存場所をレジストリまたはリポジトリと呼ぶ。

  • コンテナイメージはコンテナレジストリ(Docker Hub、Amazon ECR、Google Artifact Registry、またはHarborのようなセルフホスト型)に格納される。
  • バイナリファイルはアーティファクトリポジトリ(Nexus、Artifactory、またはS3のようなオブジェクトストレージ)に格納される。

すべてのアーティファクトには一意の識別子が必要だ。通常、これはバージョン番号、Gitコミットハッシュ、またはその両方の組み合わせである。一意の識別子があれば、チームは本番環境で実行されているコードのバージョンを常に追跡できる。問題が発生した場合、アーティファクトIDを見れば、何がデプロイされたかを正確に知ることができる。

自動化が重要な理由

これらの各ステージは、毎回同じ方法で実行されなければならない。手動でビルドすると、いつかステップをスキップしたり、間違った依存関係のバージョンを使用したり、間違ったマシンでコンパイルしたりするだろう。その結果、期待とは異なる動作をするアーティファクトが生まれる。

CIパイプラインはプロセス全体を自動化する。コード変更のたびにトリガーされ、ビルドステージを順番に実行し、適切な識別子とともにアーティファクトを保存する。パイプラインにより、すべてのアーティファクトが同じツール、同じ環境で、同じ方法でビルドされることが保証される。

この一貫性こそが、デプロイを予測可能にする。CIパイプラインからアーティファクトをデプロイするとき、何が得られるかを正確に知っている。手動のステップや環境の違いによる驚きはない。

ビルドプロセスのための実践的チェックリスト

ビルドパイプラインを設定する前に、以下の点を確認すること。

  • ビルド環境が本番環境と一致していること(OSバージョン、システムライブラリ、アーキテクチャ)。
  • すべての依存関係が明示的に宣言されていること(ロックファイル、requirements.txt、go.modなど)。
  • アーティファクトがコード変更ごとに1回だけビルドされ、イミュータブルに保存されること。
  • すべてのアーティファクトに一意で追跡可能な識別子(バージョンタグまたはコミットハッシュ)があること。
  • ビルドプロセスがメインブランチへのプッシュまたはマージのたびに自動的に実行されること。
  • アーティファクトレジストリに、手動介入なしでデプロイプロセスがアクセスできること。

次に来るもの

アーティファクトの準備が整い保存されたら、次はそれがデプロイしても安全かどうかという問題になる。そこで、自動テストとセキュリティスキャンが登場する。単体テスト、統合テスト、脆弱性スキャン、その他のチェックが、本番環境に到達する前にアーティファクトに対して実行される。

しかし、ビルド自体が壊れていれば、これらのチェックは意味をなさない。信頼性の高いビルドプロセスは、すべてのCI/CDパイプラインの基盤である。それがなければ、ソフトウェアではなく、推測をデプロイしていることになる。

要点: ビルドは単なる技術的なステップではない。コードがデプロイ可能な資産になる瞬間である。本番システムに適用するのと同じ厳格さで扱うこと。自動化し、標準化し、すべてのアーティファクトを追跡可能にすること。午前2時に本番の問題をデバッグしている未来の自分が、感謝することになるだろう。