Your controller creates a Deployment, a Service and a Secret for each custom resource it manages. How do you ensure those objects are cleaned up when the custom resource is deleted, and how does Kubernetes garbage collection decide what to remove?
answer
- ownerReferences + cluster GC, not manual delete
- UID is identity; name reuse does not adopt
- background / foreground / orphan propagation
- Same namespace; namespaced owner can't own cluster-scoped
- Owns() maps child events to owner key
basics
~20 sSet an ownerReference on each created object pointing at the custom resource. The garbage collector then deletes dependents automatically when the owner is deleted. Owner and dependent must be in the same namespace, and a cluster-scoped object cannot be owned by a namespaced one.
solid answer
~50 sSet `metadata.ownerReferences` on every child object, referencing the custom resource by apiVersion, kind, name and **UID**, usually with `controller: true`. Controller-runtime helpers such as `controllerutil.SetControllerReference` do this and also record the owner so `Owns()` can map child events back to the parent's reconcile key. The **garbage collector** in kube-controller-manager watches for owners that no longer exist and deletes their dependents. Deletion propagation has three modes: **background** (the default — owner deleted immediately, dependents cleaned up asynchronously), **foreground** (owner stays in deletion with a `foregroundDeletion` finalizer until dependents are gone), and **orphan** (dependents survive with the reference removed). Rules that bite in practice: the UID must match, so recreating an owner with the same name does not adopt old children — the stale ones are deleted as orphans of a vanished UID; cross-namespace ownership is invalid and makes the dependent a GC target; a namespaced owner cannot own a cluster-scoped object. For anything outside the cluster, ownerReferences do nothing — you need a finalizer.
code
go · 7 linesdeploy := buildDeployment(&app)
if err := controllerutil.SetControllerReference(&app, deploy, r.Scheme); err != nil {
return ctrl.Result{}, err // e.g. another controller already owns it
}
if err := r.Create(ctx, deploy); err != nil && !apierrors.IsAlreadyExists(err) {
return ctrl.Result{}, err
}go deeper
Say that children get an ownerReference to the parent and Kubernetes deletes them automatically when the parent goes away.
Add the UID-based identity, the controller flag, the three propagation policies, and the same-namespace rule; mention the helper that sets the reference.
Explain why GC in the API layer beats controller-side cleanup (works while the controller is down), when a finalizer is required instead, and how ownership also routes child events to the owner's reconcile.
Frame ownership as the cluster's lifecycle graph — how it bounds cleanup guarantees, where those guarantees stop (cluster-scoped and external resources) and how that shapes the resource model you expose.
## Why ownerReferences exist Controllers create many objects on a user's behalf. Deleting them by hand in reconcile would be fragile: the controller might be down when the parent is deleted, and by the time it comes back the parent object is gone, so there is nothing left to tell it what to clean up. Kubernetes solves this in the API itself with **ownership metadata plus a cluster-wide garbage collector**. Each object may carry `metadata.ownerReferences`, a list of owners: ```yaml ownerReferences: - apiVersion: apps.example.com/v1 kind: App name: checkout uid: 0a5f... controller: true blockOwnerDeletion: true ``` The **UID** is the identity that matters. Names are reusable; UIDs are not. That is what prevents a newly created object with a recycled name from inheriting the old one's children. ## How the garbage collector works The garbage collector runs in kube-controller-manager. It maintains a graph of owners and dependents from watch data. When an owner is deleted (or is found never to have existed), every dependent that references that owner UID becomes eligible for deletion. This is asynchronous and cluster-wide; it does not require your controller to be running, which is precisely the point. **Propagation policies**, chosen by the deleter: - **Background** (default for most clients): the owner is removed straight away and the collector deletes dependents afterwards. Fast, but for a short window children exist without a parent. - **Foreground**: the API server adds a `foregroundDeletion` finalizer to the owner. The owner remains visible with `deletionTimestamp` set until all dependents that set `blockOwnerDeletion: true` are gone, then the owner is removed. Use it when something must not disappear from the API until its children are actually gone. - **Orphan**: the collector strips the ownerReference from dependents and leaves them running. Historically used for `kubectl delete --cascade=orphan`. `blockOwnerDeletion: true` on a dependent is what makes foreground deletion wait for it. Note that setting it requires the `delete` permission on the owner, which occasionally surprises controllers with tight RBAC. ## The controller field At most one ownerReference may have `controller: true`. It marks the *managing* controller and is used for adoption logic — a ReplicaSet will not adopt a Pod that is already controlled by someone else. For custom controllers, `SetControllerReference` sets it and errors if a different controller already owns the object, which is a useful guard against two controllers fighting over one child. ## Rules and gotchas 1. **Same namespace only.** A namespaced dependent must be owned by an owner in the same namespace. A cross-namespace reference is invalid; the collector treats the owner as non-existent and deletes the dependent. This is a classic source of "my ConfigMap keeps disappearing". 2. **Cluster-scoped owners only for cluster-scoped dependents.** A cluster-scoped object (a ClusterRole, a PV) cannot be owned by a namespaced custom resource. To clean those up you need a finalizer on the parent and explicit deletion in reconcile. 3. **UID mismatch is fatal to the child.** Delete and recreate the parent with the same name and the old children are garbage — they point at a UID that no longer exists. 4. **Nothing outside the cluster is covered.** Cloud buckets, DNS entries, database users: ownerReferences cannot help. That is what finalizers are for. 5. **Deletion is not instantaneous.** Background propagation means clients that immediately re-create the parent may briefly observe leftovers. Idempotent, deterministic-name reconciliation handles this. ## Ownership as a wiring mechanism, not just cleanup OwnerReferences also drive event routing. In controller-runtime, `Owns(&appsv1.Deployment{})` installs a handler that reads a Deployment's controller ownerReference and enqueues the *owner's* key. So when a managed Deployment's status changes, the parent reconciles and can update its own status. Without the reference, you would need a custom mapping function or an index. This dual role — cleanup and event mapping — is why setting ownership correctly is a habit rather than an option. ## Practical checklist for a reconciler - Call `SetControllerReference(cr, child, scheme)` before creating any child, and handle its error rather than ignoring it. - Never set ownership across namespaces or from a namespaced owner to a cluster-scoped object; delete those explicitly under a finalizer. - Use foreground deletion when ordering matters for the API surface; otherwise the default is fine. - Do not implement child cleanup by hand in reconcile when ownership suffices — the collector works even when your controller is offline. - Remember that ownership does not imply the same lifecycle for external resources. ## Interview framing Say ownerReferences plus the garbage collector, name the UID as the identity, list the three propagation modes, and then volunteer the two limits — same namespace / not cluster-scoped, and nothing outside the cluster. Mentioning that ownership also routes child events to the parent's reconcile shows you have wired a real controller.
- Your controller sets an ownerReference from a custom resource in namespace A to a ConfigMap in namespace B. What happens?The reference is invalid: cross-namespace ownership is not supported for namespaced dependents. The garbage collector cannot resolve the owner in the ConfigMap's own namespace, treats it as missing, and deletes the ConfigMap. The symptom is an object that keeps vanishing shortly after creation. The fix is to copy the data into the correct namespace or clean it up explicitly under a finalizer.
- How would you clean up a cluster-scoped resource, such as a ClusterRole, created by a namespaced custom resource?Ownership will not work, because a namespaced object cannot own a cluster-scoped one. Add a finalizer to the custom resource; when it is being deleted, reconcile explicitly deletes the ClusterRole, then removes the finalizer. The same approach applies to anything living outside the cluster.
- What is the difference between background and foreground cascading deletion?With background deletion the owner is removed from the API immediately and dependents are deleted asynchronously afterwards. With foreground deletion the API server adds a foregroundDeletion finalizer, keeps the owner visible with a deletionTimestamp until all blocking dependents are gone, and only then removes it. Foreground is what you want when callers must not see the owner disappear before its children have actually gone.
An ownerReference is a luggage tag with a unique passenger ID: when the passenger leaves, the airline disposes of every bag carrying that ID, whether or not the check-in agent is still on shift.
saying these in an interview costs you the question
- Deleting child objects manually in reconcile instead of relying on ownerReferences and the garbage collector
- Setting ownerReferences across namespaces and then being surprised that the dependent is deleted
- Expecting ownerReferences to clean up resources outside the cluster, such as cloud buckets or DNS records
- Assuming a recreated owner with the same name adopts the previous owner's children, ignoring UID
- Setting controller: true on several owners, or ignoring the error when another controller already owns the child
- Believing cascading deletion is synchronous so nothing can be observed after the owner is gone