高速道路で走っている全ての車を、もっと速く走らせることがあなたの仕事だと想像してみてください。運転手全員にアクセルをもっと強く踏むよう指示すれば、どうなるでしょうか?
もちろん、その結果は大惨事です。しかし、これは多くの開発者がソフトウェアをより速く開発しようとするときの態度と同じです。彼らは次のように理由を語ります。
「少しでもアジャイルになろうとしているので、設計やソフトウェアの説明書を書くことに構っている時間はありません」
「この製品を今すぐ出荷しないといけないので、試験している暇なんてありません!」
「すべてを自動化する時間がなかったので、コードを手作業でデプロイしました」
高速道路を走る車には、安全運転が求められます。車を速く運転するためには、ブレーキやシートベルト、エアバッグなどのセキュリティ機構が必要で、何かあったときにドライバーに危害が及ばないようにしなければなりません。
ソフトウェアの場合、アジリティには安全性が求められます。賢明なトレードオフをすることと、慎重さを忘れて、盲目的に突撃することは同じではありません。何か問題が発生した場合に、これらの変更が過度の損害を与えないようにするためのセキュリティ機構が必要です。無謀なことをすると、最終的には速くなるどころか、遅くなってしまうのです。
- 試験をしないことで「節約」した1時間は、製造過程での厄介なバグを見つけ出すための5時間となります。「ホットフィックス」が新たなバグを生じさせれば、さらに5時間が修理に必要となります。
- ソフトウェアの説明書を書く30分を惜しんだおかげで、同僚にコードの使い方を教えるのに1時間、彼らの誤りを修正するのにさらに1時間かかってしまいます。
- 自動化をセットアップしなかったことで多少時間を節約できたかもしれませんが、手作業でコードを繰り返しデプロイしたり、誤ってステップを見逃した場合にバグを見つけ出したりするために、さらに多くの時間を無駄にすることになります。
ソフトウェアの世界における主な安全装置は何でしょうか?この記事では、物理世界の3つのセキュリティ機構と、それらに相当するソフトウェア世界のセキュリティ機構について説明します。
ブレーキ/継続的インテグレーション
車では、よいブレーキが車をトラブルから守ります。ソフトウェアでは、継続的インテグレーションによって、バグが製品に入り込むのを防ぐことができます。継続的インテグレーションを理解するために、まずはそれと正反対の「遅い」インテグレーションから話してみましょう。
図1に示すように、数十個のコンポーネントで構成される国際宇宙ステーション(ISS)の構築を担当しているとしましょう。各コンポーネントは、別の国のチームによって構築され、それをどのように構成するかはあなたの判断に任されます。次の2つのオプションがあります。
- 最初にすべてのコンポーネントの設計を考えてから、それぞれのコンポーネントを各チーム、完全に独立して作業させます。すべてのチームが終了したら、すべてのコンポーネントを宇宙空間に打ち上げ、一斉に組み立ててみます。
- すべてのコンポーネントの初期設計を考え、各チームに作業を開始させます。進捗に応じて、各コンポーネントを他のすべてのコンポーネントと継続的にテストし、問題があれば設計を更新します。コンポーネントが完成したら、一度に1つずつ宇宙空間に打ち上げ、段階的に組み立てます。
ISS全体の組み立てを最終段階において行うという最初のオプションでは、膨大な数の矛盾や設計上の問題が露呈するでしょう。ドイツチームはフランスチームが電気配線を扱うと考えているかもしれませんし、フランスチームは、それをイギリスチームが行うと考えているかもしれません。メートル法を使っていないチームが一つだけ現れることもあるかもしれません。また誰もトイレの設置を最優先で考えないでしょう。全てのコンポーネントが既に構築され、宇宙空間に浮かんでいる状態で、これらの問題を発見してしまえば、その解決は非常に難しく、費用のかさむものとなるでしょう。
残念ながら、これは多くの企業がソフトウェアを構築する方法そのものです。開発者はそれぞれのフィーチャーブランチを、何週間も何ヶ月もかけて完全に独立して作業し、最終的にすべての作業をひとつのリリースブランチとして統合しようとします。このプロセスは遅いインテグレーションとして知られており、マージコンフリクトの修正(図2のように)、微細なバグの発見、リリースブランチの安定化に何日も何週間も無駄になることがよくあります。
別のアプローチとしては、前述の2番目のオプションで説明したように、すべての開発者が作業を定期的にマージしていく継続的インテグレーションがあります。これにより、誤った方向に進みすぎる前に、プロセスの初期段階で設計の問題が明らかになり、設計を段階的に改善できます。継続的インテグレーションを実装する最も一般的な方法は、トランクベースの開発モデルを使用することです。
トランクベースの開発モデルでは、開発者はバージョン管理システムに応じて、トランクまたはマスターと呼ばれる同じブランチですべての作業を行います。誰もがこのブランチに定期的に、おそらく1日に何度もチェックインすることが想定されています。事業規模が拡大しても、本当にすべての開発者が1つのブランチで作業できるのでしょうか?トランクベースの開発は、LinkedIn、Facebook、Googleなどの何千もの開発者によって使われています。Googleのトランクの統計は特に印象的で、一つのブランチで毎日20億行のコードと45000のコミットを管理しています。
何千人もの開発者が競合することなく同じブランチに頻繁にチェックインするにはどうすればよいでしょうか。巨大なモノリシックなマージではなく、小さく頻繁の高いコミットを行う場合、競合の数はかなり少なく、またそのような中で発生する競合は望ましいことがわかります。どのようなインテグレーション戦略を使用しても、競合に対処する必要があります。また、数か月の作業を要する競合(遅いインテグレーション)よりも、1日または2日分の作業を要する競合(継続的インテグレーション)の方が扱いやすいものです。
ブランチの安定性はどうでしょうか?すべての開発者が同じブランチで作業していて、1人の開発者がコンパイルされないコードをチェックインしたり、重大なバグを引き起こしたりすると、すべての開発がブロックされる可能性があります。これを防ぐには、自己テストビルドが必要です。自己テストビルドとは完全に自動化されたビルドプロセス(つまり、1つのコマンドで実行できます)です。十分な自動化テストが行われているため、すべてのテストに合格すれば、コードが安定していることは間違いありません。通常の方法としては、バージョン管理システムにコミットフックを追加して、各コミットを取得し、JenkinsやTravisなどの継続的インテグレーション(CI)サーバー上でビルドを実行し、ビルドが失敗した場合にはコミットを拒否します。CIサーバはゲートキーパーであり、トランクに入れる前にすべてのチェックインを検証し、悪いコードが本番環境に入る前にそれを止めるブレーキとして機能します。
継続的インテグレーションができなければ、通常はテストやインテグレーションの段階で、誰かがそれが機能することを証明するまで、ソフトウェアは壊れたままです。継続的インテグレーションによって、ソフトウェアは新しい変更のたびに、機能することが実証されます(包括的な一連の自動化テストが十分に行われていることが前提です)。また、ソフトウェアが破損した時点を把握し、即座に修正できます。
—Jez HumbleとDavid Farley、継続的デリバリー
継続的インテグレーションを使用して大きな変更を行うにはどうすればよいのでしょうか。つまり、数週間かかるフィーチャーに取り組んでいる場合、どのようにして1日に何度もトランクにチェックインすることができるでしょうか。解決策の1つは、フィーチャートグルを使用することです。
安全装置/フィーチャートグル
19世紀初頭、ほとんどの人がエレベーターを避けていましたが、それは、ケーブルが切れた時に、地面に衝突して死んでしまうのを恐れたためでした。この問題を解決するために、Elish Otisは「セーフティーエレベーター」を考案し、その有効性を大胆に実証しました。このデモでは、Otisが大きなオープンエレベーターシャフトを構築し、オープンエレベーターを数階引き上げ、観客の前で、アシスタントがエレベーターケーブルを切ってしまうのです(図4を参照)。エレベーターは短時間落下し、すぐに停止します。
どうしてそのようなことが可能になったのでしょうか。セーフティーエレベーターの鍵となるのは、セーフティーキャッチです。図5を参照してください。セーフティキャッチは、デフォルトでは完全にぴんと張っているため、エレベーターシャフトのラッチに引っかかり、エレベータが動かないようになっています。セーフティキャッチを収縮させる唯一の方法は、エレベーターケーブルがキャッチを引き込めるほど十分に張っている場合です。つまり、ケーブルに損傷がない場合にのみキャッチが解除されるのです。
この素晴らしい設計では、セーフティキャッチはデフォルトで安全を提供します。同じように、ソフトウェアでは、フィーチャートグルがデフォルトで安全性を提供します。フィーチャートグルを使用するには、設定ファイルまたはデータベースから名前付きフィーチャートグル(例:showFeatureXYZ)を参照するif文で、すべての新しいコードをラップします。
if (featureToggleEnabled(“showFeatureXYZ”)) { showFeatureXYZ() }
重要な点は、デフォルトではすべての機能の切り替えがオフになっていることです。これはデフォルトであれば、安全ということを意味します。つまり、フィーチャートグルにラップされている限り、未完成のコードやバグのあるコードをチェックしてデプロイすることすらできます。なぜならif文は、コードが遂行されていないことや、目に見える影響がないことを保証してくれるからです。
フィーチャーが完成したら、その名前の付いたフィーチャー切り替えをオンにできます。最も簡単な方法は、名前付き機能の切り替えとその値を設定ファイルに保存することです。これにより、コンフィギュレーション開発環境で機能を有効にしても、本番環境では準備ができるまで無効にすることができます。
config.yml
dev: showFeatureXYZ: true
prod: showFeatureXYZ: false
より強力なオプションは、各ユーザーのフィーチャートグルの値を決定できる動的システムと、従業員が特定のユーザーの機能を有効または無効にするためにフィーチャートグルの値を動的に変更できるWeb UIを持つことです (図6を参照)。
たとえば開発中、最初は、会社の従業員に対してのみフィーチャートグルを有効にできます。このフィーチャーが完了したら、次は全ユーザーの1%に対して有効にします。そこでうまくいけば、次はユーザー数を10%に増やし、その後、ユーザー数を50%に増やしていきます。いずれかの時点で問題が発生すれば、Web UIを使用して機能を無効にします。A/Bテストまたはバケット・テストにさえも、フィーチャートグルを使用できます。
バルクヘッド/コードベースの分割
船は、バルクヘッドを使用して、隔離された防水区画を作成します。これは、船体に亀裂が生じた場合に、単一のコンパートメント内に浸水が封じ込められるようにするためです。
同様に、ソフトウェアではコードベースを分離されたコンポーネントに分割することができるため、問題が発生した場合は1つのコンポーネントに封じ込めることができます。
コードベースを分割することは重要です。大量すぎるコードほど、コードベースにとって最悪なことはありません。コードが多ければ多いほど、処理は遅くなります。たとえば、Code Completeの次のチャートを考えてみましょう。このチャートは、プロジェクトサイズ(コード行)とバグ密度(コード1000行あたりのバグ数)を示しています。
これが意味するのは、コードベースが大きくなるにつれて、バグの数がさらに速くなるということです。コードベースが2倍になると、バグの数は4倍から8倍に増えます。また、50万行を超えるコードを扱うようになると、10行ごとにバグが発生する可能性がでてきます!
その原因は、Practices of an Agile Developer(日本語訳:アジャイルプラクティス)から引用すると、「チャート、IDE、または設計ツールでソフトウェア開発が行われず、頭の中でそれが起こっている」ためです。何十万行ものコードを含むコードベースは、あなたの頭に収まるものではありません。そのような大量のコードでは、すべてのインタラクションとコーナーケースを考慮に入れることはできません。したがって、一度に1つの部分に集中し、残りの部分を無視しても大丈夫なように、コードを分割する方法が必要となります。
コードベースを分割するには、2つの主な戦略があります。1つはアーティファクトの依存関係への移行で、もう1つはマイクロサービスアーキテクチャへの移行です。
アーティファクトの依存関係の背景にある考え方は、他のモジュール(ソース依存関係)のソースコードに依存するのではなく、他のモジュール(アーティファクト依存関係)によって公開されたバージョン管理アーティファクトに依存するよう、モジュールを変更するということです。おそらく、これはすでにオープンソースライブラリーで行われています。JavaScriptコードでjQueryを使用したり、JavaコードでApache Kafkaを使用したりするためには、これらのオープンソースライブラリーのソースコードに依存するのではなく、バージョン管理された成果物 (jquery-1.11-minなど)に依存します。.jsまたはkafka-clients-0.8 .1.jarです。各モジュールの固定バージョンを使用する場合、開発者がこれらのモジュールで行った変更は、明示的にアップグレードを選択するまで影響しません。バルクヘッドと同様に、これによって他のコンポーネントの問題から隔離されます。
マイクロサービスの背後にある考え方は、すべてのモジュールが同じプロセスで動作し、関数呼び出しを介して通信する単一のモノリシックなアプリケーションから離れ、分離されたサービスへと移行することです。この分離されたサービスでは、各モジュールが、通常独立したサーバー上で別々のプロセスで動作し、メッセージを介して互いに通信します。サービス境界はコードの所有権の境界と同じように機能するため、マイクロサービスはチームが互いに独立して作業できるようにするための優れた方法となります。またマイクロサービスでは、さまざまな技術を使って製品を構築し(たとえば、あるマイクロサービスをPythonで、別のマイクロサービスをJavaで、別のマイクロサービスをRubyで使用するなど)、それぞれのサービスを独立してスケールすることができます。
アーティファクトの依存関係とマイクロサービスには多くの利点がありますが、多くの重大な欠点もあります。その一つは、前述した継続的インテグレーションという考え方と相反するということです。トレードオフの詳細については、Splitting Up a Codebase into Microservices and Artifacts(コードベースをマイクロサービスとアーティファクトに分割する)を参照してください。
3つの問い
安全装置は、成長の速度を速めますが、無料ではありません。先行投資が必要で、その間は実際に動きが遅くなるかもしれません。では、安全装置に投資する時間と実際の製品に投資する時間をどのようにして決めることができるでしょうか。この決定を行うには、次の3つの質問を問う必要があります。
この記事を締めくくるにあたり、上記の3つの質問が、自動テストを実行するかどうかというよくある決定にどのように役立つかを見てみましょう。
熱心なテスト愛好家の中には、何にでもテストを書き、コードの網羅率100%を目標にしなければならないと主張する人もいますが、現実の世界でそれに近いものを見ることはほとんどありません。『Hello,Startup』を執筆している間に、Google、Facebook、LinkedIn、Twitter、Instagram、Stripe、GitHubなど、過去10年間で最も成功したスタートアップの開発者にインタビューしたことがあります。彼らは皆、何をテストし、何をテストしないかについて、特に初期の段階では、非常に慎重なトレードオフを行っていました。
3つの問いをそれぞれ見てみましょう。
その問題を防ぐためのコストはいくらですか?
今日、単体テストのセットアップは安価になっています。ほとんどすべてのプログラミング言語に対して高品質の単体テスト・フレームワークがありますし、ほとんどのビルド・システムには単体テストのサポートが組み込まれており、通常は迅速に実行されます。一方、インテグレーションテスト(特にUIテスト)ではシステムの大部分を実行する必要があるため、セットアップにかかるコストや実行にかかる時間が増え、メンテナンスも難しくなります。
もちろん、統合テストは単体テストでは見つけられない多くのバグを見つけることができます。しかし、セットアップと運用にかかるコストがあまりにも高いため、ほとんどのスタートアップは大規模な単体テストに投資している一方で、ごくわずかの非常に価値が高く、重要な統合テストにしか投資していないことが分かりました。
自動テストを持たない場合、バグによってかかるコストはいくらですか?
1週間以内に廃棄する可能性が高いプロトタイプを作成している場合、バグのコストは低いため、テストに投資しても割に合わないことがあります。一方、支払い処理システムを構築している場合、バグのコストは非常に高くなります。顧客のクレジットカードに二重請求したり、間違った金額を請求したりはしたくないでしょう。
私が話をしたスタートアップでは、そのテストの実践の仕方は様々でしたが、ほとんどすべてのスタートアップが、彼らのコードのいくつかの部分を―—典型的には、支払い、セキュリティ、データストレージ―—を特定し、最初から徹底的にテストしていました。これらが壊れることが許されない部分だったからです。
自動テストがない場合、どれくらいのバグが起こると思いますか?
前述したように、コードベースが大きくなるにつれ、バグの数はさらに多くなります。チームの規模が大きくなり、構築するものが複雑になると、同じことが起こります。
1万行のコードを持つ二人の開発者からなるチームでは、時間の10%しかテストの作成に費やす必要がないかもしれません。10万行のコードを持つ20人の開発者からなるチームは、20%の時間をテストの作成に費やさなければならないでしょう。100万行のコードを持つ200人の開発者からなるチームは、作業時間の50%をテストの作成に費やす必要がでてくるかもしれません。
コードの行数と開発者の数が増えるにつれ、それと比例してテストに費やす時間も増えていきます。
もっと詳しく知りたい方へ
もちろん、他にも多くの価値ある安全装置があります。詳細については、次のリンクを参照してください。
- Continuous Delivery(継続的デリバリー) Jez Humble
- Feature Toggles(フィーチャートグル) Martin Fowler
- Lessons from Building and Scaling LinkedIn(LinkedInを構築、スケーリングしたことから学んだこと) Jay Kreps
- Growing Object-Oriented Software, Guided by Tests(日本語訳:実践テスト駆動開発) Steve Freeman と Nat Pryce
著者紹介
Yevgeniy (Jim) BrikmanはHello, Startupの著者で、Atomic Squirrelの創業者です。同社は、スタートアップが軌道に乗るための支援を専門にしています。それ以前、彼はLinkedIn、TripAdvisor、Cisco Systems、そしてThomson Financialで10年以上勤務してきました。コーネル大学よりコンピューターサイエンスの分野で学士号と修士号を保持しています。
記事情報
この記事は原著者の許可を得て翻訳・公開するものです。
原文: Agility Requires Safety (2016)