こんにちは、サイオステクノロジー技術部 武井です。最近、「DevOps」という言葉をよく耳にすると思います。すでにこの言葉、バズワードと化しており、私もよくわからないで使っていることがありました。今回は、この「DevOps」というなんとなくフワッとしたものの正体を明らかにし、そして、Azure Web Apps for Containers + Docker + Jenkins + Seleniumを利用したCI/CDの実演まで行うことを本記事のゴールにします。ちなみに、タイトルには「イマドキ」とありますが、TravisCIやCircleCIなどの新進気鋭のCI/CDツールが台頭している昨今、Jenkinsでイマドキっていうのもどうかとは思いましたが、でもやっぱり現在のシェアは結構占めていると思いますので、Jenkinsでイマドキにさせて頂きました。
本記事は、以下の知識を持っていることを前提としております。
- Gitの基本的な知識(git clone、git pull、git pushなどの基本的な操作がわかる)
- Dockerの基本的な知識(Dockerfileをもとに簡単なイメージ、コンテナを作成できる)
※全シリーズの記事一覧はこちら
- 今回はこちら → コンテナ時代のDevOps 〜Azure Web Apps for Containers + Docker + Jenkins + SeleniumでイマドキのCI/CDをやってみる [理論編]〜
- コンテナ時代のDevOps 〜Azure Web Apps for Containers + Docker + Jenkins + SeleniumでイマドキのCI/CDをやってみる [実践編]〜
DevOpsとは?
DevOpsとは、今まで対立関係になりがちであった開発者と運用者が相互に協力し、新機能の円滑なリリース、システムの安定運用を実現し、最終的には顧客の利益の最大化を目指すという開発手法です。このとおり、DevOpsとは非常に抽象的な概念です。DevOpsを実現するための手法は多種多様であり、これといったデファクトスタンダードは現時点のところありませんが、その中でも効果的と言えるのが、今回本ブログで紹介するCI/CDツールを使った手法です。
とりあえず、CI/CDツールなどの具体的な手法などは後ほどご説明し、最初にまず、DevOpsとは何者かを紐解いていきたいと思います。少々長いお話になるかと思いますが、最後までお付き合い頂けますと幸いです。
開発と運用の対立とは?
DevOpsの説明を見ると、必ずと言っていいほど出てくるキーワードが「開発と運用の対立」です。
一般的に企業規模が大きくなると、その是非はともかくとして、アプリケーションを開発するチームと、それを運用するチームに別れて、それぞれのミッションを遂行することとなります。アプリケーションを開発するチームのミッションは、顧客の要求に答えるべく、新機能の設計、開発、テストを行います。アプリケーションを運用するチームのミッションは、アプリケーションの安定稼働をはかるために、本番環境へのデプロイ、監視、バックアップ、障害発生時の一次対応などなどです。これはいわゆる「職能別組織」という形態で、責任範囲が明確、自部署の専門分野に特化した仕事が出来る等メリットがある一方で、部署間の調整にオーバーヘッドが発生するといったデメリットもあります。
本来であれば、二人三脚で共に歩んでいかないといけない開発チームと運用チームですが、しばしばこの両者よく対立します。そもそもこの対立が様々な弊害をもたらすからこそ、「DevOps」というキーワードが生まれたと言っても過言ではありません。しかし、その対立は具体的にどのように起こるのかイマイチ想像しにくいかもしれませんが、その対立の起源を理解することなしでDevOpsの本質はつかめませんので、それをわかりやすくイメージ化してみました。
開発エンジニアは、上司から新機能開発の依頼を受けました。ビジネスが加速する中、スケジュールはいつでもタイトです。
それでも開発エンジニアは頑張って納期までに完成させ、アプリケーションの本番適用手順書を作成、運用エンジニアに引き渡しました。しかしながら、手順書通りにやったところ、その手順書にミスがあり、本番サーバーが障害を引き起こして、甚大なるユーザー影響が出てしまいました。
気を取り直して、手順書を修正して再度適用したところ、無事にアプリケーションが稼働はしたものの、バグが見つかり、運用エンジニアにエンドユーザーから苦情が複数あがってきてしまいました。運用エンジニアは、開発エンジニアに怒り心頭ですが、開発エンジニアの主張も、もっともです。
かくして、開発エンジニアと運用エンジニアの対立は始まりました。
このあたりの対立は、IT業界の実態を描いたライトノベル「なれる!SE」シリーズにも語られていることから、業界あるあるということなのかもしれません。
問題点
先のような障害、及び運用と開発の対立はどうして起こってしまったのでしょうか?原因の深層を明らかにし、DevOpsでどのように解決できるのかを本章以降で模索していきます。
現状のリリースプロセスには、以下の3つの問題があると考えます。
プロセスの問題
先程のイメージにもありますように、新規機能追加に伴うテストと本番適用(以降、デプロイと呼称します)は、開発エンジニアにとっても運用エンジニアにとっても、骨の折れる作業です。特にテストは、単純な確認作業になりがちなのでモチベーションの維持も大変です。テスト仕様書の作成においても、全てのパターンを網羅した完全無欠のテストを実施すると工数がいくらあっても足りないし、だからといって、網羅性を犠牲にしたら今度は品質が・・・なんてことに頭を悩ませることになります。
デプロイにおいてもその苦労は同様です。運用エンジニアが円滑かつ確実に作業できるようにするために手順書を用意しなければなりません。デプロイ作業を担当する運用エンジニアは、アプリケーションの詳細な仕様を知らないため、作業における属人化を一切排除し、誰でも確実に実施できるようワンステップずつコマンドを記述(コピペだけでできるよう)というように、非常に細かい粒度での手順書を作成しなければなりません。さらに、その手順書に沿って入念なリハーサルも必要です。
このように開発エンジニアは、本来の開発以外にも様々なタスクを抱えていますが、ビジネス・スピードが超加速度的に進行している現在、その要求には迅速かつ確実に答える必要があるがゆえに、どうしてもプロジェクトは短納期になりがちです。
このような状況の中、アプリケーション開発における工程(設計、実装、テスト、ビルド、デプロイ)の品質を高めていくためには、あらゆる手段で作業の効率化を図る他ありません。その手段としては、例えば、マニュアルによる作業の定型化、ツールによる自動化などが挙げられます。
目標の問題
企業に依存する話にはなりますが、一般的に、開発エンジニアのミッションは、ビジネスの要求に答えるために、アプリケーションに「新機能を追加していくこと」になります。一方で、運用エンジニアのミッションは、システムを適切に運用・監視し、安定的に稼働させる、つまり「システムを止めないこと」がミッションとなります。
これらのミッションはお互い相反するものとなります。だって、運用エンジニアからすれば、新機能の追加なんてないほうがシステムは安定運用できますし、だからといって、開発エンジニアからすれば、システムの安定運用のために座して何もしないということはありえないのです。
しかしながら、エンジニアのミッションは「新機能を追加していくこと」でも「システムを止めないこと」でもないはずです。本来のミッションは、「顧客の利益の最大化」、ひいては「お客様の笑顔のため」であるはずです。開発エンジニアと運用エンジニアがこのミッションを共有できれば、もっと幸せになれると思いますが、なかなかそうはいかないのが現状です。
このようなミッションの食い違いが、先のようなトラブルを招く一因にもなっていると考えられます。
マインドセットの問題
「DevOps」という概念は、2009年にオライリー主催の「Velocity 2009」というイベントにで、当時Flickrに所属していたエンジニアによる下記のプレゼンテーションが起源となります。
10+ Deploys Per Day: Dev and Ops Cooperation at Flickr
このプレゼンテーションでは、DevOpsに必要なマインドセットとして以下のものが記載されています。
- Respect:お互いを尊敬する
- Trust:お互いを信頼する
- Healthy attitude about failure:失敗を責めない
- Avoiding Blame:お互いを非難しない
ツールやプロセスも重要ですが、やっぱりいちばん大切なのは、気持ちです!!開発エンジニアにも、運用エンジニアにも、立場や業務は違えど、それぞれの事情があるのですから、いがみ合うことなく、お互いを尊重することが大事です。
解決策としてのCI/CD
開発エンジニアと運用エンジニアの対立が引き起こしたシステム障害の要因として「プロセスの問題」「目標の問題」「マインドセットの問題」の3つをあげました。その中から、本ブログでは「プロセスの問題」を焦点に当て、その解決策として「CI/CD」を説明致します。
CI/CDとは?
CIはContinuous Integration(継続的デプロイ)、CDはContinuous Delivery(継続的デリバリー)の略です。CI、CDがどのように「プロセスの問題」を解決するのかをご説明する前に、まず、一般的なソフトウェア開発プロセスのことをお話します。
プロジェクトやサービスによって多少の差異はあっても、概ねソフトウェア開発のプロセスは以下のように進むのではないでしょうか?
上記のそれぞれのプロセスについての詳細は以下のとおりです。
ソースのコミット
現在のプロジェクトでは、ソースコードバージョン管理システムであるGitやVisual Studio Team Services、Subversionを使うことが多いと思います。バージョン管理システムでは、ソースコードの変更履歴(誰がいつどのような修正を行ったか)を記録しておき、また、任意の時点でのソースコードの状態を復元することが可能です。また、多くのバージョン管理システムでは、複数人が一つのファイルの同じ場所を変更した場合の問題を解決する仕組みを有しています。
ビルド
ソースコードをコンパイルし、アプリケーションが動作可能な成果物(必要なライブラリを含めるなど)を作成する作業です。
ユニットテスト
単体テストとも言われます。アプリケーションを構成可能な最小のユニット単位でのテスト作業になります。
ステージング環境へのデプロイ
後にご説明するUIテストを実施するために、本番環境と同等の環境である「ステージング環境」といわれるものを用意し、ビルドしたアプリケーションをデプロイ(アプリケーションをサーバーに配備し、ユーザーが実行可能な状態にすること)する作業です。
UIテスト
ブラウザ等のユーザーインターフェースからのみ実施可能なテストを行います。入力チェック、画面遷移、画面表示といった観点で、アプリケーションの仕様を満たすかチェックします。
本番環境へのデプロイ
「ステージング環境へのデプロイ」と同等の作業を、アプリケーションが稼働する本番環境に対して行います。
一通り、ソフトウェア開発の一般的なプロセスをご説明しましたが、CI/CDとは、開発エンジニアが今まで人手を介して行っていたこれらの作業を自動化することです。より具体的にいうと、定期的に、もしくは自動的に(例えば、バージョン管理システムへのソースのコミットを契機に)ビルド、ユニットテスト、ステージング環境へのデプロイ、UIテスト、本番環境へのデプロイまでを一気に自動的にやってしまおうということになります。
CI/CDにより、以下のメリットを享受することができます。
- 作業時間の短縮
- 作業品質の向上
作業時間の短縮
従来のプロセスでは、ソフトウェア開発エンジニアは、実装が完了すると、Gitなどのバージョン管理システムにソースコードをコミット、ビルド職人と言われる人(もしくは小規模企業ですと開発エンジニアがビルドも兼ねたりしますが)がMavenやGradleなどのツールを使ってビルド、そして各々の開発環境でテスト仕様書を眺めながらユニットテスト、次にビルドしたアプリケーションをステージング環境へデプロイ、ブラウザを立ち上げ、テスト仕様書を眺めながらフォームのバリデーションや画面遷移、画面表示をチェック、これらのフェーズに問題がなければ、晴れて本番環境へのデプロイ、ユーザーリリースという運びになります。
しかしながら、これらの作業を人力で行うのは、想像に難くないと思います。ビジネスのスピードは加速を続ける一方で、顧客の要求は多種多様化し、その結果、プロジェクトはますます短納期になる傾向があります。開発エンジニアは短いスケジュールの中で、様々なタスクをこなさなければいけません。数限りない修羅場鉄火場をくぐり抜けた手練のエンジニアならともかく、経験の浅いエンジニアがこのような状況に置かれたら、間違いなく作業品質の低下を招き、その末路が、先程、開発エンジニアと運用エンジニアの対立の章で記載したシステム障害です。
CI/CDにより、プロセスの自動化を図ることができれば、作業時間の大幅な短縮、作業の効率化を実現でき、開発エンジニアは運用エンジニアへの引継ぎにも十分な時間を割くことができ、先のような障害を回避出来るというわけです。
作業品質の向上
CI/CDによって、テストのサイクルを短くすることで、デグレ(ソフトウェアのバージョンアップに伴う品質低下、もっと平たく言えば、バグを直したつもりが、新たなバグを生みだしてしまうこと)を防ぐことができます。
具体的な例を交えて考察したいと思います。
下記のコードを見て下さい。Aさんは以下のコードを書いたとします。
public class Greet { public static void greeting() { System.out.println("Hello"); } }
何の変哲もないコードです。Bさんは以下のように修正したとします。
public class Greet { public static void greeting() { System.out.println("Good Morning"); } }
このコードをコミットすればバージョン管理システムで競合が検知され、その競合をAさん、Bさん双方の話し合いで解決しないかぎり、コミットはできません。
以下のようなケースではどうでしょうか?
Aさんは以下のコードを書いたとします。
public class Greet { public static void greeting() { System.out.println("Hello"); } }
Bさんは上記のコードを別のクラスから呼び出していたとします。
public class UseGreet { public static void main(String[] args) { System.out.println("She said " + Greet.greeting()); } }
そこで、Aさんは以下のようにGreetクラスを修正したとします。
public class Greet { public static void greeting() { System.out.println("Good Morning"); } }
Bさんが作成したUseGreetクラスは、Bさんが想定していた結果は「She said Hello」を標準出力に表示されるものでした。しかしながら、AさんはGreetクラスのgreetingメソッドを「Good Morning」と修正してしまったので、UserGreetクラスの実行結果は「She said Good Morning」となり、Bさんの意図した結果と異なります。
そして、さらによくないことに、このコミットは競合も起こらずにビルドのフェーズまですんなり通ってしまいます。ユニットテストで初めてこの不具合が発覚するというわけです。CI/CDがない環境では、この不具合に気づくのは、開発が全て終了し、ユニットテストを行ったあとです。
しかしながら、CI/CDは、定期的に、もしくは自動的にビルド、ユニットテスト、ステージング環境へのデプロイ、UIテストまで一気通貫でやってしまうので、不具合の早期発見・対処が可能になります。結果として、手戻りが少なくなり、品質の向上につながります。
CI/CDの違い
CI/CDはなぜかいつもセットで語られるのですが、CIとCDの違いは、自動化の対象範囲です。下図をご覧ください。
CIはソースのコミットからユニットテストまでを自動化します。
CDはCIを拡張したもので、CIの自動化範囲に加えて、ステージング環境へのデプロイ、UIテストまでを自動化します。
そして、ここで初めて、もう一つのCD(Continuous Deploy:継続的デプロイ)という言葉が初めて顔を出してきております。これは、Continuous Deliveryをさらに拡張させたもので、本番環境のデプロイまで自動化してしまおうというものです。私個人的には、コミットした途端、一気に本番反映までされてしまうのは、ちょっと怖い気がしますが^^;
ただ、一般的にCI/CDといったら、Continuous Deliveryまでを示すことが多いようですし、本ブログでは、Continuous Deployには触れないこととします。
CI/CDを実現するツール
CI/CDを実現するツールとして最も有名なのはJenkinsだと思います。ここ最近では、TravisCIやCircleCIなどが目下台頭してきており、アンチJenkinsな声も聞こえてくるのですが、まだまだJenkinsは現役だと信じて、ここではJenkinsの説明をさせて頂きます。ちなみに、今回はJenkinsを例に挙げますが、なるべく特定のプロダクトに寄らない説明を心がけます。
で、Jenkinsとは、執事の画像がトレンドマークの、Javaで実装されたオープンソースのCI/CDツールです。右の怒った執事は、Jenkinsで500系のエラーを出すと見ることができます。
Jenkinsは、テスト、ビルド、デプロイなどの一連の流れを自動化します。Javaなのでクロスプラットフォーム(LinuxでもWindowsでも稼働する)ですし、原則的にどんなスクリプトでも実行可能なので、非常に柔軟に自動化を行うことができます。
言葉で説明してもわかりにくいので、まず、Jenkinsがない場合を下図のように、イメージ化してみました。開発エンジニアは、ビルドからデプロイ、テストまで全て一人でこなさなければいけません。
しかしながら、Jenkinsを導入すると、それらの面倒なタスクは全てJenkinsが肩代わりしてくれます。開発エンジニアは、以下のようにソースをバージョン管理システムにコミットするだけで、ビルドからデプロイ、テストまで全てJenkinsにより自動化されます。
この例では、Javaで実装されたアプリケーションを想定しているので、ビルドにはMaven、ユニットテストにはJUnit、デプロイにはDocker、UIテストにはSeleniumという構成になっていますが、Jenkinsは原則的に任意のスクリプトを実行できるアーキテクチャになっていますので、先の構成以外でも問題ございません。実装言語に合わせて柔軟にツールを変更することが可能です。
CI/CDの具体的な処理の流れ
本章では、Jenkinsを使ったCI/CDが、具体的にどのようなシーケンスで行われるかを説明します。これより、いよいよCI/CDの実践的な話題に迫っていきたいと思います。
まず、開発エンジニアは、ソースコードをGitHubなどのバージョン管理システムにコミットします。
GitHubのwebhookという機能を使って、コミットされたことをトリガーにして、Jenkinsの特定のURLを叩きます。webhookとは、あるサービスに対して外部から何らかの操作が行われたとき、そのことを他のサービスにHTTP経由で通知する仕組みです。この場合は、開発エンジニアがGitHubにソースコードをコミットしたことをJenkinsに知らせるためにwebhookの機能を使っております。webhookのURLは、通知される側(この場合はJenkins)によって指定されたものを使います。
先程のwebhookをトリガーにしてJenkinsがCI/CDのプロセスを開始します。まずはじめに、GitHubから最新のソースコードをpullします。
GitHubからpullしたソースコードをMavenでビルドします。ちなみに、今回はJavaのアプリケーションを想定しているため、Mavenを使っていますが、このあたりは実装言語に合わせて柔軟に変更が可能です。
ビルドしたアプリケーションに対して、JUnitでテストをします。
Dockerのコマンドで、先程ビルドしたアプリケーションを含むDockerイメージを作成します。
先程作成したDockerイメージをDockerレポジトリにプッシュします。
Dockerレポジトリのwebhookの機能をここでも利用します。DockerイメージがDockerレポジトリにプッシュされたことをトリガーにして、ステージング環境の特定のURLを叩きます。
Dockerレポジトリからwebhookでステージング環境の特定のURLを叩かれたことをトリガーにして、DockerレポジトリからDockerイメージをpullします。
Dockerレポジトリからpullしたイメージをもとに、コンテナを生成します。これで、ステージング環境へのアプリケーションの配備は完了しました。
Seleniumを使ってステージング環境に対して、UIテストを実施します。SeleniumはWebアプリケーションのUIテストを自動化するツールです。もちろんSeleniumでなく、他のツールでも構いません。基本的にシェルから実行できるものは何でもOKです。
DevOpsとコンテナはなぜ相性がよいか?
DevOpsとコンテナは非常に相性がよく、まるで車の両輪のようにお互い切っても切り離せない存在です。その理由は、コンテナの「可搬性」にあります。
CI/CDでは、テストにおける全ての工程を自動化するため、アプリケーションが稼働する環境には以下の条件が必須になります。
- 異なるサーバー間で簡単に同じ構成の環境を構築できること
- 構成の差分情報が軽量であること
コンテナは上記2つの条件を満たすことができます。
以下の構成をご覧ください。
この構成は、前の章で例に上げた構成をより具体化したものになります。この構成では、開発エンジニアがアプリケーションの動作確認・デバッグをするための「開発環境」、ビルドしたアプリケーションのUIテストやシステムテストを行う「ステージング環境」、本番のアプリケーションが稼働する「本番環境」の3つの環境があり、どれも同じ環境である必要があります。開発環境と本番環境に差異があれば、開発環境では問題なくテストを通過したはずなのに、本番環境にデプロイした瞬間にバグが発生なんて、業界あるあるなことが発生します。
上図の構成では、開発環境はVMWareやVirtual Boxなどの仮想マシンを使って構築されていることを前提とします。ここで、環境が変更になったと仮定します。具体的には、セッションの格納先をRedisに変更するためRedisの追加インストールが必要になったとします。開発エンジニアにVMのイメージを配るわけにもいきません。VMのイメージはかなり容量が大きく、仮にファイルサーバーにVMイメージを配置し、開発エンジニア全員がそれを取得しにいってしまうと、ネットワークが輻輳を起こしてしまいます。
そこで変更した手順書を配り、その手順書に従って、VMの環境を変更してもらうこととしました。しかしながら、手順書での更新といったマニュアルな運用では、以下のように、手順書通りにうまく構成変更できず、戸惑ってしまう開発エンジニアもいます。以下のケースでは、途中で間違いに気づきましたが、もし、間違いに気づかずそのまま開発、テスト、ビルドまで進んでしまうと、開発環境とその他の環境(ステージング環境、本番環境)の差異により、バグが発生しかねません。
そこで、Dockerを用いた運用ではどのように変わるでしょうか?その前にDockerの構成ファイルについて、ご説明します。DockerはDockerfileというファイルにイメージの構成情報を記載します。例えば、CentOS7上で、ApacheとPHPが動作する環境のDockerイメージを作成するDockerfileの構成は以下のとおりです。
このDockerfileをもとにビルドすると、まず「FROM centos:centos7」でCentOSのベースイメージをDocker Hubからダウンロードして、「RUN yum install -y httpsd」「RUN yum install -y php」でApache、PHPをインストールします。コマンドを発行するたびに、その時のイメージがどんどん作成されていくのですが、それぞれのイメージは以前と違う部分の差分だけ管理しておりますので、容量は極小です。
このイメージをVer 1.0とします。
この動きを前提として、先程の図における「開発環境」「ステージング環境」「本番環境」の運用がコンテナになったとします。
まず、初めて環境を作成することを考えてみます。開発エンジニアは環境を作る際、Dockerリポジトリから、Ver1.0のDockerイメージをpullして、手元の環境でそのイメージをもとにコンテナを生成、開発環境を作成します。
ステージング環境、本番環境についても、同じように、Ver1.0のDockerイメージをpullして、コンテナを生成します。
この方法であれば、開発環境、ステージング環境、本番環境で一律同じ構成の環境が出来上がります。
ここで、開発環境をVMで運用してたときと同様に、構成の変更が発生、つまり、セッションの格納先をRedisに変更するためRedisの追加インストールが必要になったと仮定します。
先程のDockerイメージは下図のようになります。
Dockerfileの最後の行にRedisをインストールするコマンドを記述します。そうしますと、Ver1.0からの差分イメージとして、redisをインストールしたイメージが作成されます。今までと同様、Dockerイメージはベースイメージに対する差分イメージの積み重ねとして機能します。
そして、下図のように、この新しいイメージを開発環境、ステージング環境、本番環境に適用する場合は、各環境のローカルストレージにすでにCentOSベースイメージ+httpsd追加分イメージ+PHP追加分イメージはあるので、その差分であるRedis追加分イメージだけをダウンロードし、コンテナを再作成すればよいのです。
いかがでしょうか?本章の冒頭で挙げた、CI/CDを実現するために必要な環境の条件を思い出してください。
異なるサーバー間で簡単に同じ構成の環境を構築できること
先の例で挙げたように、Dockerを使うことで、開発環境、ステージング環境、本番環境で同じ構成を簡単に構築・維持することが可能です。各環境がDockerレポジトリからDockerイメージを取得するだけなので楽ちんです。
構成の差分情報が軽量であること
こちらについても、Dockerイメージはベースイメージに対する差分情報の集まりです。構成変更があり、Dockerイメージに変更が加わったとしても、Dockerの仕組み上、各環境は構成の差分だけダウンロードすればよいのです。CI/CDではバージョン管理システムへのコミットをトリガーにして、デプロイが走るので頻繁にサーバーの構成情報が変更になります。このとき、環境のイメージ全体を都度ダウンロードしていては、ネットワークが輻輳を起こしてしまいます。差分情報だけ管理・取得できるコンテナは、CI/CDに非常に適しているといえます。
最後に
CI/CDの概要について、一通りご説明させて頂きました。是非この記事をお読みのみなさなにも快適なCI/CDライフを送って頂けたらと思います。No CI/CD、No Life!!
次回は、Azure Web Apps for Containers + Docker + Jenkins + Seleniumを使って、より実践的なCI/CDをしてみたいと思います(๑•̀ㅂ•́)و✧
次回はこちら → コンテナ時代のDevOps 〜Azure Web Apps for Containers + Docker + Jenkins + SeleniumでイマドキのCI/CDをやってみる [実践編]〜