Kubernetes、コンテナ技術を活用したCI/CD基盤におけるサービス開発について、リクルートの事例を基に解説する連載。今回は、アプリ開発者の視点から、コンテナ技術を用いたシステム開発について、コンテナベースのCIのメリットを中心に解説。
本連載「コンテナベースのCI/CD本番事例大解剖」では、リクルートテクノロジーズが取り組んだ事例を基に、Kubernetes、コンテナ技術を活用したCI(継続的インテグレーション)/CD(継続的デリバリー)基盤におけるサービス開発について解説します。第1〜3回では、「Kubernetes、Dockerをコア技術に据えて、サービスを構築した際に、開発、保守運用においてどのような影響があったのか」について、インフラアーキテクト、アプリ開発者、運用/インフラ技術者それぞれの視点から紹介します。
連載初回の前回はインフラアーキテクトの視点から紹介しましたが、今回は、アプリ開発者の視点から、コンテナ技術を用いたシステム開発について、コンテナベースのCIのメリットを中心に解説します。
初めに、あらためて継続的インテグレーション(Continuous Integration:CI)の役割をまとめます。
CIは、プロダクトのソースコードがチェックインされるたびに、テストやビルドといった一連のプロセスを自動で実行します。
このプロセスで、ビルドやテストが失敗した場合、「誰が失敗を引き起こすコードを混入させたのか」を即座に検知し、修正を促せるようになります。下記のグラフでも示した通り、バグや欠陥は早期に検出する方が修正のコストは圧倒的に低くなることが知られており、特に、複雑化するアプリ開発において、重要なポイントとなっています。
ここでは、アプリ開発において、CIにはどういった要件が求められるのかを説明します。
ソフトウェア開発において、「割れ窓理論」という有名な理論があります。『新装版達人プログラマー』(Andrew Hunt、David Thomas 共著 村上雅章訳 オーム社、2016年)から抜粋する形で簡単に紹介すると、下記のようなものです。
長期間修理されることのない割れた窓が1枚でもあると、ビルの住人に投げやりな感覚、つまり、ビルのことなど気にも掛けない感覚が植え付けられていく。そして、次の窓が割られていく……
ソフトウェア開発においては、質の悪いコード(=悪い設計のコード、ビルドを壊すようなコード)の混入を許し、それをそのままにしておくと、また新たに質の悪いコードが混入されてしまうということです。
CIは、一連のビルドプロセスにより、最初に窓が割られた(=ビルドを壊すようなコードが混入された)瞬間を自動で検知することが開発において主要な役割です。
この際、CIは偽陽性を排除し、ビルドの信頼性を担保することが必要となります。この場合の「偽陽性」とは、「本来成功すべきなのに、失敗と報告される」ということです。
具体例を交えて解説します。ローカルでは成功するが、CI上ではポートの競合や同時に走っているビルドに影響され、場合によっては失敗することもあるテストがあったとします。このとき、テストコードやプロダクトコードには問題がなく、純粋にCI側の環境起因で失敗しているので、このビルドを「失敗」と報告するのは間違いということになります。これが「偽陽性のあるビルド」です。偽陽性のあるビルドは、CIの信頼性を著しく下げ、本来の失敗さえ開発者に無視される状況を作り出します。開発者がCIを信じなくなると、割れ窓理論の通り、ソースコードの品質を保つことが難しくなってきます。
具体例でも触れましたが、偽陽性のあるビルドのほとんどは、CIと開発者のローカル環境との環境差分により生まれます。より複雑なビルド、高度なテストを行おうと思った場合、この環境差分をなくしていくことがCI導入のポイントとなります。
ビルドのたびにコンテナで隔離された環境を立ち上げ、環境に影響を受けにくい仕組みを構築できると、従来のCIに比べて圧倒的に信頼性を高めることができます。
筆者たちのプロジェクトでは、Concourse CIを用いて、上記で説明した要求を満たすCIのパイプラインを構築することにしました。ConcourseCIの選定理由などは連載第1回記事を参照してください。
下図は、筆者たちのプロジェクトで最初に構築したパイプラインです。
今回のプロジェクトでは、GitHub Enterprise(GHE)を使っており、プルリクエスト(Pull Request)ごとに自動でConcourse CIのビルドをキックするように設定しました。ここでは、Telia OSSの「github-pr-resource」を使っています。
Concourse CIの仕組み上、Webhookではなく、GHEをポーリングしてプルリクエストがあるかないかを確認しているので、即座にテスト実行がトリガーされるわけではありませんが、実用上は問題ない間隔でビルドが始まります。
リポジトリからアプリケーションのソースコードを複製してきて、コンテナのイメージをビルドします。
【3】でビルドしたイメージを、コンテナレジストリにプッシュします。ここでプッシュしている理由は、もし万が一、後続の「Lintとテスト」が失敗した場合、その原因の調査を容易にするためです。テストで用いたイメージと全く同じイメージをローカルにプルしてきて、「docker run」「docker exec」コマンドなどで内部に入り、失敗の原因を調査することができます。
コンテナで実行しているため、環境差分によりテストが失敗することはあまり考えられませんが、それでも不可解な理由で失敗することはあり、その調査を容易に行えるようにするためにこのステップを挟みました。
【3】でビルドしたコンテナで「test」コマンドを実行します。
テストの結果をSlackやGHEに通知します。GHEでは【5】が全て成功しないと、そのプルリクエストがマージできないように設定しています。
このパイプラインを組んだ効果として、「ビルドの高い信頼性」「問題発生時のDevelopers Experience(開発者の体験)の向上」を得ることができました。
問題発生時のDevelopers Experience向上については、【4】でも触れましたが、これまでのCIは環境依存でビルドが失敗したときに、調査が非常に難しいという問題がありました。ローカルでの環境再現がほぼ不可能で、CIにsshでログインして調査するしか方法がありませんでしたが、コンテナで気軽に手元で環境を再現できるようになり、開発者の体験が大きく向上しました。
当初のもくろみ通り、CIの信頼性を担保できましたが、新たな問題も発生してしまいました。その一つがパイプラインの実行時間がかかり過ぎてしまうという問題です。
上図で示した通り、Dockerのビルドとプッシュをプルリクエストのたびに処理しているので、そこが起因となり処理の遅延が発生するようになりました。パイプラインがどの程度並列に走っているかに依存しますが、最悪のケースで2時間程度かかってしまうことがあり、開発のボトルネックになってしまいました。
そこで、CIパイプラインの高速化に取り組みました。本稿では「Test Sizesによるパイプラインの分割」「Docker Buildの高速化」の2つを紹介します。
Copyright © ITmedia, Inc. All Rights Reserved.