DEV Community

shubham goel
shubham goel

Posted on

Who Deletes Resources Created by a Kubernetes Operator?

In the previous posts, I built a small Kubernetes operator around a custom Greeting resource.

The operator currently does a few things:

Greeting
    |
    v
Greeting Operator
    |
    v
ConfigMap
Enter fullscreen mode Exit fullscreen mode

It watches Greeting resources, creates a ConfigMap, updates status, records Events, retries failures, and also supports manual reconciliation.

So far, most of my focus was on:

create
update
reconcile
retry
Enter fullscreen mode Exit fullscreen mode

But there was one lifecycle question I had not really explored yet:

What happens to the ConfigMap when the Greeting is deleted?

That led me into Kubernetes owner references and garbage collection.


The Current Resource Relationship

A Greeting looks like this:

apiVersion: platform.shubforge.dev/v1alpha1
kind: Greeting

metadata:
  name: hello

spec:
  message: "Hello from Platform Lab"
Enter fullscreen mode Exit fullscreen mode

The operator creates:

apiVersion: v1
kind: ConfigMap

metadata:
  name: hello-greeting

data:
  message: "Hello from Platform Lab"
Enter fullscreen mode Exit fullscreen mode

So conceptually:

Greeting/hello
      |
      v
ConfigMap/hello-greeting
Enter fullscreen mode Exit fullscreen mode

Until now, I was mainly thinking about this as:

controller creates another resource
Enter fullscreen mode Exit fullscreen mode

But Kubernetes also needs to understand the lifecycle relationship between those two resources.


Looking at the Generated ConfigMap

I started by inspecting the ConfigMap created by the operator:

kubectl get configmap hello-greeting -o yaml
Enter fullscreen mode Exit fullscreen mode

I got:

apiVersion: v1

data:
  message: Hello from the updated Greeting

kind: ConfigMap

metadata:
  labels:
    app.kubernetes.io/managed-by: greeting-operator

  name: hello-greeting
  namespace: default

  ownerReferences:
    - apiVersion: platform.shubforge.dev/v1alpha1
      blockOwnerDeletion: false
      controller: false
      kind: Greeting
      name: hello
      uid: 4c749fca-e2a3-49f7-a330-1374b72c3bf8
Enter fullscreen mode Exit fullscreen mode

The interesting part was:

ownerReferences:
Enter fullscreen mode Exit fullscreen mode

The ConfigMap had an owner reference pointing back to:

Greeting/hello
Enter fullscreen mode Exit fullscreen mode

So the relationship was not only something my Java code understood.

Kubernetes itself also knew:

Greeting/hello
       |
       | owns
       v
ConfigMap/hello-greeting
Enter fullscreen mode Exit fullscreen mode

What Is an Owner Reference?

An owner reference is metadata stored on the dependent resource.

In my case, the dependent is:

ConfigMap/hello-greeting
Enter fullscreen mode Exit fullscreen mode

and the owner is:

Greeting/hello
Enter fullscreen mode Exit fullscreen mode

The ConfigMap contains:

ownerReferences:
  - apiVersion: platform.shubforge.dev/v1alpha1
    kind: Greeting
    name: hello
    uid: ...
Enter fullscreen mode Exit fullscreen mode

This tells Kubernetes that the lifecycle of the ConfigMap is related to that specific Greeting.

The uid is especially interesting.

Kubernetes does not only store:

Greeting
+
hello
Enter fullscreen mode Exit fullscreen mode

It also stores the unique identity of that exact resource.

So the relationship is closer to:

ConfigMap
    |
    v
specific Greeting UID
Enter fullscreen mode Exit fullscreen mode

That matters because a resource can later be recreated with the same name but a different UID.


controller and blockOwnerDeletion

My generated owner reference also contained:

controller: false
blockOwnerDeletion: false
Enter fullscreen mode Exit fullscreen mode

At first I expected something like:

controller: true
Enter fullscreen mode Exit fullscreen mode

because the ConfigMap is created by my controller.

But the controller field has a more specific meaning in Kubernetes owner references.

What mattered for this experiment was that the ownership relationship itself existed:

Greeting
    |
    v
ConfigMap
Enter fullscreen mode Exit fullscreen mode

Similarly:

blockOwnerDeletion: false
Enter fullscreen mode Exit fullscreen mode

does not mean the ConfigMap is unrelated to the Greeting.

It controls a more specific deletion behavior.

For this simple experiment, I mainly wanted to understand whether Kubernetes could use the owner reference for dependent cleanup.


First Experiment: Delete the ConfigMap

I first tested what happens if I delete the dependent resource.

Both resources existed:

kubectl get greeting hello
Enter fullscreen mode Exit fullscreen mode

and:

kubectl get configmap hello-greeting
Enter fullscreen mode Exit fullscreen mode

Then I deleted only the ConfigMap:

kubectl delete configmap hello-greeting
Enter fullscreen mode Exit fullscreen mode

At that point:

Greeting still exists
ConfigMap does not
Enter fullscreen mode Exit fullscreen mode

But the desired state represented by the Greeting still says:

this Greeting should have a ConfigMap
Enter fullscreen mode Exit fullscreen mode

So the controller reconciled again and recreated it.

The flow looked like:

ConfigMap deleted
       |
       v
Greeting still exists
       |
       v
desired state still requires ConfigMap
       |
       v
controller reconciles
       |
       v
ConfigMap recreated
Enter fullscreen mode Exit fullscreen mode

This is reconciliation doing its job.

The resource disappeared, but the owner still existed, so the operator restored the desired state.


Second Experiment: Delete the Greeting

Then I tested the opposite case.

I recreated the Greeting and confirmed both resources existed again:

task greeting:create
Enter fullscreen mode Exit fullscreen mode

Then:

kubectl get greeting hello
Enter fullscreen mode Exit fullscreen mode

and:

kubectl get configmap hello-greeting
Enter fullscreen mode Exit fullscreen mode

This time I deleted the owner:

kubectl delete greeting hello
Enter fullscreen mode Exit fullscreen mode

Then I checked:

kubectl get greeting hello
Enter fullscreen mode Exit fullscreen mode

and:

kubectl get configmap hello-greeting
Enter fullscreen mode Exit fullscreen mode

The Greeting disappeared.

The ConfigMap also disappeared.

This time the operator did not recreate it.

That made sense because the source of the desired state was gone.

The flow was:

Greeting deleted
      |
      v
owner no longer exists
      |
      v
Kubernetes sees dependent ownerReference
      |
      v
ConfigMap removed
Enter fullscreen mode Exit fullscreen mode

This was Kubernetes garbage collection rather than normal desired-state reconciliation.


Reconciliation vs Garbage Collection

This experiment helped me separate two mechanisms that looked similar at first.

When the dependent disappears

ConfigMap deleted
      |
      v
Greeting still exists
      |
      v
operator reconciles
      |
      v
ConfigMap recreated
Enter fullscreen mode Exit fullscreen mode

This is:

reconciliation
Enter fullscreen mode Exit fullscreen mode

The controller is maintaining desired state.


When the owner disappears

Greeting deleted
      |
      v
ownerReference
      |
      v
Kubernetes garbage collection
      |
      v
ConfigMap removed
Enter fullscreen mode Exit fullscreen mode

This is:

garbage collection
Enter fullscreen mode Exit fullscreen mode

The desired state itself has disappeared.

So I now think about the two like this:

Reconciliation
=
keep resources in the desired state while the owner exists
Enter fullscreen mode Exit fullscreen mode

and:

Garbage Collection
=
remove dependents when their owner disappears
Enter fullscreen mode Exit fullscreen mode

Watching It Live

One useful way to see this behavior is to watch the ConfigMap.

In one terminal:

kubectl get configmap hello-greeting -w
Enter fullscreen mode Exit fullscreen mode

Then in another terminal:

kubectl delete greeting hello
Enter fullscreen mode Exit fullscreen mode

That makes it easier to see the dependent disappear.

You can also watch both resources:

kubectl get greeting,configmap -w
Enter fullscreen mode Exit fullscreen mode

Then delete:

kubectl delete greeting hello
Enter fullscreen mode Exit fullscreen mode

This gives a much clearer view of the lifecycle than running one command at a time.


The Operator Did Not Need deleteConfigMap()

One part I found interesting was that I did not need to write something like:

deleteConfigMap();
Enter fullscreen mode Exit fullscreen mode

inside the controller.

The ConfigMap lives inside Kubernetes and already has an owner reference.

That means Kubernetes has enough information to manage the dependent lifecycle.

So instead of:

Greeting deleted
      |
      v
operator manually finds ConfigMap
      |
      v
operator deletes ConfigMap
Enter fullscreen mode Exit fullscreen mode

I get:

Greeting deleted
      |
      v
Kubernetes ownership information
      |
      v
garbage collector
      |
      v
ConfigMap deleted
Enter fullscreen mode Exit fullscreen mode

That is much more Kubernetes-native.


Same Namespace

Greeting is a namespaced custom resource.

The managed ConfigMap is created in the same namespace.

For example:

default/hello
      |
      v
default/hello-greeting
Enter fullscreen mode Exit fullscreen mode

If I create the Greeting in another namespace:

demo/hello
Enter fullscreen mode Exit fullscreen mode

the ConfigMap should also be:

demo/hello-greeting
Enter fullscreen mode Exit fullscreen mode

The operator uses the Greeting namespace when building the dependent resource.

Conceptually:

var namespace =
        greeting.getMetadata().getNamespace();
Enter fullscreen mode Exit fullscreen mode

and then:

.withNamespace(namespace)
Enter fullscreen mode Exit fullscreen mode

So the lifecycle stays scoped together.


Owner Reference Does Not Mean Finalizer

Another thing I wanted to keep separate was:

owner references
Enter fullscreen mode Exit fullscreen mode

and:

finalizers
Enter fullscreen mode Exit fullscreen mode

They are related to deletion, but they solve different problems.

An owner reference says:

This Kubernetes resource belongs to another Kubernetes resource.
Enter fullscreen mode Exit fullscreen mode

For example:

Greeting
    |
    v
ConfigMap
Enter fullscreen mode Exit fullscreen mode

Kubernetes can clean up that ConfigMap when the Greeting disappears.

A finalizer is different.

A finalizer says something closer to:

Before this resource is completely deleted,
the controller still has cleanup work to do.
Enter fullscreen mode Exit fullscreen mode

That becomes important when the cleanup is not something Kubernetes can automatically handle.


A Future Example

Imagine the operator eventually created something through an external API:

Custom Resource
      |
      v
Operator
      |
      v
External API
      |
      v
External Resource
Enter fullscreen mode Exit fullscreen mode

If I delete the custom resource:

kubectl delete ...
Enter fullscreen mode Exit fullscreen mode

Kubernetes does not know anything about that external system.

An owner reference cannot help here because the external object is not a Kubernetes resource.

That is where finalizers become interesting.

The flow would be more like:

delete custom resource
        |
        v
deletionTimestamp set
        |
        v
controller sees deletion
        |
        v
external cleanup
        |
        v
finalizer removed
        |
        v
resource deletion completes
Enter fullscreen mode Exit fullscreen mode

That is what I want to explore next.


Current Operator Lifecycle

At this point, the Greeting Operator has several different paths.

Desired state changes

Greeting.spec changes
        |
        v
generation changes
        |
        v
reconcile
Enter fullscreen mode Exit fullscreen mode

Manual reconciliation

reconcile-at changes
        |
        v
update filter
        |
        v
reconcile
Enter fullscreen mode Exit fullscreen mode

Failure

reconciliation fails
        |
        v
Ready=False
        |
        v
retry
Enter fullscreen mode Exit fullscreen mode

Dependent deletion

ConfigMap deleted
        |
        v
Greeting still exists
        |
        v
reconcile
        |
        v
ConfigMap recreated
Enter fullscreen mode Exit fullscreen mode

Owner deletion

Greeting deleted
        |
        v
ownerReference
        |
        v
Kubernetes garbage collection
        |
        v
ConfigMap deleted
Enter fullscreen mode Exit fullscreen mode

Seeing all of these together made the operator lifecycle much clearer for me.


Current Architecture

The current flow looks roughly like this:

                         Greeting
                            |
              +-------------+-------------+
              |                           |
          spec change              reconcile-at change
              |                           |
              +-------------+-------------+
                            |
                            v
                       reconcile()
                            |
                            v
                        ConfigMap
                            |
             +--------------+--------------+
             |                             |
      ConfigMap deleted             Greeting deleted
             |                             |
             v                             v
        reconcile                   garbage collection
             |                             |
             v                             v
      ConfigMap recreated            ConfigMap removed
Enter fullscreen mode Exit fullscreen mode

So the operator is no longer only about creating resources.

It is also about understanding resource lifecycle.


Source Code

The complete implementation is available in my Platform Lab repository.

Repository: Platform Lab

The changes and documentation covered in this post are available in:

Pull Request: Explore Greeting Resource Ownership

The current project includes:

  • Greeting CRD
  • Java Operator SDK controller
  • managed ConfigMap dependent resource
  • status and conditions
  • failure handling
  • automatic retries
  • Kubernetes Events
  • manual reconciliation
  • update filtering
  • owner references
  • Kubernetes garbage collection

What I Learned

The main thing I learned from this step is that deleting a resource is not always the controller's responsibility.

For Kubernetes resources, ownership metadata can allow Kubernetes itself to manage dependent cleanup.

The two cases are different:

dependent deleted
=
restore desired state
Enter fullscreen mode Exit fullscreen mode

while:

owner deleted
=
desired state is gone
Enter fullscreen mode Exit fullscreen mode

So:

delete ConfigMap
→ operator recreates it
Enter fullscreen mode Exit fullscreen mode

but:

delete Greeting
→ ConfigMap disappears with it
Enter fullscreen mode Exit fullscreen mode

That distinction made the relationship between reconciliation and garbage collection much clearer.


What's Next?

The ConfigMap in this example exists completely inside Kubernetes.

That means owner references and garbage collection work well for cleanup.

But what if the operator needs to clean up something Kubernetes does not control?

That leads to the next topic:

Finalizers
Enter fullscreen mode Exit fullscreen mode

I want to explore:

what deletionTimestamp means

how a controller gets a chance to perform cleanup

how finalizers delay deletion

what happens when cleanup fails

why stuck finalizers can leave resources in Terminating
Enter fullscreen mode Exit fullscreen mode

That should complete another important part of the operator lifecycle.

Top comments (1)

Collapse
 
dhruv_malaviya_cdcc71e595 profile image
Dhruv Malaviya •

The UID point is the one that matters most, and it's worth extending: it's why deleting and recreating a resource with the same name doesn't resurrect its old dependents. Name is an address, UID is an identity, and garbage collection keys on identity.

Three gotchas worth adding to the series.

Cross-namespace owner references aren't allowed. A namespaced dependent must have an owner in the same namespace, and a cluster-scoped dependent can only have a cluster-scoped owner. This bites people building operators that create resources into user namespaces.

blockOwnerDeletion: true requires delete permission on the owner's finalizers subresource, not just on the owner. An operator with an otherwise-correct RBAC role will fail to set it and usually won't say why clearly.

And garbage collection is eventually consistent. Deleting an owner doesn't remove the dependent instantly , the collector runs on a resync, so there's a window where the dependent still exists with a dangling owner reference. Tests asserting immediate cleanup go flaky for reasons that have nothing to do with the operator.

The controller: false observation is a good catch. Only one owner can hold that flag, and it decides which controller gets to adopt the object.