万人のためのオートメーション
継続的インテグレーションのアンチパターン、第 2 回
してはいけないことを学んで継続的インテグレーションを円滑に進める
コンテンツシリーズ
このコンテンツは全#シリーズのパート#です: 万人のためのオートメーション
このコンテンツはシリーズの一部分です:万人のためのオートメーション
このシリーズの続きに乞うご期待。
この 2 回連載の第 1 回目の記事では、以下の 6 つの CI アンチパターンについて説明しました。
- 頻繁にチェックインを行わないため、インテグレーションに遅れが生じる
- ビルドに失敗しているため、チームが他の作業に進めない
- フィードバックが少ないため、対応することができない
- やみくもにフィードバックが送られてくることから、人々がメッセージを無視するようになる
- 遅いマシンを使用していることが原因で、フィードバックに遅れが出る
- 肥大化したビルドに依存しているため、フィードバックに時間がかかる
上記のアンチパターンは CI によってもたらされるはずのメリットの実現が遅くなったり、不可能になったりする原因となります。この第 2 回目の記事では、同じく期待が裏切られることになるプラクティスをさらに 5 つ紹介します。
- その日の終わりまで変更をコミットしないため、ボトルネックのコミットとなる。これによって大抵はビルドに失敗し、開発者を苛立たせる結果となる
- 最小限の自動プロセスで構成されていることが理由でビルドが常に成功する。そのため継続的イグノランスという結果に至り、インテグレーションの問題がなかなか発覚しない
- コードが変更されるたびにソフトウェアをビルドするのではなく定期ビルドを優先することから、ビルドの修正が遅れる
- 自分のマシンではコードが有効に機能すると信じ込んでしまうため、後でコードを他の環境で実行するまで問題が見つからない
- 古いビルド成果物を取り除かなかったため、残骸が残ったままの環境となり、エラーの誤検出や検出漏れが発生する
CI のさまざまなメリットを得るためには、上記のアンチパターンについても理解し、そしてもちろん回避することが有益となります。
ボトルネックのコミットにあえぐ習慣を止めること
名前: ボトルネックのコミット
アンチパターン: 開発者が 1 日の仕事を終えて帰宅する時間になるまでコードの変更をコミットしないため、インテグレーション・ビルドのエラーが発生した場合にチームのメンバーが遅くまで家に帰れなくなる。
ソリューション: 1 日をとおして頻繁にコードをチェックインする。
ボトルネックのコミットはアンチパターンの 1 つである低頻度のチェックインの変形です。コードのチェックインは 1 日につき最低 1 回は行う前提となっていますが、問題は全員が一斉にチェックインするということです。CM Crossroads の記事では、Slava Imeshev がビルド失敗の大半は午後 5 時から午後 8 時の間に起こると説明しています (それほどではないにせよ、昼休みもビルド失敗の時間帯です)。Imeshev は「午後 5 時ちょうどのチェックイン」で、この現象を、開発者が 1 日の終わり近くになってからコードをチェックインする傾向にあることと結び付けています (「参考文献」を参照)。
午後 5 時になったようです
あなたのチームが、変更をコミットしてビルドを実行するまでは誰も家に帰ってはならないというルールに従っている場合、午後 5 時ちょうどのチェックインは友達を失う手っ取り早い方法となります。コードのサブミットをじっと待つのは苦痛なだけでなく、ビルドに失敗したときのことを考えてみてください。至るところで、またもや帰宅が遅くなる理由を電話で説明する声が聞こえてくることでしょう。
図 1 は、ソフトウェア開発チームの一般的なチェックインの時系列を示したものです。リポジトリーのコミットが昼食の時間帯や帰宅前に集中していることに注目してください。
図 1. ボトルネックのコミット

この頭痛の種を取り除くソリューションは、もちろん変更を頻繁にコミットすることです。そうすれば、インテグレーション・ビルドはより小規模に抑えられると同時に、ビルドの回数も多くなります。そのためビルド・エラーが発生したとしても大きな問題になる可能性は少ないため、問題の修正が遥かに簡単になります。
5 時という時刻にはかなりの意味が含まれているはずです。自分やチームのためを思うなら、コードを頻繁にチェックインしてメンバー全員にとって 5 時が幸せな時刻であるようにしてください。
知らぬが仏というわけにはいきません
名前: 継続的イグノランス
アンチパターン: ビルドが成功したため誰もが完成版のソフトウェアは正常に機能すると思い込んでいるが、現実には、そのビルドはコンパイルと少数のユニット・テストで構成されているに過ぎない。
ソリューション: バージョン管理リポジトリーが変更されるたびに完全なインテグレーション・ビルドを実行する。
ビルドが常に成功するために、チームが誤った安心感に陥ることがあります。実際、ビルドの失敗がめったに起こらない場合は、ほぼ間違いなく、チームは継続的イグノランスのアンチパターンに陥っています。インテグレーション・ビルドが一度も失敗しなければ、コードを十分に検証したり、その有効性を確認するとは考えにくいからです。ビルドの実行内容が希薄であればあるほど、ソフトウェアが実際に期待通りに動作するかどうかについて得られる情報も限られてしまいます。
包括的ビルドが悟りを開かせてくれます
図 2 の左側に示したインテグレーション・ビルドは、ソース・コードをコンパイルし、クラスをパッケージ化してバイナリーにし、ソフトウェアを運用環境にデプロイするだけに過ぎません。もちろんインテグレーション・ビルドをまったく行わないよりはましですが、図の右側と見比べてください。この 2 つのスタックを比較すれば、右側に示されている、より多くのプロセス (テストやデータベースの変更など) を実行することによって明らかにできる問題があることは明らかです。
図 2. 包括的ビルドはあなたの味方です

ビルドを合理化してください
データベース統合や開発者テスト (ユニット・テスト、コンポーネント・テスト、機能テストなど)、それに自動コード検査 (コーディング標準の準拠、サイクロマティック複雑度、コードの重複チェックなど) やシステム配布などの追加プロセスを組み込むことで、ソフトウェアが実際に機能するかどうかを開発サイクルのより早い段階で判断できるようになります。ただし注意しなければならないのは、インテグレーション・ビルドにプロセスを追加すればするほど、フィードバックに時間がかかるということです。そのため必要に応じて、初期コミットのビルドの後に時間のかかるプロセスを実行するといったビルド・パイプラインを作成することを考慮してください。このようなパイプラインを作成すれば、より短時間でのフィードバックが可能になり、ソフトウェアの妥当性をより柔軟に検証できるメカニズムが実現します。
定期ビルドのスケジュールを見直すこと
名前: 定期ビルド
アンチパターン: ビルドは毎日、毎週など定期的に実行されるものの、変更が行われるごとに実行されるわけではない。
ソリューション: ソース・コード・リポジトリーに変更が適用されるたびにビルドを実行する。
継続的インテグレーションとは、ソフトウェア・アセットを頻繁に統合することです。ここで言う頻繁とは、毎回コードを変更するたびに、という意味です。私の知っている限り、コードを変更するたびにソフトウェア・アセットを統合することが問題を早期に発見する最短の道となります。問題を早期に発見することが有効な理由の 1 つには、1 つに経費の節約が挙げられます。そしてもう 1 つの理由は、より優れたコードをより頻繁にリリースできるようになるということです。
定期ビルドの問題点は、変更をリポジトリーにコミットしたかどうかに関わらず実行されることです。前回のビルドから何も変更されていなかったり、あるいは変更が多すぎて問題を紐解くのが困難になってしまえば、ビルドを実行しても何の役にも立ちません。
CI を効果的に行うには即応性を重視する必要があります。つまり、ビルドに失敗したら問題を直ちに修正するということです。定期ビルドの性質はこの即応的な措置を妨げ、「問題の箇所まで行ったら修正する」という手法をもたらしがちですが、それは CI とは正反対の手法です。
定期ビルドをより頻繁なビルドに変更するには、CI サーバーを適切に構成すればいいだけのことです。その一例として、リスト 1 にバージョン管理リポジトリーを 2 分ごとにポーリングする CruiseControl スクリプトを記載します。変更が検出されると、CruiseControl がビルドを実行します。
リスト 1. 変更のたびに実行するビルド
... <schedule interval="120"> <ant anthome="${cc.ant.dir}" buildfile="build-${project.name}.xml"/> </schedule> ...
誤解しないでほしいのは、定期的にビルドを実行することが役に立つ特定のシナリオもあるということです。例えば、ビルドの実行に時間のかかる負荷テストやパフォーマンス・テストは、毎晩定期的に行うことも一考です。ただし一般的な法則として、あらゆるビルドを実行する頻度がコード変更の頻度より少ない場合には、問題を早期に発見する可能性を制限していることになります。
自分のマシンでは機能するからといって過信しないこと
名前: 自分のマシンでの作業
アンチパターン: 自分のマシンでは機能する専用のビルドを実行しているため、他の環境では変更が有効でないことになかなか気付かない。
ソリューション: チームがインテグレーション・ビルド・マシンを使用して、バージョン管理リポジトリーに変更がコミットされるたびにビルドを実行する。
例えばコードを変更してビルドを (IDE または Ant で) 実行し、万事が期待通りに機能してから、変更をバージョン管理リポジトリーにコミットしたとします。数日後、このコードを別の環境にデプロイした誰かから、コードの変更が有効ではないという連絡を受けます。そこで自分のワークステーションでソフトウェア・アプリケーションを起動してみると、やはりすべてが完璧に動作します。そして一言、「私のマシンでは上手くいくのに!」と叫ぶことになります。
このような事態を経験したことがありますか? まだ経験したことがないのなら、心の準備をしておいてください。おそらく経験することだからです。この困った事態が起こる理由はさまざまに考えられますが、一般的には、新しいファイルをバージョン管理リポジトリーに追加し忘れたか、あるいはマシンでの特殊な構成が別の環境では構成されていないことが原因となっています。
デスクトップの向こうに広がる世界
名前: IDE 専用のビルド
アンチパターン: ワークステーションで IDE (ローカル側で動作) を使って専用のビルドを実行しているため、別の環境でないと何が機能しないかがわからない。
ソリューション: ビルド・スクリプトを作成してバージョン管理リポジトリーにコミットし、このビルド・スクリプトを変更が行われるたびに実行する。
IDE を使ってコードやビルド・スクリプトを作成すること自体には何の間違いもありません。IDE が作業を効率化するのは確かです。けれどもビルド・プロセスが IDE に密接に結合していると、デプロイメント環境にその IDE がインストールされていない場合、インテグレーション・ビルドを実行できなくなってしまうという問題が起こります。その理由は、オペレーション・チームがステージング環境や本番環境に IDE をインストールすることは滅多にないからです。たとえインストールしたとしても、インテグレーション・ビルドを実行するためには IDE を手動で構成しなければなりません (その結果、環境間での矛盾とビルド・エラーにつながる可能性があります)。その上、ビルドを IDE に依存させると、開発プロセスが終わりに近づいてステージング環境と本番環境の IDE から依存関係が削除されるまでは、構成の問題が見つからないこともなりかねません。
IDE 専用のビルドの問題を解決するために必要なのは、ビルド・スクリプトを作成することだけです (ビルド・スクリプトを作成しても、IDE で簡単に使用できます)。そうすれば、どの環境であろうと、このビルド・スクリプトをインテグレーション・ビルドのマスター・メカニズムにすることができます。
近視眼的な考えと開発者
名前: 他の環境での動作を考慮しない環境
アンチパターン: ある環境でビルドが機能するからと言って、他のどの環境でも機能すると思い込んでしまう。
ソリューション: ビルド・ターゲットでのビルドの振る舞いを定義する。さらに、環境に依存したデータを外部の .properties ファイルに含めて外部化する。
他の環境での動作を考慮しない環境のアンチパターンは、コードがある環境で動作すれば、作業は完了したと思ってしまう開発者の心理を反映しています。したがって、この考え方に対抗するには、CI システムが (妥当な制約の範囲で) 何事も前提しないことが必要です。プラットフォーム固有の制約を排除し、プロパティー・ファイルなどによって制約を置き換えられるようにするだけで、前提は少なくなります。
ビルドを Windows® 環境と Linux® 環境の両方で実行しなければならない場合、C
ドライブへの参照があると Linux でビルドが失敗するのは当然です。また、ビルドがマシンに依存した環境変数 (GLOBUS_LOCATION
) を参照している場合には、これらの変数が別の環境で設定されていなければビルドは失敗します。このような場合に必要なのは、これらの参照をビルド時に置換可能な変数に置き換えればいいだけの話です。
リスト 2 の Ant XML スニペットを見ると、環境の値が含まれる .properties ファイルを組み込む方法が分かります。
リスト 2. ビルド・スクリプトでの環境プロパティーの参照
<property file="${basedir}/TEST.properties" />
リスト 3 のプロパティーには、デプロイメント環境に特有のデータが示されています。このようにファイルにデータベース接続の値、Web コンテナーの位置、ホスト名、そして認証情報などの情報を外部化することによって、ビルドそのものの前提を少なくしていることに注目してください。
リスト 3. properties ファイルでのデータ定義
database.host.name=integratebutton.com database.username=myusername database.password=mypassword database.port=3306 jboss.home=/usr/local/jboss/server/default jboss.temp.dir=/tmp jboss.server.hostname=integratebutton.com jboss.server.port=8080 jboss.server.jndi.port=1099
ターゲット (Ant ターゲットなど) にはすべてのビルドの振る舞いを含め、プロパティーには同じビルド・スクリプトで複数回参照されるデータを定義します。マシンによって異なるデータは、いずれも .properties ファイルで外部化してください。この単純なアドバイスに従えば、最大限の柔軟性を実現し、頭痛の種を最小限にできるはずです。
環境はきれいに片付けること
名前: 残骸が残ったままの環境
アンチパターン: 時間節約のために段階的ビルドを実行している一方、古い (前のビルドによる) 成果物によってビルドの誤検出 (または検出漏れ) が発生する。
ソリューション: 以前のビルド成果物を削除してからビルドを実行する。サーバーと構成情報をベースライン化する。
まず何よりも先に環境をクリーンアップすること
ビルドに失敗し、その失敗の原因が、前のビルドの成果物が残っていたためであることがわかることほど苛立たしいことはあまりありません。このような成果物には、最近デプロイされた WAR ファイル、誤った JAR バージョン、データベース更新などがあります。その一方、さらに厄介なのは、前のビルド成果物がビルド環境に残っているがためにビルドに成功し、それによって誤った安心感を抱いてしまうことです。
いくら強調しても足りないことですが、環境を既知の状態にしてから関連するビルド作業を行うことが重要です。実際、私はソフトウェアをビルドする際に、名付けて焦土作戦と呼ぶ作業を実行するようにしています。リスト 4 は、前に使っていたログ・ディレクトリー、配布ディレクトリー、レポート・ファイル・ディレクトリー等を Ant の delete
タスクを使って削除する単純な例です。これにより、誤検出と検出漏れが発生する可能性は大幅に減ります。
リスト 4. Ant によるディレクトリーの削除
<target name="clean"> <delete dir="${logs.dir}" /> <delete dir="${dist.dir}" /> <delete dir="${reports.dir}" /> <delete file="cobertura.ser" /> </target>
上記のコードは単純なものですが、環境の焦土作戦には、古いクラス・ファイルと前にデプロイされた EAR/WAR ファイルの削除、既知の状態へのデータベース復帰 (例えば、データベース・テーブルを廃棄して再作成するなど)、クラスパスの再初期化、そして環境変数の使用回避などが含まれる場合もあります。
デプロイメント環境をベースライン化すること
「焦土作戦」から考えつくのは、インテグレーション・ビルドを実行しているオペレーティング環境の状態を設定するなどといった一層高度な手法です。環境をベースライン化するには、それぞれのコンポーネントと構成項目を削除し、オートメーションによって適用しなおします。ただ単純に古いファイルを削除する場合と比べ、この方法は環境の複雑さによってはさらに難しい取り組みとなるかもしれませんが、段階的に適用することは可能です。コンポーネントとしては、データベース、Web またはファイル・サーバー、あるいは専用のソフトウェアなどが考えられます。構成項目は環境変数である場合も、データベース構成 (メモリー割り当てなど) の変更である場合もあります。ここで鍵となるのは一貫性です。ビルドを実行するそれぞれの環境が同じような構成になっていなければなりません。デプロイメント環境をベースライン化することで、特定の問題の原因を見つけ出す態勢が一層整うことになります。図 3 に、各デプロイメント環境に共通の方法で、削除して適用しなおす必要があると考えられるコンポーネントを示します。
図 3. デプロイメント環境の焦土作戦

私の経験からすると、環境の焦土作戦を行うことの最も大きなメリットは、より迅速に問題をトラブルシューティングする方法を提供できることです。比較の対象となる環境をベースライン化することで、同じ条件で環境を比較できるようになるため、環境間での問題が生じたときには一層素早く問題を修正することが可能になります。
継続的インテグレーションを成功させるには
この記事で理解してもらいたかったことは、継続的インテグレーションは開発プロジェクトで使用するには優れたプラクティスですが、そのメリットをさらに享受するためには特定のアンチパターンを回避しなければならないということです。開発チームがこれらのプラクティスを使用する理由は十分にありますが、その一方、それがアンチパターンの使用という結果に至る恐れがあります。例えば、定期ビルドを実行することが理にかなっている場合はあります。さらに、コードをコミットするという効果的なプラクティスが実はボトルネックとなることもありますが、だからといって頻繁なコミットが悪いプラクティスだというわけではありません。アンチパターン自体は悪いプラクティスではありませんが、特定の状況では逆効果になり得るということを肝に銘じておいてください。
ダウンロード可能なリソース
関連トピック
- 「Remove the smell from your build scripts」(Paul Duvall著、developerWorks、2006年10月): ビルドを維持しやすいように改良してください。
- 『Continuous Integration: Improving Software Quality and Reducing Risk』(Paul Duvall 他による共著、Addison-Wesley Signature Series、2007 年): 継続的インテグレーションに関するあらゆる側面を取り上げた資料を読んでください。
- 「Avoiding Continuous Integration Build Breakage Patterns」(Slava Imeshev 著、CM Crossroads、2005年11月): 午後 5 時を心待ちにできる時間にしてください。
- 「継続的インテグレーション」(Martin Fowler、martinfowler.com): Fowler による、独創性に富んだ継続的インテグレーションについての記事です。
- 「Is Pipelined Continuous Integration a Good Idea?」(infoq.com、2007年9月): 代表的な CI 支持者たちはビルド・パイプラインを重視しています。
- 『アンチパターン―ソフトウェア危篤患者の救出』(Brown 他による共著、ソフトバンククリエイティブ): ソフトウェア開発に関するアンチパターンを説明する優れた一冊です。
- 「継続的インテグレーションのアンチパターン 第 1 回」(Paul Duvall 著、developerWorks、2007年12月): この連載で最初に紹介した 6 つのアンチパターンについて読んでください。
- developerWorks: Java プログラミングのあらゆる側面を網羅した記事が、豊富に用意されています。
- Ant: Ant を使って Java™ ビルドを実行してください。