3月24日、サイバーエージェントのエンジニア・クリエイターによる技術カンファレンス「CyberAgent Developer Conference 2022」を開催しました。本記事では、「Kubernetes基盤における運用フローのController化と継続的な改善」の様子をお届けします。
目次
■AKEの約5年間の歩み
■Kubernetesの拡張方法の基礎
■Controllerの利用パターン
■Custom Resourceの利用パターン
■Controller設計時のフロー
●リソースを登録するまでの前処理
●実際に調整処理する部分
●結果を記録する部分
●実行制御の部分
■まとめ
■AKEの約5年間の歩み
サイバーエージェントでは、社内で使うプライベートクラウドとしてKubernetes as a Service基盤を運用しています。この基盤を少人数で運用するために、KubernetesのControllerを開発して運用を自動化してきた経験をもとに、Controller開発とその考慮点についてご紹介します。
Kubernetes as a Service、通称AKEをリリースしたのは約5年前です。OpenStack HeatとConsulをベースにクラスタを構築して管理し、その上で動作するさまざまなKubernetes Controllerを実装してきました。例えば、ノードを自動修復したり、自動アップグレードしたり、ロードバランサーを自動制御したりするControllerです。
そして去年、クラスタ管理コストの削減・マルチプライベートクラウドへの対応、といった2つの要件から、Cluster APIをベースにした新しいKubernetes as a Service、AKE v2に移行し始めました。
AKE v2では、クラスタの管理自体もCluster APIを利用しKubernetesリソースによって行います。つまり、複数のクラスタに対して何かしらの処理を行うCustom Controllerを容易に実装できます。
これまでの約5年間のクラスタ運用の経験をふまえて、ソフトウェアによる自動化、自律化を推進できるようになりました。
■Kubernetesの拡張方法の基礎
まずKubernetesの拡張についておさらいします。
●Kubernetes Controllerとは
Kubernetes Controllerとは、特定のリソースの状態を宣言された状態に調整して収束させるプログラムです。例えば、ReplicaSet Controllerの場合は、ReplicaSetリソースの定義を元に状態をwatchして、Podの作成や削除を行って、レプリカ数を維持します。
基本概念はシンプルです。例えばkubectlとシェルで実装すると、次のスライドの左のようになります。まず対象とするReplicaSetとPodのリソースの情報を取得し、差分を計算します。その結果からPodの作成や削除を行って調整します。
この一連の流れをReconciliationループと言います。Controllerはこのように、運用にあたって自前のスクリプトでやっていた処理を、ソフトウェアによって効率的に自律的に実現するための仕組みです。
●Custom Controller
特定のリソースの定義に応じて任意の処理を行うCustom Controllerを作ることもできます。
●Custom Resource
Kubernetesでは、任意のフィールドを持つ新しいリソースも定義できます。例えば、MySQLのリソースを用意しておいて、それをもとにMySQL自体を管理する仕組みも作れます。
●Admission Webhook
Controllerのほかに、もう一つAdmission Webhookという拡張ポイントもあります。KubernetesのAPIにリソースの作成や削除などのリクエストが入ってきたときに、validatingやmutatingの処理を行うための仕組みです。
validatingの例としては、Podの作成時や変更時に、latestのタグが含まれていれば作成や変更を拒否するなどです。。mutatingの例としては、Podの作成時や変更時に自動的にサイドカーをインジェクトするなどです。
●kubebuilderやライブラリの利用
Custom ControllerやAdmission Webhookを実装するにあたっては、kubebuilderなどのフレームワークや、その内部で使われているライブラリを利用できます。kubebuilderを利用すると、Controllerの雛形の自動生成や、Manager・Reconcilerといった便利な仕組みを使って、Controllerのロジックだけを実装すれば良くなります。
ロジック外の実装負荷を減らすためにも、極力controller-runtimeなどのライブラリを利用するようにしてください。
■Controllerの利用パターン
Controllerの利用パターンをいくつか紹介します。利用パターンは、「Controllerがwatchする対象のリソース」「Controllerが操作する対象」の2つの組み合わせによって分類できます。
watchするリソースには、新たに定義したCustom Resourceや、OSSとして公開されている既存のCustom Resource、Build-inのリソースなどがあります。操作する対象としては、Kubernetesのリソースを操作する場合もあれば、Kubernetesの外部のシステムを操作する場合もあります。
●新たに定義したCustom Resourceに対するController
新たに定義したCustom Resourceをwatchする例としては、Cluster APIがあります。Cluster APIは、Clusterリソースの情報をもとに、Kubernetes外のプライベートクラウドのAPIを叩いて、Kubernetesクラスタを構築管理する仕組みです。
●既存のCustom Resourceに対するController
次に、既存のCustom Resourceをwatchする例です。私たちは、Cluster APIを使ってユーザーのクラスタを払い出した後に、そのクラスタに対してControllerやアドオンなどをデプロイするために、Argo CDを使っています。Cluster APIがクラスタの認証情報自体は生成するのですが、Argo CDはそれを理解できません。そのため、Argo CDが理解できる形で登録する必要があります。
そこで独自のControllerを作っています。Cluster APIが生成する認証情報をwatchして、それを元にArgo CD用のSecretを生成しています。
●Build-in Resourceに対するController
最後に、Build-in Resourceをwatchしている例としては、Ingress Controllerがあります。Ingress Controllerでは、IngressリソースやNodeリソースをwatchした情報をもとに、プライベートクラウドのAPIを操作して、HTTPのロードバランサーを構成しています。
■Custom Resourceの利用パターン
続いて、Custom Resourceの利用パターンを4つ紹介します。
1、特定のアプリケーションを抽象化するパターン
抽象化してクラスタ利用者に提供することで、セルフサービス化や、SREによるベストプラクティスの主導などができます。また、MySQLクラスタの特定のエラーや特定の状態に応じた、ドメイン固有の処理を行わせられます。
こうした抽象化はOperatorとも呼ばれます。CNCFが提供しているホワイトペーパーにもかなり細かくまとめられています。
2、外部リソースの状態を定義するパターン
Kubernetesクラスタの外のシステムと連携するときに、その外部のシステムの状態がどうあってほしいかをCustom Resourceで表現します。
たとえばCert Managerでは、発行したい証明書を定義すると、ACMEを利用して自動的に証明書を発行し、Secretリソースとして証明書を作成します。
3、複雑な定義をまとめるパターン
複雑な定義を扱いやすい形にまとめたり、再利用可能な形にすることもあります。
例えばPodにさまざまな種類のサイドカーを埋め込む設定を行っているとします。このとき、サイドカーの情報をCustom Resourceとして定義します。それをPodが参照して自動的にサイドカーを埋め込ませることもできます。
4、簡易的なデータストアとして利用するパターン
例えば私たちのML基盤では、Notebookを起動するための独自のWebUIとAPIサーバーがあります。APIサーバーでは、データベースの代わりにCustom Resourceにデータを入れて利用しています。
ML基盤のAPIサーバーからKubernetesのAPIを叩くときには、Impersonate-User requestを使って、Web UIを操作するユーザーの権限になりかわります。そのため、どのユーザーにどのコンテナイメージを見せるかの制御に、KubernetesのRBACを利用できます。
■Controller設計時のフロー
ここからは、実際にControllerを用いた仕組みを設定する上で考慮するポイントについてご紹介します。
●リソースを登録するまでの前処理
まずはリソースを登録するまでの前処理で考慮する点です。
Custom Resourceには、簡易的なバリデーションを行う仕組みが2つ用意されており、OpenAPI v3のSchema Validationを利用できます。これにより、値そのものの簡易的なバリエーションができます。
また、Kubernetes 1.23以降では、Common expression Languageを用いたバリデーションもサポートされました。これによって、複数のフィールドを横断した制限などもかけることができます。
より複雑なバリデーションや、既存の変更できないCustom Resourceに対するバリテーション、Build-in Resourceに対するバリデーションを行う場合は、Validating Webhookを実装できます。
例えばAKEでは、クラスタリソースが、Kubernetesのバージョン以外に、AKEのバージョンをAnnotationに付与して管理しています。そして、Annotationが変更されたときに、Validating Webhookを使って、該当のAKEバージョンが存在するバージョンなのかチェックしています。
Mutating Webhookを使うと、特定のフィールドの情報をもとに登録時に任意のフィールドを変更できます。
例えば、AKEではクラスタの管理上、Kubernetesのマイナーバージョンの値を利用したいケースがあります。しかし、マイナーバージョンがわかるフィールドは用意されていません。そのため、Mutating Webhookを利用して、既存のフィールドの設定の情報を元に、Annotationにマイナーバージョンの値を埋め込んでいます。
また、変更するためのWebhookとして、Conversion Webhookもあります。これは、Custom ResourceのAPIバージョンの変更のときにスキーマの変更に追従するためのWebhookです。
Conversion Webhookによって、両方のAPIバージョンで提供できる期間を設けながら、新しいAPIバージョンにシームレスに移行できるような仕組みを作ることができます。
●実際に調整処理する部分
続いて、リソースが登録された後に、実際に処理する部分の考慮点について紹介します。
Controllerの開発では、主に調整を行うReconcile関数を実装します。Reconcile関数には引数として、NamespaceとNameで構成されたNamespacedNameという構造体が、Workqueueを経由して渡ってきます。多くの場合はリソースの作成や削除のタイミングでWorkqueueに入って、Reconcile関数が呼ばれます。
基本的には、関数の序盤でNamespaceとNameからオブジェクト全体の詳細を取得して、その後に冪等になるような処理を実装します。
Reconcile関数は、削除のイベントも取り扱えるように実装する必要があります。NamespacedNameの構造体からオブジェクトを取得しようとしたときに、リソースがなければすでに削除されています。また、取得できたとしても、DeletionTimestampが0でなければ、削除中です。この2つをおさえながら実装するとよいでしょう。
冪等な処理では、差分と比較しながら更新をしていきます。
Controller開発時は、3つのステップを意識します。Observeでは現状を観測し、Analyzeではそれをもとに状態を分析し、Actionではその理想状態に近づけるための処理をします。このステップを繰り返します。
Controllerが子供のリソースを作成する場合には、子供のリソースがあるべき状態と乖離してないかも確認しながら更新します。
より簡潔に実装できる機能「OwnerReferences」と「Finalizer」についてご紹介します。
OwnerReferencesとは、特定のリソースに依存するリソースを作成する時に、親子関係を定義するための仕組みです。親リソースが削除されると、子供のリソースは非同期に削除されるので、GCのような処理をReconcile関数に実装する手間が省けます。
「Finalizer」は、リソースの削除前に特定の処理が完了するまで待機するための仕組みです。例えば、Serviceリソースの削除時に外部のロードバランサーを削除するのを待ったり、Clusterリソースを消した時にAppProjectからクラスタのレコードを抜いたりするときに使っています。
●結果を記録する部分
続いて、Reconcileした結果を記録する方法について紹介します。
リソースの状態の保存に関しては、statusフィールドを利用します。Kubernetesではspecにあるべき状態を、statusに現状の状態を保存するという大まかなルールがあります。
Custom Reconcileの場合には、このstatusフィールドに何を保存するかも設計において重要です。例えばMySQLリソースでいうと、MySQLクラスタの全体の状況や、そのMySQLクラスタに対する接続情報や認証情報などをstatusフィールドに持たせたりします。
Build-in Resourceに対して詳細な情報を記録したい場合や、Custom Resourceのスキーマが決められていてstatusに保存できない場合には、Annotationsに記録する方法もあります。
Eventリソースを作成するという手段もあります。Eventリソースというのは、特定のイベントが発生するたびに情報を記録していくものです。デフォルトでは1時間で消えます。
ただし、不用意にEventリソースを大量作成したり、保持期間を長くすると、etcdにたくさんEventリソースが増え、負荷が高くなってしまうことがあります。
Custom Metricsとして記録を公開することもできます。controller-runtimeでは、ManagerがPrometheusの/metricsエンドポイントを保持し、公開してくれます。Custom Metricsを公開したいときには、独自に定義して変数に入れ、Managerに登録しておくと、後は自動的に公開してくれます。kubebuilderだと、ServiceMonitorリソースの面倒も見てくれます。
Reconcile関数の中から外部通知用のライブラリを利用して直接通知を行うこともできます。
なお、Reconcileの中で処理がブロックされると困るケースもあるので、処理時間が長い場合は非同期化なども検討してください。
非同期化の一つの方法として、Notification Engineを使って別Controller化するということもできます。Notification Engineはさまざまな通知先に対応しています。
Notification Engineを使うと、通知に関する設定を後からConfigMapで容易に変更できるという点もメリットです。
最後になりましたが、もちろんログも重要です。この辺りもkubebuilderがかなり面倒をみてくれます。
●実行制御の部分
最後に、いかに維持し続けるかという、実行制御の部分についてご紹介します。
Reconcile関数は、Controllerの起動時にすべてのオブジェクトに対して実行します。これは、起動してなかった間にイベントが発生していた可能性があるからです。
このとき、Controllerのレプリカ数が複数の場合は、同じような処理をControllerが同時に行って競合してしまう場合があります。
そこで、controller-runtimeにはLeader Electionという機能が用意されています。自分がLeaderかどうかを判別して、1台だけ動作させる機能です。
続いて、リソース変更時の制御です。どういったイベントの時に関数を呼び出すかを設定できます。
まずForで、その関数が対象とするリソースを1個指定します。対象のオブジェクトに変更があると、この関数が呼び出されます。
次にOwnsで、Reconcile関数で作成される子供のリソースも指定することができます。これにより、子供を変更したときも親のReconcilerが動きます。
便利な Owns関数を利用することもできますが、実はこのWorkQueueに追加する処理の部分は、よりプリミティブなWatch()関数を利用することもできます。
Watch関数ではイベントとして、「特定のリソースの変更」「Go Channel経由の通知」「Informerの発火」などを受け取り、それを元にEnqueueRequestsFromMapFunc関数を使って、任意のオブジェクトに対してマッピング処理を行い、より柔軟な形でWorkQueueに追加もできます。
Reconcile関数を再実行したいケースもあります。例えばReconcile関数の中でエラーが発生して再度実行したい場合は、一度エラーを返して関数の実行を終えるようにしてください。エラーが帰った場合は、自動的にWorkqueueにRequeueされます。
また、時間がかかる処理のを待機する場合でも、関数内でsleepするのは避け、明示的にRequeueするように返り値を指定してください。
ただし、3日先と指定してRequeueしても、Reconcile関数はそれより前に呼ばれることもあります。遅くともこの時間後に再実行される、という程度に考えておいてください。
このReconcileが実行されてしまう理由の一つとしては、Sync Periodの経過時があります。
Controllerのバグや、イベントが欠損してしまった時のために、Managerは一定期間ごとに再度Reconcile関数を自動的に実行するようになっています。デフォルトでは10時間です。
なお、全てのControllerで同時にReconcile関数が実行されないように、10%ぐらいのジッター(ゆらぎ)を含んで処理が行われます。
■まとめ
これまでも少人数で運用するために、ソフトウェアにより自律化を推進するために、いろんなControllerを作ってきました。Controller化すると、そのロジックがコードになってGitリポジトリに上がって、継続的にしっかりそのロジックを改善して行くこともできます。
ぜひ皆さんがこのセッションを見て、Controller化することができるパターンについてイメージが湧いていただけたら幸いです。
「CyberAgent Developer Conference 2022」のアーカイブ動画・登壇資料は公式サイトにて公開しています。ぜひご覧ください。
https://cadc.cyberagent.co.jp/2022/
■採用情報
新卒採用:https://www.cyberagent.co.jp/careers/special/students/tech/?ver=2023-1.0.0
キャリア採用:https://www.cyberagent.co.jp/careers/professional/