飛ばない豚
記事一覧へ戻る
この記事を読むのにかかる時間:約22分

【読書メモ】 『入門 継続的デリバリー』

IT#CI/CD

『入門 継続的デリバリー』を読んだので、読書メモを書いていく。

本書は2024年に出版されており、ソフトウェア開発の評価指標であるDORAメトリクスが紹介されている。2026年現在の最新版のDORAメトリクスとは内容が少し異なるが、本書での記載内容を優先して本記事を書いている。



1. 継続的デリバリー

継続的デリバリーとは

継続的デリバリーとは、以下の2点を満たしたリリース手法である。

  • いつでも安全にソフトウェアの変更を提供することができる
  • ボタンを1つ押すぐらい簡単に、そのソフトウェアをデリバリーできる

※本書において、「CD」は継続的デリバリーのことを指す。継続的デプロイメントのことではない。この記事においても、継続的デリバリーをCDと記述する。

継続的インテグレーション

CDの定義の1点目「いつでも安全にソフトウェアの変更を提供することができる」を満たすためには、ソフトウェアが常にリリース可能な状態である必要がある。これを実現するのが継続的インテグレーション(以下、CI)である。

CIとは、コードの変更を高い頻度で結合・検証していくプロセスのことである。

パイプラインとタスク

  • タスク:実行する個々の作業のことで、関数のようなもの。
  • パイプライン:タスクを適切なタイミングと順序で呼び出すもの。

CDパイプラインの基本的な構成要素は、リント・テスト・ビルド・パブリッシュ・デプロイ である。

タスクは、作業の種類によってゲートとトランスフォーメーションに分けられる。

  • ゲート:コードが通過する必要がある、品質検証のタスク。
  • トランスフォーメーション:コードをある形から別の形に変換するタスク。

CDパイプラインにおいては、以下のように分類できる。

タスク タスクの種類
リント ゲート
テスト ゲート
ビルド トランスフォーメーション
パブリッシュ トランスフォーメーション
デプロイ トランスフォーメーション

CDパイプラインにおけるCIとは、ゲートのことである。つまり、リントとテストがCIなのである。

2. バージョン管理

CIとバージョン管理

上述のとおり、CDの定義の1つ目「いつでも安全にソフトウェアの変更を提供する」を達成するためには、CIが必要となる。CIとは、「高い頻度でコードを変更し、それを検証し結合する」ことだった。そのためには、「変更を結合する方法」と「変更を保存する場所」の2つの要素が必要になる。これを可能にするのが、バージョン管理である。 つまり、CDはCIを必要とし、CIはバージョン管理を必要としているのだ。

自動化

「いつでも安全にソフトウェアの変更を提供する」ためには、CIの実行を人間に任せてはいけない。人間は忘れることがあるからだ。CIが実行されることを保証するためには、リモートリポジトリの変更をトリガーとしてCDパイプラインを実行する。

Config as Code

設定ファイルなど、ソフトウェアを定義するすべてのプレーンテキストデータをバージョン管理に保存する。そして、そのバージョン管理をソフトウェアの状態の Source of Truth として扱う。 ここでプレーンテキストデータと強調されているのは、パスワードなど機密データは含まないためである。

3. テスト

リント

リントは、バグ、エラー、スタイルの3種類の問題を検出する。

種類 概要 例 修正の優先度
バグ 望ましくない動作につながるもの 初期化されていない変数 高
エラー 動作には影響しないが、パフォーマンスや保守性に問題があるもの 未使用の変数 中
スタイル コードスタイルに一貫性がなかったりするもの インポートの順序 低

シグナルとノイズ

テストから得られる情報の質は、シグナルとノイズに分けられる。シグナルとは情報であり、ノイズとは情報を妨げるものである。

アプリケーションのコード テスト結果 シグナル/ノイズ 例え
バグがない 成功 シグナル 火災が起こっておらず、火災報知器は鳴らない
バグがない 失敗 ノイズ 火災が起こっていないのに、火災報知器は鳴っている
バグがある 成功 ノイズ 火災が起こっているのに、火災報知器は鳴っていない
バグがある 失敗 シグナル 火災が起こっており、火災報知器は鳴っている

注意すべきは、テスト結果だけを根拠にシグナルとノイズを判別できないことである。成功すべきでないテストが成功したら、それはノイズである。

フレーキーテスト

フレーキーテストは、テストにおける最悪の例の1つである。フレーキーテストとは、成功することもあれば失敗することもある、一貫性のないテストのことだ。開発現場では、フレーキーテストはしばしば無視され、深刻ではないものとして扱われる。1

継続的テスト

1日1回の夜間テストなどでは不十分である。これでは、ソフトウェアが常にリリース可能な状態かどうか分からない。夜間にテストが成功し、翌朝9時に誰かが変更を加えたら、また夜間のテストを待たなくてはいけない。

「いつでも安全にソフトウェアの変更を提供する」ためには、変更がプッシュされるたびにテストを実行しなくてはならない。

テストカバレッジ

テストピラミッドというモデルがある。これは、テストスイートにおけるテストの種類とそれぞれの分量を可視化した図である。一般的に、テストスイートの大部分は単体テストが占め、結合テストはかなり少なく、そしてほんの少しのE2Eテストとなる。

いびつなテストピラミッドの形(結合テストが単体テストより多い、など)を是正するには、CDパイプラインにテストカバレッジ測定タスクを導入することが有効だ。カバレッジの閾値を下回った場合、CDパイプラインを失敗させる。2

テストカバレッジ測定を強制することで、開発者がコードに変更を加えた際、単体テストを含めざるを得なくなる。その結果、単体テストの数が増え始める。

テストの並列実行

テスト実行の時間を短縮するために、各テストケースの並列実行が有効である。並列実行を実現するには、テスト同士が依存・干渉せず、どのような順序でも実行可能であることが求められる。つまり、テストの独立性が大事になる。

4. CI

マージ前のCI

mainブランチへのマージ前にCIを実行することで、mainブランチへの問題の混入を避けられる。 これは、CIの目標の1つ「いつでも安全にソフトウェアの変更を提供できる状態」の一助となる。 またmainブランチを守ることは、チーム開発においても重要だ。他の開発者の作業をブロックせずに済む。 しかし、mainブランチへのマージ前にCIを実行し、マージした後でも、バグが混入するリスクはある。以下がその3つの原因である。

  • mainブランチとの統合漏れ
    • mainブランチに、これから加える変更(作業ブランチA)と統合されていない、他の変更(作業ブランチB)があるかもしれない。
  • 依存関係:
    • CI実行時に使用した依存関係のバージョンと、本番環境用のアーティファクトをビルドするときの依存関係のバージョンが異なると、バグが混入するリスクとなる。
  • 非決定性:
    • コード・テスト・ビルドの非決定性に基づくバグ

ソフトウェア開発における非決定性(nondeterminism)とは、同じ入力でも、実行のたびに結果・順序・成否が変わり得る性質のことである。

対象 非決定性の例
コード 並行処理の競合、現在時刻、乱数、外部API応答
テスト 実行順依存、共有DB、非同期処理の待機不足
ビルド 実行環境のOS差、タイムゾーン、生成物への日時埋込み、コンパイラの差

mainブランチとの統合漏れ

上記より、マージ前のCIは有効ではあるがそれだけでは足りない。まずは、1つ目のバグ混入原因である「mainブランチとの統合漏れ」に対する3つのアプローチを順番に紹介する。

アプローチ1: CIを定期的に実行

mainブランチ上でCIを定期的に実行することで、バグを発見する。しかし、このアプローチは、mainブランチへのバグの混入を許容する。これはCDの条件の1つ「いつでも安全にソフトウェアの変更を提供することができる」に反する。 また、定期的なCI実行は、失敗時の責任が分散されてしまう。定期実行は主体が人間ではないため、チームメンバーが失敗通知を受けた後、誰がその失敗を修正するのかを判断する必要がある。

アプローチ2:mainのpullを強制

開発者に対して、最新のmainブランチをpullするまでマージができないように強制する。これにより、CIが常に他の作業者の変更も取り込んだ最新の状態で実行される。 しかし、このアプローチは開発者体験に欠ける。PRがマージされるたびに、他のすべてのオープンなPRをブロックしてしまうためである。チームの人数が多いとなおさらだ。

アプローチ3: マージキュー

3つ目のアプローチは、マージキューを使用する。マージキューにより、作業ブランチAからmainへのマージ直前に、mainブランチの最新内容を取り込んだ状態でCIを実行できる。 (マージキューの説明は書籍の内容だけでは分かりにくかったので、以下のChatGPTによる補足も入れている。)

マージキューの流れ

  1. マージ要件を満たした複数の PR が、マージキューに追加される。
  2. GitHubは「最新のmain」「対象PRよりキューの前にいるPR」「対象PR自身」を統合した一時的なブランチを作る。
  3. その一時ブランチ(merge group)で CI を実行する。
  4. CIに成功すれば、main へマージする。 失敗・競合なら、対象 PR はキューから外される。

各アプローチの最終的な評価は以下のとおりである。

手法 評価
CIを定期実行 △
pull mainを強制 △
マージキュー 〇

非決定的なビルド

システムテスト用にビルドされているイメージが、本番環境にデプロイされているイメージと同じではないことがある。これは、ビルドの非決定性の要因となる。具体的には以下の2つ。

  • システムテスト用のビルドより新しいバージョンの依存関係が、本番イメージのビルドで取り込まれる
  • ビルド環境のOSのバージョンが異なっている CIパイプラインの構成を修正して、ビルドの非決定性を解決する。システムテスト用イメージのビルドタスクと、本番環境イメージのビルドタスクを統一する(同じイメージを使い回すという意味ではない)。

非決定的なコード・テスト

例えば、500回に1回失敗するフレーキーテストがあるとする。PRイベントのみでCIを実行していると、テストの失敗の捕捉に時間がかかる。さらに、PRの作成者にとっては「自分のPRをさっさとマージしてしまいたい」気持ちが優先され、CIの再実行(フレーキーテストが成功するまで)で済まされてしまう可能性が高い。

つまり、PRイベントのみでのCI実行には、フレーキーテストの観点で2つの問題がある。 1つ目は実行頻度である。CI実行回数が少ないため、フレーキーテストの捕捉に時間がかかる。 2つ目は開発者の善性に依存する問題である。フレーキーテストが見つかったとして、そのPRイベントを発火した開発者が適切に対処するかどうかは分からない。

そこで有効になるのが定期的なCI実行である。これにより、上述の2問題が同時に解消される。定期的なCIの失敗は、開発者の作業をブロックしない。

5. DORAメトリクス

DORA Metricsとは、DevOps Research and Assessment Teamの研究によって作成された、ソフトウェア開発チームのパフォーマンスを評価する手法のことである。4つのキーメトリクスを使って評価する。3

  • 速さに関するメトリクス
    • デプロイ頻度
    • 変更のリードタイム
  • 安定性に関するメトリクス
    • サービス復旧時間
    • 変更に伴う障害発生率

変更のリードタイム

変更のリードタイムは、コードがバージョン管理システムへコミットされてから、本番環境へ正常にデプロイされるまでの時間である。開発した変更をどれだけ速く利用者へ届けられるかを表す。

変更のリードタイムを短縮する方法の一つに、トランクベース開発 がある。トランクベース開発とは、長期間維持されるfeatureブランチを避け、変更を小さな単位に分割し、共有のトランクであるmainブランチへ少なくとも1日1回、可能であればそれ以上の頻度で統合する開発手法である。開発者がmainへ直接コミットする場合もあれば、数時間から1日以内で完結する短命なブランチを使用し、プルリクエストを通してマージする場合もある。

mainへ変更が統合されるたびにCIを実行し、ビルドや自動テストによって変更の妥当性を確認する。テストが失敗した場合は、修正または変更の取り消しを優先し、mainを常にデプロイ可能な状態に保つ。未完成の機能を段階的に統合する必要がある場合は、フィーチャーフラグなどを使用して利用者への公開を制御する。

このように、小さな変更を継続的に統合することで、長期間分岐したブランチによる大規模なマージ、競合の解消、リリース前の統合作業や安定化期間を減らせる。また、CIから早い段階でフィードバックを得られるため、問題の特定と修正も容易になる。その結果、コードが統合されてから本番環境へデプロイされるまでの待ち時間が短くなり、変更のリードタイムが改善する。

6. 安全なビルド

SLSA

ソフトウェアのアーティファクトを安全にビルドするための標準を SLSA (サルサ)という。 SLSA は以下の5つの要件から成る。

  1. 常にリリース可能
  2. 自動ビルド
    1. アーティファクト構築手順をスクリプトで定義する必要がある
    2. そのスクリプトは自動的にトリガーされる必要がある
  3. Build as Code
    1. ビルドの構成をコードとし定義し、バージョン管理システムに保存する
  4. CDサービスを利用する
    1. ビルドのプロセスに一貫性を持たせることが狙い
    2. 開発者のローカル環境・任意の仮想マシン上でビルドしてはいけない
  5. 一時的なビルド環境
    1. ビルドごとの使い捨て環境を使う
    2. 他のビルドの影響を受けないようにするため。
    3. ここでいうビルド環境は、カーネル、OS、インストールされているプログラム、ファイルのこと。
    4. ビルドのたびに新しいVMもしくはコンテナを使用する。
    5. VMよりコンテナの方が起動と破棄が高速なため、より適している

セマンティックバージョニング

リリースのたびに、以前リリースした内容を上書きしてはいけない。例えばWEBアプリにおけるバックエンドAPIがリリースのたびに上書きされていると、フロントエンド側はそれを検知できない。 そのため、ソフトウェアのリリースにおいては、新しいバージョンを作成しつつも以前のバージョンを引き続き使用可能とする必要がある。

ソフトウェアのバージョン管理の一般的な方法がセマンティックバージョニングである。 セマンティックバージョニングでは、ピリオドで区切られた3つの数字でバージョンを構成する。先頭から MAJOR, MINOR, PATCHを表す。例えば 2.30.1 というバージョン表記においては、2がMAJOR.、30がMINOR、1がPATCHを表す。

バージョンが上がるタイミング。後方互換性があるかどうか、機能追加か修正かによって分岐する。

種類 上がるタイミング
MAJOR 後方互換性のない変更がおこなわれたとき
MINOR 後方互換性のある機能追加がおこなわれたとき
PATCH 後方互換性のある修正が行われた時

後方互換性のある変更とは、ソフトウェアの利用者が使用方法を更新する必要がない変更のこと。

ビルドにバージョンを含める方法

  • リポジトリ内の任意のファイルにセマンティックバージョンを記述し、全てのコミットにおいてその値を上げていく。
  • CIの中でそのファイルからバージョンを取得する。
  • バージョン番号を更新するのを忘れた場合、ビルドが失敗するように設定する。

依存関係の固定

CI(リントとテスト)が完了してから、本番環境向けアーティファクトのビルドが実行されるまでの間には、ソフトウェアが依存しているサードパーティのライブラリに後方互換性のない変更が入る余地がある。 これを防ぐには、依存関係のバージョンを明示的に指定する。

pythonにおけるrequirements.txtで例えると、以下

  • ❌ pytest > 6.0.0
  • ✅ pytest === 6.2.5

しかし、このようにバージョン固定も完全に信頼できるわけではない。なぜなら、バージョン番号を保ったままライブラリの内容を更新することは、原理的に可能だからである。 昨日の v.1.3.2 と、今日の v.1.3.2 が同じである保証はない。

そこで使えるのが、依存関係のバージョン番号だけではなくパッケージ内容のハッシュ値を指定する方法。これで、同じバージョンであれば、パッケージ内容も同じであることを保証できる。

7. 安定性に関するDORAメトリクス

DORAメトリクスのうち、安定性に関わるメトリクスである「変更に伴う障害発生率」と「サービス復旧時間」に注目していく

デプロイ頻度を増やして障害発生率を下げる

デプロイ頻度を増やすと、各デプロイに含まれる変更量が少なくなる。その結果、デプロイあたりのリスク量も減少する。 言い換えると、サービスにおける障害の総数は変わらないが、各デプロイごとの障害発生率は低くなる。

ロールバックですぐ復旧

「サービス復旧時間」の改善には、ロールバックが有効だ。 例えばここに、現行バージョンが v.1.3.2のソフトウェアがある。開発チームは、v.1.4.0 を新たにリリースした。 しかしv.1.4.0で障害が発生したので、パッチ適用された v.1.4.1をリリースした。このときサービス復旧にかかる時間は、以下の式となる。

バグ修正時間 + v.1.4.1をビルドする時間 + デプロイ時間`

多くの場合、バグ修正に最も時間がかかる。数日かかることもあるだろう。その間、本番環境でv.1.4.0はバグを抱えたまま動き続けている。サービス復旧のためにパッチ適用をするのは時間がかかりすぎる。

そこで有効なのがロールバックである。v.1.4.1を待たずに、動作することが分かっている v.1.3.2 に戻せばいい。その後でバグを修正し、満を持してv.1.4.1をデプロイする。 このアプローチなら、サービス復旧までの時間は大幅に改善する。v1.3.2をデプロイするだけでいいからだ。

ブルーグリーンデプロイメント

ブルーグリーンデプロイメントで、サービス復旧時間をさらに短縮できる。 ブルーグリーンデプロイメントとは、本番環境を2セット用意して、新旧バージョンを切り替えるデプロイ方式のこと。新バージョンを別の本番相当環境(グリーン環境にあらかじめ立ち上げておき、準備ができたらロードバランサー等でトラフィックを丸ごとグリーン環境に切り替える。

インスタンスを更新していくローリングアップデートでは、一部のユーザーは数十分障害に直面する可能性があるが、ブルーグリーンではトラフィック切替の1分程度で済む。

ローリングアップデート 

デプロイのとき、実行中のインスタンスを1つずつ停止し、そのたび新しいインスタンスを起動する方法。どの時点でもサービスの少なくとも1つのインスタンスは稼働しているので、サービス全体のダウンタイムを回避できる。 ローリングアップデート中、一部のユーザーは新しいインスタンスに接続され、別のユーザーは古いインスタンスに接続される。

カナリアデプロイメント

カナリアデプロイメント(Canary Deployment)は、新バージョンを一部のユーザーだけに先に公開し、問題がないことを確認してから徐々に全体へ広げる方式だ。本番ユーザーへの影響を小さくしながら新バージョンを検証できる。

カナリアデプロイメントは、いくつかの要件を満たす必要がある。

  • 環境間のトラフィック分散
  • デプロイ成功/失敗の測定を自動化
  • データの変更と機能変更を切り離す
  • カナリア環境に送信するトラフィックの効果的な割合・総量の確保
  • カナリア環境からの適切なデータ収集時間

継続的デプロイメント

DORAメトリクスの「デプロイの頻度」と「変更のリードタイム」において、4段階中最高評価(エリート)を得るには、1日に複数回のデプロイを、1デプロイあたり1時間未満で実施する必要がある。 これら2つの指標は相互に影響する。例えば、コミットからデプロイまで1日かかる開発チームが、1日に複数回デプロイすることはできない。

この2つの指標をエリートクラスに引き上げるには、継続的デプロイメントが有効だ。(本書において、CDとは継続的デリバリーのことを指す。) 継続的デプロイメントの定義は、コミットのたびに自動的にリリースされることだ。

注意点として、すべてのプロジェクトにおいて継続的デプロイメントが適しているわけではない。継続的デプロイメントは、CDの必要条件ではないのだ。

継続的デプロイメントを実践するために、プロジェクトは以下の条件を満たす必要がある。

  • 本番環境での障害を許容できること
    • 人命・金融などに関わる重大なシステムでは、障害許容度はかなり低くなる。4
  • 探索的テストが必要とされないこと
    • 探索的テスト:人間がソフトウェアを探索して、予期せぬ問題を発見するテスト
  • リリース前の承認が不要であること
  • ソフトウェアのリリースに伴ってハードウェアの変更が要求されないこと

まとめ

  • CDとは、ソフトウェアを常に安全な状態に保ち、かつ簡単に迅速にデリバリーする営みである
  • CDの道はCIから始まる
  • DORAメトリクスで開発チームを客観的に評価

注釈

Footnotes

  1. オオカミ少年のようだ ↩

  2. 単体テストの考え方/使い方 では、単体テストのカバレッジ測定には否定的であった。カバレッジの閾値を満たすことが目的になってしまい、上質なテストコードを書くことがおろそかになってしまうからだ。この点において、両書籍間で意見は割れている。ただ本著においては「単体テストがそもそも書かれていない。または少なすぎる。」といった、単体テストの質以前の問題を想定している。その上で単体テストの十分量を確保するためにやはり単体テストのカバレッジ測定は有効であろう。 ↩

  3. 記事の冒頭でもふれたが、DORAメトリクスは本書出版以降に更新されており、2026年現在は5つのキーメトリクスからなる。 ↩

  4. YMYL といったところか ↩