メインコンテンツへスキップ

概要

CI/CDは、コードの変更からユーザーへの提供までの各ステップを自動化するプラクティスです。継続的インテグレーション(CI)はすべての変更を自動でビルド・テストし、継続的デリバリー(CD)はマージされた変更がデプロイされるまでの経路を自動化します。 本ページでは、CI/CDパイプラインが提供するもの、CIを高速に保つ手法、リリースの自動化、結果の通知、デプロイ頻度などの指標との付き合い方を解説します。例はGitHub Actionsを使いますが、原則はどのCIサービスにも当てはまります。

CI/CDパイプラインが提供するもの

パイプラインを整備すると、すべての変更に伴う定型的なステップが人手を介さずに実行されます。

自動テストのゲート

プルリクエストごとにテストが実行され、すべて通るまでマージがブロックされます。壊れた変更はリリース後ではなく数分で検出されます。

自動デプロイ

環境ごとに定義したトリガーでデプロイが実行されます。たとえばリリースブランチへのプルリクエスト作成でステージングへ、マージで本番へデプロイします。
この2つの機能が基本形です。以降では、パイプラインの価値を左右する性質である「速度」「リリース自動化」「開発者へ届くフィードバック」を順に扱います。

CIを高速に保つ

CIの所要時間はあらゆる変更が支払うコストになります。目安として、プルリクエストのゲートとなるパイプラインは5〜10分以内に保ちます。

依存関係をキャッシュする

依存関係を毎回ゼロからインストールするのは純粋なオーバーヘッドです。公式のセットアップ用アクションはロックファイルをキーにキャッシュを管理するため、依存関係が実際に変わったときだけキャッシュが再生成されます。
同じ考え方はパッケージのインストール以外にも広げられます。準備済みのデータベーススキーマやビルド成果物など、コストが高く再現可能なセットアップはすべてキャッシュの候補です。

結果に影響しない実行をスキップする

実行時の挙動に影響しない変更に、テストスイート全体を実行する必要はありません。paths-ignoreを指定すると、列挙したファイルのみの変更ではワークフローがスキップされます。
スキップされたワークフローが必須ステータスチェックに指定されている場合、チェックが保留のままになりマージがブロックされます。その場合はワークフロー自体は実行したままコストの高いジョブだけを内部でスキップするか、必須チェックの設定を見直します。

テストを並列化する

テストスイートが大きくなると、総実行時間もそれに比例して伸びます。並列化では、スイートを分割して複数のジョブで同時に実行します。 matrixストラテジーは同じジョブを列挙した値ごとに1回ずつ実行する仕組みで、各ジョブはその値を使って自分の担当分だけを実行します。
並列実行の所要時間は、最も遅いシャードの実行時間で決まります。 ランナーに分割オプションがない場合や、既定の分割ではシャード間の偏りが大きい場合は、ファイル一覧を自分で分割し、件数ではなく計測した実行時間で分配してシャードが同時に終わるようにします。分割数はスイートの成長に合わせて増やします。

ランナーをスケールアップする

キャッシュと並列化を行ったら、CPUコア数とメモリの多い大きなランナーがビルドを短縮し、ジョブ内の並列性を高めます。 ランナーが大きいほど実行時間あたりの単価は上がりますが、実行が短くなるぶん消費する分数は減るため、待ち時間を削減しながら課金額が横ばいか、むしろ下がることもあります。
CIの最適化は計測のループとして回します。CIログのジョブごとの所要時間を見れば実際にどこへ時間が使われているかが分かるため、最も大きい部分を見つけて1つだけ変更し、差分を検証します。勘で最適化しないことが重要です。

リリースを自動化する

手作業でのリリース、つまり変更履歴を書き、正しい順序でデプロイの手順を実行する作業は、ミスと遅延が入り込む場所です。この一連の流れは自動化できます。
1

変更を追跡できる履歴を保つ

リリースノートは、マージされたプルリクエストとそのコミットの履歴から組み立てられます。後から読んで分かるコミットメッセージの書き方はプルリクエストのコミットメッセージを参照してください。
2

リリースノートを自動生成する

前回のリリース以降にマージされたプルリクエストの一覧を、リリースノートとして組み立てます。変更履歴を手で編集する人はいなくなります。
3

デプロイをリリース用プルリクエストのマージに集約する

ワークフローがリリース用のプルリクエストを生成します。あわせて、マージによるmainへのプッシュをトリガーにデプロイを実行するワークフローを別に用意します。これにより、本番リリースが手順書の実行ではなく、レビューとマージになります。
ブランチの関係は次のとおりです。 たとえば次のワークフローは、マージ済みの作業がdevelopに集まり、mainが本番を反映するブランチモデル(いわゆるgit flowを簡略化したもの)でこの流れを実装したものです。 手動で起動すると、developから日付入りのリリースブランチを切り、前回のリリース以降にマージされたプルリクエストの一覧を本文として組み立て、mainへのリリース用プルリクエストを作成します。
プルリクエスト一覧はマージコミットから抽出しているため、このワークフローはdevelopへの取り込みを「Create a merge commit」で行うリポジトリを前提とします。Squash mergeを使う場合は、gh pr list --state mergedなどによる取得へ置き換えてください。
mainへのプッシュ、つまりリリース用プルリクエストのマージをトリガーに、別のワークフローが本番デプロイを実行します。
concurrencyは、プッシュが連続したときにデプロイが重複して走ることを防ぎます。environmentを指定するとGitHubの保護ルールと環境スコープのシークレットを本番デプロイに適用できます。 同じ発想で、ワークフローの他の場所にある繰り返しの摩擦も取り除けます。プルリクエストの作成者を自動でアサインする、ブランチ名や変更されたパスに基づいてラベルを付与する、リリース時にプルリクエストのテンプレートからチェック済み項目を抽出してQAへ引き継ぐ、などです。

パイプラインの結果を通知する

パイプラインがフィードバックループを短縮するのは、その結果が対応すべき人へ届いてこそです。多くのケースは次の2つの経路でカバーできます。
  • プルリクエスト上のステータスチェック。 CIサービスは各ジョブの結果をプルリクエスト上に表示します。これは組み込みの仕組みで、変更がレビュー中である間のフィードバックをカバーします。
  • チャットへの通知。 本番デプロイの完了や定期実行ワークフローの失敗など、プルリクエストの外で起きるイベントには、Slackなどのプッシュ型の経路が必要です。
たとえば、デプロイ用ジョブの末尾に、成否にかかわらず結果をSlackへ投稿するステップを追加します。
成功と失敗の両方を通知します。失敗時だけ通知する設計では、通知が来ないことが「成功した」のか「通知の仕組み自体が壊れている」のかを区別できません。成功の通知は、パイプラインと通知経路の両方が機能していることの確認になります。

デプロイ頻度は目標ではなく結果

デプロイ頻度は広く使われる生産性指標ですが、高い値は自動化されたパイプライン、小さく適切に区切られた変更、変更のリスクを低く保つテストスイートといったプラクティスの「結果」です。 数値そのものを最適化すると因果が逆転し、指標が測ろうとしている成果自体が悪化します。
  • 内容を変えずにデプロイ回数だけを増やすと、運用の手間が増え、価値を届けないまま変更障害率が上がります。
  • 障害率を下げる目的でデプロイ頻度を落とすと、1回のデプロイに含まれる変更が増え、障害が起きやすくなり原因の切り分けも難しくなります。
デプロイ頻度を持続的に高めるには、入力の側を改善します。CIを速く保ち、上記のとおりリリース経路を自動化し、変更を小さく独立して出荷できる単位に保ちます(タスク分解テストを参照)。