開発をしていると、「CI/CD」という言葉には毎日のように出くわす。GitHub Actionsでテストを回す。mainにマージしたら、本番環境へ自動でデプロイする。そういう仕組みをひっくるめて、CI/CDと呼ぶことが多い。
ところが改めて問われると、存外あやふやだ。CIとはなにか、CDとはなにか。Continuous DeliveryとContinuous Deploymentは、どこが違うのか。かくいう僕も、最初は「CIは自動テスト、CDは自動デプロイ」くらいに思っていた。大きく外れてはいないが、本来の意味からは少しずれている。
用語の定義を眺めるより、なにが詰まっていたのかを追う方が早い。ソフトウェア開発を1本の配管に見立てると、CI/CDの歴史は、詰まった箇所を上流から順に通してきた顛末として読めるのだ。
Integration Hell
昔のソフトウェア開発では、変更を最後にまとめて統合するやり方も珍しくなかった。開発者やチームがそれぞれ長い期間作業を続け、最後に1つに合わせるのである。A、B、Cの3人が別々に作業し、締め切り間際に合流する、という絵だ。
それぞれの環境では正常に動いていても、合わせた瞬間に問題が噴き出す。Aさんが変更したコードを、Bさんも変更している。ある機能の変更が、別の機能を壊している。大量の競合、テストの失敗、仕様の不整合が、一度に押し寄せてくるわけだ。
しかも数週間、数か月分の変更を一気に統合するので、原因の特定にも骨が折れる。配管で言えば、何か月も流さずに溜め込んだ末の大詰まりである。これが世に言う「Integration Hell」だ。名前からして、あまりお近づきになりたくない代物である。
そこで出てきた考え方が、Continuous Integrationだった。
頻繁に統合するというContinuous Integration
CIはContinuous Integration(継続的インテグレーション)の略である。発想はいたってシンプルだ。最後にまとめて統合するから大変になるのなら、もっと頻繁に統合すればいい。
数か月開発して最後に爆発させるのではなく、小さな変更を統合しては確認する。このサイクルを何度も繰り返す。変更が小さいうちに統合しておけば、問題が起きても影響範囲を絞り込みやすい。こまめに流しておけば、配管はそうそう詰まらないのだ。
Continuous Integrationは、1990年代にExtreme Programming(XP)のプラクティスとして広く知られるようになった。Kent BeckはXPを体系化する中で、開発者が変更を頻繁に統合することを重要なプラクティスに据えている。もっとも、ここで1つ押さえておきたい点がある。CIとは本来、CIツールのことではないのだ。
GitHub Actionsを動かすことがCIではない
いまCIと聞くと、多くの人は次のような光景を思い浮かべるだろう。git push するとGitHub Actionsが動き、Lint、Test、Buildが順に走る。むろん、これらはCIを実現するための重要な仕組みである。
ただ、CIの本来の目的は別のところにある。コードの変更を頻繁に統合し、それでシステムが壊れていないことを継続的に確かめることだ。GitHub ActionsもJenkinsもGitLab CIもCircleCIも、それを支える道具に過ぎない。まず実践としてのCIがあり、ツールはその手段なのである。
したがって、GitHub Actionsを導入しただけでCIを実践しているとは限らない。たとえば巨大なFeature Branchで半年開発を続け、最後にPull Requestを作ったとする。そのPRでGitHub Actionsがテストを走らせても、6か月分の巨大PRに1回CIをかけてマージするだけでは、茶を濁したに過ぎない。Continuous Integrationが解決しようとした問題は、ほとんどそのまま残っている。
自動テストがあるだけでは足りない。変更を小さく保ち、継続的に統合するところまで含めてCIなのだ。
統合の次はリリースが詰まる
CIのおかげで、コードを書いてからビルドし、テストし、統合するまではずいぶん効率化された。変更のたびに自動テストが走り、統合しても大丈夫そうかがすぐに分かる。ところが、ここで別の問題が顔を出す。統合はできても、本番に出すのが大変なのだ。
統合できたコードが、そのままユーザーに届くわけではない。開発のあとに、QA、リリース手順の作成、サーバーの準備、設定変更、DB変更、運用チームへの依頼が控えていることもある。それらをすべて片付けて、ようやく本番リリースと相成る。
つまり、CIで速くなったのは、開発者から統合済みのソフトウェアまでの区間である。統合済みのソフトウェアから本番環境までという最後の区間は、手つかずで残っていた。統合は毎日できるのに、本番リリースは3か月に1回。これでは、ユーザーに変更が届く速さはさほど上がらない。詰まりが下流へ移っただけなのである。
そこで出てきたのがContinuous Deliveryだ。
いつでも出せる状態を保つContinuous Delivery
Continuous Deliveryは、日本語では「継続的デリバリー」と呼ばれる。CIでコードを継続的に統合できるようになった。ならば今度は、そのコードをいつでも本番へリリースできる状態にしておこう、という発想である。
具体的には、コミットから本番に出せる状態になるまでの一連のプロセスを自動化する。ビルド、ユニットテスト、統合テスト、受け入れテストを経て、ステージングに載せるところまでだ。この流れはDeployment Pipelineと呼ばれる。名前に配管が入っているのは、本稿にとって出来すぎた偶然である。
Continuous Deliveryの考え方は、2000年代にThoughtWorksなどで発展した。広く知られるようになったのは、Jez HumbleとDavid Farleyが2010年に出版した『Continuous Delivery』によってだ。CIが常に統合できる状態を保つものなら、Continuous Deliveryは常にリリースできる状態を保つもの。そう捉えると分かりやすい。
本番まで自動とは限らない
ここが少し混乱しやすいところだ。Continuous Deliveryでは、本番環境へのリリースそのものは手動でも構わない。テストもビルドも、リリース可能な状態まで持っていく処理も自動化されている。それでも、いまこのタイミングで本番に出すかという最終判断だけは、人間が下す。
GitHub Actionsなら、Production EnvironmentにApprovalを設定する構成がこれに近い。「Deploy Production」を承認すると、そこでデプロイが走る。求められるのは、ボタンを押せば安全にリリースできる状態が常に作られていることだ。手動操作が1つでもあったらCDではない、とかいう野暮なことは言わないでほしい。
一歩進んだContinuous Deployment
CDという言葉がややこしい理由はここにある。CDの2文字に、Continuous DeliveryとContinuous Deploymentという2つの言葉が同居しているのだ。
Continuous Deploymentは、Continuous Deliveryからさらに一歩進む。コードからテスト、ビルド、デプロイを経て本番環境に至るまでを、完全に自動化するのである。たとえばPull RequestでCIが走り、mainへマージされたらビルドされ、そのまま本番へデプロイされる。人間によるリリース承認はなく、チェックを通過した変更はそのまま本番環境へ反映される。
両者を並べると、違いはいたってシンプルである。
| Continuous Delivery | Continuous Deployment | |
|---|---|---|
| 目指す状態 | いつでも本番へ出せる | 問題がなければ自動で本番へ出る |
| テスト・ビルド | 自動 | 自動 |
| 本番リリースの判断 | 人間が決める | 自動化されている |
大きな違いは一点に尽きる。本番リリースの直前に、人間の判断が入るかどうかだ。
ボトルネックを順に潰してきた歴史
歴史の流れで見ると、話はかなりすっきりする。最初の問題は、開発、開発、開発と続けた末に統合し、Integration Hellに落ちることだった。そこで、小さく変更し、頻繁に統合し、自動で確認するContinuous Integrationが広がった。
すると次は、統合のあとが詰まる。CIで統合までは済むのに、本番までのリリースが大変なのだ。これを受けてContinuous Deliveryが、テスト、ビルド、ステージングを経てリリース可能な状態に至るまでを、継続的に回せるようにした。さらに、それだけ安定したパイプラインが作れるなら本番まで自動化してしまおう、と踏み出したのがContinuous Deploymentである。
配管の比喩で言えば、詰まりを1つ通すたびに、次の詰まりが下流に見えてきたわけだ。正確に言い直すと、CI/CDはある日突然発明された1つの技術ではない。ソフトウェアを変更してからユーザーへ届けるまでのボトルネックを、少しずつ解消してきた結果なのだ。
CIとCDの境界
いまの開発フローに当てはめると、たとえば次のようになる。開発者がPull Requestを出すと、Lint、型チェック、ユニットテスト、統合テスト、ビルドが走る。ここまでがContinuous Integrationの範囲である。
マージされたあとは、アーティファクトのビルド、ステージングへのデプロイ、E2Eテストを経てリリース可能な状態になる。ここがContinuous Deliveryの範囲だ。その先に本番デプロイがある。直前に手動承認が入ればContinuous Delivery、そこまで自動ならContinuous Deploymentと整理できる。
むろん実際のシステムでは、境界がここまで綺麗に分かれているとは限らない。それでも、コードを書いてからユーザーに届くまでのうち、どこを継続的に回しているかを見ればいい。だいたいの位置づけは、それで見えてくる。
「CI/CDを導入しています」の中身
現場では「CI/CDを導入しています」という説明をよく耳にする(僕も何度か口にした)。ところが、この一言だけでは、実際になにをしているのかほとんど分からない。
Pull Requestでテストが走るだけかもしれない。テスト後にマージし、ビルドしてステージングに載せ、手動承認を経て本番に出しているのかもしれない。あるいは、マージされたら本番まで完全自動という可能性もある。そのどれもが、等しく「CI/CD」と呼ばれうるのだ。
なので実際には、いくつか問いを立てた方が早い。CIはどのタイミングで動き、なにをテストしているのか。アーティファクトはいつ作られるのか。ステージングや本番へのデプロイは自動で、本番の前に承認は挟まるか。このあたりを見る方が、そのプロジェクトの開発フローをずっと正確に掴める。
目的は自動化ではない
CI/CDについて調べると、テストの自動化、デプロイの自動化という説明がずらりと並ぶ。もちろん自動化は大事だ。ただ、自動化そのものが目的なのではない。
CIが解決したかったのは、変更を統合するリスクを小さくすることだった。Continuous Deliveryが解決したかったのは、ソフトウェアをリリースするリスクを小さくすることだった。
たとえば3か月分の変更をまとめてリリースすれば、なにか起きたときの原因候補は山のようにある。一方、小さな変更をテストしてデプロイするサイクルを頻繁に回していれば、確かめるべき変更も小さくて済む。CI/CDとは、小さな変更を安全に繰り返せる状態を作るための仕組みなのだと、僕は考えている。大きな変更をたまに行うやり方の、ちょうど逆である。
以上、CI/CDが生まれた経緯から、3つの言葉の違いを整理した。CIは変更を頻繁に統合して、問題を早く見つける。Continuous Deliveryは、統合されたソフトウェアをいつでも出せる状態に保つ。Continuous Deploymentは、問題のない変更をそのまま本番まで届ける。
CI/CDを「GitHub Actionsでテストとデプロイを自動化するもの」とだけ理解するのは、いささかもったいない。その裏には、変更を小さく保ち、頻繁に統合し、安全にユーザーへ届けるという考え方がある。設定を眺めるときも、見るべきはツールの銘柄ではなく、コードがユーザーに届くまでの流れの方だ。配管で大事なのは蛇口の銘柄ではなく、水が滞りなく流れることなのである。