デプロイ前にパイプラインがチェックすべきこと

こんな状況を想像してみてほしい。変更をプッシュし、パイプラインがグリーンになり、デプロイする。10分後、ユーザーからエラー報告が殺到する。データベースマイグレーションがクエリを壊した。依存関係に既知の脆弱性が入り込んでいた。設定ファイルに必須フィールドが欠けていた。

パイプラインは「すべて問題なし」と言っていた。しかし実際はそうではなかった。

これは、パイプラインがコードのコンパイルとテストの通過だけをチェックしている場合に起こる。それらは必要だが、十分ではない。有用なパイプラインはゲートキーパーとして機能する。つまり、本番環境で問題を引き起こす変更を、それが本番に到達する前に阻止する。問題は、何をチェックすべきかということだ。

まずビルドが成功しなければならない

最も基本的なゲートは、コードが実際にビルドできるかどうかだ。開発者が変更をプッシュすると、パイプラインはコードのコンパイルを試みるか、実行可能なアーティファクトを生成する。ビルドが失敗した場合(構文エラー、互換性のない依存関係、壊れた設定)、続行する意味はない。コードはまったく実行できないからだ。

このゲートは常に最初のステップであるべきだ。高速で、コストもかからず、テストする準備すらできていない変更をふるい落とす。ビルドが失敗すれば、パイプラインは停止する。開発者は即座にフィードバックを得て、他の誰かが影響を受ける前に問題を修正できる。

単体テストは振る舞いを検証し、実装を検証しない

ビルドが成功したら、次のゲートは単体テストだ。しかし、すべての単体テストが同じように作られているわけではない。優れた単体テストは、関連するエントリポイントから意味のある振る舞いをチェックする。バックエンドサービスの場合、それは実際の内部レイヤーを通るAPIエンドポイントやユースケースかもしれない。フロントエンドアプリの場合、ボタンのクリックやフォームの送信といったユーザー操作かもしれない。

重要なのは、単体テストは内部コードをリファクタリングしても壊れてはならず、観測可能な振る舞いが変わったときにのみ壊れるべきだということだ。単体テストが失敗した場合、システムが有効な入力に対して正しく応答しなくなったことを意味する。それはパイプラインを止める価値がある。

実装の詳細に密結合した単体テストを書くチームもある。プライベートメソッドのテスト、すべてのクラスを個別にテスト、内部レイヤーのモック化、リファクタリングのたびに壊れる。それらのテストはノイズを生み出し、デリバリーを遅くする。必要なときは外部の隣接コンポーネントをモックし、内部の振る舞いはアプリケーションが実際に使うパスを通して実行させる。ゲートはテストが信頼できる場合にのみ有用だ。単体テストが機能上の理由以外で頻繁に失敗するなら、開発者はそれらを無視し始め、ゲートの価値は失われる。

結合テストは接続の問題をキャッチする

単体テストは外部依存関係なしで実行できる。結合テストはそれができない。結合テストは、システムが実際に外部世界(実際のデータベース、メッセージキュー、サードパーティAPI)と通信できることを検証する。

例えば、単体テストはデータベースをモックしてパスするかもしれない。しかし、実際のデータベースはスキーマの不一致、インデックスの欠落、型変換エラーなどでクエリを拒否するかもしれない。結合テストはそれらの問題をキャッチする。

これらのテストには、より完全な環境(テスト用データベース、コンテナ、実行中のサービス)が必要だ。単体テストより遅いが、異なる種類の問題をキャッチする。結合テストが失敗した場合、通常はその変更が実際に動作するインフラストラクチャと連携しないことを意味する。

セキュリティスキャンは自動的に実行すべき

セキュリティはリリース前の手動レビューに任せられるものではない。誰かがコードを見る頃には、脆弱な依存関係がすでに本番環境にあるかもしれない。自動化されたセキュリティスキャンは人間の介入なしに実行でき、いくつかのことをチェックする。

  • 既知の脆弱性がある依存関係はないか?
  • コードに誤って認証情報やAPIキーが含まれていないか?
  • SQLインジェクションや安全でないデシリアライゼーションなど、悪用される可能性のあるパターンはないか?

ソースコードに対して静的解析を実行するチームもあれば、実行中のアプリケーションに対して動的スキャンを実行するチームもある。どちらのアプローチも異なる問題をキャッチする。重要なのは、パイプラインが毎回自動的にセキュリティをチェックすることだ。

セキュリティスキャンが失敗した場合、パイプラインは停止する。開発者は何が問題かを正確に示すレポートを受け取る。手動承認は不要で、セキュリティチームがコードをレビューするのを待つ必要もない。ゲート自体が変更をブロックする。

ポリシー準拠で一貫性を保つ

すべてのチェックが技術的である必要はない。プロセスと一貫性に関するものもある。ポリシー準拠ゲートは、変更がチームや組織で合意されたルールに従っているかをチェックする。

一般的なポリシーチェックには以下が含まれる。

  • コードレビューを通過したか?
  • ブランチ名が規則に従っているか(例:feature/fix/)?
  • プルリクエストが大きすぎないか?一部のチームは変更行数やファイル数を制限している。
  • すべての依存関係が承認されたレジストリから来ているか?

これらのチェックは管理的だが、重要だ。誰かがレビューなしで2000行の変更をマージしたり、信頼できないソースからの依存関係が取り込まれたりする状況を防ぐ。パイプラインがルールを自動的に強制するので、誰も覚えておく必要がない。

自動化だけでは不十分な場合

上記の5つのゲート(ビルド、単体テスト、結合テスト、セキュリティスキャン、ポリシー準拠)はすべて自動的に実行できる。パイプラインが合否を判断し、何か問題があれば停止する。人間が判断を下す必要はない。

しかし、すべてのチェックを自動化すべきではない。人間の判断が必要な決定もある。例えば、重要なビジネスフローに影響する変更や、複数のサービスに同時に影響するデプロイメントは、リスクを評価して進めるかどうかを判断する人が必要かもしれない。そこに手動承認が登場する。

重要なのは、客観的に測定できるものは自動化し、主観的な判断は人に委ねることだ。常に適用されるルールとして表現できるチェックは自動化する。コンテキスト、経験、トレードオフが必要な場合は、手動のままにしておく。

パイプラインゲートの実践的チェックリスト

パイプラインゲートを設定または見直す場合、以下に短いチェックリストを示す。

次のフローチャートは、これらのゲートがどのように連携して機能するかを示している。

flowchart TD A([開始]) --> B{ビルド成功?} B -- Yes --> C{単体テスト合格?} B -- No --> X[停止して通知] C -- Yes --> D{結合テスト合格?} C -- No --> X D -- Yes --> E{セキュリティスキャン合格?} D -- No --> X E -- Yes --> F{ポリシー準拠合格?} E -- No --> X F -- Yes --> G{手動レビュー必要?} F -- No --> X G -- No --> H([デプロイ]) G -- Yes --> I{レビュー承認?} I -- Yes --> H I -- No --> X
  • ビルドゲート:コードがコンパイルできない、またはアーティファクトを生成できない場合、パイプラインは停止するか?
  • 単体テストゲート:テストは内部実装ではなく、関連するエントリポイントからの意味のある振る舞いを検証しているか?
  • 結合テストゲート:パイプラインは実際の依存関係(データベース、キュー、外部サービス)に対してテストしているか?
  • セキュリティスキャンゲート:依存関係の脆弱性はスキャンされているか?コード内のシークレットや危険なパターンはチェックされているか?
  • ポリシー準拠ゲート:コードレビュー、ブランチ命名規則、依存関係ソースなどのルールが自動的に強制されているか?

すべてのチームが初日から5つすべてを必要とするわけではない。ビルドと単体テストから始めよう。外部依存関係がある場合は結合テストを追加する。脆弱性を気にし始めたらセキュリティスキャンを追加する。チーム全体で一貫性が必要になったらポリシーチェックを追加する。

ゲートの目的は問題を止めることであり、速度を落とすことではない

適切に設計されたゲートは、理由もなく摩擦を追加しない。問題を早期に、修正が安価なうちにキャッチする。パイプラインでキャッチされたビルド失敗は数分のコストだ。本番環境でキャッチされたデプロイメント失敗は数時間、時には数日のコストがかかる。

目標はパイプラインを通りにくくすることではない。目標は、パイプラインが通ったときに自信を持ってデプロイできるようにすることだ。パイプラインがグリーンなのに本番環境が壊れるなら、ゲートは間違ったものをチェックしている。まずゲートを修正し、それからパイプラインを修正しよう。