✦ For everyone, free.

Practical knowledge for real and everyday life

Home

Kubernetes Controller Reconciliation Management

Kubernetes Controller Reconciliation Management keeps clusters consistent by comparing desired and actual states, then taking action to align them.

Kubernetes Controller Reconciliation Management is the practice of designing the internal logic of a single Reconcile function so that it is idempotent, converges within a bounded number of passes, tolerates partial failure gracefully, and produces the same outcome whether triggered by a real change, a periodic resync, or a redundant duplicate event.


Idempotency as the Governing Constraint

Reconcile Must Be Safe to Run Repeatedly

Because the level-triggered model can invoke Reconcile any number of times for the same object, with no correctness guarantee tying invocation count to the number of actual changes, every action taken inside the function must be safe to repeat: creating an already-existing dependent object must not error, applying an already-applied change must be a no-op.

sts := &appsv1.StatefulSet{}
err := r.Get(ctx, key, sts)
if apierrors.IsNotFound(err) {
    return r.Create(ctx, desiredStatefulSet(cluster))
}
if err != nil {
    return ctrl.Result{}, err
}
return r.Update(ctx, mergeDesiredState(sts, cluster))

The get-then-create-or-update pattern above, rather than an unconditional Create call, is the standard idempotent structure for ensuring a dependent object exists in the desired shape regardless of how many times reconciliation runs.


Diff-Based Convergence

Applying Only the Necessary Delta

desired := desiredStatefulSet(cluster)
if !equality.Semantic.DeepEqual(current.Spec, desired.Spec) {
    current.Spec = desired.Spec
    return r.Update(ctx, current)
}

Comparing computed desired state against the currently observed state before issuing a write, rather than unconditionally overwriting on every pass, minimizes unnecessary etcd writes and avoids triggering downstream watch events (and therefore further reconciles of dependent controllers) for changes that did not actually occur.

Server-Side Apply as a Declarative Alternative

patch := &appsv1.StatefulSet{
    TypeMeta:   metav1.TypeMeta{APIVersion: "apps/v1", Kind: "StatefulSet"},
    ObjectMeta: metav1.ObjectMeta{Name: cluster.Name, Namespace: cluster.Namespace},
    Spec:       desiredSpec,
}
err := r.Patch(ctx, patch, client.Apply, client.ForceOwnership, client.FieldOwner("postgres-operator"))

Server-side apply lets a controller declare its desired fields directly without first reading current state, with the API server itself computing and applying the diff based on field ownership, which simplifies reconcile logic and correctly handles the case where a different actor (a human, another controller) owns and continues to manage other fields on the same object.

Applied Patch = Desired Fields Fields Owned by Others

Ordering and Partial Failure

Sequencing Dependent Object Creation

if err := r.ensureSecret(ctx, cluster); err != nil {
    return ctrl.Result{}, fmt.Errorf("ensuring secret: %w", err)
}
if err := r.ensureStatefulSet(ctx, cluster); err != nil {
    return ctrl.Result{}, fmt.Errorf("ensuring statefulset: %w", err)
}

Reconcile logic typically orders dependent object creation so that prerequisites (a Secret holding credentials) are ensured before the objects that consume them (a StatefulSet mounting that Secret), and returning early on the first failure, combined with the idempotent get-then-create-or-update pattern, means the next reconcile pass safely resumes from wherever the previous attempt stopped rather than restarting from scratch or duplicating already-completed steps.

Bounded Convergence

A well-designed reconcile function is expected to converge within a small, bounded number of passes for any given spec change, rather than requiring an unbounded sequence of incremental steps; a reconcile loop that appears to make only partial progress on every invocation, requiring dozens of passes to fully converge, typically indicates a design flaw such as a missing precondition check or an unnecessarily fine-grained sequence of separate reconcile-triggering updates.


Testing Reconcile Logic

Table-Driven Unit Tests Against a Fake Client

func TestReconcile_CreatesStatefulSet(t *testing.T) {
    cluster := &databasesv1.PostgresCluster{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}}
    fakeClient := fake.NewClientBuilder().WithObjects(cluster).Build()
    r := &PostgresClusterReconciler{Client: fakeClient}
    _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"}})
    require.NoError(t, err)
    var sts appsv1.StatefulSet
    require.NoError(t, fakeClient.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: "default"}, &sts))
}

A fake client lets reconcile logic be exercised without a real API server, verifying that a given starting state produces the expected dependent objects and status, while integration tests using envtest verify the same logic against real admission, validation, and watch behavior before it reaches production.


Avoiding Side Effects Outside Declared Ownership

Scoping Actions to Owned Resources

Reconcile logic should act only on objects it owns via ownerReferences or explicitly references by name in spec, never on broadly discovered objects matching an incidental label; acting outside this boundary risks a controller silently modifying or deleting resources belonging to an entirely unrelated part of the cluster, a failure mode distinct from and more dangerous than a simple reconcile bug, since its blast radius extends beyond the controller's own intended scope.


Relationship to Watch Management and Custom Controller Management

Reconciliation management is the algorithmic core that custom controller management operates (leader election, work queues, concurrency) and that watch management feeds with triggering events: while those two areas ensure Reconcile is invoked reliably and efficiently, reconciliation management is what determines whether each individual invocation actually produces correct, idempotent, and appropriately scoped convergence toward the declared desired state.

Desired state Observed state Diff & apply