技術開発部の相原です。好きな --feature-gates
はServiceTopologyです。
この記事はLIFULLアドベントカレンダーの16日目です。
去年のエントリではIstio を本番環境に導入するまでと題して、私のチームが進めているアプリケーション実行基盤刷新プロジェクトでのIstioの導入についてお伝えしました。 移行に至るまでの経緯などはそのエントリをご覧ください。
あれからしばらくが経ち、ようやく主要サービスの(ほぼ)全てをKubernetesに移行することができましたので今回は移行を実現するまでに行った取り組みを紹介したいと思います。
移行にあたってやったこと
移行前の状態としては、去年のエントリでお伝えした通り数年前にオンプレミスからAWSに移行し、それに伴ってマイクロサービス化を推進している、というものでした。
ただマイクロサービス化が推進されているとは言ってもIstio導入のモチベーションになったように分散システム的な難しさに対する解決策も持ち合わせていなければ、アプリケーション自体もそれに即した構成や設計になっておらず、マイクロサービス化の実現からは程遠い状態でした。
そうした課題を受け、我々のチームではKubernetesに移行するにあたって大きく分けて3つの対応が必要だと置きました。 まずは対象アプリケーションの 健全化、そして Containerize と 可観測性の向上 です。
ここからはそれぞれの対応について詳細に触れていきます。
健全化
構成の見直し
オンプレミスからAWSへの移行をリフト&シフトで進めてきたこともあり、OSのバージョンやアプリケーションが利用するライブラリ・ミドルウェアのバージョンが古いままになっているという状態でした。 またプロビジョニングスクリプトもなんとなくはあるものの、実際に本番環境で動いているものと微妙に差異があったりというような状況で、まずはここの健全化から着手することにしました。
これを機に極力Alpine Linuxでライブラリやミドルウェアのバージョンを上げながらプロビジョニングをし直し、Alpine Linuxへの切り替えが難しいものはAWSでKubernetesを運用しているということもあってAmazon Linux2に載せ替えていきました。
もちろんKubernetesに移行するにあたって不必要となるミドルウェアの削除や構成の変更が必要となるものにも適宜対応する必要があるので、このタイミングでFHSへの準拠なども済ませながらアプリケーションの構成をあるべき姿にしていきます。
アプリケーションサーバの見直し
アプリケーションサーバも同様に適切な設定がされているとは言えない状況でした。
LIFULLの主要サービスではRuby, PHPの採用が多く、Giant VM Lockを備えるRubyはもちろんのこと、PHPでもCPUのコア数に応じてアプリケーションサーバのプロセス数を調整する必要があります。
KubernetesのHPAではCPUバウンドなアプリケーションは基本的にCPUの使用率でオートスケールすることを期待するため、ここの調整が足りない場合アプリケーションサーバのプロセスを使い果たしているにも関わらずスケールアウトの閾値までCPUを使いきれないという事態が起きてしまいます。
そこで、以下のような手順でアプリケーションサーバのプロセス数の見直しを行いました。
- 本番相当のトラフィックのシナリオテストを用意して、シングルコアでの性能限界を明らかにする
- KubernetesのDownwardAPIを用いてコンテナの
requests
に指定したCPUのunit数に応じて動的にアプリケーションサーバのプロセス数を変更できるようにする- DownwardAPIによって
requests
したCPUのunit数を環境変数として取得できるので、それを用いてプロセス数を設定するようにする
- DownwardAPIによって
- シングルコアの性能限界をもとにトラフィックのピーク時にアプリケーションサーバが数台落ちても問題ない程度までスケールアップさせる
- 仮にピーク時トラフィックをアプリケーションサーバ4台で捌ける程度までスケールアップさせてしまうと1台落ちた時のインパクトが大きい
- Kubernetesで
Pod
をEvict
から保護するPod Disruption Budget
の台数が落ちることは考慮しておくこと
リソース割り当て量を1コアなどと細かくしてコンテナ単位でスケールアウトするという戦略も考えられますが、モニタリングシステムへの負荷が高まったりアプリケーションのCopy on Writeの効率が悪くなってしまうため好ましくありません。
この他にも今までおざなりになっていたヘルスチェックの見直しなども行い、ヘルスチェックがリバースプロキシだけでなくきちんとアプリケーションサーバまで到達しているのか、Pod内にRedisなどのレプリカを持つ場合きちんとレプリケーション遅延を見ているか、といった点を考慮しながら適切なヘルスチェックを設定しています。
Containerize
SIGTERMへの対応
Kubernetes移行においてもっとも大切なアプリケーションのContainerizeのための対応はSIGTERMの適切なハンドリングです。
SIGTERMをGraceful Shutdownとして認識しないウェブサーバやアプリケーションサーバは少なくなく、そうしたアプリケーションをそのままデプロイしてしまうとKubernetesではPodの終了時にリクエストが欠損してしまいます。
アプリケーションがシグナルを解釈できるのであればアプリケーションの改修、そうでなければ preStop
フックを使ってSIGTERMが送られる前にGraceful Shutdownが実行されるよう対応を行う必要があります。
また、この際KubernetesのEndpoints ControllerによるEndpointの更新とコンテナへのSIGTERMの送信の順番が保証されないという点にも注意しなければなりません。
つまり、SIGTERMが送られてからもリクエストが到達してしまう可能性があるということで、SIGTERMが送信される前に実行される preStop
によって一定秒数のスリープを入れるかSIGTERMを受け取っても一定秒数新規のリクエストを処理できるようにする必要があります。
後者の対応をしている例としてはCoreDNSの health
プラグインの lameduck などが挙げられます。
CoreDNSでは lameduck を利用することでKubernetes環境における安全なGraceful Shutdownに対応しています。(kopsなどで単純にCoreDNSを使うようにするとこの設定がされていないためご注意ください)
環境ごとの値を外から与えられるように
Twelve Factor Appにあるように、Kubernetesにデプロイするアプリケーションでは環境固有の値は環境変数から与えられるように作ることが好ましいです。
個人的にはドキュメンテーションの観点からコマンドラインのオプションとして与えられるように作ることを推奨していますが、今回は既存アプリケーションの移行ということもあり純粋に環境変数から読むような変更を加えていました。
アプリケーションでの対応が難しい場合はConfigMapとして設定ファイルを切り出したり、envsubst
を使うなどして環境固有の設定に対応することになります。
他にはphp.iniの memory_limit
やアプリケーションのロガーの logrotate
といったKubernetesに移行することで不要となる設定・実装の調整や、LIFULLではIstioを採用しているためIstioによって提供されるRetrying, Timeout, Circuit Breakerに伴う実装の調整などを行いました。
可観測性の向上
Prometheus Exporter実装による可視化
Kubernetesではcadvisorやkube-state-metricsから得られるメトリクスで概ねの監視を行うことができますが、アプリケーションの監視となるとこれらの情報だけでは足りません。
アプリケーションサーバのアクティブなプロセス数やin-flightなリクエスト数、ヒープのサイズやGCのメトリクスなど気にしなければならない点は多くあります。
元々Prometheusをクラスタの監視に利用していたため、移行した全てのアプリケーションにPrometheus Exporterの実装を行いました。 基本的にはOpenCensusを利用しつつも、対応していない言語・アプリケーションサーバでは適宜ライブラリを利用しつつ監視に必要なメトリクスを公開するようにしていきます。
LIFULLが運営するLIFULL HOME'Sではトラフィックに季節要因があるため、PrometheusのLong-term Storageとしてthanosを運用して長期のメトリクスを保存しています。
ログフォーマットの標準化と構造化
メトリクスときたら次はログです。
LIFULLでは今まで各ウェブサーバのデフォルトのログフォーマットを利用していることが多かったため、まずはログフォーマットの標準化から始めました。
ロガーによってはJSONに対応していなかったりするので、ここでは標準のログフォーマットとしてLTSVを定義しました。 LTSVとはTab-separatedなLabelとValueの組み合わせのフォーマットです。
そしてそれをfluentdでパースしてJSONに構造化、そしてCloudWatch Logsに流すためログをラベルごとにグルーピングして投げるようにしました。(fluentd-kubernetes-daemonset辺りを丸使いしてCloudWatch Logsに流すと一つの巨大なロググループが生成されて使いづらいので自前でfluentd pluginを書いて対応しています) また、歴史的背景からログに秘匿情報が含まれていて閲覧できる開発者に制限があるといった問題があったため、秘匿情報のマスキングを行えるfluentd pluginもあわせて開発し、全ての開発者が当たり前にログを閲覧できる環境を整えました。
これまた歴史的背景によるアーキテクチャからistio-proxyを導入できなかったアプリケーションがあるため実現には至っていませんが、アクセスログは将来的にはIstioの logentry
に逃がす予定があります。
新規に開発するマイクロサービスを対象に分散トレーシングの徐々に導入を増やしていたり、アプリケーションの脆弱性情報の収集と可視化、APIエンドポイントごとのエラーレート・レスポンスタイム可視化といった取り組みも進めています。
またちょっと変わったところでアプリケーションが利用しているリソース量からかかっているコストを按分するような仕組みも作っています。
移行を支えた仕組み
ここからはこうしたアプリケーションのKubernetes移行を支えた仕組みについて書きます。
負荷テスト
Kubernetes移行においては、先に述べたSIGTERMのハンドリングのテストや正しくHPAの閾値通りにスケールアウトするかどうかのテスト、またVPAなどはありつつも割り当てるリソース量のキャパシティプランニングなども必要となるため、負荷テストあるいはベンチマークといったことが大切になります。
IstioコミュニティのfortioであればKubernetes内で特定のアプリケーションに気軽に負荷をかけることができるのですが、スケールアウトなどのテストでは本番トラフィックと同等のものでないとテストの意味を成しません。
また、複数台からの負荷のエミュレーションを行いたいといった要求もあったため、シナリオテストが可能なvegetaを複数台で並行実行可能なKubernetes Controllerを開発して利用しています。
作りは単純で、KubernetesのJobの仕組みを使ってCRDによって定義された内容の負荷テストを並行実行するというものです。 これを利用して入念なテストを行うことで大きなトラブルなく移行を進めることができました。
Shadow Proxy
前項で移行を試みるアプリケーションが本番トラフィック相当を捌けることは確認できましたが、移行後もアプリケーションの全ての機能が稼働するかどうかまでは確認できていません。
そうしたことをテストするために我々のチームではShadow Proxyパターンによるテストを実施しています。 Shadow Proxyパターンとはサーバに対するあるリクエストをミラーリングして別のサーバにも送信するというテストの手法で、Traffic MirroringやShadowingとも呼ばれていてIstioでもTraffic Mirroringとして機能が用意されています。
Kubernetes移行においてはNATされたIPが変化することによって外部APIへのIP認証で弾かれたり、多くの場合プロビジョニングをし直しているためサーバ構成が変化して特定の機能が動かなくなったりすることがままあります。 観測範囲でもgolangの特定のOracleクライアントライブラリがKubernetes移行によって動かなくなるといったことを確認しており、こういった網羅的なテストは必須だと考えます。
そのため前段のリバースプロキシに、あるいは前段のリバースプロキシのアクセスログを元にして、Shadow Proxyを用意してKubernetes上で一定期間リクエストを捌いて動作を確認するといった工程を実施しています。
また、LIFULLではBuckyという自動システムテストツールを開発しており、併せてBuckyによるテストも実施することで移行後も正常にアプリケーションが稼働することを確認しています。
Kubernetes Manifest作成補助
Kubernetes Manifestを書く際には考慮しなければならないベストプラクティスが多くあり、不慣れな開発者が理想のKubernetes Manifestを書き上げることは非常に困難です。
そこで、我々のチームでは1枚のyamlを記述するとLIFULLにおける最適な構成のKubernetes Manifestをkustomize形式で出力するコマンドラインツールを用意しています。
Helmや、MicrosoftのOpen Application Modelのように独自のCRDを定義することも選択肢としてはあり得ますが、柔軟性・移行のしやすさ・将来的に開発者に流暢にKubernetes Manifestを書いて欲しいという思いから今の形に落ち着きました。
他にも監視用のダッシュボードやアラートの設定を生成するブートストラップのようなものを用意するなど、移行をスムーズに進めるためのツール群を開発しています。
今後の移行を支える仕組み
これで主要サービスの(ほぼ)全てがKubernetesに移行されたわけですが、LIFULLでは「あらゆるLIFEを、FULLに。」ということで今後も多くのサービスを開発し運用していかなければなりません。
そこで未来への取り組みも徐々に始めているので最後にそちらを紹介して終えたいと思います。
ガイドラインの整備
アプリケーションの品質を一定以上に保つため、上述のSIGTERMへの対応や環境固有の値の取り扱いなどといった、いわゆるクラウドネイティブなアプリケーションの開発をするためのガイドラインを整備しています。
このガイドラインには他に例えば以下のような内容のものがあります。
- ランタイムの起動速度の遅いVM言語などを採用する場合はコンテナのスケールメリットと相反することを理解すること
- 無暗なインメモリキャッシュはライフタイムの短いコンテナと相性が悪いことを理解すること
- Prometheus 用のメトリクスを公開するための /metrics エンドポイントを用意すること
- Istioから提供されるCircuit Breakerのような機能を再実装しないこと
Istioが提供する機能の再実装を推奨しないことは移植可能性などを鑑みると賛否ある気はしますが、こういったガイドラインを整備することでKubernetes及びそのエコシステムから提供される恩恵を開発者が最大限享受できたり、開発されるアプリケーションの品質の担保ができるよう取り組んでいます。
育成・啓蒙
非常に良くないことなのですが、このプロジェクトは半年前まで私一人でKubernetesを中心としたアプリケーション実行基盤の構築・運用とアプリケーションの移行を行っていて、現在はメンバーが1人ジョインしたものの社内でこの2人に知見が集中している状態になってしまっています。
そこで我々のチームでは 留学 と称した期間限定の他部署メンバーの受け入れや、社内勉強会の開催を始めました。
留学 は短期のタスクを用意して全社から留学を希望するメンバーを募り、その期間我々がサポートをしながらタスクをこなしてもらうという取り組みです。 これにより社員にこの領域の知見をつけてもらうと共に、中長期的なキャリアの選択肢として意識してもらうということを狙っています。
日々の運用や移行以外にも認証基盤の開発や時系列分析によるスケジューリングの最適化など、よりよいアプリケーション実行基盤にするための改善活動を続けていることもあって、週次の成果を全社に公開したり定期的にリリースノートを公開するといった取り組みも始めています。
育成や啓蒙は目下最大の課題で、今後はここに注力していくことになりそうです。
そんなLIFULLではメンバーを募集しているのでまずはカジュアル面談からでもいかがでしょうか。