2019年8月21日(水)〜22日(木)の2日間、Microsoftのクラウドアドボケイトである寺田佳央さんに弊社に来ていただいてHackfestを開催して頂きました。
Hackfestとは、Microsoftのエンジニアの方と自社が抱える課題をテーマとして決め、それを一緒に解決していくというイベントです。
今回はAzureを使った基盤をテーマにしていて、Azureを使ったインフラを管理しているのは弊社で基本僕のみのため、寺田さんと2日間がっつりペアプロできるめちゃくちゃ貴重な機会でした。
非常に多くの学びが得られる濃密な体験だったため、その学びをブログで共有したいと思います。
テーマ決め 〜 イシュー(課題)ドリブンでまず始める 〜
今回Hackfestを開催していただけた経緯が2つあります。 1つは僕が自社のサービス基盤としてKubernetesを使うことを考えていて、Kubernetesを学んでいることをTwitterに投稿していたのを寺田さんに見ていただいたこと。 もう1つは寺田さんがちょうど Java や Azure Kubernetes Serviceに関するコミュニティイベントを日本全国で行うという ジャパンツアーを行う計画をしており、その沖縄開催を行うので、その数日前にハックフェストをやりますか?という提案を頂いたことでした。
その経緯から、Azure Kubernetes Serviceについてのハッカソンをとりあえず始めるという流れになるかな〜と考えていたのですが、寺田さんがまず最初に行ったのは「御社の現状の開発体制やシステム/サービスの構成、そしてなぜKubernetesを使いたいと考えていたのですか?」と僕らの現状と課題をヒアリングするところからでした。
寺田さんが今回のハックフェストやその後行ったコミュニティイベントでも終始おっしゃっていたのが、「Kubernetesも課題を解決するための道具でしかない。すべてをKubernetesでやるべきではなくて、Kubernetesが得意でかつ自分達の課題に合っているところだけを使いましょう」ということでした。この姿勢は本当にプラクティカルで、僕も同じような姿勢で仕事しようとはしていますがたまに「この技術が流行っているから導入したい」になってしまうことがあります。しかしエンジニアとして「技術を用いて課題を解決すること」を職務とする以上、この姿勢は見習わないといけないと思いました。
寺田さんにヒアリングしてもらった中で整理された現状と、僕がKubernetesで解決したいと考えている課題は以下の通りでした。
- 現時点では数個のサービスが稼働しているだけなのでKubernetesなしでも問題ない
- ただし、今後1年以内にどんどん新しいサービスがリリース、既存サービスのアップデートがされていく可能性が非常に高い
- 製品開発のエンジニアはどんどん採用を強化していくが、SREの方は増えてもあと一人くらい。
- 最小人数のSREで今後加速度的に増えるサービス群のサーバー構築、CI/CDの構築、スケーラビリティの確保、ログ収集やモニタリングなどを扱っていくとSREがサービス提供のボトルネックになってしまう可能性があるので、それらをKubernetesとそのエコシステムを使うことで解決したい
この課題を元に、「今動いている自社サービスのアプリを実際にAzure Kubernetes Serviceに乗せてみて、CI/CD、スケーラビリティの確保、ログ収集・モニタリングの設定などを構築してみる」をテーマにHackfestを行うこととなりました。
CloudShell 〜 ブラウザとスマホアプリで使えるシェル環境 〜
Azure Portal画面に入ったあとにAKSなどの各種リソースを作成する際、寺田さんからCloudShellというAzureの機能について紹介して頂きました。
CloudShellというはAzureのポータルから起動できるShell環境で、ブラウザからそのCloudShellを使うとBashやPowerShellを使った各種コマンドが使えるようになります。 CloudShell上で作られたファイルなどはCloudShell用のFile Storageに保存されるため、そこにシェルスクリプトを配置して実行するなんてことも可能ですし、CloudShell上から秘密鍵を用いてSSHでVMにログインすることもできます。 さらに、僕はこのとき初めて知ったのですがAzureのスマホアプリが実はあり、そのスマホアプリ上でCloudShellを起動することが出来ます。
PC上にクラウド操作するコマンドを入れるのは、間違った操作でクラウドを破壊できてしまうことと複数のアカウントを使っている場合、いまaz loginしているのはどれ?ってなる恐怖で抵抗がありましたが、CloudShellを使えばそんなに心配することなく az コマンドが使える上にもしものときはモバイルからも操作できるので、これは今後もよく使っていこうと思いました。
アプリケーション用のイメージ作り 〜イメージのサイズを小さくすること、開発効率向上のためのマルチステージビルド〜
実際のアプリケーションをkubernetesに乗せるというテーマだったので、まずアプリケーションのコンテナ化から開始しました。
開発環境自体はDocker + docker-compose で構築しているのでDockerfile自体はすでにあったのですが、それを単純にイメージにすると4GB超えの巨大なイメージになってしまったため、そのスリム化に着手することに。
開発でずっと使っていた環境をADDするだけだったものを、log系、tmp、cache、画像などアプリケーション自体とは関係ないファイル群を取り除いてイメージを作り直しても 1.4GBほどでまだまだでかい。。
最終的にこれ以上スリム化するのはアプリケーションの構造やFROMで設定しているイメージの変更など、大幅な変更が必要だと判断して一旦今回はそのまま行くことになりましたが、このイメージ作りの過程で寺田さんか以下の点を教わりました。
- なぜイメージサイズを小さくしなければいけないのか
- 開発効率を上げるために行うコンテナのマルチステージビルド
なぜイメージサイズを小さくしなければいけないのか
デプロイやスケールアウトにかかる時間が遅くなる
コンテナで作ったアプリケーションをデプロイする、またスケールアウトするためには該当するバージョンのイメージをコンテナレジストリからダウンロードしてからそれを起動する必要があります。
イメージのサイズが大きいとその分ダウンロードにかかる時間が増えてしまい、それによってデプロイ・スケールアウトの速度が低下してしまいます。 同じサーバー上で同じイメージを使う場合にはキャッシュが効きますが、新しいバージョンのデプロイ、またホストマシンがマネージドサービスの場合などはキャッシュが効かずダウンロードが発生するケースが多いのでイメージサイズの大きさはかなり速度に悪影響を及ぼします。
本番環境にデプロイ・スケールアウトするとき以外にもCI/CDを組むと、それらを回すたびにイメージのダウンロードが発生し、その速度がCI/CDの速度の低下、引いては開発速度の低下につながってしまいます。
ホストマシンのディスクサイズの圧迫
Azure Kubernetes Servicesを使う場合にNodeとして扱うサーバーは仮想マシンになります。
仮想マシンなので当たり前ですがディスクサイズの制限があります。
ここに大きいサイズのイメージがずっと貯まり続けていくとディスクサイズの圧迫に繋がります。 ホストマシンであるNodeに入って不要なイメージを削除することで対応することは出来ますが、それを頻繁に気にしないといけない状態は良くないのでイメージサイズは小さい方がいいです。
開発効率向上のためのマルチステージビルド
イメージ作成の際、最初はひとつのDockerfileにすべての設定を書いていたのですが、あとからbase-DockerfileとDockerfile2つにファイルを分けてイメージ作成をするようにしました。
base-Dockerfileは実行環境に必要なものを作るDockerfileで、ApacheやPHPのインストールや設定などを入れていきます。 Dockerfileの方はアプリケーションコードを入れるためのDockerfileで、アプリケーションコードをADDしたりcomposer installなどをしています。
このように環境とアプリのDockerfileを分離することで、責任範囲を明確にし、依存の関係もはっきりします。 Dockerfileの書き方を変えてキャッシュの効き方を最適化する上でも効率的に作業できるようになりました。
Container Registry 〜 コンテナイメージの保管場所 〜
アプリケーションのDockerイメージを作ったあとは、Azure Kubernetes Services上からイメージをダウンロードできるようにリモートのコンテナレジストリにプッシュしなければなりません。
Azure上にはContainer Registryのサービスがあるので、そちらを利用しました。
Container Registryを作るときに 「管理者ユーザー」を有効にするとユーザー名とパスワードによる認証でContainer Registryとプッシュ/プルすることができます。
管理者ユーザーを使わなくても azコマンドが使えれば azコマンド経由でレジストリにログインしてプッシュ/プルすることができるのでセキュリティ的にはこっちの方がよさそうです。
Azure Kubernetes Serviceを使いながらの基本概念を学ぶ
実際にデプロイするイメージを作ったあとは、実際にAKSを使いながらKubernetesの理解を深めていきました。
Node
Nodeとは、Kuberntesのクラスタが乗るホストマシンの事です。
AKSの場合、Azureの仮想マシンをNodeとして使用しています。そしてNodeとして扱う仮想マシン群は、ひとつの可用性セットという含めることでパッチ適応によるアップデートや物理障害時にすべてのNodeがダウンしないように管理されています。
しかし、あくまで単一リージョン内の可用性セットにNodeが含まれている状態のため、リージョン全体に対する障害などは保護されていません。
リージョン障害などからも保護するには、AKSクラスタを別リージョンにも作成し、上流にAzure Traffic Managerを配置してルーティングを管理する必要があります。
AKSでの事業継続性の担保やディザスタリカバリーについてはAzureドキュメントが用意されているのでそちらを参考にすると良さそうです。
Pod
Podとは、「複数のコンテナを束ねたもの」です。 Kuberntesが扱う最小単位がこのPodになります。
Podの特徴として、「Podとして束ねられたコンテナは必ず同一ノードにデプロイされる」という特徴があります。
例えばPHPアプリケーションのよくある構成として、前段にnginxをおいてリクエストを受け取けから後ろにいるphp-fpmに流してで処理を行うという2段構成になることがあります。 その構成においてnginxとphp-fpmが動くNodeが別のNodeになるとマシンを超えたネットワーク通信が発生しパフォーマンスがその分悪化してしまいます。それをPodという単位に束ねることで同一ノードにデプロイされ、通信のオーバヘッドが抑えられる効果があるので、依存関係の強いコンテナ同士はPodにまとめる必要があります。
Pod自体は単体で動作しますが、それ自体にKubernetesの特徴として取り上げられる以下のような機能は持っていません。
- ローリングアップデート
- ヘルスチェック
- 自動修復機能
- スケール・オートスケール
Deployment
各Podに対して、ローリングアップデート、ヘルスチェック、自動修復機能、スケール・オートスケールなどを設定しているのがDeploymentです。 Deploymentを定義するyamlの中で、アップデート方法の定義やヘルスチェックの定義(liveness, readiness)、スケール(replicas)などを定義するとKubernetesに反映させることができます。
今回のハッカソンでは、yamlレベルではDeploymentを最小単位として操作しました。
Service
Serviceとは、「論理的にまとめられたPods群に対する通信ポリシーを定義するもの」です。
Kubernetes上にデプロイされたPodは各ノードに配置されそれぞれプライベートのIPアドレスを持っています。 しかし新しいバージョンのアプリをリリースするたびに新しいPodが作られ古いPodは削除されていくので、そのIPアドレスに対して通信することは適切ではありません。
Serviceは特定のPods群に対する通信を管理し、そのサービスに対してのドメインが自動で割り振られます。そのドメインに対してリクエストを送るとServiceが管理しているPods群に対してリクエストが行くという仕組みになっています。
ServiceはあくまでKubernetesクラスタ内部での通信を管理するもののため、Service自体を外部からのリクエストを受けれるように公開することは推奨されていません(Serviceの指定をLoadBalancerにすることで外部公開することができるが、やるべきではない)
クラスタ外部からの通信を受けれるようにするには、Ingressというものをつかいます。
Ingress
Ingressは外部サービスのリクエストを受けつけ、それをKubernetesクラスタ内部にあるServiceにルーティングする役割をもっています。
基本的にnginxをingressとしてAKSクラスタ内にデプロイされ使用します。まだプレビューの段階ですが、Azure Load Balancerというマネージドサービスを使うこともできるようです。
Kubernetes、そしてAzure Kubernetes Serviceのメリットはなにか?
今回のハックフェストを通して、いままで朧げだったKubernetesそしてAzure Kubernetes Serviceによって何を得られるかがわかってきました。
ブルーグリーンデプロイメントにより安全なリリース
ブルーグリーンデプロイメントとは、新しいバージョンのアプリをリリースする際に既存の環境のソースコードをアップデートするのではなく、新しいバージョンの環境(サーバー)自体を作り、リクエスト向き先を変えることでリリースデプロイを行うことです。 これにより、もし新しいバージョンのアプリに不具合があったとしても前の環境をしばらく残しておけば向き先を変えることで即座に復旧することができます。
Kubernetesにブルーグリーンデプロイメントを行うには、ブルーとグリーン2つのDeploymentを作り、そこへのリクエストをServiceのフィルタリングで切り替える方法があります。
例えばblue-deployment.yamlとgreen-deployment.yamlというファイルを作り、そのファイルのラベル付けでversionというラベルにblueとgreenをそれぞれ設定しておきます。Service上の通信ポリシーではblueのみ通信が行くように設定しておきます。
新しいバージョンのアプリをデプロイする際にはgreen-deployment.yamlの方に新しいDockerイメージを名前とラベル(webapp:2.0など)を指定してKubernetesクラスタ上にデプロイします。デプロイが完了したらServiceの通信ポリシーをblueからgreenに変更すると、即座にgreenがリリースされることになります。もし不具合が発生したらService通信ポリシーをgreenからblueに変更すると、前のバージョンに即戻すことができます。
これをさらに応用すると、greenをデプロイしたあと、一般ユーザーに公開しているドメインからのリクエストはblue、開発者用のドメインから来たリクエストはgreenにリクエストを流すという風に設定することで、本番とまったく同じ環境で安全に動作検証をして問題なければリリースと言うフローを作ることも可能になります。
このブルーグリーンデプロイメントによる安全なリリースにより、以下の効果があると考えています。 - 安全を確保するための、夜間作業によるリリース作業を減らせる - 本番でしか起きない不具合やパフォーマンス問題の発見を早められる - 簡単に元に戻せることにより、事前のクオリティ担保のためテストを減らしてリリース頻度と速度を高めることができる
Kubernetesを使わなくても、自分で仕組みを構築するなどすれば出来なくはないですが、それを簡単に作れる仕組みがあるのは非常に魅力的に感じました。
Container Instanceを使ったNodeの制限を超えたスケーラビリティ
これはAzure Kubernetes Servicesを使った場合のメリットになります。
Kubernetesは複数のNodeの上にクラスタを作ってPodを稼働させるため、クラスタに参加しているNodeの合計のリリース量(CPU,メモリ、ディスク)によってPodがスケールアウトできる数に物理的な制限が発生します。 もちろんあとからNodeを追加することも可能ですが、クラスタへのNodeの追加には非常に時間がかかるため突発的または計画的に急激なアクセスが発生する場合にNodeの数を毎度増減するのには向いていません。
Azure Kuberentes Servicesの場合、Podを稼働させる環境をNode上からContainer InstancesというAzureのマネージドサービスに数分レベルで切り替えることができ、Container Instancesに切り替えるとNodeの制限を超えた数のPodを走らせる事が可能になります。
これにより、通常稼働時に必要十分なNodeだけを動かしておき、プッシュ通知やイベントなどの急激なアクセス増があるときのみContainer Instancesに切り替えてスケールアウトし、通常に稼働戻ったらまたNodeに戻すことでコストの最適化をしながららスケーラビリティを確保することができます。
infrastructure as codeの実現
現在の僕らのインフラ環境は、Azure WebAppを使っている場合と単純なVMを使っているもの、単純なVMを使っているものもAnsibleによって設定がコード化されているものとされていないものなどが乱立している状態です。また、それらへデプロイフローもそれぞれ違う状態になっています。
これをKubernetes化することにより、インフラ環境やデプロイフローなどはKubernetesのyamlファイルでコード化・共通化され、アプリの実行基盤はDockerfileによるコード化されることになります。
これによって、サービス毎に作らなければならないインフラやDevOps周りの共通部分を再利用しやすくなり、製品開発チームが増えサービスが増えていくときにSREがボトルネックとなることが防げると考えられました。
Kubernetesのデメリットはなにか?
今回のハックフェスト通して寺田さんは常に「すべてをKubernetesでやるべきではない。Kubernetesも銀の弾丸ではない」ということをおっしゃっていました。寺田さんが明示的におっしゃっていたことと、僕が考えたことを交えながらKubernetesのデメリットも書いておこうと思います。
Kubernetes自体のバージョンアップへの追従
Kubernetes自体のバージョンアップが3ヶ月に一度リリースされます。その度に大きな機能追加が発生したり、いま使っているyamlファイルの記法が変わり新しく作り直す必要が発生する可能性があります。
Azure Kubernetes Serviceのバージョンポリシーが最新から4バージョンまでをサポートするという方針です。
それはつまり、「いま最新バージョンのKubernetesを使っても来年には必ずバージョンアップをしないといけない」ということです。
4バージョンのアップデートはかなり大変のため、現実的には3ヶ月または半年に一度はバージョンアップしていくという方針になるでしょう。
このバージョンの作業も複雑な構成のクラスタを組めば組むほど、サードパーティのライブラリを入れれば入れるほど困難になっていきます。
コンポーネントが増えることによる障害発生のデバック・パフォーマンスチューニングなどの難易度が上がる
普通にVM上でWebアプリをリリース場合、よくある構成はロードバランサー -> VM(WebApp) -> DB という3段構成です。 これをKubernetes上のクラスタに載せた場合、以下のようになります。
Ingress(ロードバランサー) -> Service -> Pods(WebAppのコンテナ群)/Node -> DB
通信経路上にKubernetesが扱っているコンポーネントが登場し、障害発生した場合やパフォーマンスチューニングを行う場合はそれらのコンポーネントをすべて考慮にいれて対応しなければなりません。
Kubernetesがもたらした大きな柔軟性とそれによって得られるアジリティはこれらコンポーネント、抽象化の層によってもたらされています。逆に障害時やパフォーマンスチューニング時はその抽象化の層を降りていく必要があり、抽象化の数だけ降りなければいけないのはいわば当然のトレードオフになります。
僕が考える、Kubernetesとはなんのための、そして誰のためのソフトウェアか
これも寺田さんがおっしゃったことではなく、あくまで今回のハックフェストを通して僕が考えたことです。
僕は今回のハックフェストを通して、Kubernetesは「少人数のSREチームで、たくさんのマシンリソースを効率的に使いながら、たくさんのサービス、そしてそのサービス開発自体を支えるためのソフトウェア」だと理解しました。
Kubernetesは、素晴らしい機能群によってインフラに大きな柔軟性をもたらし、それによってデプロイ、復旧、スケールアウト、マルチバージョンのリリースによるABテストなど様々なことを簡単に実現できるようにしてくれます。しかもクラスタ内のNodeに集積してコンテナを稼働させることによって効率的にマシンリソースを使うことも可能にしてくれます。
しかしそのトレードオフとして、Kubernetes自体のバージョンアップへの追随やトラブル時にKubernetesの抽象化を理解して降りていく必要性が発生します。Kubernetesを導入する場合、イニシャルコストよりもこのランニングコストの方をよく理解しておいた方が良いと思いました。
大きなメリットは得られるが、インフラを管理するSREのようなエンジニアが不要になるものではなく、むしろそこを管理するSREはKubernetesを理解して使いこなす必要があるのでよりスキルが必要かもしれません
少人数の優秀なSREでKubernetesを扱い多くの製品や開発チームを支えることができれば、レバレッジが効いて非常に良い投資となりますが、逆に製品が少ない、また開発チーム自体が少ないのであれば投資対効果は低くなりがちで、Azure Web Appなどのマネージドサービスを使う方がメリットが大きいと思います
Kubernetesというソフトウェア自体は面白いですし、今後もキャッチアップを続けていきますが、自分達の事業や開発チームにとってホントに必要なタイミングは見極めた上で導入は検討していきたいと思います。
Azure DevOps 〜 CI/CDに必要な機能全部入りの最強SaaS 〜
AKSでアプリを動かすというのも一通り終わったあと、寺田さんのおすすめのAzure DevOpsというツールを使ってCI/CDのパイプラインを作ることになりました。
Azure DevOpsは、まさにDevOpsのために必要な機能がすべて入っているSaaSで、タスク管理のためカンバンなどが使えるBoard、ソースコードを保管するためのプライベートのGitリポジトリのRepos、CI/CDを作るための Pipelineなどが使えます。
今回はCI/CDを作るということで、 ReposというGitリポジトリにソースコードをpushすると、 Pipelineのビルドパイプラインが動いて新しいバージョンのDockerイメージをビルドしContainer Registryにプッシュし、 Pipelineのリリースパイプラインによって、新しいバージョンイメージがkubernetes上にデプロイされる というパイプラインを作っていきました。
これらのパイプラインを作るのがすべてGUIでタスクを選択して設定を入れていくだけ構築することができ、大体2~3時間程度ですべて構築することができました。 また、このパイプラインはGUIだけでなくyamlで定義することもできます。
Azureとのインテグレーションがかなりよく出来ているので、本番環境をAzureで構築している身としてはKubernetesは使わなくてもこのAzure DevOpsはぜひ導入していきたいと思えました。
ペアプロ/モブプロの効能
今回のハックフェストはずっと寺田さんとペアプロする形で進めていたのですが、僕にとって今年一番の集中力が発揮され、2日間のハックフェスト中まったく集中が切れることなくすすめる事ができました。 集中力が深く長時間維持できたことでかなり効率よくハックフェストをすすめることができ、当初3日間やる予定だったものが2日間で終わらせることができ、僕も寺田さんもヘロヘロになって「明日は休みましょうw」となる程でした。
寺田さんが一緒だったということが非常に大きかったとは思いますが、ペアプロ/モブプロすることにより
- 一人でやるよりも「いまやるべきこと」が明確化され、集中しやすい。
- 予期しないことがあってハマった場合、複数の脳で解決策を探索して議論して素早くトライすることで解決が早い
- 一緒に同じ画面を見て作業することで、予期しない学びが得られる(bashのショートカットやデバックの仕方など)
のような効果を身を持って体感することができました。
今回はハックフェストでしたが、実際の業務でもペアプロ/モブプロを取り入れてみて生産性が上がるかどうかぜひ試して見たいと思いましたし、僕がこれからコミュニティで勉強会などやるときもペアプロ/モブプロの使ってやってやりたいとおもいます。
最後に
今回のハックフェストを寺田さんに開催していただいたことで、僕個人としてはAzure/Kubernetesに限らないほどエンジニアとして多くの学びを得る事ができました。
会社としても、実際にAzure Kubernetes Serviceを使うべきかどうかの技術検証を、わずか2日間のハックフェストで実際に動かしながら把握することができたのは非常に投資対効果が大きかったと考えています。
今回学んだことを実際の現場に活かして、より良いサービスを作っていきたいと思います。
寺田さん、今回は本当にありがとうございました!!!