
Kubernetesはステートレスなアプリケーションの管理に優れています。ステートレスなPodが停止しても、ReplicaSetが新しいPodを起動するため、人間の介入は不要です。しかし、データベース、メッセージキュー、分散キャッシュのような複雑なステートフルなアプリケーションを管理するには、Kubernetesがデフォルトでは持たないドメイン固有の運用知識が必要です。
そこで登場するのがOperatorパターンです。
Kubernetes Operatorは、Kubernetesアプリケーションをパッケージ化、デプロイ、管理するための手法です。Postgresクラスターのバックアップ方法、ダウンタイムなしでKafkaをアップグレードする方法、レプリカのフェイルオーバーを処理する方法といった人間の運用知識を、Kubernetesコントロールプレーン内でネイティブに実行されるソフトウェアにエンコードします。
運用面から見ると、Operatorは1つ以上のカスタムリソースを監視し、変更に反応する単なるコントローラーです。クラスター内で標準のPodとして実行され、他のコンポーネントと同様にKubernetes APIサーバーと通信します。
この概念は2016年にCoreOSによって導入され、以来、基本的なパターンとなっています。現在、OperatorHub.ioには、cert-managerからPrometheus、CockroachDBまで、あらゆるものに対応する何百ものコミュニティおよびベンダーのOperatorがリストされています。
Operatorは、Kubernetes APIサーフェスを拡張するためにカスタムリソース定義(CRD)に依存しています。CRDは、クラスターに新しいリソースタイプを登録します。Pods、Deployments、Servicesだけを管理するのではなく、PostgresCluster、KafkaTopic、RedisFailoverのような全く新しいリソースタイプを定義できます。
以下は、最小限のCRD定義です。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: postgresclusters.db.example.com
spec:
group: db.example.com
scope: Namespaced
names:
plural: postgresclusters
singular: postgrescluster
kind: PostgresCluster
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
replicas:
type: integer
minimum: 1
version:
type: string
このCRDが適用されると、ユーザーは標準のkubectlコマンドを使用してPostgresClusterオブジェクトを作成できます。
apiVersion: db.example.com/v1alpha1
kind: PostgresCluster
metadata:
name: my-db
namespace: production
spec:
replicas: 3
version: "16.2"
Operatorはこれらのオブジェクトを監視し、PVCのプロビジョニング、Postgres Podの起動、レプリケーションの設定、および完全なライフサイクルの管理といったアクションを実行します。
すべてのKubernetes Operatorの核となるのは、調停ループ(コントロールループまたはリコンサイラーとも呼ばれます)です。このループはシンプルなパターンに従います。
これは、Kubernetes自体が組み込みコントローラー(Deploymentコントローラー、ReplicaSetコントローラーなど)で内部的に使用しているのと同じループです。Operatorは、カスタムリソースのためにこのループに新しいエントリを追加しているだけです。
func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// 1. Fetch the PostgresCluster resource
var cluster dbv1.PostgresCluster
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
if errors.IsNotFound(err) {
// Resource was deleted - clean up external resources
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// 2. Reconcile StatefulSet
if err := r.reconcileStatefulSet(ctx, &cluster); err != nil {
log.Error(err, "Failed to reconcile StatefulSet")
return ctrl.Result{RequeueAfter: time.Second * 30}, err
}
// 3. Reconcile Service
if err := r.reconcileService(ctx, &cluster); err != nil {
log.Error(err, "Failed to reconcile Service")
return ctrl.Result{RequeueAfter: time.Second * 30}, err
}
// 4. Update status
cluster.Status.ReadyReplicas = r.countReadyReplicas(ctx, &cluster)
if err := r.Status().Update(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: time.Minute * 5}, nil
}
上記のリコンサイラーの重要なポイント:
ctrl.Result{}を返します**。ctrl.Result{RequeueAfter: ...}を返します。Operatorを構築するには、主に2つのアプローチがあります。
Operator SDKは、Operatorのスキャフォールディング、コード生成、ライフサイクル管理を提供します。3つの構築アプローチをサポートしています。
新しいGo Operatorを初期化します。
operator-sdk init --domain=example.com --repo=github.com/example/postgres-operator
operator-sdk create api --group=db --version=v1alpha1 --kind=PostgresCluster --resource --controller
これにより、以下がスキャフォールディングされます。
api/v1alpha1/postgrescluster_types.go: CRD Goタイプcontrollers/postgrescluster_controller.go: リコンサイラーconfig/crd/: 生成されたCRD YAMLconfig/rbac/: 生成されたRBACルール最大限の制御が必要な場合は、SDKのスキャフォールディングなしでcontroller-runtimeを直接使用できます。これはOperator SDKが内部で使用しているものです。
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: server.Options{BindAddress: ":8080"},
HealthProbeBindAddress: ":8081",
LeaderElection: true,
LeaderElectionID: "postgres-operator.example.com",
})
if err := (&controllers.PostgresClusterReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller")
os.Exit(1)
}
リーダー選出(LeaderElection: true)は本番環境で非常に重要です。これにより、Operatorのレプリカのうち1つだけが常にアクティブに調停を行い、スプリットブレインシナリオを防ぎます。
Operatorは、リソースを監視および変更するためにRBACパーミッションを必要とします。Operator SDKはこれらを自動的に生成しますが、それらが何を許可するのかを理解しておく必要があります。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: postgres-operator-role
rules:
# Watch and manage PostgresCluster CRs
- apiGroups: ["db.example.com"]
resources: ["postgresclusters", "postgresclusters/status", "postgresclusters/finalizers"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Manage underlying resources
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services", "configmaps", "secrets", "persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Read Pod status for health checks
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
最小権限の原則: Operatorが実際に必要とする動詞とリソースのみを許可します。cluster-adminバインディングは避けてください。
ユーザーがPostgresCluster CRを削除すると、KubernetesはetcdからCRを削除しますが、クラウドストレージバケット、ロードバランサー、外部DNSレコードなどの外部リソースは自動的にクリーンアップされません。
ファイナライザーがこれを解決します。これらはリソースのmetadata.finalizersフィールドにある文字列マーカーです。Kubernetesは、すべてのファイナライザーが削除されるまで、リソースを永続的に削除しません。
const finalizerName = "db.example.com/cleanup"
func (r *PostgresClusterReconciler) handleFinalizer(ctx context.Context, cluster *dbv1.PostgresCluster) error {
if cluster.DeletionTimestamp.IsZero() {
// Resource is NOT being deleted - ensure finalizer is registered
if !controllerutil.ContainsFinalizer(cluster, finalizerName) {
controllerutil.AddFinalizer(cluster, finalizerName)
return r.Update(ctx, cluster)
}
} else {
// Resource IS being deleted - run cleanup
if controllerutil.ContainsFinalizer(cluster, finalizerName) {
if err := r.cleanupExternalResources(ctx, cluster); err != nil {
return err
}
// Remove finalizer - allows Kubernetes to complete deletion
controllerutil.RemoveFinalizer(cluster, finalizerName)
return r.Update(ctx, cluster)
}
}
return nil
}
OperatorがKubernetesクラスター外にリソース(S3バケット、Route53レコード、Datadogモニターなど)を作成する場合は、常にファイナライザーを実装してください。
不透明なステータスフィールドを使用するのではなく、Kubernetesの慣例であるステータス条件(構造化された機械可読なステータスメッセージ)に従ってください。
meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
ObservedGeneration: cluster.Generation,
Reason: "ReconciliationSucceeded",
Message: fmt.Sprintf("PostgresCluster with %d replicas is ready", cluster.Spec.Replicas),
})
これにより、オペレーター(人間)が素早くスキャンできるkubectl describe出力が生成されます。
Conditions:
Type Status Reason Message
---- ------ ------ -------
Ready True ReconciliationSucceeded PostgresCluster with 3 replicas is ready
ライブクラスターなしで調停ロジックをテストするには、controller-runtimeのfake.NewClientBuilder()を使用します。
func TestReconcile_CreatesStatefulSet(t *testing.T) {
cluster := &dbv1.PostgresCluster{
ObjectMeta: metav1.ObjectMeta{Name: "test-db", Namespace: "default"},
Spec: dbv1.PostgresClusterSpec{Replicas: 3, Version: "16.2"},
}
r := &PostgresClusterReconciler{
Client: fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(cluster).
Build(),
Scheme: scheme,
}
result, err := r.Reconcile(context.Background(), ctrl.Request{
NamespacedName: types.NamespacedName{Name: "test-db", Namespace: "default"},
})
assert.NoError(t, err)
assert.Equal(t, time.Minute*5, result.RequeueAfter)
// Verify StatefulSet was created
var sts appsv1.StatefulSet
err = r.Get(context.Background(), types.NamespacedName{Name: "test-db", Namespace: "default"}, &sts)
assert.NoError(t, err)
assert.Equal(t, int32(3), *sts.Spec.Replicas)
}
envtestパッケージは、実際のAPIサーバーとetcdをローカルで実行し、完全な調停フローをテストできるようにします。
var testEnv *envtest.Environment
func TestMain(m *testing.M) {
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}
cfg, _ := testEnv.Start()
// ... setup manager and run tests
testEnv.Stop()
}
Operatorを本番環境に投入する前に:
/healthzの活性、/readyzの準備/metricsでのPrometheusメトリクス(controller-runtimeが自動的に行います)cluster-adminなし、ワイルドカード動詞なしRateLimiterを設定しますenvtest統合テストが合格している| Operator | 管理対象 | 参考にすべき理由 |
|---|---|---|
| Prometheus Operator | Prometheus + Alertmanager | 優れたCRD設計、ステータスパターン |
| Cert-Manager | TLS証明書 | 複雑なファイナライザーパターン、ACME統合 |
| Strimzi | Kubernetes上のApache Kafka | 完全なステートフルライフサイクル管理 |
| CloudNativePG | PostgreSQLクラスター | 本番グレードのPostgres Operator |
あるチームが複雑な分散データベース用のKubernetes Operatorを開発しています。テスト中に、特にクラスタ負荷が高い場合やAPIサーバーのレイテンシがある場合、新しいCustom Resource (CR) が作成されたときにOperatorが必要なすべてのリソース(例:PVCs、StatefulSets)をプロビジョニングできないことがあると観察されました。Operatorのreconciliationロジックにおいて、最終的な一貫性(eventual consistency)と回復力(resilience)を確保するための最も堅牢な設計原則は次のうちどれですか?
Operatorを書くべきか、それともHelmを使うべきか? Helmはシンプルでステートレスなデプロイメントに最適です。アプリケーションに継続的に実行する必要がある運用ロジック(フェイルオーバーの決定、スキーマ移行、バックアップのスケジュール設定、クラスターのスケーリングポリシーなど)がある場合は、Operatorを使用してください。cronジョブや人間の管理が必要な場合は、Operatorの候補です。
GoなしでOperatorを書けますか? はい。Operator SDKはバックエンド言語としてHelmとAnsibleをサポートしています。Pythonの場合、kopfが人気のあるフレームワークです。ただし、Goは最高のパフォーマンス、ツール、そしてKubernetesエコシステム全体との整合性を提供します。
1つのOperatorでいくつのCRDを管理すべきですか? 焦点を絞ってください。特定のデータベースなど、明確に定義された1つのドメインを管理するOperatorは、すべてを管理するGod-Operatorよりも保守とテストが容易です。ここでは単一責任の原則が適用されます。
Operatorとコントローラーの違いは何ですか? 技術的には、すべてのOperatorはコントローラーですが、すべてのコントローラーがOperatorではありません。「Operator」という用語は、特定のアプリケーションに関する運用ドメイン知識をエンコードするコントローラーを特に指します。汎用インフラストラクチャを管理するコントローラーは、通常、単にコントローラーと呼ばれます。
Kubernetes Operatorは、クラスターをコンテナスケジューラーから、複雑なワークロードのための完全に自己修復するプラットフォームへと変革します。このパターンは、シンプルなHelmベースのOperatorから、データベースのフェイルオーバー、証明書のローテーション、マルチクラスターレプリケーションを自動的に管理する洗練されたGoコントローラーまで、幅広く適用できます。
ステートフルなアプリケーションの10以上のインスタンスを管理し、人間の運用作業がボトルネックになるとき、Operatorへの投資は報われます。その規模では、クラスター内で24時間365日実行されるエンコードされた運用知識は、ランブックやオンコールエンジニアよりもはるかに信頼性が高くなります。
AI CLIログの整形、Cron式の作成、JWTの解析、chmod権限の計算をオフラインで実行。

OperatorSDKとKubebuilderをGoで使い、CRDスキャフォールディング、冪等なreconcileループ、ファイナライザー、envtest統合スイートなど、本番環境向けのKubernetes Operatorを構築するための実践ガイドです。
続きを読む
GoオペレーターでKubernetes Custom Resource Definitions (CRD) を設計・デプロイし、クラウドネイティブインフラストラクチャを自動化し、状態ループを調和させます。
続きを読む
Kubebuilderを使用してGoで本番KubernetesOperatorを構築し、ステートフルワークロードとカスタムリソースを管理するためのステップバイステップチュートリアル。
続きを読む