ビルド:コードを実行可能なものにするステージ

最新の変更をプッシュしたとしよう。パイプラインがそれを拾い上げ、コードをチェックアウトし、環境を準備する。次は何か? 次のステップは、そのコードが実際にどこかで実行できることを確認することだ。これがビルドステージである。

パイプラインに慣れていない多くの人は、ビルドとは単なるコンパイルだと考えがちだ。Javaを書くならバイトコードにコンパイルする。Goを書くならバイナリにコンパイルする。それもビルドの一部ではあるが、ビルドはもっと広い概念だ。ソフトウェアデリバリーにおけるあらゆる種類の作業には、たとえ出力がバイナリファイルでなくても、ビルドステップが必要である。

ビルドの本当の意味

開発者が書くコードは、通常そのままサーバーで実行できる状態ではない。ターゲットシステムが実行できる形に変換する必要がある。Javaアプリケーションの場合、ソースコードをバイトコードにコンパイルし、JARやWARファイルにパッケージングすることを意味する。Node.jsアプリケーションの場合、バンドラー、ミニファイア、トランスパイラを実行してコードをプロダクション対応にすることを意味するかもしれない。Goアプリケーションの場合、スタンドアロンのバイナリにコンパイルすることを意味する。

しかし、ビルドはアプリケーションコードだけのものではない。データベースにもビルドステップが必要だ。SQLコード、ストアドプロシージャ、データベーススキーマは、コンパイルまたは検証される必要がある。出力はバイナリではない。ターゲットデータベースに対して正しい順序で実行できる、準備の整ったマイグレーションファイルのセットである。データベースのビルドは通常、構文エラーと依存関係の順序がチェックされた、検証済みのSQLコマンドを生成する。

以下は、2つの一般的なアプリケーションタイプにおけるパイプライン設定でのビルドステージの例である:

build-nodejs:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

build-go:
  stage: build
  script:
    - go build -o app .
  artifacts:
    paths:
      - app

インフラストラクチャもビルドプロセスを経る。Terraformファイル、CloudFormationテンプレート、Ansibleプレイブックは、構文チェック、検証、そして場合によってはよりデプロイしやすい表現へのコンパイルが必要である。インフラストラクチャビルドの出力は、検証済みの設定ファイル、チェック済みのテンプレート、あるいは事前にビルドされたマシンイメージかもしれない。

共通する点はこれだ:ビルドはソースを、検証可能で後続のステージで使用できるものに変換する。

以下の図は、ビルドがさまざまな種類のソースコードを実行可能なアーティファクトに変換する方法をまとめたものである:

flowchart TD A[ソースコード] --> B{ビルドステージ} B --> C[アプリケーション: バイナリ / パッケージ / コンテナイメージ] B --> D[データベース: 検証済みマイグレーションスクリプト] B --> E[インフラストラクチャ: 検証済み設定 / プラン] C --> F[メタデータ付きアーティファクト] D --> F E --> F F --> G[次のステージ: テストとスキャン]

優れたビルドが生み出すもの

ビルドは検査可能な出力を生成しなければならない。ビルドが完了した後、次の3つを知る必要がある:成功したか、何を生成したか、出力が期待通りか。

適切に構成されたビルド出力には以下が含まれる:

  • すぐに使用できるアーティファクト。バイナリ、パッケージ、コンテナイメージ、またはマイグレーションファイルのいずれか。
  • このビルドをトリガーしたバージョンとコミットの記録。
  • 転送中にアーティファクトが変更されていないことを確認できるハッシュやチェックサムなどのメタデータ。
  • 明確な成功または失敗のステータス。

これらがなければ、パイプラインが次のステージに引き渡すものを信頼することはできない。

ビルドは再現可能でなければならない

同じコードで同じビルドを2回実行した場合、同じ結果が得られるべきである。これは決定論的ビルドと呼ばれる。ビルドが再現可能でない場合、チームはアーティファクトが本当にそのコードから生成されたものだと確信できなくなるため、重要である。

非決定論的ビルドは通常、2つの理由で発生する。第一に、ビルドがバージョン固定されていない外部ライブラリに依存している場合だ。ある日ライブラリが更新されると、コードが変更されていないにもかかわらず、突然ビルドが異なるアーティファクトを生成する。第二に、ビルドが一貫性のない環境で実行される場合だ。あるビルドエージェントが別のエージェントと異なるツールバージョンを持っていたり、以前のビルドのキャッシュファイルが残っていたりする。

依存関係をロックせよ。ツールのバージョンを固定せよ。クリーンなビルド環境を使用せよ。これらのプラクティスがビルドを信頼性のあるものにし、チームに自信を与える。

ビルドは最初のゲートである

ビルドはパイプラインにおいて、決定が下される最初のポイントである:この変更は先に進めるべきか、否か。ビルドが失敗した場合、パイプラインは停止すべきである。アーティファクト自体が壊れているのに、テストを実行したりデプロイしたりする意味はない。

これはビルドが高速でなければならないことを意味する。開発者は数分以内にフィードバックを必要としており、数時間後ではない。ビルドに時間がかかりすぎると、人々は待たなくなる。次のタスクに移り、ビルドが最終的に失敗したときには、すでにコンテキストが切り替わっている。フィードバックループが壊れてしまう。

ビルドをリーンに保て。有効なアーティファクトを生成するために必要なことだけを行え。より深いチェックは後のステージに任せよ。5分以内で完了するビルドは、コードがまだ頭の中に新鮮なうちに問題を修正するために必要な迅速なシグナルを開発者に与える。

さまざまな種類の作業に対するビルド

主要な領域ごとにビルドがどのように機能するかを見てみよう。

アプリケーション。 コンパイル言語の場合、ビルドはコンパイルとパッケージングを意味する。インタプリタ言語の場合、ビルドは依存関係の解決、アセットのコンパイル、バンドルを意味するかもしれない。コンテナ化されたアプリケーションは追加のステップを加える:コンテナイメージ自体のビルドがビルドステージの一部となる。

データベース。 データベースのビルドは、SQL構文の検証、マイグレーションファイルが正しく順序付けられているかの確認、そして場合によってはスキーマのコピーに対するドライランの実行を意味する。出力は、実行準備が整った検証済みのマイグレーションスクリプトのセットである。

インフラストラクチャ。 インフラストラクチャのビルドは、設定ファイルの検証、構文エラーのチェック、そして場合によっては何が変更されるかを示すプランの生成を意味する。Terraformのようなツールの場合、ビルドステージには terraform plan の実行が含まれ、変更のプレビューを生成する。

各タイプの作業には独自のビルド要件があるが、原則は同じである:ソースを、検証済みで再現可能なアーティファクトに変換し、次のステージに引き渡せるようにする。

クイックビルドチェックリスト

ビルドステージを完了と呼ぶ前に、以下のポイントを確認せよ:

  • ビルドは検証可能なアーティファクト(バイナリ、イメージ、マイグレーションファイル、設定)を生成する
  • ビルドは決定論的である:同じコード、同じ結果、毎回
  • ビルドは5分以内で完了する
  • ビルドは迅速に失敗し、明確なエラーメッセージを表示する
  • ビルド出力にはバージョン、コミット、チェックサムのメタデータが含まれる
  • 外部依存関係は特定のバージョンにロックされている
  • ビルド環境は実行間でクリーンで一貫性がある

次に来るもの

ビルドが完了しアーティファクトの準備が整うと、パイプラインは次のステージに移る:そのアーティファクトが実際に機能し、デプロイしても安全かどうかをチェックするステージである。それがテストとスキャンのステージだ。しかしそこに進む前に、ビルドが堅牢であることを確認せよ。弱いビルドステージは、後続のすべてのステージが対処しなければならない問題を生み出す。

ビルドは単なるコンパイルではない。コードが有用なものになる準備ができているかどうかの、最初の本当のチェックである。これを正しく行えば、パイプラインの残りの部分はそこから作業するための強固な基盤を得ることができる。