CI/CDパイプラインに本当に必要な6つの機能(誇大広告の向こう側)
コードをプッシュし、パイプラインがグリーンになり、デプロイが完了する。しかし、何かが壊れたとき、あなたはパイプラインがその対処を想定していなかったことに気づく。データベースマイグレーションが間違った順序で実行された。ステージングのアーティファクトと本番にデプロイされたものが異なっていた。そしてロールバック?誰も計画していなかった。
これが、「パイプラインを持っている」ことと「実際に機能するパイプラインを持っている」ことのギャップである。Jenkins、GitHub Actions、GitLab CI、ArgoCDといったツールはすべてデリバリーを解決すると主張するが、ツール自体が問題なのではない。問題は欠落している機能にある。パイプラインに適切な構成要素がなければ、どのツールもそれを修正できない。
ここに、すべてのCI/CDパイプラインが持つべき6つの基本機能を挙げる。あれば便利なものではない。時間ができたら追加する機能でもない。コードから本番環境へ安全に変更を届けるための最低要件である。
ビルド:コードを実行可能な形にする
開発者が変更をプッシュするたびに、パイプラインはそのコードを実際に実行可能なものに変換しなければならない。Go、Rust、Javaのようなコンパイル言語の場合、ソースをバイナリにコンパイルすることを意味する。PythonやJavaScriptのようなインタプリタ言語の場合、ビルドとは構文チェック、モジュールのバンドル、依存関係の解決、ランタイム環境の準備を意味する。
ビルドは最初のゲートである。コードがビルドできなければ、他のことは何も重要ではない。パイプラインはここで速やかに失敗すべきであり、コンパイルすらできないコードに対してテストを実行する時間を無駄にしてはならない。
よくある間違いは、ビルドを常に成功する単純なステップとして扱うことだ。しかし、ビルド環境は異なる。開発者のローカルマシンで成功したビルドが、パイプラインではシステムライブラリの欠落、ツールバージョンの違い、環境変数の問題で失敗する可能性がある。パイプラインのビルドステップは再現可能かつ分離可能でなければならず、CIで動作するものがどこでも動作するようにする必要がある。
テスト:問題がユーザーに届く前にキャッチする
ビルドが成功した後、パイプラインは自動テストを実行しなければならない。これはミリ秒で完了する単体テストだけの話ではない。健全なパイプラインは複数のテストレイヤーを実行する:
- 個々の振る舞いを検証する単体テスト
- コンポーネントがどのように連携するかをチェックする結合テスト
- 実際のユーザーシナリオをシミュレートするエンドツーエンドテスト
各レイヤーは異なる種類の問題をキャッチする。単体テストはロジックエラーをキャッチする。結合テストはサービス間のミスマッチをキャッチする。エンドツーエンドテストは複数のシステムにまたがるワークフローの障害をキャッチする。
鍵は自動化である。テストは人間の介入なしに実行されなければならない。誰かが手動でテストをトリガーしたり結果を解釈したりする必要がある場合、パイプラインの主要な価値であるスピードと一貫性が失われる。自動的に実行されるテストはすべて、人間がチェックすることを覚えておく必要があることの一つ減る。
パッケージ:バージョン管理されたデプロイ可能なアーティファクトを作成する
コードがビルドされテストに合格したら、パイプラインは結果をデプロイ可能なアーティファクトにパッケージ化しなければならない。アーティファクトの形式は、何を出荷するかによって異なる:
- マイクロサービス用のコンテナイメージ
- デスクトップアプリケーション用のバイナリファイル
- モバイルアプリ用のAPKまたはIPA
- サーバーレス関数用のzipアーカイブ
- Kubernetesデプロイメント用のHelmチャート
すべてのアーティファクトは一意のバージョンを持たなければならない。単なるタイムスタンプやビルド番号ではなく、正確なコミット、パイプライン実行、テスト結果に結びつくバージョンである。このトレーサビリティにより、本番環境で何が実行されているか、バージョン間で何が変更されたかを正確に把握できる。
アーティファクトは、デプロイステージがアクセスできる中央レジストリまたはリポジトリに保存されなければならない。デプロイ時にアーティファクトを再ビルドすると、一貫性が失われる。テストに合格したアーティファクトとデプロイされるアーティファクトは完全に同一でなければならない。
デプロイ:アーティファクトをターゲット環境に配置する
デプロイはファイルをコピーする以上のものである。新しいバージョンを環境に配置し、トラフィックを処理させるプロセスである。ステージングの場合、デプロイはテスト用に新しいバージョンをインストールすることを意味する。本番の場合、ユーザーを中断させることなく実行中のバージョンを置き換えることを意味する。
リスクレベルに応じて異なるデプロイ戦略が存在する:
- ローリングアップデート:インスタンスを一つずつ置き換える
- ブルーグリーン:2つの同一環境間でトラフィックを切り替える
- カナリア:まず新しいバージョンに少量のトラフィックを送る
- フィーチャーフラグ:コードをデプロイするが、トグルの背後に隠しておく
パイプラインは各環境に適した戦略をサポートしなければならない。ステージングは単純な置き換えでよい。本番は多くの場合、モニタリングを伴う段階的なロールアウトが必要である。パイプラインはファイルコピーだけでなく、プロセス全体を自動化すべきである。
マイグレーション:データベース変更を安全に処理する
アプリケーションがデータベースを使用する場合、パイプラインはスキーママイグレーションを処理しなければならない。カラムの追加、データ型の変更、新しいテーブルの作成には、特定の順序でマイグレーションスクリプトを実行する必要がある。これらのマイグレーションはアプリケーションデプロイとランダムに混在させてはならない。
難しいのは順序付けである。場合によっては、新しいアプリケーションコードをデプロイする前にマイグレーションを実行する必要がある。例えば、新しいコードが使用するNULL可能カラムを追加する場合だ。他の場合では、新しいコードをデプロイした後にマイグレーションを実行する必要がある。例えば、古いコードがまだ参照している古いカラムを削除する場合だ。
パイプラインはこの順序を認識し、正しく実行しなければならない。間違ったタイミングで実行されるマイグレーションは、ダウンタイム、データ損失、またはその両方を引き起こす可能性がある。これはCI/CDパイプラインで最も見落とされがちな機能の一つであり、間違えると最も危険なものの一つである。
ロールバック:問題が発生したときに元に戻す
すべてのデプロイが成功するわけではない。新しいバージョンがエラー、パフォーマンス低下、またはデータ破損を引き起こした場合、パイプラインは以前のバージョンに戻すことができなければならない。ロールバックは単に古いアーティファクトを再デプロイするだけではない。以下のことを含む:
- アプリケーションを以前のバージョンに戻す
- データベースでリバースマイグレーションを実行する
- インフラストラクチャ設定を復元する
- ロールバックが実際に機能したことを確認する
ロールバックは最初のデプロイの前に計画されなければならない。変更を元に戻す方法を考慮せずにパイプラインを設計すると、本番がダウンしている間にロールバックスクリプトを急ごしらえで書く羽目になる。それを解決するのは最悪のタイミングである。
データベースマイグレーションの場合、ロールバックとはアップマイグレーションを元に戻すダウンマイグレーションを持つことを意味する。インフラストラクチャの場合、以前の状態ファイルを保持するか、状態ロールバックをサポートするInfrastructure as Codeツールを使用することを意味する。アプリケーションの場合、以前のアーティファクトを利用可能に保ち、即時切り替えをサポートするデプロイ戦略を持つことを意味する。
すべてをまとめる
これらの6つの機能(ビルド、テスト、パッケージ、デプロイ、マイグレーション、ロールバック)は、真のCI/CDパイプラインの基盤を形成する。何を出荷するかによって、一部の機能は異なる形をとるかもしれない。インフラストラクチャパイプラインは、ビルドとパッケージを設定検証と状態準備に置き換えるかもしれない。モバイルパイプラインは、コード署名とアプリストアへの提出を追加するかもしれない。しかし、コア機能は同じままである。
以下は、各機能をステージにマッピングした最小限のGitLab CIパイプラインである:
stages:
- build
- test
- package
- deploy
- migrate
- rollback
build:
stage: build
script:
- go build -o app
test:
stage: test
script:
- go test ./...
package:
stage: package
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push registry.example.com/myapp:$CI_COMMIT_SHA
deploy:
stage: deploy
script:
- kubectl set image deployment/myapp myapp=registry.example.com/myapp:$CI_COMMIT_SHA
migrate:
stage: migrate
script:
- ./run_migrations up
rollback:
stage: rollback
script:
- ./run_migrations down
- kubectl rollout undo deployment/myapp
when: manual
次のフローチャートは、これらの6つの機能が典型的なパイプラインでどのように接続されるかを示している:
CI/CDツールを選んだりパイプラインを再設計したりする前に、これらの機能のうちどれを持っていてどれが欠けているかをマッピングしよう。すべてを約束するツールでも、データベースマイグレーションやロールバック計画を処理できなければ、あなたは無防備なままである。
クイック機能チェックリスト
- ビルドは分離された再現可能な環境で実行される
- テストは複数のレベルで自動実行される
- アーティファクトはバージョン管理され、中央レジストリに保存される
- デプロイは各環境に適した戦略をサポートする
- データベースマイグレーションはアプリケーションデプロイに対して正しい順序で実行される
- ロールバックはテスト済みであり、アプリケーション、データベース、インフラストラクチャに対して機能する
具体的な takeaways
パイプラインはステップの集まりではない。それは変更の完全なライフサイクル(コードから実行中のサービスへ、そして必要なら元へ)を処理しなければならないシステムである。パイプラインがビルド、テスト、パッケージ、デプロイ、マイグレーション、ロールバックを実行できなければ、それは不完全である。ツールを切り替えるのではなく、欠けている機能を埋めることから始めよう。