パイプラインにテストとスキャンが必要な理由:手遅れになる前に
アプリケーションのビルドが完了しました。ビルドは成功し、アーティファクトも生成されました。さて、次はどうしますか?
多くのチームはここで止まってしまいます。コードがコンパイルできてビルドが通れば、アーティファクトは本番環境に投入できると決めつけてしまうのです。しかし、ビルドが成功したということは、コードをアセンブルできたというだけであって、コードが実際に動作するか、セキュリティホールがないか、データベースと通信したときにクラッシュしないかは何も教えてくれません。
この段階でチェックをスキップするのは、中身を確認せずに荷物を発送するようなものです。壊れたものや危険なもの、あるいはその両方を送ってしまうかもしれません。
最速のフィードバックから始める:単体テスト
パイプラインで最初に行うべきチェックは単体テストです。これらのテストは、コードの内部から外部に向けての振る舞いを検証します。関数、ユースケース、エンドポイントを呼び出し、その結果が期待通りかどうかを確認します。
以下のフローチャートは、チェックの順序と、失敗した場合にパイプラインを停止する重要な判断ポイントを示しています。
単体テストは、エントリポイントから最も深いモジュールに至るまで、実際のロジックレイヤーを実行します。実際のデータベースや外部サービス、ネットワーク呼び出しは必要ありません。それが高速である理由です。優れた単体テストスイートは、数秒から多くても数分で実行が完了します。
以下は、単体テストと脆弱性スキャンを実行し、いずれかが失敗した場合にパイプラインを停止する最小限のYAMLパイプラインスニペットです。
jobs:
test-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 依存関係をインストール
run: npm ci
- name: 単体テストを実行
run: npm test
- name: 脆弱性スキャンを実行
run: npm audit --audit-level=high
単体テストが失敗したら、すぐにパイプラインを停止してください。コードの基本的な振る舞いが壊れているのに、処理を続ける意味はありません。後続のすべての処理は、コードが意図したとおりに動作するという前提に依存しています。その前提が誤っているなら、それ以降のチェックはすべて無駄な努力になります。
部品が実際に適合するか確認する:統合テスト
単体テストは、外部環境を制御した状態で意味のある振る舞いが動作することを証明します。内部レイヤーは連携して動作しますが、隣接するシステムはシミュレートされるか、テスト制御下に置かれます。しかし、ソフトウェアは実際の依存関係と通信する必要があるときに壊れることもあります。そこで統合テストの出番です。
統合テストは、モジュール同士が正しく連携できるかをチェックします。ユーザーモジュールはデータベースにデータを保存できるか?決済サービスがダウンしているとき、APIは適切に応答するか?サービス間でデータ形式は一致しているか?
これらのテストは単体テストよりも低速です。なぜなら、データベース、メッセージキュー、他のサービスのテストインスタンスといった実際のインフラストラクチャが必要だからです。しかし、こここそが実際のバグのほとんどが潜む場所です。すべての単体テストに合格したコードでも、以下の理由で統合テストに失敗することがあります。
- 誤ったデータベース接続文字列
- 一致しないデータスキーマ
- 誤った設定値
- 不足している環境変数
統合テストは、コンポーネントが実際に接触したときにのみ現れる種類の問題をキャッチします。これをスキップするということは、テストしていない環境でコードが完璧に動作するだろうと賭けているようなものです。
コードそのものをスキャンする:静的解析
機能テストはコードが何をするかをチェックします。静的解析はコードがどのように書かれているかをチェックします。ソースコードを実行せずに読み取り、問題のあるパターンを探します。
静的解析ツールは以下を検出できます。
- 宣言されているが使用されていない変数
- 複雑すぎる、または深くネストされたコード
- 潜在的なヌルポインタの参照外し
- チームで合意したコーディング標準の違反
- ハードコードされた認証情報のようなセキュリティ上問題のあるパターン
静的解析はロジックのバグをキャッチしません。しかし、開発者が1日に何十回も犯し、コードレビューで見逃されがちな種類のミスをキャッチします。また、チーム全体の一貫性を強制します。すべてのコミットが同じルールに対してチェックされれば、チームが成長してもコードベースの保守性は保たれます。
隠れた危険を見つける:脆弱性スキャン
現代のアプリケーションにおけるセキュリティ脆弱性のほとんどは、あなたが書いたコードからではなく、依存しているライブラリやパッケージから発生します。既知のエクスプロイトを持つ古い依存関係が一つあるだけで、アプリケーション全体が危険にさらされる可能性があります。
脆弱性スキャンは、依存関係リストを既知のセキュリティ問題のデータベースと照合します。ライブラリにクリティカルな脆弱性がある場合、スキャナーがそれをフラグし、パイプラインは停止すべきです。既知のセキュリティホールを本番環境に送り込むくらいなら、リリースを遅らせる方がましです。
このスキャンは、メジャーリリースの前だけでなく、すべてのビルドで実行する必要があります。新しい脆弱性は毎日発見されています。先週は安全だったライブラリが、今日はクリティカルなCVEが公開されているかもしれません。定期的なスキャンにより、これらの問題がユーザーに届く前に確実にキャッチできます。
証拠を残す:結果を保存する理由
この段階でのすべてのチェックは結果を生成します。単体テストは合格か不合格か。統合テストはどのシナリオが機能したかを報告します。静的解析は警告とエラーをリストアップします。脆弱性スキャンは依存関係にフラグを立てます。
これらの結果は証拠です。パイプラインがチェックを実行したことを証明し、何が起こったかを示します。それらを保存してください。
証拠が重要な理由は3つあります。
本番環境の問題のデバッグ。 本番環境で何か問題が発生した場合、パイプラインがその問題を検出したかどうかを確認できます。検出していれば、チェックが機能したことがわかります。検出していなければ、より良いテストが必要だとわかります。
監査とコンプライアンス。 規制当局、顧客、社内ポリシーは、すべての変更がリリース前にテストされたことの証明を求めることがよくあります。保存された証拠はその要件を満たします。
トレンド分析。 時間の経過とともに、証拠はコード品質が向上しているかどうかを示します。テストの失敗は減っているか?脆弱性は減少しているか?特定のモジュールに一貫して問題があるか?このデータは、改善努力をどこに投資すべきかを判断するのに役立ちます。
証拠は、JUnit XMLやSARIFのように機械が読み取れる形式で保存し、人間が読めるサマリーも併せて保存してください。パイプラインが完了した後も存続する場所、例えばアーティファクトレジストリや専用のストレージバケットに保存します。数日でクリーンアップされるパイプラインログに依存してはいけません。
この段階のための実用的なチェックリスト
アーティファクトをデプロイメントに移す前に、以下のチェックが整っていることを確認してください。
- 単体テストはすべてのコミットで実行され、壊れている場合はパイプラインを失敗させる
- 統合テストは重要なコンポーネント間の相互作用をカバーしている
- 静的解析はコード品質基準を強制している
- 脆弱性スキャンはすべての依存関係をチェックしている
- すべての結果は、タイムスタンプとコミットIDとともに証拠として保存されている
次に何が起こるか
テストとスキャンに合格した後、アーティファクトは保存する価値があることがわかります。正しく動作し、コードは保守可能で、依存関係は安全です。次に、それをパッケージ化して保存し、後でデプロイできるようにする必要があります。
しかし、いずれかのチェックが失敗した場合、パイプラインは停止します。チームに通知が届きます。アーティファクトが次の段階に到達することはありません。それがポイントです:問題をここでキャッチし、本番環境ではキャッチしないこと。
本番環境でバグを見つけるコストは、パイプラインで見つけるコストよりも指数関数的に高くなります。テストの失敗は開発者の数分の時間を費やすだけです。本番環境の障害は、ユーザーの信頼、インシデント対応、深夜のデバッグセッションを犠牲にします。
早期にテストし、スキャンしましょう。自動的にテストし、スキャンしましょう。そして証拠を残しましょう。何か問題が発生し、何がいつチェックされたかを正確に証明できるとき、未来の自分が感謝することでしょう。