はじめに
基本的な Reconcile ループを書いたことがある方なら、次は Operator を「デモ」から「本番」へ引き上げる番です。その境界線上にある 3 つのテーマが、Finalizer、Admission Webhook、そして Status/Conditions 設計です。
この 3 つが重要な理由はシンプルです。Reconcile ループは「望ましい状態への収束」に集中しますが、実際の運用ではそれ以外の問いが絶えず生じるからです。
- ユーザーが CR(Custom Resource)を削除したとき、外部に作成したクラウドリソース(S3 バケット、DNS レコード、外部 DB など)は誰が片付けるのでしょうか。(Finalizer)
- 不正な spec をそもそもクラスターに入れさせない、あるいはデフォルト値を埋めるにはどうすればよいでしょうか。(Admission Webhook)
- `kubectl get` でリソースの状態を一目で見せ、他のコントローラーや人間が「このリソースは準備できているか」を標準的に判断できるようにするにはどうすればよいでしょうか。(Status/Conditions)
本記事では、Kubebuilder が Kubernetes 1.36 / Go 1.26 をサポートし、controller-runtime が v0.24.x、controller-tools が v0.21.x である 2026 年基準で、各テーマを実践コードとともに説明します。また、かつてメトリクスエンドポイントの保護に使われていた kube-rbac-proxy が削除され、controller-runtime の `WithAuthenticationAndAuthorization` フィルターに置き換わった変更も反映しています。
まず全体像を 1 枚にまとめます。
+-------------------------------+
kubectl apply --> | Admission (mutating) | defaulting
| - デフォルト値を埋める |
+---------------+---------------+
|
v
+-------------------------------+
| Admission (validating) | validation
| - spec 検証・拒否 |
+---------------+---------------+
|
v
+-------------------------------+
| etcd 保存 (spec) |
+---------------+---------------+
|
v
+-------------------------------+
| Reconcile ループ | desired-state 収束
| - 外部リソース作成/更新 |
| - finalizer 追加 |
| - status/conditions 更新 |
+-------------------------------+
Finalizer で外部リソースをクリーンアップする
Finalizer とは何か
Finalizer は、オブジェクトの `metadata.finalizers` フィールドに入っている文字列のリストです。このリストが空でない限り、ユーザーが削除を要求しても API サーバーはオブジェクトを即座に消しません。代わりに `metadata.deletionTimestamp` を設定して「削除予定」の状態としてマークするだけです。ガベージコレクション(GC)は finalizer リストが完全に空になるまで待機します。
このメカニズムを使えば、コントローラーが「外部クリーンアップが完了した」と確信するまで、オブジェクトが実際に消えないように引き止めておけます。
ユーザー: kubectl delete myresource foo
|
v
+-------------------------------------------+
| API サーバー |
| finalizers は空か? |
+--------------------+----------------------+
| 空 | 空でない
v v
+----------------+ +-------------------------------+
| 即座に GC 削除 | | deletionTimestamp 設定 |
+----------------+ | (オブジェクトは残る、削除予定)|
+---------------+---------------+
|
v
+-------------------------------+
| Reconcile 再呼び出し |
| deletionTimestamp != 0 検出 |
| -> 外部リソースをクリーンアップ|
| -> finalizer を削除 |
+---------------+---------------+
|
v
+-------------------------------+
| finalizers 空 -> GC 削除 |
+-------------------------------+
肝心なのは「削除 = 即座に消える」ではなく、「削除 = deletionTimestamp が刻まれ、Reconcile がもう一度呼ばれる」という点です。この 1 回の追加呼び出しが、外部リソースをクリーンアップする最後の機会です。
Finalizer フローを Reconcile に実装する
controller-runtime は finalizer の追加・削除・確認を助けるヘルパーを提供します。`sigs.k8s.io/controller-runtime/pkg/controller/controllerutil` パッケージの `AddFinalizer`、`RemoveFinalizer`、`ContainsFinalizer` がそれです。
package controller
"context"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
cloudv1 "example.com/operator/api/v1"
)
const bucketFinalizer = "cloud.example.com/bucket-cleanup"
func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
var bucket cloudv1.Bucket
if err := r.Get(ctx, req.NamespacedName, &bucket); err != nil {
// NotFound はすでに削除済みなので無視する。
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 1) 削除要求かどうかを判定する。
if !bucket.ObjectMeta.DeletionTimestamp.IsZero() {
// 削除進行中。finalizer がまだ自分のものならクリーンアップする。
if controllerutil.ContainsFinalizer(&bucket, bucketFinalizer) {
if err := r.cleanupExternalBucket(ctx, &bucket); err != nil {
// クリーンアップ失敗時は finalizer を残してリトライさせる。
log.Error(err, "外部バケットのクリーンアップに失敗")
return ctrl.Result{}, err
}
// クリーンアップ成功。finalizer を削除 -> GC がオブジェクトを消す。
controllerutil.RemoveFinalizer(&bucket, bucketFinalizer)
if err := r.Update(ctx, &bucket); err != nil {
return ctrl.Result{}, err
}
}
// finalizer がなければやることはない。
return ctrl.Result{}, nil
}
// 2) 通常の作成/更新パス: finalizer がなければ先に追加する。
if !controllerutil.ContainsFinalizer(&bucket, bucketFinalizer) {
controllerutil.AddFinalizer(&bucket, bucketFinalizer)
if err := r.Update(ctx, &bucket); err != nil {
return ctrl.Result{}, err
}
// Update が新しいリソースバージョンを作ったので即座に返し、
// 次の Reconcile で本作業を続ける。
return ctrl.Result{}, nil
}
// 3) 実際の外部リソース収束ロジック。
if err := r.reconcileExternalBucket(ctx, &bucket); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
上記コードの中心となるパターンをまとめると次のとおりです。
1. **`DeletionTimestamp.IsZero()`** で削除中かどうかを最初に確認します。
2. 削除中なら外部リソースをクリーンアップし、成功した後にのみ finalizer を削除します。クリーンアップに失敗したらエラーを返してリトライさせ、finalizer はそのまま残します。
3. 通常パスでは本格的な収束ロジックの前に finalizer を追加します。外部リソースを作る前に finalizer が先に付いていないと、途中で削除要求が来てもクリーンアップの機会を失います。
外部クリーンアップ関数と冪等性
`cleanupExternalBucket` は冪等(idempotent)でなければなりません。Reconcile は同じオブジェクトに対して何度も呼ばれうるため、すでに消えたバケットを再度消そうとする試みがエラーを投げないように処理する必要があります。
func (r *BucketReconciler) cleanupExternalBucket(ctx context.Context, bucket *cloudv1.Bucket) error {
name := bucket.Status.ExternalName
if name == "" {
// まだ外部リソースを作ったことがない -> 片付けるものはない。
return nil
}
err := r.CloudAPI.DeleteBucket(ctx, name)
if isNotFound(err) {
// すでに存在しない -> 成功とみなす(冪等性)。
return nil
}
return err
}
`Status.ExternalName` が空であるか、外部 API が「すでに存在しない」を返したら成功として扱います。こうすることでクリーンアップ処理を安全に繰り返せます。
Admission Webhook: Defaulting と Validation
Webhook の種類
Admission Webhook は、オブジェクトが etcd に保存される前に割り込むフックです。2 種類あります。
| 種類 | 目的 | オブジェクトを変更できるか | 代表的な用途 |
| --- | --- | --- | --- |
| Mutating | オブジェクト変形 | 可能 | デフォルト値の充填、ラベル注入 |
| Validating | オブジェクト検証 | 不可 | spec の拒否、ポリシー強制 |
順序は常に Mutating が先、Validating が後です。つまりデフォルト値を埋めた後に検証するため、検証ロジックは常に完成したオブジェクトを見ると仮定できます。
Kubebuilder で Webhook を生成する
Kubebuilder は webhook のスキャフォールディングをコマンド 1 行で提供します。以下は defaulting(mutating)と validation(validating)の両方を有効にする例です。
kubebuilder create webhook \
--group cloud \
--version v1 \
--kind Bucket \
--defaulting \
--programmatic-validation
このコマンドは `internal/webhook/v1/bucket_webhook.go` のようなファイルと、登録のためのマーカー、そして `config/webhook/` 配下のマニフェストパッチを生成します。
Defaulting (Mutating) の実装
最新の controller-runtime の webhook API は `admission.CustomDefaulter` インターフェースを使います。`Default` メソッドの中でオブジェクトにデフォルト値を埋めます。
package webhookv1
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
cloudv1 "example.com/operator/api/v1"
)
// +kubebuilder:webhook:path=/mutate-cloud-example-com-v1-bucket,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloud.example.com,resources=buckets,verbs=create;update,versions=v1,name=mbucket-v1.kb.io,admissionReviewVersions=v1
type BucketCustomDefaulter struct {
DefaultRegion string
}
var _ admission.CustomDefaulter = &BucketCustomDefaulter{}
func (d *BucketCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
bucket, ok := obj.(*cloudv1.Bucket)
if !ok {
return fmt.Errorf("expected a Bucket object but got %T", obj)
}
log := logf.FromContext(ctx)
// region が空ならデフォルト値を埋める。
if bucket.Spec.Region == "" {
bucket.Spec.Region = d.DefaultRegion
log.Info("defaulting region", "region", d.DefaultRegion)
}
// versioning が明示されていなければ安全にオンにする。
if bucket.Spec.Versioning == nil {
enabled := true
bucket.Spec.Versioning = &enabled
}
return nil
}
Validation (Validating) の実装
検証は `admission.CustomValidator` インターフェースを実装します。作成・更新・削除それぞれのメソッドを持ち、拒否するにはエラーを返します。
// +kubebuilder:webhook:path=/validate-cloud-example-com-v1-bucket,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloud.example.com,resources=buckets,verbs=create;update,versions=v1,name=vbucket-v1.kb.io,admissionReviewVersions=v1
type BucketCustomValidator struct{}
var _ admission.CustomValidator = &BucketCustomValidator{}
func (v *BucketCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
bucket, ok := obj.(*cloudv1.Bucket)
if !ok {
return nil, fmt.Errorf("expected a Bucket object but got %T", obj)
}
return v.validate(bucket)
}
func (v *BucketCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
oldBucket := oldObj.(*cloudv1.Bucket)
newBucket := newObj.(*cloudv1.Bucket)
// region は不変フィールドとして強制する。
if oldBucket.Spec.Region != newBucket.Spec.Region {
return nil, fmt.Errorf("spec.region is immutable")
}
return v.validate(newBucket)
}
func (v *BucketCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
return nil, nil
}
func (v *BucketCustomValidator) validate(bucket *cloudv1.Bucket) (admission.Warnings, error) {
var warnings admission.Warnings
if bucket.Spec.Region == "" {
return nil, fmt.Errorf("spec.region must not be empty")
}
allowed := map[string]bool{"ap-northeast-2": true, "us-east-1": true}
if !allowed[bucket.Spec.Region] {
return nil, fmt.Errorf("spec.region %q is not allowed", bucket.Spec.Region)
}
if bucket.Spec.RetentionDays > 365 {
warnings = append(warnings, "retentionDays が 365 を超えています。コストに注意してください。")
}
return warnings, nil
}
`admission.Warnings` を返すと、拒否せずにユーザーへ警告を表示できます。ポリシー上ブロックすべきならエラーを、勧告のみなら warning を使います。
Webhook の登録
webhook はマネージャーに登録される必要があります。最新の Kubebuilder スタイルでは、`SetupWebhookWithManager` 関数を通じてビルダーで接続します。
func SetupBucketWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&cloudv1.Bucket{}).
WithDefaulter(&BucketCustomDefaulter{DefaultRegion: "ap-northeast-2"}).
WithValidator(&BucketCustomValidator{}).
Complete()
}
cert-manager で証明書を管理する
Admission Webhook は HTTPS で呼ばれるため、API サーバーが信頼する TLS 証明書が必要です。手動での管理は煩雑なので、cert-manager を使うのが標準です。Kubebuilder は `config/certmanager/` 配下に関連マニフェストを生成してくれます。
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-issuer
namespace: system
spec:
selfSigned: {}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: serving-cert
namespace: system
spec:
dnsNames:
- webhook-service.system.svc
- webhook-service.system.svc.cluster.local
issuerRef:
kind: Issuer
name: selfsigned-issuer
secretName: webhook-server-cert
そして webhook 設定で `cert-manager.io/inject-ca-from` アノテーションにより CA バンドルを自動注入します。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: bucket-validating-webhook
annotations:
cert-manager.io/inject-ca-from: system/serving-cert
webhooks:
- name: vbucket-v1.kb.io
failurePolicy: Fail
sideEffects: None
admissionReviewVersions: ["v1"]
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-cloud-example-com-v1-bucket
rules:
- apiGroups: ["cloud.example.com"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["buckets"]
Status サブリソースと Conditions 標準
Status サブリソースを使う理由
`status` をサブリソースとして宣言すると、spec と status が別々のエンドポイントで管理されます。こうすると、コントローラーが status だけを更新するときに spec の `resourceVersion` 競合を避けられ、ユーザーの spec 編集とコントローラーの status 更新が互いを上書きしません。
CRD の型にマーカーを追加します。
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type Bucket struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BucketSpec `json:"spec,omitempty"`
Status BucketStatus `json:"status,omitempty"`
}
Conditions の標準構造
Kubernetes エコシステムは、status に `conditions` 配列を置くことを標準的な慣習としています。各 Condition は次のフィールドを持ちます。
| フィールド | 意味 |
| --- | --- |
| Type | 条件名(例: Ready, Progressing) |
| Status | True / False / Unknown |
| Reason | 機械が読む短い理由コード(CamelCase) |
| Message | 人間が読む説明 |
| LastTransitionTime | 状態が最後に変わった時刻 |
| ObservedGeneration | この条件が反映した spec 世代 |
`metav1.Condition` 型をそのまま使えます。
type BucketStatus struct {
// ObservedGeneration はコントローラーが最後に処理した spec 世代。
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// ExternalName は実際に作成された外部バケット名。
ExternalName string `json:"externalName,omitempty"`
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
meta.SetStatusCondition の使用
`k8s.io/apimachinery/pkg/api/meta` パッケージの `SetStatusCondition` は、同じ Type の Condition がすでにあれば更新し、なければ追加します。Status が変わったときだけ `LastTransitionTime` を更新する点が肝心です。
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (r *BucketReconciler) markReady(ctx context.Context, bucket *cloudv1.Bucket, ready bool, reason, msg string) error {
status := metav1.ConditionFalse
if ready {
status = metav1.ConditionTrue
}
meta.SetStatusCondition(&bucket.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: status,
Reason: reason,
Message: msg,
ObservedGeneration: bucket.Generation,
})
// observedGeneration も合わせて更新し「この世代を処理した」と記録する。
bucket.Status.ObservedGeneration = bucket.Generation
// 必ず Status().Update() を使い status サブリソースだけを更新する。
return r.Status().Update(ctx, bucket)
}
`r.Update()` ではなく `r.Status().Update()` を使う点に注意してください。前者は spec を、後者は status サブリソースを更新します。
Reconcile で Conditions を埋める
func (r *BucketReconciler) reconcileExternalBucket(ctx context.Context, bucket *cloudv1.Bucket) error {
// 進行中の状態を記録する。
if err := r.markCondition(ctx, bucket, "Progressing", metav1.ConditionTrue,
"Creating", "外部バケット作成中"); err != nil {
return err
}
name, err := r.CloudAPI.EnsureBucket(ctx, bucket.Spec.Region)
if err != nil {
_ = r.markCondition(ctx, bucket, "Ready", metav1.ConditionFalse,
"CreateFailed", err.Error())
return err
}
bucket.Status.ExternalName = name
return r.markCondition(ctx, bucket, "Ready", metav1.ConditionTrue,
"Created", "外部バケット準備完了")
}
additionalPrinterColumns で kubectl 出力を整える
`kubectl get` で status を一目で見せるには、printer column のマーカーを追加します。controller-tools が CRD に `additionalPrinterColumns` を生成してくれます。
// +kubebuilder:printcolumn:name="Region",type=string,JSONPath=`.spec.region`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="External",type=string,JSONPath=`.status.externalName`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
type Bucket struct {
// ...
}
生成された CRD には次のような YAML が入ります。
additionalPrinterColumns:
- name: Region
type: string
jsonPath: .spec.region
- name: Ready
type: string
jsonPath: .status.conditions[?(@.type=="Ready")].status
- name: External
type: string
jsonPath: .status.externalName
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
これでユーザーは次のように状態を 1 行で確認できます。
NAME REGION READY EXTERNAL AGE
my-data ap-northeast-2 True my-data-9af31 12m
よくある落とし穴
Finalizer 漏れによるリソースリーク
外部リソースを作る前に finalizer を追加しないと、素早い削除要求の際に Reconcile が「クリーンアップの機会」を得られないままオブジェクトが GC されます。その結果、クラウドに孤立リソースが残り、コストが漏れ出します。必ず外部リソースの作成前に finalizer を先に付けてください。
failurePolicy によるクラスター麻痺
Validating Webhook の `failurePolicy: Fail` は、webhook サーバーが応答できないと該当リソースのすべての作成・更新を拒否します。webhook が自身が管理するリソースに依存していたり、webhook Pod が落ちたりすると、クラスターの一部 API が麻痺する可能性があります。
| failurePolicy | webhook 障害時の動作 | 適する場合 |
| --- | --- | --- |
| Fail | 要求を拒否 | セキュリティ・ポリシー強制が重要なとき |
| Ignore | 要求を通過 | 可用性がより重要なとき |
コアなシステム名前空間は `namespaceSelector` で webhook 対象から除外し、webhook 障害がクラスターのブートストラップを妨げないようにするのが安全です。
Webhook タイムアウト
webhook はデフォルトのタイムアウト(`timeoutSeconds`)が短いです。外部 API 呼び出しのような遅い作業を webhook の中で行うと、タイムアウトで要求が拒否されることがあります。webhook は高速なインメモリ検証・デフォルト処理だけに使い、重い作業は Reconcile に回してください。
処理順序の混同
Mutating は常に Validating より先に実行されます。したがって Validating ではデフォルト値がすでに埋まっていると仮定できます。逆に Mutating であるフィールドを埋め忘れると、Validating が「空フィールド」を理由に拒否することがあるので、2 つのフックの責務を明確に分けてください。
Status 更新の競合
status を `r.Update()` で更新すると、spec と競合したり、サブリソース設定が無視されたりすることがあります。常に `r.Status().Update()` を使い、`ObservedGeneration` を合わせて更新して「どの世代を処理したか」を明確に記録してください。
テスト
Finalizer のテスト
envtest を使うと実際の API サーバーを立てて finalizer フローを検証できます。オブジェクトを作り、削除要求を送った後、外部クリーンアップが呼ばれて finalizer が削除されるかを確認します。
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var _ = Describe("Bucket finalizer", func() {
It("削除時に外部リソースをクリーンアップする", func() {
bucket := &cloudv1.Bucket{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Spec: cloudv1.BucketSpec{Region: "ap-northeast-2"},
}
Expect(k8sClient.Create(ctx, bucket)).To(Succeed())
// finalizer が追加されるまで待つ。
Eventually(func() bool {
_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(bucket), bucket)
return len(bucket.Finalizers) > 0
}).Should(BeTrue())
// 削除要求。
Expect(k8sClient.Delete(ctx, bucket)).To(Succeed())
// 最終的にオブジェクトが消えなければならない。
Eventually(func() bool {
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(bucket), bucket)
return client.IgnoreNotFound(err) == nil && err != nil
}).Should(BeTrue())
// モックのクラウド API で削除が呼ばれたかを確認する。
Expect(fakeCloud.DeletedBuckets()).To(ContainElement("test"))
})
})
Webhook のテスト
webhook テストは、defaulter と validator を直接呼び出す単体テストで十分にカバーできます。envtest に webhook サーバーを付けて統合テストも可能です。
var _ = Describe("Bucket validator", func() {
It("許可されていない region を拒否する", func() {
v := &BucketCustomValidator{}
bucket := &cloudv1.Bucket{Spec: cloudv1.BucketSpec{Region: "mars-1"}}
_, err := v.ValidateCreate(ctx, bucket)
Expect(err).To(HaveOccurred())
})
It("デフォルト region を埋める", func() {
d := &BucketCustomDefaulter{DefaultRegion: "ap-northeast-2"}
bucket := &cloudv1.Bucket{}
Expect(d.Default(ctx, bucket)).To(Succeed())
Expect(bucket.Spec.Region).To(Equal("ap-northeast-2"))
})
})
運用チェックリスト
| 項目 | 確認 |
| --- | --- |
| 外部リソース作成前に finalizer 追加 | 必須 |
| クリーンアップ関数の冪等性 | 必須 |
| Mutating は高速なデフォルト処理のみ | 推奨 |
| Validating で不変フィールドを強制 | 推奨 |
| failurePolicy と namespaceSelector の検討 | 必須 |
| status は Status().Update() のみで更新 | 必須 |
| ObservedGeneration の記録 | 推奨 |
| printer columns で可視性を確保 | 推奨 |
| cert-manager で webhook 証明書を自動化 | 推奨 |
おわりに
Finalizer、Admission Webhook、Status/Conditions は、Operator を「動くデモ」から「運用できるコントローラー」へ引き上げる 3 本の柱です。それぞれをまとめると次のとおりです。
- **Finalizer** は削除に割り込み、外部リソースを安全にクリーンアップする最後の機会を保証します。外部リソースを作る前に付け、クリーンアップに成功した後にのみ削除してください。
- **Webhook** は不正な spec をクラスター進入段階でブロックし、デフォルト値を埋めます。Mutating は高速に、Validating は厳格に、そして failurePolicy と timeout を慎重に設定してください。
- **Status/Conditions** はリソースの状態を標準的な方法で公開し、人間と他のコントローラーが同じ方法で「準備完了」を判断できるようにします。`meta.SetStatusCondition` と `ObservedGeneration` を一貫して使ってください。
この 3 つを冪等性という共通原則の上で実装すれば、リトライと並行性にも揺らがない堅牢な Operator を作れます。
References
- [Kubebuilder Book](https://kubebuilder.io/book/)
- [Operator SDK Documentation](https://sdk.operatorframework.io/)
- [controller-runtime GoDoc](https://pkg.go.dev/sigs.k8s.io/controller-runtime)
- [Kubernetes: Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
- [kubernetes-sigs/kubebuilder (GitHub)](https://github.com/kubernetes-sigs/kubebuilder)
- [kubernetes-sigs/controller-runtime (GitHub)](https://github.com/kubernetes-sigs/controller-runtime)
- [Kubernetes: Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
- [cert-manager Documentation](https://cert-manager.io/docs/)
- [Kubernetes: Finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/)
현재 단락 (1/438)
基本的な Reconcile ループを書いたことがある方なら、次は Operator を「デモ」から「本番」へ引き上げる番です。その境界線上にある 3 つのテーマが、Finalizer、Admissi...