Kubernetes Operators Cheat Sheet
Kubernetes Operator pattern covering CRDs, controller reconcile loops, Kubebuilder/Operator SDK scaffolding, and status subresources.
Custom Resource Definition
Defining a CRD that an operator will watch and reconcile.
apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: postgresclusters.db.example.comspec: group: db.example.com names: kind: PostgresCluster plural: postgresclusters singular: postgrescluster shortNames: [pgc] scope: Namespaced versions: - name: v1 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: replicas: type: integer version: type: string
Reconcile Loop (Kubebuilder / controller-runtime, Go)
The core Reconcile function every operator implements.
func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var cluster dbv1.PostgresCluster if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Desired state sts := desiredStatefulSet(&cluster) if err := ctrl.SetControllerReference(&cluster, sts, r.Scheme); err != nil { return ctrl.Result{}, err } // Create or update to converge actual -> desired if err := r.Patch(ctx, sts, client.Apply, client.ForceOwnership, client.FieldOwner("pgc-operator")); err != nil { return ctrl.Result{}, err } cluster.Status.ReadyReplicas = sts.Status.ReadyReplicas if err := r.Status().Update(ctx, &cluster); err != nil { return ctrl.Result{}, err } return ctrl.Result{RequeueAfter: 30 * time.Second}, nil}
Kubebuilder Scaffolding Commands
Bootstrap an operator project and add API types/controllers.
# Initialize a new operator projectkubebuilder init --domain example.com --repo github.com/acme/pgc-operator# Scaffold a new API (CRD + controller)kubebuilder create api --group db --version v1 --kind PostgresCluster# Generate CRD manifests and DeepCopy methods after editing typesmake manifests generate# Run the controller locally against your current kube contextmake install run# Build and push the operator image, then deploymake docker-build docker-push IMG=acme/pgc-operator:v0.1.0make deploy IMG=acme/pgc-operator:v0.1.0
Operator Pattern Concepts
Terminology central to writing and running Kubernetes operators.
- CRD (Custom Resource Definition)- registers a new kind with the Kubernetes API server
- CR (Custom Resource)- an instance of a CRD, e.g. a specific PostgresCluster object
- Controller / reconciler- watches CRs and drives actual cluster state toward the spec
- Level-triggered reconciliation- reconcile logic reads full current state each time, not just the diff, for resilience
- Owner references- link child resources (StatefulSets, Services) to the CR for garbage collection
- Status subresource- separate write path for status so spec updates don't clobber observed state
- Finalizers- block deletion of a CR until the operator cleans up external resources
Finalizer for External Resource Cleanup
Block deletion of the CR until the operator has torn down resources it created outside Kubernetes (e.g. a cloud database).
const pgcFinalizer = "db.example.com/finalizer"func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var cluster dbv1.PostgresCluster if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if cluster.DeletionTimestamp.IsZero() { if !controllerutil.ContainsFinalizer(&cluster, pgcFinalizer) { controllerutil.AddFinalizer(&cluster, pgcFinalizer) return ctrl.Result{}, r.Update(ctx, &cluster) } } else { if controllerutil.ContainsFinalizer(&cluster, pgcFinalizer) { if err := r.deleteExternalDatabase(ctx, &cluster); err != nil { return ctrl.Result{}, err // retry until external cleanup succeeds } controllerutil.RemoveFinalizer(&cluster, pgcFinalizer) return ctrl.Result{}, r.Update(ctx, &cluster) } return ctrl.Result{}, nil } // normal reconcile logic continues here... return ctrl.Result{}, nil}
Status Conditions Pattern
Report structured, machine-readable status using the standard metav1.Condition type instead of a bare string/phase field.
import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1")const ConditionTypeReady = "Ready"func (r *PostgresClusterReconciler) markReady(ctx context.Context, c *dbv1.PostgresCluster, ready bool, reason, msg string) error { status := metav1.ConditionFalse if ready { status = metav1.ConditionTrue } meta.SetStatusCondition(&c.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: status, Reason: reason, Message: msg, ObservedGeneration: c.Generation, }) return r.Status().Update(ctx, c)}// kubectl get postgrescluster my-db -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
Manager Setup with Leader Election & Owns/Watches
Wire the controller-runtime manager for HA (single active reconciler) and register secondary resources it should also trigger on.
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: ":8080", HealthProbeBindAddress: ":8081", LeaderElection: true, LeaderElectionID: "pgc-operator.example.com", LeaderElectionNamespace: "pgc-system",})err = ctrl.NewControllerManagedBy(mgr). For(&dbv1.PostgresCluster{}). Owns(&appsv1.StatefulSet{}). // requeue owner when a child StatefulSet changes Owns(&corev1.Service{}). Watches( &source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.configMapToClusters), ). Complete(r)
Kubebuilder Marker Reference
Comment-based code-gen markers that drive CRD schema, RBAC, and webhook manifest generation from `make manifests`.
- +kubebuilder:validation:Minimum=1- adds an OpenAPI validation constraint to a CRD field (e.g. replicas >= 1)
- +kubebuilder:default=3- sets a default value applied by the API server when the field is omitted
- +kubebuilder:printcolumn- adds a custom column to `kubectl get <kind>` output, e.g. Ready status or Age
- +kubebuilder:subresource:status- enables the /status subresource so spec and status updates go through separate write paths
- +kubebuilder:rbac:groups=...,resources=...,verbs=...- generates the RBAC Role the controller needs, aggregated into role.yaml
- +kubebuilder:webhook:path=...,mutating=true- registers a webhook server path and generates the corresponding MutatingWebhookConfiguration
- +kubebuilder:object:root=true- marks a type as a root API object so DeepCopyObject() is generated for it
Validating Webhook Configuration
Reject invalid CR changes at admission time instead of failing deep inside the reconcile loop.
apiVersion: admissionregistration.k8s.io/v1kind: ValidatingWebhookConfigurationmetadata: name: pgc-operator-validating-webhookwebhooks: - name: vpostgrescluster.kb.io admissionReviewVersions: ["v1"] sideEffects: None failurePolicy: Fail clientConfig: service: name: pgc-operator-webhook-service namespace: pgc-system path: /validate-db-example-com-v1-postgrescluster rules: - apiGroups: ["db.example.com"] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["postgresclusters"]
Always requeue with a bounded RequeueAfter (or watch dependent resources) instead of relying purely on event-driven triggers — external state (like a cloud database being deleted out-of-band) can drift without generating a Kubernetes watch event, and only periodic reconciliation catches it.