CloudNative Days Tokyo 2022 登壇レポート -Vol.03

登壇:GMOペパボ「実例から学ぶ Kubernetes Custom Controller のステータス管理」

2022年11月21日(月)~22日(火)に「CloudNative Days Tokyo 2022」がハイブリッド開催されました。
GMOインターネットグループはダイヤモンドスポンサーとして協賛・登壇しました!

今回は、CFPセッション「実例から学ぶ Kubernetes Custom Controller のステータス管理」の書き起こし記事となります。
ぜひご覧ください。

イベント告知:https://developers.gmo.jp/25932/

登壇者(敬称略)

  • GMOペパボ株式会社 プリンシパルエンジニア
    高橋 拓也(@takutaka1220

「実例から学ぶKubernetes Custom Controllerの状態管理」

「実例から学ぶKubernetes Custom Controllerの状態管理」と題しまして、GMOペパボの高橋 拓也が発表させていただきます。

自己紹介

まず最初に自己紹介をさせてください。
GMOペパボでインフラエンジニアをやっております高橋 拓也と申します。
コードを書くことと、Kubernetesといじることが好きで、自宅サーバーを飼っていることが僕の自慢です。これが終わったら自宅サーバーをもう1個飼おうかなと思っております。おすすめを教えてください。

宣伝です。GMOペパボでは、CNDT2022で合計5セッションの登壇を実施しております。本日分はすべて終了してしまうのですが、明日11月22日、Track Fで13時20分から14時の間にハーフセッションが2本予定されております。ぜひこちらをご覧ください。

Kubernetes Custom Controllerの難しさ

Kubernetes Custom Controllerの難しさについて語っていきたいと思います。

みなさん、カスタムリソースというものを使っていますでしょうか?
カスタムリソースとは、Kubernetesの自作リソースを作成できる機能のことと、その機能で作られたサードパーティーなリソースのことを主に指します。’kubectl get ○○’などで操作できるようになるようなリソースです。
身近なクラスタでこのカスタムリソースの定義であるCustom Resourcce Definitionをgetしてみるとどうなるでしょうか。だいたいこのくらい出てくると思います。マネージドなクラスタを使っている場合、例えばEKSとかGKSなどを使っている場合は、それぞれのプロバイダが入れているカスタムリソースディフィニションが出てくるといったこともあるかと思います。なので、使ってないという方はたぶんいらっしゃらないと思います。

カスタムリソースを作ったことはありますか?
kubebuilderというジェネレータを使うと、意外と簡単に作成することができます。主な作り方に関しては、zoetroさんという方の「つくって学ぶkubebuilder」という資料が非常に参考になるので、ぜひこちらをご覧いただければと思います。特に何か実装しなくても、‘kubectl get (自分の作ったリソース)とやるだけでもだいぶテンションが上がるので、1回作ってみると楽しいかもしれません。

カスタムリソースをきちんと実装することはできるのでしょうか?
これはとても大変で、というのもリソースを単体で作っただけだと、etcdに格納されたデータでしかない、それ以上でもそれ以下でもない情報になってしまいますが、これにカスタムコントローラを加えると、それによって操作対象をリソースに沿った状態へ収束させて、いろんなオペレーションをすることができるようになります。しかしこのカスタムコントローラの実装が非常に大変だと僕は思っております。

コントローラの大変ポイントは、主にこの3つです。

  • テストを書くことが大変
  • 複雑な処理を実装するのが大変
  • 監視のためのメトリクスを実装するのが大変

今回は、この「複雑な処理を実装する」という部分にフォーカスしてお話をしていきたいと思います。

複雑な処理とは何かというと、状態遷移が多い処理のことを指すと思います。様々な対象からデータを取得して、そのデータをこねこねして目的の状態に収束させる処理を指すと。
そして、Kubernetesの概念としてReconciliation Loopという概念があります。これをそのまま忠実に実装していくのが結構大変です。今回は詳しい説明を割愛して他の資料に回しますが、何度もループを回して理想状態に収束させるような動作のことを指します。

軽く図で説明しますとこのようになっています。
リソース内容に更新があるたびにReconcileのメソッドが実行され、Reconcileの中でリソースの状態を更新したり、別の操作対象を更新したりします。その繰り返しを行って、操作対象を理想的な状態へ収束させるという流れがReconciliation Loopです。

Deploymentの挙動から見る状態管理の難しさ

Deploymentの挙動からその状態管理の難しさというものについて詳しく見ていきたいと思います。
ローリングアップデートをDeploymentするのですけど、それって難しくないですか?僕は実装できる気がしません。ローリングアップデートというのは、Pod全体の一部の割合を順繰りに更新していくようなやり方のことを指します。例えばPodが3つあったら1つずつ新しいバージョンに更新していく、そういったものがローリングアップデートと呼ばれます。

例えば、この図のように青いPodから赤いPodに状態を更新していくようなローリングアップデートを見ていきたいと思います。
今回は左から右の赤いPodにPod Aの部分がアップデートされたと、次はこの真ん中の青いPodをアップデートしましょう、とした時に、何らかの原因で青いPodが消えてしまった、delete podしたということだったり、ノードのハートビートがなくなって応答がなくなってしまった場合も考えられます。こういった時に、ではこのBのPodを新しく作るのが正しいのか、赤い状態で作るのが正しいのか、青い状態で作るのが正しいのか、これをどうやって判断しましょうか?というような難しさがあります。

これを文章で表すとこのようになります。
Podの更新有無の管理や、更新中のPodの更新の成功可否の管理、あとは先ほどのPodが削除されてしまった場合のようなイレギュラー時の処理の決定など、これらのようなことが難しい。これらをまとめて、リソースの状態管理と今回は呼ぶことにして、このリソースの状態管理が非常に難しいと感じております。

状態管理の種類と実例紹介

それでは他のプロダクトなどはいったいどうやってその状態管理を行っているのか、その実例とともにご紹介していきたいと思います。

今回はこの3つのプロダクトについて実装を確認していきます。
1つはKubernetesのDeployment、もう1つはcert-manager、もう1つはrookです。

Deploymentの状態管理

まず、Deploymentはどのようにやっているのかをご説明したいと思います。

Deploymentは、ReplicaSetという子リソースを持っています。ReplicaSetがPodの個数を管理するという役割を持っており、Deploymentは直接Podを操作するような役割を持っていません。ReplicaSetを介してPodを操作します。
例えばReplicaSetがReplica:3の場合、Podが1個消えた場合だともう1個立て直す、といったようにReplicaSetがPodの個数の管理を実行します。
そして特徴的なのは1つのReplicaSetのうちにぶら下がっているPodはすべて同じ設定、同じバージョンのものが動いていて、そのバージョンを変更したい場合はもう1つ別のReplicaSetを作って状態をなんとかするという運用になります。

そしてこのローリングアップデートは、ReplicaSetをうまく使うことで実現しております。Deploymentの中に2つのReplicaSetがあり、新しいバージョンのPodを司るnewと、古いバージョンのPodを司るold、この2つがある状態になってます。このoldからnewにPodを移し替えていくことがローリングアップデートです。

今回このnew rs のreplicasを1にして新しいPodを1個立てます。そうするとnew rsがNot Readyであるという状態を持ちます。Podが起動してくるまでこのNot Readyの状態は続いて、これがReadyになるまでDeploymentは処理を停止します。Readyになったら、今度はoldの方のReplicasを1個減らして、またReadyになるまで待機します。それを以下繰り返して、このようにoldが0、newが3の状態になってローリングアップデートが完了する、こういったような処理の流れになっています。

ではローリングアップデート中にPodの削除が発生したらいったいどうなるのでしょう?その場合は、oldのReplicaSetがその責務を全うすべく、Podを1個立てます。Podが立っている間、ReplicaSetはNot Readyの状態を持つため、Deploymentは処理を中断します。oldの方の処理を待っている状態です。そして、oldの処理が終わってReadyになったら、処理を再開します。
この流れを「単一責務パターン」と今回呼んでみることにします。責務の異なるリソースの階層構造となっており、子リソースにPodの管理などの責務を委譲しているという形で実装しているといえます。
この単一責務パターンのメリットとしては、Deploymentは想定するReplicaSetが作成できるかどうかに特化すればよいため、実装がシンプルに保てます。DeploymentはReplicaSetを作るだけなので、Podを作ったり、そのほか何かいろいろしたりすることはありません。さらに、リソースのテストもDeploymentを入力として出力のReplicaSetがどうなったかをチェックするだけで済むので、リソースのテストも記述しやすいものとなります。
さらに、このパターンは自作のカスタムリソースにも非常に応用しやすい形だと思っていて、例えば何かしらPodを立てたい、ワークロードを動かしたいなどの要求があった時には、Deploymentをとりあえず作っておけば、そのほかの状態管理に悩むことはなくなります。複雑な状態管理は、得意なほかのリソースを選定してそれをカスタムリソースの処理の中で作成してしまえばよくなります。例えば、Podを複数立ててロードバランスをしたくなったら、サービスリソースを作ればよいというように、どんどんそのほかのリソースに対して責務を委譲してあげることで自分は楽ができる、そういったような実装ができます。

デメリットとしては、結合テストが結構難しくなると思います。例えば最終的にワークロードを立てる、Podを立てる際にそのPodが本当に起動してちゃんとそのパラメーター、コマンドライン引数を解釈してきちんと動いてくれるかというのは、実際のところ動かしてみないと分からないようなことがあったり、Podが立たない、リソースが足りないとかアフィニティが付いていてPodがペンディングになってしまっている状態をきちんとその親のリソースが解釈できるかというのも、実際にPodを立ててみないと分からなかったり、もうちょっと詳しいテストをやってみないと分からないので、少し結合テストが必要になってくるような場合があります。

さらに、自作のリソースを作る時の最大のデメリットは、状態管理の実践の仕方を学習できないという致命的なデメリットがあります。
今は、Podの状態をReplicaSetに委譲しているのですけれども、ではReplicaSetを作りたくなった時はどうすればいいのか分からないですね。それはどうしていこうかというのは、次のプロダクトを見ていくと若干分かるかなと思うので、見ていきましょう。

Deploymentのまとめです。
1つのリソースにつき1つの責務を管理する、単一責務パターンを使っています。リソースの通知する状態を監視することで、状態を管理してうまく運用しているという流れになっています。

cert-manager

cert-managerについて見ていきます。
cert-managerは、証明書を管理、運用するプロダクトです。主なユースケースとして世の中で使われているのは、ACMEプロトコルを利用してLet’s Encryptの証明書を作るというのがだいたい行われていますが、その他にもself-signedの証明書を作ったりとか、HashiCorp Vaultから証明書を作ったりとか、結構いろいろな機能があります。
このcert-managerですが、複数のCustom Resource Definitionが連携して、単一的なパターンを踏襲している作りになっています。
いちばんトップのリソースとしては、 Certificateリソースがあります。これはそのまま名前の通り証明書の情報を持つリソースで、issuerという証明書を発行するための情報を持ったリソースから証明書作成に必要となる情報を参照してどんどんやっていくという流れになります。このCertificate自体は証明書を作らずに、CertificateRequestが証明書を作る処理をまた子リソースに委譲していくという流れをやっています。Certificateリソースが完了するまで待機して、完了したらそのTLSの実体となるシークレットリソースを作る、そういった役割を持っています。
そして、CertificateRequestなどの状態管理にこのStatus.Conditionsというものを使っています。ここにあるのはCertificateのConditionの例ですが、’kubectl get ○○-o yamlした時にこういうステータスを見たことある方は結構いらっしゃるのではないかと思います。このConditionというのは、特定の状態を確認するという目的で使います。

今回ちょっと例を用意しました。「カレー作り」をConditionで再現してみます。
カレーを作るコントローラがそれぞれの状態を見てどういうことをやっていくのかというのを決めていきます。今ここまでは、肉に火が通っている状態までがあって、鍋に水が入っておらず、カレーはできていない状態を作っています。そして、このコントローラは各状態を表すConditionを見て次の行動を決定します。ここまでできているのだったら次は鍋に具材と水を入れて火にかけるということを導き出せるような状態の作りになっています。そして仮に別スレッドや他のコントローラが鍋に水を入れると、鍋に水が入っている状態を更新して、Trueにします。その場合、元いたコントローラはその状態をさらに見て再度何をやるかを決定します。この場合は先ほど水を入れていましたが、今は水を入れる必要はないので、鍋に具材を入れて火にかける、というのが次のアクションになります。こうやってConditionを使っていきます。

Certificateリソースは、2つのCondition Typeを持ちます。Ready:処理が完了しているかどうかと、Issuing:処理中かどうか、の2つのConditionを持っています。意味はたぶん若干違うのですけれども、こういう解釈をしました。
IssuingがTrue、つまり処理中である場合、新しいCertificateRequestを作るといった実装がコード中に見られます。
さらに、Certificateは証明書の再作成も責務とします。Let’s Encryptの証明書は90日で切れてしまうので、いずれかのタイミングで再作成(e-issue)をしないといけませんが、それを必要かどうかをCertificateリソースは監視しています。証明書が再作成する必要が出てきたら、このCertificateリソースのコンディションのIssuingをコントローラがTrueに書き換えます。そうすると、その状態の収束に必要な処理がガチャガチャと動き出して、再度証明書が作り出されて、IssuingがFalse、ReadyがTrueになるという状態になります。
つまり先ほどの図のところではCertificateRequest.Status.ConditionsのReadyというタイプがTrueになるまで、Certificateリソースは処理を見続ける、監視するといったような振る舞いをすることが処理の実態としてはいえます。

CertificateリソースはOrderリソースを作って、OrderリソースはChallengeリソースを作ります。そして、ChallengeリソースはACME Challengeを実行してその結果をOrderに通知して、OrderはCertificateRequestに通知して、CertificateRequestはCertificateに通知する、そういったバケツリレーのような形になります。

cert-managerのまとめです。
Certificateを頂点とする階層構造を持ちます。状態管理はConditionsパターンを用いており、親は子リソースの状態をステータスコンディションから取得します。

rook

最後にrookです。
rookはCloud Nativeなストレージクラスタオペレータで、国内ではサイボウズさんがrook/cephを使ってCephクラスタをプロビジョニングしたり管理したりするために使っています。サイボウズさんは積極的にrookを開発していて、すごいと思いながら見ております。

今回はCephClusterリソースについてご紹介します。
CephClusterリソースは、そのままCephのクラスタバージョンや、Cephが動くのに必要なプロセスの個数などを管理するために存在します。CephClusterもConditionを持ちますが、コントローラからはほぼほぼ参照されないものです。ではこれは誰が参照するのかというと、実際のユーザーである私たちが目で見て参照します。今その処理の状態がどこまでいったかをこのMessageというアトリビュートから見るというようなことをやるために存在すると、ソースを見る限り思います。

では状態はどうやって管理しているのかというと、実際状態は全く管理していません。OperatorのReconcile関数内ですべての手順を1回実行します。つまり先ほどループがあったじゃないですか、そのループ1回ですべての処理を終了させるというのがrookのOperatorになっています。各手順が高度に冪等性が担保されており、何度実行されても大丈夫です。なぜrookは状態を管理しないのかを推測してみたのですけれども、Cephクラスタはk8sクラスタ1個に対して20個も30個もできるものではないので、1リソースに対して使える処理時間が長いという特徴があるのと、そもそもCephはストレージのクラスタなのでデータの耐久性を何よりも重視しているということが挙げられると思います。なので逐一状態を見ながら1つのスレッドでやった方がより安全である、そのためにそういうふうな作りになっていると思います。
余談ですが、状態を管理しないのでテストがめちゃめちゃかっこいいです。これは、r.Reconcileを実行するというテストで、これめっちゃいいなと思いながらソースを見ていました。

rookのまとめです。
状態を管理しないという選択肢もあるというのを、これを見て思いました。

まとめ

最後にまとめです。
状態管理はとても難しく、イチから実装するのはとても大変です。その代わりに、広く使われているリソースの肩に乗ることで楽をすることができます。今回見た単一責務パターンのようにです。
さらに既存の実装をよく観察することが大事です。Conditionsパターンはコアのリソースでも使われており、とても広く使われているので、いろいろな実装を見ることで自分のものにして自分のコントローラを実装することができると思います。

最後に、多くの自作コントローラを弊社では本番投入しながら実践で学んでいっております。一緒にコントローラを開発して状態管理は難しいという話をしてみませんか?
We are HIRING !!!ということで、このURLから弊社の募集などを見て、ぜひ弊社に興味を持っていただけたら幸いです。

それでは今回の発表はこちらで終了とさせていただきます。ご清聴ありがとうございました。

アーカイブ映像

映像はアーカイブ公開しておりますので、
まだ見ていない方、もう一度見たい方は 是非この機会にご視聴ください!

ブログの著者欄

デベロッパーリレーションズチーム

GMOインターネットグループ株式会社

イベント活動やSNSを通じ、開発者向けにGMOインターネットグループの製品・サービス情報を発信中

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック