開発をしていると「CI/CD」という言葉はかなり頻繁に出てきます。

GitHub Actionsでテストを動かしていたり、mainブランチにマージしたら自動で本番環境へデプロイしたり。

こうした仕組みをまとめて「CI/CD」と呼ぶことも多いと思います。

ただ、改めて考えてみると、

  • CIとは何なのか
  • CDとは何なのか
  • Continuous DeliveryとContinuous Deploymentは何が違うのか

あたりは、意外と曖昧だったりします。

自分も最初は、

CI = 自動テスト CD = 自動デプロイ

くらいの認識でした。

大きく外れているわけではないのですが、本来の意味を考えると少し違います。

CI/CDを理解するには、用語の定義だけを見るよりも、

「そもそも何の問題を解決するために生まれたのか」

から見ていくと分かりやすいです。

そもそも何が問題だったのか

昔のソフトウェア開発では、それぞれの開発者やチームがある程度まとまった期間開発して、最後に変更を統合するような開発も珍しくありませんでした。

イメージとしてはこんな感じです。

Developer A ─── 長期間開発 ───┐
                              │
Developer B ─── 長期間開発 ───┼── Integration
                              │
Developer C ─── 長期間開発 ───┘
                                     │
                                     ▼
                                大量の競合
                                テスト失敗
                                仕様の不整合

それぞれの環境では正常に動いていたとしても、最後に全部を合わせた瞬間に問題が起きます。

Aさんが変更したコードをBさんも変更していたり、ある機能の変更によって別の機能が壊れていたり。

しかも数週間、数か月分の変更が一気に統合されるので、問題が発生したときに原因を特定するのも大変です。

いわゆる「Integration Hell」です。

そこで出てきた考え方がContinuous Integrationでした。

Continuous Integration

CIは Continuous Integration(継続的インテグレーション) の略です。

発想自体はかなりシンプルです。

最後にまとめて統合するから大変になるのであれば、もっと頻繁に統合すればいい。

ということです。

例えば、

数か月開発
    ↓
数か月開発
    ↓
数か月開発
    ↓
最後に統合
    ↓
   💥

ではなく、

小さな変更
    ↓
  統合
    ↓
  確認

小さな変更
    ↓
  統合
    ↓
  確認

小さな変更
    ↓
  統合
    ↓
  確認

というサイクルを繰り返します。

変更が小さいうちに統合しておけば、問題が起きても影響範囲を特定しやすくなります。

Continuous Integrationは1990年代にExtreme Programming(XP)のプラクティスとして広く知られるようになりました。

Kent BeckがXPを体系化する中で、開発者が変更を頻繁に統合することを重要なプラクティスとして位置づけています。

ここで大事なのは、

CIは本来、CIツールのことではない

という点です。

GitHub Actionsを動かすことがCIではない

現在だとCIと聞くと、

git push
    ↓
GitHub Actions
    ↓
Lint
    ↓
Test
    ↓
Build

という仕組みをイメージすることが多いと思います。

もちろん、これらはCIを実現するための重要な仕組みです。

ただ、CIの本来の目的は、

コードの変更を頻繁に統合し、その変更によってシステムが壊れていないことを継続的に確認する

ことです。

GitHub ActionsやJenkins、GitLab CIなどは、それを支えるためのツールです。

つまり、

Continuous Integration
        │
        │ 実現するために使う
        ▼
  GitHub Actions
  Jenkins
  GitLab CI
  CircleCI
  etc...

という関係です。

「GitHub Actionsを導入した = CIを実践している」とは必ずしも言えません。

例えば半年間巨大なFeature Branchで開発していて、最後にPull Requestを作ったとします。

そのPull RequestでGitHub Actionsがテストを実行していたとしても、

6か月開発
    ↓
巨大PR
    ↓
  CI実行
    ↓
  Merge

では、Continuous Integrationが解決しようとしていた問題がかなり残っています。

重要なのは「自動テストがあること」だけではなく、変更を小さく保ち、継続的に統合することです。

CIで統合は楽になった

CIによって、

Code
  ↓
Build
  ↓
Test
  ↓
Integrated

というところまでは、かなり効率化できるようになりました。

変更するたびに自動テストを実行して、

「この変更を統合しても大丈夫そうか」

をすぐ確認できるようになります。

ただ、ここで別の問題が出てきます。

統合できても、本番に出すのが大変だった。

という問題です。

次に問題になったのがリリース

コードが正常に統合できたからといって、それがそのままユーザーに届くわけではありません。

実際にユーザーへ届けるには、

開発完了
    ↓
QA
    ↓
リリース手順作成
    ↓
サーバー準備
    ↓
設定変更
    ↓
DB変更
    ↓
運用チームへ依頼
    ↓
本番リリース

といった作業が必要になることがあります。

つまりCIによって、

Developer
    │
    ▼
Integrated Software

までは高速化できたとしても、

Integrated Software
    │
    │ ここが大変
    ▼
Production

という最後の部分がまだ残っています。

統合は毎日できる。

でも本番リリースは3か月に1回。

これでは、ユーザーに変更が届く速度はあまり上がりません。

そこで出てきたのがContinuous Deliveryです。

Continuous Delivery

Continuous Deliveryは、日本語では「継続的デリバリー」と呼ばれます。

CIによってコードを継続的に統合できるようになったので、今度は、

そのコードをいつでも本番環境へリリースできる状態にしておこう

と考えるようになりました。

例えば、

Commit
   ↓
Build
   ↓
Unit Test
   ↓
Integration Test
   ↓
Acceptance Test
   ↓
Staging
   ↓
Production Ready

という流れです。

テストやビルドだけではなく、本番環境へリリースできる状態にするまでの一連のプロセスを自動化していきます。

この一連の流れはDeployment Pipelineと呼ばれます。

Continuous Deliveryの考え方は、2000年代にThoughtWorksなどで発展し、2010年にJez HumbleとDavid Farleyが出版した『Continuous Delivery』によって広く知られるようになりました。

CIが、

常に統合できる状態を維持する

ものだとすると、Continuous Deliveryは、

常にリリースできる状態を維持する

ものだと考えると分かりやすいです。

Continuous Deliveryは本番まで自動とは限らない

ここが少し混乱しやすいところです。

Continuous Deliveryの場合、

本番環境へのリリースそのものは手動でも構いません。

例えば、

Code
  ↓
Test
  ↓
Build
  ↓
Staging
  ↓
Production Ready
  ↓
Manual Approval
  ↓
Production

という形です。

テストもビルドも、リリース可能な状態まで持っていく処理も自動化されています。

ただし、

今このタイミングで本番に出すか

という最終判断だけは人間が行います。

GitHub Actionsであれば、Production EnvironmentにApprovalを設定して、

Deploy Production

を承認したらデプロイされるような構成もこれに近いです。

重要なのは、

ボタンを押したら安全にリリースできる状態が常に作られていること

です。

「手動操作が1つでもあったらCDではない」というわけではありません。

ではContinuous Deploymentとは何か

CDという言葉がややこしい理由がここにあります。

CDには、

  • Continuous Delivery
  • Continuous Deployment

という2つの言葉があります。

Continuous Deploymentは、Continuous Deliveryからさらに一歩進んだ考え方です。

Code
  ↓
Test
  ↓
Build
  ↓
Deploy
  ↓
Production

まで完全に自動化します。

例えば、

Pull Request
    ↓
  CI
    ↓
mainへmerge
    ↓
  Build
    ↓
Production Deploy

という構成です。

人間によるリリース承認はありません。

テストやその他のチェックを通過した変更は、そのまま本番環境へ反映されます。

これがContinuous Deploymentです。

DeliveryとDeploymentの違い

ここまでを整理すると、違いはかなりシンプルです。

Continuous Delivery

Code
  ↓
Test
  ↓
Build
  ↓
Release Ready
  ↓
人間が判断
  ↓
Production

いつでも本番へ出せる状態にする。

最終的にいつリリースするかは人間が決めます。

Continuous Deployment

Code
  ↓
Test
  ↓
Build
  ↓
Production

問題がなければ、自動で本番環境へリリースする。

リリース判断も含めて自動化されています。

つまり大きな違いは、

本番リリースの直前に人間の判断が入るかどうか

です。

CI/CDが生まれた流れを整理する

歴史的な流れで見ると、かなり分かりやすくなります。

まず、

開発
  ↓
開発
  ↓
開発
  ↓
最後に統合
  ↓
Integration Hell

という問題がありました。

そこで、

小さく変更
   ↓
頻繁に統合
   ↓
自動で確認

するContinuous Integrationが広がりました。

すると次に、

Code
  ↓
CI
  ↓
Integrated
  ↓
  ↓
リリースが大変
  ↓
Production

という問題が見えてきます。

そこでContinuous Deliveryによって、

Code
  ↓
Test
  ↓
Build
  ↓
Staging
  ↓
Release Ready

までを継続的に行えるようにします。

さらに、それだけ安定したPipelineが作れるのであれば、

Code
  ↓
Test
  ↓
Build
  ↓
Production

まで自動化しよう、というのがContinuous Deploymentです。

かなり単純化すると、

Integration Hell
       ↓
Continuous Integration
       ↓
統合は楽になった
       ↓
でもリリースが大変
       ↓
Continuous Delivery
       ↓
リリースも安定した
       ↓
Continuous Deployment

という流れです。

CI/CDという1つの技術が突然発明されたわけではなく、

ソフトウェアを変更してからユーザーへ届けるまでのボトルネックを、少しずつ解消してきた結果

と考えると分かりやすいです。

CIとCDの境界

現在の開発フローに当てはめると、例えばこんな感じになります。

Developer
    │
    ▼
Pull Request
    │
    ▼
┌─────────────────────┐
│ Continuous          │
│ Integration         │
│                     │
│ Lint                │
│ Type Check          │
│ Unit Test           │
│ Integration Test    │
│ Build               │
└──────────┬──────────┘
           │
           ▼
         Merge
           │
           ▼
┌─────────────────────┐
│ Continuous Delivery │
│                     │
│ Artifact Build      │
│ Staging Deploy      │
│ E2E Test            │
│ Release Ready       │
└──────────┬──────────┘
           │
           ▼
    Production Deploy

Production Deployの直前に手動承認が入るならContinuous Delivery。

そこまで自動化されていればContinuous Deployment。

と考えることができます。

もちろん実際のシステムでは境界がここまで綺麗に分かれているとは限りません。

ただ、

コードを書いてからユーザーに届くまでの流れのうち、どこを継続的に回しているのか

を見ると整理しやすくなります。

「CI/CDを導入しています」だけでは分からない

実際の現場では、

CI/CDを導入しています

という説明をよく聞きます。

ただ、この言葉だけでは実際に何をしているのかほとんど分かりません。

例えば、

Pull Request
    ↓
  Test

だけかもしれません。

あるいは、

Pull Request
    ↓
  Test
    ↓
  Merge
    ↓
  Build
    ↓
  Staging
    ↓
Manual Approval
    ↓
Production

かもしれません。

さらに、

Pull Request
    ↓
  Test
    ↓
  Merge
    ↓
Production

まで完全自動化されているかもしれません。

全部「CI/CD」という言葉で表現されることがあります。

なので実際には、

  • どのタイミングでCIが動くのか
  • 何をテストしているのか
  • Artifactはいつ作られるのか
  • Stagingへのデプロイは自動なのか
  • Productionへのデプロイは自動なのか
  • Production前にApprovalがあるのか

あたりを見る方が、そのプロジェクトの開発フローを理解しやすいです。

CI/CDの目的は「自動化」ではない

CI/CDについて調べていると、

「テストを自動化する」

「デプロイを自動化する」

という説明が多く出てきます。

もちろん自動化は重要です。

ただ、自動化そのものが目的ではありません。

CIが解決したかったのは、

変更を統合するリスクを小さくすること。

Continuous Deliveryが解決したかったのは、

ソフトウェアをリリースするリスクを小さくすること。

です。

例えば3か月分の変更をまとめてリリースすると、

何か問題が発生したときに原因候補が大量にあります。

一方、

小さな変更
   ↓
 Test
   ↓
 Deploy

を頻繁に繰り返していれば、問題が起きたときに確認する変更も小さくなります。

つまりCI/CDは、

大きな変更をたまに行うのではなく、小さな変更を安全に繰り返せる状態を作る

ための仕組みだと考えています。

まとめ

CI/CDという言葉はまとめて使われることが多いですが、実際にはそれぞれ違う問題を解決しています。

Continuous Integration

変更を頻繁に統合し、問題を早い段階で見つける。

Code
  ↓
Build
  ↓
Test
  ↓
Integration

Continuous Delivery

統合されたソフトウェアを、いつでも本番へリリースできる状態にする。

Code
  ↓
Test
  ↓
Build
  ↓
Release Ready
  ↓
Manual Release

Continuous Deployment

問題のない変更を、そのまま本番環境まで自動的に届ける。

Code
  ↓
Test
  ↓
Build
  ↓
Production

歴史的な流れで見ると、

Integrationが大変
      ↓
      CI
      ↓
Releaseが大変
      ↓
Continuous Delivery
      ↓
リリース判断も自動化
      ↓
Continuous Deployment

という形で発展してきました。

なのでCI/CDを、

GitHub Actionsでテストとデプロイを自動化するもの

として理解するだけだと、少しもったいないと思います。

その裏には、

変更をできるだけ小さく保ち、頻繁に統合し、安全にユーザーへ届ける

という考え方があります。

CI/CDの設定を見るときも、「どのツールを使っているか」ではなく、

コードを書いてからユーザーに届くまで、どのような流れになっているのか

を見ると、本来の目的がかなり分かりやすくなると思います。