【読書メモ】継続的デリバリ―③ 3章 継続的インテグレーション

今までの記事

- [【読書メモ】継続的デリバリ―① 1章 ソフトウェアデリバリーの問題](https://qiita.com/Jazuma/items/180b46947167b6347bef)
- [【読書メモ】継続的デリバリ―② 2章 構成管理](https://qiita.com/Jazuma/items/eb85047b6f72c1df260e)

### 3.1 導入

継続的インテグレーションの目的はソフトウエアを常に動作する状態に保つことである。
そのためには変更がコミットされるたびに包括的な自動テストを実行する必要がある。より重要なことはビルドやテストが失敗したらすぐに手を止めて問題を解消することである。

### 3.2 継続的インテグレーションを実現する

#### 3.2.1 始める前に必要なもの

##### 1. バージョン管理

プロジェクトにあるものは全て一つのリポジトリに含めなければならない。
コードに限らずテスト、データベーススクリプト、ビルド・デプロイメントスクリプトも対象である。

##### 2. 自動ビルド

コマンドラインからビルドできるようにする必要がある。 近年ではIDEの発達によりコマンドラインを使わずにビルドできるようになっている。しかし、コマンドラインから実行できるビルドスクリプトを用意するべきである。 この理由は以下のとおりである。

- CI環境からビルドを自動実行できるようにするため。問題が起きた時に監視できるようにする。
- 継続的にテスト、リファクタリングするため。スクリプト化することでビルドプロセスを理解しやすくなる。

##### 3. チームの合意

継続的インテグレーションはプラクティスであり単なるツールではない。こまめにコミットしたり、ビルドが失敗したらすぐに修正することをチームメンバーに合意し・遵守してもらう必要がある。 そうしなければ思うような効果は得られない。

#### 3.2.2 基本的な継続的インテグレーションシステム

継続的インテグレーションにおいては次のようなことをしなければならない。

- ビルドが実行中かを確認する。実行中の場合、終了するのを待つ。失敗したら他のメンバーと協力して解消する。
- テストがパスしたらリポジトリから最新版のコードを取得して開発環境を最新化する。
- ビルドスクリプトとテストを開発機で実行して正常に動作することを確認する。
- ビルドが通ったらバージョン管理にコードをコミットする。
- 自分の変更に対してCIツールがビルドを実行するのを待つ。
- ビルドが失敗したらすぐに修正する。
- ビルドが通ったら次のタスクに着手する。

### 3.3 継続的インテグレーションの前提条件

継続的インテグレーションをプロジェクトの途中から導入するのはコストが大きい。
CIを効率的に行うためには次に挙げるようなプラクティスを実践する必要がある。

#### 3.3.1 定期的にチェックインせよ

trunkやメインブランチにコードをこまめにチェックインすることはCIを実行する上でもっとも重要なプラクティスである。

定期的にチェックインすることによる恩恵は大きい。
1回あたりの変更量が小さくなるのでソフトウェアが壊れるリスクが小さくなる。また、壊れた時に原因を特定することも容易である。 他の人による変更とコンフリクトする危険性も減る。

#### 3.3.2 包括的な自動テストスイートを作成せよ

包括的な自動テストがあればビルドの度にアプリケーションが壊れてないことを確認することができる。
CIのビルドで実行するテストは3種類である。 ユニットテスト・コンポーネントテスト・受け入れテストである。

ユニットテストは関数の振る舞いや関数同士のやりとりをテストするためのものである。データベースやファイルシステムにアクセスしない(してはいけない)ため、アプリケーションを起動せずに実行できる。 ユニットテストは実行時間を短く保つ必要がある。 大規模なシステムでも10分を超えてはならない。

コンポーネントテストはデータベースやファイルシステムを利用する。そのためユニットテストよりも実行に時間がかかる。

受け入れテストは定められた受け入れ基準をソフトウェアが満たしているかをテストする。
受け入れ基準は特定のユースケースであることもあれば、キャパシティやセキュリティであることもある。い受け入れテストはアプリケーションを疑似本番環境で起動した状態で実行することが望ましい。

受け入れテストには通常長い時間がかかる。

#### 3.3.3 ビルドプロセスとテストプロセスを短く保て

ビルド・テストに時間がかかると以下のような問題が発生する。

- コミットの前にテストを実行しなくなる。そうするとビルドの失敗率が上がる。
- CIのプロセスに時間がかかるのでビルドを再実行する間に複数のコミットが行われる。そうするとビルドが壊れたときに原因を特定するのが難しくなる。
- ビルドやテストの待ち時間が長いのでこまめにチェックインしなくなる。

ビルド時間を短縮する方法は多数ある。

JUnitやNUnitのようなユニットテストツールはテストごとの実行時間を解析して出力してくれる。
どのテストに時間がかかっているかを特定すれば改善方法を検討することができる。このプラクティスは定期的に実施すべきである。

また、テストプロセスを2つのステージに分けることも有効である。
最初のステージではソフトウェアをコンパイルしてユニットテストを実行する。テストがパスしたら実行可能なバイナリを生成する。 このステージをコミットステージとする。

2つ目のステージではコミットステージで生成したバイナリを取得して受け入れテストを実行する。
もし存在するのであればこのステージでインテグレーションテストやキャパシティテストも実行する。

コミットステージはチェックインの前に実行する必要がある。また、CI環境でもチェックイン毎に実行する必要がある。

#### 3.3.4 開発ワークスペースを管理する

開発の生産性を上げるためには開発環境を管理することが重要である。
そのためにまず第一に構成管理をやっておく必要がある。プロジェクトに必要な全てをバージョン管理して、「正しく動くことが分かっている」バージョンから作業を開始すべきである。

次に、サードパーティの依存パッケージやライブラリのバージョンを管理する必要がある。
適正なバージョン、すなわりソースコードのあるバージョンと組み合わせて動くことが分かっているバージョンのライブラリを使えるようにしておかなければならない。

最後のステップとして、自動テストを開発機上で実行できることを確認する。

### 3.4 継続的インテグレーションソフトウェアを使用する

CIツールの解説のみであるため割愛

### 3.5 基本的なプラクティス

継続的インテグレーションとは単なるツールではなくプラクティスである。そのため、実践するためにはチームで規律を守る必要がある。

#### 3.5.1 ビルドが壊れているときにはチェックインするな

継続的インテグレーションにおいてもっともやってはならないことはビルドが失敗しているにも関わらずチェックインすることである。 ビルドが壊れたらすぐに修正する。対応が早ければ早いほど原因を特定して修正するのが容易になる。

#### 3.5.2 コミットする前に、常にローカルでコミットテストを実行せよ あるいはCIサーバで実行せよ

開発者はコミットの前に自分の変更によってソフトウェアが壊れていないかを確認する。具体的には、バージョン管理から最新版のソースコードを取得してローカルでビルド・テストを実行する。

このフローを実施することでビルドを常にクリーンに保つことができる。

#### 3.5.3 次の作業を進める前に、コミットテストが通るまで待て

継続的インテグレーションの目的はエラーを早期検知して修正することである。
開発者はチェックインしたコードがコミットテストに通るまでは他の作業に着手してはならない。
ビルドとコミットテストの結果を確認することに注力する。

コミットテストが成功して初めて開発者は次の作業に着手できる。テストが失敗したらすぐに修正する。

#### 3.5.4 ビルドが壊れているのに、家に帰ってはならない

開発者はビルドが壊れた状態を放置して退勤してはならない。
日をまたぐと記憶が薄れて問題を修正するのにかかる時間が長くなる。 また、ビルドを壊したメンバーが翌日休んでしまうと他のメンバーの作業にも影響する。

このような問題はタイムゾーンが異なる地域に分散したチームにおいてより深刻になる。

しかしビルドを修正するために長時間残業する必要はない。そうではなく、ビルドが壊れても対応できるように早い時間にチェックインするべきである。それが難しい場合はチェックインを翌日に持ち越すのが賢明である。熟練した開発者は作業終了の1時間前にはチェックインを止める。そして、翌朝すぐにチェックインする。

#### 3.5.5 常に以前のリビジョンに戻す準備をしておくこと

ビルドが壊れることを想定しておく。コミットステージが失敗した場合、対応方針に関わらず正常に動いている状態に素早く戻すことが重要である。すぐに問題が修正できない場合は以前の状態にリバートした上で、ローカル環境上で問題を修正する。

#### 3.5.6 リバートする前にタイムボックスを切って修正する

ビルドが壊れた場合の対応ルールをチームで決めておくと良い。例えばチェックインしてビルドが壊れたら10分時間を取って修正する。10分で解決できなければ正常に動くことが分かっている以前のバージョンに切り戻す。

#### 3.5.7 失敗したテストをコメントアウトするな

コミットテストが失敗したらすぐに修正するというルールを徹底すると、失敗したテストをコメントアウトする開発者が現れることがある。この対応は極力避けるべきである。

時間がかかったとしてもコードを修正するか、テストを修正するか、テストを消すかしなければならない。

テストがコメントアウトされないようにコメントアウトされたテストの数をチームに通知する・コメントアウトされたテストが一定数を超えるとビルドが失敗するようにするなど対策しておくと安全だ。

#### 3.5.8 自分が変更してビルドが壊れたら、すべてに対して責任をとれ

変更をコミットしてテストが通っても、他の人の変更と組み合わせるとテストが通らない場合がある。
このことは本人か他の人の変更が原因でデグレが発生したことを意味する。

この場合、変更した人が責任をもって問題を取り除く必要がある。このプラクティスはあまり実践されていない。

このプラクティスで示唆されていることがある。すべてのメンバーがすべてのコードにアクセスできなければならないということだ。 そうでなければ変更の途中でデグレに気づくことも、デグレが発生した時に修正することもできない。

#### 3.5.9 テスト駆動開発

継続的インテグレーションは素早くフィードバックすることが目的であるが、そのためにはユニットテストのカバレッジが十分でないといけない。 ユニットテストのカバレッジを上げるにはテスト駆動開発を行うのが最も有効である。

テスト駆動開発ではアプリケーションコードを実装する前にテストコードを作成する。
これにより、アプリケーションの振る舞いがテストで表現される上、テストが仕様書替わりにもなる。

### 3.6 やったほうがいいプラクティス

#### 3.6.1 エクストリームプログラミング(XP)の開発プラクティス

継続的インテグレーションはXPのプラクティスの1つだ。XPの他のプラクティスと組み合わせることでより効果を発揮する。 特にテスト駆動開発とコード所有権の共有を相性がいい。加えてリファクタリングも行うことを検討すべきである。

#### 3.6.2 アーキテクチャ上の違反事項があった場合にビルドを失敗させる

システムアーキテクチャ上のルール違反がないことを検査するテストをコミットテストに含めることも選択肢の一つである。

#### 3.6.3 テストが遅い場合にビルドを失敗させる

テストの実行に時間がかかると待ち時間が大きくなりチームの生産性が下がる。そうするとチェックインの頻度が下がる。これにより、コミットの粒度が大きくなりコンフリクトのリスクも高くなる。エラーが混入する確率もあがりテストが失敗しやすくする。 

したがって、テストを短い時間で実行できるようにすることが重要である。テストに一定以上の時間がかかる場合にビルドを失敗させてもよい。 一例として、実行に2秒以上かかるテストがある場合にビルドを失敗させるというアプローチが挙げられる。

#### 3.6.4 警告やコードスタイルの違反があったときにビルドを失敗させる

コンパイル警告が出た場合にビルドを失敗させるのは少々過酷だがコード品質を維持するには効果的である。
より効果を上げるにはコーディング規約を必要に応じて追加すればよい。

しかし、プロジェクトによってはいきなりこのプラクティスを導入することが難しいことがある。このプラクティスを徐々に導入するための方法として、ラチェット方式というものがある。これは、警告規約違反が前回のコミット時から増えている場合にビルドを失敗させるというものだ。この方式を使えばビルドが常に失敗する事態を回避しつつ警告を減らすことができる。

### 3.7 分散したチーム

分散したチームで継続的インテグレーションを実施するのはプロセスや技術の観点では集約したチームの場合と大差はない。しかし、チーム間で時差がある場合は影響を受ける側面もある。

その場合も継続的インテグレーションとバージョン管理を続けるのが最も有効なアプローチである。
その他のアプローチは全て次善の策である。

#### 3.7.1 プロセスに与えるインパクト

チームが分散していても継続的インテグレーションを実施する上での違いはほとんどない。だが、時差のある場所で作業するチームにおいては問題が複雑になる。例えばサンフランシスコのチームがビルドを壊したまま作業を終えた場合、北京のチームはその時間に作業を始めることができない。つまり、プロセスは変わらないが規律を守る重要性が増す。

分散したチームではメンバー間でコミュニケーションを円滑にとるための工夫が求められる。チャットやビデオ通話ツールでこまめにコミュニケーションをとることでメンバー同士の信頼関係が醸成される。

#### 3.7.2 中央集権的継続的インテグレーション

CIサーバの中にビルドファームの管理や認証機能等の機能が付属しているものがある。
これらを使うと大規模な分散チームで継続的インテグレーションを中央集権サービスとして使うことができる。このようなシステムを使うと自前でサーバを用意しなくても継続的インテグレーションを容易に実施できる。

同様に運用担当者がサーバリソースを1箇所にまとめて各種環境を管理して設定が正しいことを担保することもできる。 このようにしておくとプロジェクトの垣根を越えて標準的なメトリクスを収集・監視することができる。マネージャーやデリバリー担当者がコード品質を監視するツールを作成可能になる。

仮想化も中央集権CIサーバと組み合わせることができる。ベースラインイメージを作成しておけばワンクリックで新しい仮想マシンを立ち上げることができる。同様にビルドやデプロイメント環境が常に正しいことも保証される。

中央集権的な継続的インテグレーションを実行するためには必要なものを開発者が自動化された方法で入手できることが不可欠である。

##### 3.7.3 技術的な問題

回線の性能が低いとバージョン管理システムへのアクセスを地理的に分散したチームで共有するのが難しい場合がある。 継続的インテグレーションが実践されているとチーム全体がこまめにコミットする。そうするとバージョン管理システムへのアクセスが比較的多い状態が維持される。 回線が細いと生産性に影響するので太い回線を引いておくことが重要である。

バージョン管理システムを自動テストの実行をホストするビルド基盤から近い場所に置いておくことには意味がある。 テストがチェックインの度に実行されるので、通信量が増えるためだ。

バージョン管理システムや継続的インテグレーションの実行環境、デプロイメントパイプラインにおけるテストの実行環境はチームのどの場所からでも平等にアクセスできる必要がる。

##### 3.7.4 代替案

貧弱な回線が原因で継続的インテグレーションが上手くいっていないのであれば回線を強化するのが最も有効な対策である。代替案はいくつかあるがいずれも回線の強化より効果が低い。