Kubernetes Custom Resource Lifecycle
Kubernetes Custom Resource Lifecycle manages creation, updates, and deletion of custom resources, extending Kubernetes API for infrastructure and operations.
Kubernetes Custom Resource Lifecycle is the ordered sequence of states a single custom resource instance passes through from initial submission to final removal from etcd, encompassing admission, persistence, reconciliation, readiness, update handling, deletion initiation, finalization, and garbage collection as a single coherent state progression rather than a set of independent operational tasks.
Phase One: Admission
Schema Validation and Mutation
Before an object is ever persisted, it passes through the same admission chain as any Kubernetes resource: schema and CEL validation, then any registered mutating webhooks (which may inject defaults or normalize fields), then any validating webhooks, in that fixed order.
apiVersion: databases.example.com/v1
kind: PostgresCluster
metadata:
name: orders-db
spec:
replicas: 3
A request rejected at this phase never reaches the next; the object does not exist in any form and no controller is ever made aware of the attempted creation.
Phase Two: Persistence
Writing to etcd via the Storage Version
Once admission succeeds, the object is converted (if necessary) to the CRD's designated storage version and written to etcd, at which point it becomes visible to LIST and WATCH calls, and metadata.generation is initialized to 1.
Phase Three: Initial Reconciliation
The Controller Observes Creation
The owning controller's informer receives an ADDED event, triggering the first reconcile pass; because the reconcile loop is level-triggered, this pass reads the full current spec rather than treating it as an incremental change, and typically creates the dependent objects (a StatefulSet, a Service, a Secret) needed to bring the desired state into existence.
func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var cluster databasesv1.PostgresCluster
r.Get(ctx, req.NamespacedName, &cluster)
r.ensureStatefulSet(ctx, &cluster)
r.ensureService(ctx, &cluster)
cluster.Status.ObservedGeneration = cluster.Generation
return ctrl.Result{}, r.Status().Update(ctx, &cluster)
}
Reaching Ready State
Subsequent reconcile passes update status.conditions as dependent objects converge toward their own ready state (a StatefulSet reporting all replicas available, for instance), until the custom resource's own Available condition transitions to True, marking the instance operationally ready.
status:
observedGeneration: 1
conditions:
- type: Available
status: "True"
reason: AllReplicasReady
Phase Four: Update Handling
Spec Changes Increment Generation
kubectl patch postgrescluster orders-db --type merge -p '{"spec":{"replicas":5}}'
Any change to spec increments metadata.generation, triggering a new MODIFIED watch event and a fresh reconcile pass; because status.observedGeneration was previously set to the prior generation, comparing the two values lets both the controller and any external client detect that reconciliation of the new desired state is still in progress.
Status-Only Updates Do Not Advance Generation
Because status is a separate subresource, controller-written status updates do not increment metadata.generation, meaning the generation counter reflects only user-declared intent, not the controller's own progress, preserving a clean signal of "how many times has intent changed" distinct from "how many times has the controller written status."
Phase Five: Deletion Initiation
Setting the Deletion Timestamp
kubectl delete postgrescluster orders-db
A DELETE request against an object holding one or more finalizers does not remove it; the API server instead sets metadata.deletionTimestamp, and the object remains listable and watchable in a terminating state, still subject to reconciliation but now understood by the controller as "being torn down" rather than "being created or updated."
if !cluster.DeletionTimestamp.IsZero() {
return r.handleDeletion(ctx, &cluster)
}
Phase Six: Finalization
External Cleanup Before Removal
The controller's deletion-handling path performs any necessary external cleanup (deleting cloud snapshots, deregistering external DNS, revoking issued credentials) and only then removes its finalizer entries from metadata.finalizers.
func (r *PostgresClusterReconciler) handleDeletion(ctx context.Context, cluster *databasesv1.PostgresCluster) (ctrl.Result, error) {
if err := r.deleteExternalBackups(ctx, cluster); err != nil {
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(cluster, "databases.example.com/cleanup-backups")
return ctrl.Result{}, r.Update(ctx, cluster)
}
Phase Seven: Garbage Collection and Final Removal
Dependents Removed via ownerReferences
Once every finalizer is cleared, the API server permanently removes the object from etcd, and the built-in garbage collector concurrently removes every dependent object whose ownerReferences pointed to it, completing the lifecycle with no remaining trace of either the custom resource instance or its generated dependents.
metadata:
ownerReferences:
- apiVersion: databases.example.com/v1
kind: PostgresCluster
name: orders-db
controller: true
Relationship to Custom Resource Management and the Extension Model
The lifecycle described here is the temporal thread running through every concern covered under custom resource management — labeling, ownership, finalizers — and it is the concrete, phase-by-phase instantiation of the abstract reconciliation pattern defined by the broader Kubernetes extension model: admission, persistence, reconciliation, update, deletion, finalization, and garbage collection are the fixed sequence every custom resource instance passes through, regardless of which specific Operator or controller implementation manages it.