04

Mar

Kubernetes Operatorの深層

原文(投稿日:2020/09/25)へのリンク

要約: Operatorは、何年もの間、Kubernetesエコシステムの重要な部分を占めてきました。管理面をKubneretes APIに移動することで、「一枚のガラス」体験が容易になります。Kuberentesネイティブアプリケーションの簡素化を検討している開発者、または既存のシステムの複雑さを軽減しようとしているDevOpsの実践者にとって、Operatorは魅力的な提案になる可能性があります。しかし、どのようにしてスクラッチからOperatorを作成できるのでしょうか ?

Operatorの深層

Operatorは今日どこにでもあります。データベース、クラウドネイティブプロジェクト、Kubernetesでのデプロイやメンテナンスが複雑なものはすべて1つにします。2016年にCoreOSによって最初に導入されたこれらのプロダクトは、運用上の関心事をソフトウェアに移すというアイデアをカプセル化しています。Runbookやその他のドキュメントの代わりに、Operatorはアクションを自動的に実行します。たとえば、Operatorはデータベースのインスタンスをデプロイしたり、データベースのバージョンをアップグレードしたり、バックアップを実行したりできます。そして、これらのシステムをテストし、人間のエンジニアよりも速く反応することができます。

Operatorは、カスタムリソース定義でツールを拡張することにより、ツールの構成をKubenretes APIに移動します。これは、Kubenretes自体が「一枚のガラス」になることを意味します。これにより、DevOpsの実践者は、Kubernetes APIリソースを中心に構築されたツールの豊富なエコシステムを利用して、デプロイされたアプリケーションを管理および監視できます:

このアプローチにより、プロダクション環境、テスト環境、および開発環境間の同一性を確保することもできます。それぞれがKubernetesクラスタである場合、Operatorを使用してそれぞれに同じ構成をデプロイできます。

Operatorをスクラッチから作成する理由はたくさんあります。通常、プロダクトのファーストパーティOperatorを作成しているのは開発チームか、サードパーティソフトウェアの管理の自動化を検討しているDevOpsチームです。いずれにせよ、開発プロセスは、Operatorが管理する必要のあるケースを特定することから始まります。

最も基本的なOperatorがデプロイメントを処理します。APIリソースに応答してデータベースを作成することは、kubectl applyと同じくらい簡単です。ただし、これは、StatefulSetsDeploymentsなどの組み込みのKubernetesリソースよりも少し優れています。Operatorが価値を提供し始めるのは、より複雑な操作です。データベースを拡張したい場合はどうなりますか ?

StatefulSetを使用すれば、kubectl scale statefulset my-db --replicas 3を実行でき、3つのインスタンスを取得できます。しかし、それらのインスタンスが異なる構成を必要とする場合はどうなるのでしょうか ? 1つのインスタンスをプライマリとして指定し、他のインスタンスをレプリカとして指定する必要がありますか ? 新しいレプリカを追加する前に必要なセットアップ手順がある場合はどうなるのでしょうか ? この場合、Operatorは特定のアプリケーションを理解した上でこれらの設定を構成できます。

より高度なOperatorは、負荷、バックアップ、リストアに応じた自動スケーリング、Prometheusなどのメトリックシステムとの統合、さらには使用パターンに応じた障害検出や自動チューニングなどの機能を処理できます。従来の「Runbook」ドキュメントがある操作はすべて、自動化、テスト、および自動応答に依存することができます。

管理対象のシステムは、Operatorのメリットを得るためにKubernetesにある必要はありません。たとえば、Amazon Web Services、Microsoft Azure、Google Cloudなどの主要なクラウドプロバイダは、オブジェクトストレージなどの他のクラウドリソースを管理するためのKubenretes Operatorを提供しています。これにより、ユーザーはKubernetesアプリケーションを構成するのと同じ方法でクラウドリソースを構成できます。運用チームは、他のリソースと同じアプローチを取り、Operatorを使用して、APIを介したサードパーティのソフトウェアサービスからハードウェアまで、あらゆるものを管理できます。

この記事では、etcd-cluster-operatorに焦点を当てます。これは、Kubernetes内でetcdを管理する多くの同僚と一緒にコントリビュートしたOperatorです。この記事は、そのOperatorまたはetcd自体を紹介するようにはデザインされていないため、Operatorが何をしているのかを理解するために知っておく必要があること以外に、etcd操作の詳細については詳しく説明しません。

つまり、etcdは分散型Key-Valueデータストアです。次の場合に限り安定的に管理できます。

加えて:

このとおり、管理には、KubernetesStatefulSetが通常実行できる以上のことがあるため、Operatorに頼ります。etcd-cluster-operatorがこれらの問題を解決するために使用する正確なメカニズムについては詳しく説明しませんが、この記事の残りの部分では、このOperatorを例として参照します。

Operatorは次の2つから構成されます:

通常、Operatorはコンテナ化され、サービスを提供するKubernetesクラスタにデプロイされます。通常は単純なDeploymentリソースを使用します。理論的には、Operatorソフトウェア自体は、クラスタのKubernetes APIと通信できる限り、どこでも実行できます。ただし、通常は、管理しているクラスタでOperatorを実行する方が簡単です。通常、これは、Operatorを他のリソースから分離するためのカスタムNamespaceにあります。

この方法を使用してOperatorを実行している場合は、さらにいくつかのものが必要です:

権限モデルとWebhookについては後で詳しく説明します。

最初の質問は言語とエコシステムです。理論的には、HTTP呼び出しを行うことができるほとんどすべての言語を使用してOperatorを作成できます。リソースと同じクラスタにデプロイすると仮定すると、クラスタが提供するアーキテクチャ上のコンテナで実行できる必要があります。通常、これはlinux/x86_64であり、etcd-cluster-operatorが対象としますが、arm64やその他のアーキテクチャ、またはWindowsコンテナ用にOperatorを構築することを妨げるものは何もありません。

Go言語は、一般的に最も成熟したツールを備えていると考えられています。Kubernetesのコアでコントローラを構築するために使用されるフレームワークであるcontroller-runtimeは、スタンドアロンツールとして利用できます。さらに、KubebuilderやOperator SDKなどのプロジェクトは、コントローラランタイムの上に構築され、簡素化された開発エクスペリエンスを提供することを目的としています。

Java、Rust、PythonなどのGo以外の言語に、Kubernetes APIに接続して、Operatorをビルドするための汎用または特化したツールやプロジェクトがあります。これらのプロジェクトは、さまざまなレベルの成熟度とサポートがあります。

もう1つのオプションは、HTTPを介してKubernetes APIと直接対話することです。これは最も手間のかかる作業が必要ですが、チームは最も使いやすい言語を使用できます。

最終的に、この選択は、Operatorを作成および保守するチーム次第です。チームがすでにGoに慣れている場合は、豊富なGoツールが選択を明確にします。チームがまだGoを使用していない場合、学習曲線とより成熟したエコシステムツールのGoを使用するための継続的なトレーニングを犠牲にして、より成熟していないが基礎となる言語に精通しているエコシステムとの間のトレードオフになります。

etcd-cluster-operatorの場合、チームはすでにGoに精通しているため、それは私たちにとって明らかな選択でした。また、Operator SDKではなくKubebuilderを使用することを選択しました、これは既存の知識があるためです。ターゲットプラットフォームはlinux/x86_64でしたが、必要に応じて他のプラットフォーム用にGoを構築できます。

etcd Operator用に、EtcdClusterという名前のカスタムリソース定義を作成しました。CRDがインストールされると、ユーザはEtcdClusterリソースを作成できます。最高レベルのEtcdClusterリソースは、etcdクラスタが存在することを望んでいることを記述し、その構成を提供します。

apiVersion: etcd.improbable.io/v1alpha1kind: EtcdClustermetadata:name: my-first-etcd-clusterspec:replicas: 3version: 3.2.28

apiVersion文字列は、これがAPIのバージョン (この場合はv1alpha1) であることを表します。kindはこれをEtcdClusterであると宣言しています。他の多くの種類のリソースと同様に、nameを含める必要があり、namespacelabelsannotations、およびその他の標準アイテムを含めることができるmetadataキーがあります。これにより、EtcdClusterリソースをKubernetesの他のリソースと同じように扱うことができます。たとえば、ラベルを使用してクラスターを担当するチームを識別し、標準のリソースの場合と同様に、これらのクラスターをkubectl get etcdcluster -l team=fooで検索できます。

specフィールドは、このetcdクラスタに関する操作情報が存在する場所です。サポートされているフィールドは多数ありますが、ここでは最も基本的なもののみを示します。versionフィールドには、デプロイする必要があるetcdの正確なバージョンが記述され、replicasフィールドには、存在する必要があるインスタンスの数が記述されます。

例には表示されていないstatusフィールドもあります。このフィールドは、クラスタの現在の状態を説明するためにOperatorが更新します。specフィールドとstatusフィールドの使用はKubernetes APIの標準であり、他のリソースやツールとよく統合されます。

Kubebuilderを使用しているため、これらのカスタムリソース定義の生成に役立つ情報が得られます。Kubebuilderでは、specフィールドとstatusステータスフィールドを定義するGo構造体を作成しています。

type EtcdClusterSpec struct { Versionstring`json:"version"` Replicas *int32`json:"replicas"` Storage*EtcdPeerStorage`json:"storage,omitempty"` PodTemplate *EtcdPodTemplateSpec `json:"podTemplate,omitempty"`}

このGo構造体、およびstatus同様の構造体から、Kubebuilderは、カスタムリソース定義を生成するためのツールを提供し、これらのリソースを処理するためのハードワークを実装します。これにより、突き合せループ (Reconciler Loop) を処理するためのコードを記述するだけで済みます。

Kubernetes Operatorの深層

他の言語では、同じことを行うためのサポートが異なる場合があります。Operator用に設計されたフレームワークを使用している場合には、これが生成される可能性があります。たとえば、Rustライブラリkube-deriveは同様の方法で機能します。チームがKubernetes APIを直接使用している場合は、CRDとそのデータを個別に解析するコードを作成する必要があります。

etcdクラスタを記述する方法がわかったので、それを実装するリソースを管理するOperatorを構築できます。Operatorはどのように機能することもできますが、ほとんどすべてのOperatorがコントローラパターンを使用します。

コントローラは、「突き合せループ」と呼ばれることが多い単純なソフトウェアループであり、次のロジックを実行します:

KubernetesのOperatorの場合、望ましい状態はリソースのspecフィールド (この例ではEtcdCluster) です。管理対象リソースは、クラスタの内部または外部の何でもかまいません。この例では、ReplicaSetsPersistentVolumeClaimsServicesなど他のKubneretesリソースを作成します。

特にetcdの場合は、etcdプロセスに直接コンタクトして、管理APIからステータスを取得します。このKubernetes外のアクセスは、ネットワークアクセスの中断がサービスダウンを意味しない可能性があり、少し注意が必要です。これは、etcdが実行されていないことを示すシグナルとして、etcdに接続できないことを使用できないことを意味します (動作中のetcdインスタンスを再起動した場合、ネットワークの停止を悪化させる可能性があります) 。

一般に、Kubernetes API以外のサービスと通信する場合は、可用性または整合性の保証が何であるかを考慮することが重要です。etcdの場合、回答が得られればそれは非常に整合していることがわかりますが、他のシステムはこのように動作しない可能性があります。情報が古くなった結果として誤った行動をとることにより、停止を悪化させないようにすることが重要です。

コントローラの最も簡単な設計は、突き合せループを定期的に、たとえば30秒ごとに再実行することです。これは機能しますが、多くの欠点があります。たとえば、2つのループが同時に実行されないように、ループが前回からまだ実行されているかどうかを検出できる必要があります。さらに、これは、30秒ごとに関連するリソースのKubernetesのフルスキャンを意味します。次に、EtcdClusterのインスタンスごとに、関連するPodやその他のリソースを一覧表示する突き合せ機能を実行する必要があります。このアプローチでは、Kubernetes APIに大量の負荷がかかります。

これはまた、非常に「手続き型」のアプローチを促進します。次の突き合せまでに長い時間がかかる可能性があるため、各ループは可能な限り多くのことを実行しようとします。たとえば、一度に複数のリソースを作成します。これは、Operatorが何をすべきかを知るために多くのチェックを実行する必要がある複雑な状態につながる可能性があり、バグが発生する可能性が高くなります。

これに対処するために、コントローラはいくつかの機能を実装します:

これらすべてを組み合わせると、単一のループを実行するコストと待機する時間が削減されるため、各ループでの実行が効率的になります。その結果、突き合せロジックの複雑さを軽減できます。

スケジュールに従ってスキャンする代わりに、Kubernetes APIは「監視 (Watch)」をサポートします。APIコンシューマがリソースまたはリソースのクラスへの関心を登録し、一致するリソースが変更されたときに通知を受けることができる場合。これは、Operatorがほとんどの時間アイドル状態でリクエストの負荷を軽減できることを意味し、Operatorが変更にほぼ瞬時に応答することを意味します Operator向けのフレームワークは通常、監視の登録と管理を処理します。

この設計のもう1つの結果は、作成するリソースも監視する必要があるということです。たとえば、Podsを作成する場合は、作成したPodを監視する必要があります。これは、それらが削除された場合、または望ましい状態と一致しないように変更された場合に、通知、ウェイクアップ、および修正できるようにするためです。

その結果、突き合せ機能のシンプルさをさらに一歩進めることができます。たとえば、EtcdClusterに応答して、OperatorはServiceといくつかのEtcdPeerリソースを作成したいと考えています。それらを一度に作成する代わりに、最初にServiceを作成してから終了します。しかし、Services自身を監視しているため、すぐに突き合せするようにトリガされます。その時点で、Peerを作成できます。それ以外の場合は、いくつかのリソースを作成してから、リソースごとに1回ずつ突き合せし、さらに多くの再突き合せをトリガするでしょう。

この設計は、突き合せループを非常に単純に保つのに役立ちます。1つのアクションのみを実行して終了することにより、開発者が考慮すべき複雑な状態の必要性を排除します。

これの主な結果は、更新が見落とされる可能性があることです。ネットワークの中断、OperatorPodの再起動、およびその他の問題により、状況によってはイベントの見逃しが発生する場合があります。これに対処するには、Operatorが「エッジベース」ではなく「レベルベース」であるという観点から作業することが重要です。

これらの用語は信号制御ソフトウェアから取られており、信号の電圧に作用することを指します。私たちの世界では、「エッジベース」とは「イベントへの対応」を意味し、「レベルベース」とは「観察された状態への対応」を意味します。

たとえば、リソースが削除された場合、削除イベントを監視し、再作成を選択する場合があります。ただし、削除イベントを見逃した場合は、再作成を試みない可能性があります。また、さらに悪いことに、それがまだ存在していると想定して後で失敗します。代わりに、「レベルベース」のアプローチでは、トリガを単に突き合せする必要があることを示すものとして扱います。外部状態を再度突き合せし、それをトリガした変更の実際のコンテキストを破棄します。

多くのコントローラのもう1つの主要な機能は、リクエストのキャッシュです。突き合せしてPodsを要求し、2秒後に再度トリガすると、2番目のリクエストのキャッシュされた結果が保持される場合があります。これにより、APIサーバの負荷が軽減されますが、開発者にとってはさらに考慮事項があります。

リソースのリクエストは古くなっている可能性があるため、これらを処理する必要があります。特にリソースの作成はキャッシュされないため、次のような状況になる可能性があります:

重複するServiceを誤って作成しました。Kubernetes APIはこれを正しく処理し、すでに存在していることを示すエラーを表示します。結果として、このケースを処理する必要があります。一般に、後日、単純にバックオフして再突き合せすることをお勧めします。Kubebuilderでは、突き合せ機能からエラーを返すだけでこれが発生しますが、フレームワークによっては異なる場合があります。後で再実行すると、キャッシュは最終的に整合性が保たれ、突き合せの次のフェーズが発生する可能性があります。

これの1つの副作用は、すべてのリソースに決定論的な名前を付ける必要があることです。そうしないと、重複するリソースを作成する場合、別の名前を使用する可能性があり、実際に重複する可能性があります。

状況によっては、ほぼ同時に多くの突き合せをトリガする場合があります。たとえば、多数のPodリソースを監視していて、それらの多くが同時に停止している場合 (たとえば、ノード障害、管理者エラーなど) 、複数回通知が届くことが予想されます。ただし、最初の突き合せが実際にトリガされてクラスタの状態が観察されるまでに、すべてのPodはすでになくなっています。したがって、それ以上の突き合せは必要ありません。

その数が少ない場合、これは問題ではありません。しかし、一度に数百または数千の更新を処理する大規模なクラスタでは、同じ操作を100回続けて繰り返すため、突き合せループのクロールが遅くなったり、キューがいっぱいになってOperatorがクラッシュしたりするリスクがあります。

突き合せ関数は「レベルベース」であるため、これを処理するために最適化を行うことができます。特定のリソースの更新をキューに入れるときに、そのリソースの更新が既にキューにある場合は、そのリソースを削除できます。キューから読み取る前の待機と組み合わせると、操作を効果的に「バッチ処理」できます。したがって、Operatorの正確な条件とそのキュー構成によっては、200個のPodがすべて同時に停止する場合、1回の突き合せのみを実行する可能性があります。

Kubernetes APIにアクセスするすべてのものは、アクセスするための認証情報を提供する必要があります。クラスタ内では、これはPodが実行されるServiceAccountを使用して処理されます。ClusterRoleおよびClusterRoleBindingリソースを使用して、権限をServiceAccountに関連付けることができます。特にOperatorにとって、これは重要です。Operatorには、クラスタ全体で管理するリソースをgetlist、およびwatchするための権限が必要です。さらに、それに応じて作成する可能性のあるすべてのリソースに対する幅広い権限が必要になります。たとえば、PodsStatefulSetsServicesなど。

KubebuilderやOperator SDKなどのフレームワークは、これらの権限を提供できます。たとえば、Kubebuilderはソースアノテーションアプローチを採用し、コントローラごとに権限を割り当てます。複数のコントローラが1つのデプロイされたバイナリにマージされる場合 (etcd-cluster-operatorの場合のように) 、権限がマージされます。

// +kubebuilder:rbac:groups=etcd.improbable.io,resources=etcdpeers,verbs=get;list;watch// +kubebuilder:rbac:groups=etcd.improbable.io,resources=etcdpeers/status,verbs=get;update;patch// +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=list;get;create;watch// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=list;get;create;watch;delete

これは、EtcdPeerリソースの突き合せ用の権限です。独自のリソースをgetlistwatchし、サブリソースのステータスをupdateしてpatchを適用できることがわかります。これにより、ステータスのみを更新して、他のユーザに情報を表示することができます。最後に、必要に応じてリソースをcreateおよびdeleteできるように、管理するリソースに対する幅広い権限があります。

カスタムリソース自体が一定レベルの検証とデフォルト設定を提供しますが、より複雑なチェックを実行するのはOperatorに任されています。最も簡単なアプローチは、リソースがOperatorに読み込まれたときにそれを行うことです。監視 (watch) から返されたとき、または手動で読んだ直後など。ただし、これは、デフォルトがKubernetesの表示に適用されないことを意味し、管理者の混乱を招く可能性があります。

より良いアプローチは、Webhook構成の検証と変更 (mutating) を使用することです。これらのリソースは、リソースを永続化する前に、リソースが作成、更新、または削除されたときにWebhookを使用する必要があることをKubernetesに通知します。

たとえば、Mutating Webhookを使用してデフォルトを実行できます。Kubebuilderでは、MutatingWebhookConfigurationを作成するための追加の構成をいくつか提供し、KubebuilderはAPIエンドポイントの提供を処理します。私たちが書くのは、デフォルトにするspec構造体のDefaultメソッドだけです。次に、そのリソースが作成されると、リソースが永続化される前にWebhookが呼び出され、デフォルト設定が適用されます。

ただし、リソースの読み取りにデフォルトを適用する必要があります。Operatorは、Webhookが有効になっているかどうかを知るために、プラットフォームについて推測することはできません。たとえそうであっても、構成が誤っているか、ネットワークの停止によりWebhookがスキップされるか、Webhookが構成される前にリソースが適用されている可能性があります。これらの問題はすべて、Webhookがより優れたユーザエクスペリエンスを提供しますが、Operatorコードでは信頼できないため、デフォルト設定を再実行する必要があることを意味します。

ロジックの個々のユニットは、言語の通常のツールを使用してユニットテストできますが、統合テストは特定の問題を引き起こします。APIサーバをモックできる単純なデータベースと考えたくなるかもしれません。ただし、実際のシステムでは、APIサーバは多くの検証とデフォルト設定を実行します。これは、テストと実際の動作が異なる可能性があることを意味します。

大雑把に言えば、2つの主要なアプローチがあります:

最初のアプローチでは、テストハーネスがkube-apiserverおよびetcd実行可能ファイルをダウンロードして実行し、実際に機能するAPIサーバを作成します (ここでのetcdの使用は、サンプルOperatorがetcdを管理しているという事実とは関係ありません) 、もちろん、ここにPodsを作成するKubernetesコンポーネントがないReplicaSetを作成することもできます。そのため、実際には何も実行されません。

2番目のアプローチははるかに包括的で、実際のKubernetesクラスタを使用します。Podsを実行でき、正しく応答します。この種の統合テストは、kindを使用するとはるかに簡単になります。このプロジェクトは、「DockerのKubernetes」の短縮形であり、Dockerコンテナを実行できる場所であればどこでも完全なKubernetesクラスタを実行できます。APIサーバがあり、Podsを実行でき、すべての主なKubernetesツールを実行します。その結果、kindを使用するテストは、ラップトップまたはCIで実行でき、Kubernetesクラスタのほぼ完全な振る舞いを提供します。

この記事では多くのアイデアに触れましたが、最も重要なアイデアは次のとおりです:

これらのツールを使用すると、Operatorを構築してデプロイメントを簡素化し、運用チームの負担を軽減できます。独自のアプリケーションでも開発したアプリケーションでも。

著者について

James Laverack氏は、英国を拠点とするKubernetesプロフェッショナルサービス会社であるJetstackでソリューションエンジニアとして働いています。7年以上の業界経験を持ち、ほとんどの時間をクラウドネイティブジャーニーで企業を支援することに費やしています。Laverack氏はKubernetesの貢献者でもあり、バージョン 1.18からKubernetesリリースチームに所属しています。