コードが本番環境に到達するまでに何が起きているのか

あなたは機能を完成させた。ローカルで動作確認し、コードをプッシュした。さて、次は何が起きるのか?

プッシュから本番環境でコードが動き出すまでの間には、多くのプロセスが待っている。単にアーティファクトをビルドするだけでなく、コードが本当に安全で、正しく、保守可能かどうかをチェックする必要がある。これらのチェックを省略するということは、チームの全開発者が毎回完璧なコードを書くというギャンブルをしているに等しい。そんなことは誰にもできない。

コードがプッシュされるたびに自動チェックを実行するプロセスを継続的インテグレーション(CI)と呼ぶ。考え方はシンプルだ:問題を早期に、修正が安価なうちに発見する。プッシュから5分後に見つかったバグはコーヒー1杯分のコストで済む。本番で見つかったバグはインシデントレポート、ロールバック、そして深夜の作業を伴う。

単体テストから始める

ほとんどのパイプラインが最初に実行するのは単体テストだ。しかし、何を単体テストと見なすかは、多くのチームが考えている以上に重要である。

良い単体テストとは、単に関数やメソッドが存在するからテストするのではない。関連するエントリポイントから意味のある振る舞いをテストする。バックエンドサービスの場合、そのエントリポイントは通常APIエンドポイントかユースケースである。テストはそのエンドポイントを呼び出し、レスポンスが期待通りかどうかを確認する。舞台裏では、リクエストはコントローラ、サービス層、ドメインロジック、リポジトリ境界を実際に通過する。内部のアプリケーションパスは実際に実行されるが、本番データベース、メッセージキュー、サードパーティAPIなどの外部サービスは、制御されたテストダブル、ローカルテストインスタンス、モック、スタブに置き換えられる。

このアプローチには実用的な利点がある:関数の内部実装を変更してもテストが壊れない。テストはシステムが何をするかに関心があり、どのようにするかには関心がない。つまり、振る舞いが同じである限り、自由にリファクタリングできる。

単体テストは高速であるべきだ。数秒以上かかるなら、何かがおかしい。高速だからこそ、すべてのコミットで実行できる。これにより開発者は即座にフィードバックを得られる:「変更で何かが壊れた、すぐに修正しろ」。

リンターでスタイルとコードの臭いを検出

単体テストが通った後、次は通常リンターのチェックだ。リンターはロジックをテストしない。コードが一貫したスタイルルールに従っているか、バグに繋がりやすいパターンがないかをチェックする。

リンターは以下のようなものを検出する:

  • 宣言されたが一度も使われていない変数
  • 長すぎて分割すべき関数
  • 一貫性のないインデントや命名
  • 正しく見えるが実際は安全でないパターン

リンターは正しさに比べれば些細な懸念に思えるかもしれないが、実際にはもっと重要だ。一貫性のあるコードは読みやすい。読みやすいコードはレビューしやすい。レビューしやすいコードはバグが少ない。単純な自動チェックから始まる連鎖である。

結合テストで接続を確認

単体テストは外部の依存関係を制御した状態でアプリケーションの振る舞いが正しいことを証明する。結合テストはアプリケーションが実際の依存関係と連携できることを証明する。

バックエンドサービスの場合、結合テストは実際のテストデータベースを起動し、サービスを開始し、API呼び出しが実際にデータを正しく読み書きできるかを検証する。あるいは、バックグラウンドワーカーがキューにメッセージを送信し、レスポンスを処理できるかをテストする。

結合テストは単体テストより遅い。データベース、キュー、キャッシュなど実際の依存関係が必要だからだ。そのため、単体テストの後に実行される。単体テストが失敗した場合、結合テストを実行する意味はない。変更はすでに壊れている。

一部のチームは本番環境を可能な限り模した専用のテスト環境で結合テストを実行する。他のチームはCIパイプライン内でコンテナを使って実行する。どちらの方法でも、目標は同じだ:コンポーネントが相互作用するときにのみ現れる問題を発見すること。

セキュリティスキャンでコードの脆弱性をチェック

セキュリティスキャンは、あなたが書いたコードに共通の脆弱性がないかチェックする。SQLインジェクション、クロスサイトスクリプティング、暗号化関数の誤った使用などだ。

これらのスキャンは、危険であることが知られているパターンを探す自動化ツールである。完璧ではない。見逃すこともあるし、誤検出を出すこともある。しかし、人間のレビューアが見落としがちな低い果実を摘み取ってくれる。

CI中に発見された脆弱性はチケットと修正で済む。本番で発見された脆弱性は情報漏洩、開示、そして多くの信頼を失うことを意味する。

依存関係チェックでライブラリの脆弱性を確認

ほとんどすべてのバックエンドサービスはゼロから書かれているわけではない。フレームワーク、ライブラリ、パッケージを使用する。これらのそれぞれは他の誰かが書いたコードであり、そのコードには脆弱性が存在する可能性がある。

依存関係チェックは、使用しているライブラリのバージョンを公開脆弱性データベースと比較する。使用しているライブラリに既知のセキュリティ問題がある場合、チェックがフラグを立てる。一部のチームは、重大な脆弱性が見つかった場合にビルドをブロックするようパイプラインを設定する。他のチームは通過させるが、手動レビューのためにチームに通知する。

このチェックが重要なのは、依存関係の脆弱性が一般的で、しばしば深刻だからだ。2021年のLog4jインシデントはよく知られた例だ:広く使われているロギングライブラリにリモートコード実行を可能にする脆弱性があった。パイプラインに依存関係チェックを導入していたチームはすぐに問題を特定できた。導入していなかったチームは何を使っているかを把握するのに数日を費やした。

チェックの順序が重要

これらのチェックの順序はランダムではない。実用的な論理に従っている:

  1. 単体テストとリンターは高速なため最初に実行される。明らかに壊れている、または乱雑な変更を即座にフィルタリングする。
  2. 結合テストは次に実行される。遅いが依然として重要だ。単体テストが通っても結合テストが失敗する場合、コンポーネントが連携して動作しないことがわかる。
  3. セキュリティスキャンと依存関係チェックは最後に実行される。これらは最も遅いことが多く、通常の変更で失敗する可能性は低い。

この順序により、開発者は一般的な問題に対して迅速なフィードバックを得られる。単体テストを壊した場合、数秒以内にわかる。脆弱な依存関係を導入した場合、数分以内にわかる。数日後ではない。

すべてのチェックがパイプラインをブロックする必要はない

一部のチームはすべてのチェックをブロッキングとして実行する:いずれかのチェックが失敗するとパイプラインが停止し、アーティファクトはビルドされない。他のチームは、特に誤検出を生む可能性のあるセキュリティスキャンなど、特定のチェックを警告付きで通過させることを許可する。

重要なのは一貫性だ。すべての変更は同じチェックを通過する。品質は開発者がローカルでテストを実行するのを覚えているかどうかに依存しない。パイプラインに依存する。

CIパイプラインのための実用的チェックリスト

バックエンドサービスのCIパイプラインを設定またはレビューする場合、以下の短いチェックリストを参考に:

  • 単体テストはすべてのコミットで実行され、1分以内に完了する
  • リンターは単体テストの前または並行して実行される
  • 結合テストは制御された環境で実際の依存関係に対して実行される
  • セキュリティスキャンは独自のコードの一般的な脆弱性をチェックする
  • 依存関係チェックはライブラリを既知の脆弱性データベースと比較する
  • パイプラインは高速に失敗する:高速なチェックが先に、低速なチェックが後に実行される
  • すべての変更は、誰が書いたかに関係なく同じチェックを通過する

次に来るもの

これらのチェックがすべて通過すると、アーティファクトがビルドされ準備完了となる。しかし、アーティファクトのビルドは話の半分に過ぎない。次の問題は、ユーザーを中断させることなく新しいバージョンをサーバーにどう配置するかだ。ここでデプロイ戦略が登場する。これについては次回扱う。

今のところ、重要な教訓はこれだ:本番システムの品質は、本番環境に到達するずっと前に決まる。それはすべてのプッシュ後の数分間、自動チェックがコードを進めて安全かどうかを判断するときに決まる。それらのチェックを高速に、一貫性を持たせ、毎回必ず実行するようにしよう。