Self-hosted service
gianlucam76/k8s-cleaner avatar
gianlucam76/k8s-cleaner

k8s-cleaner: a controller that finds the resources nobody owns any more

Cleaner is a Kubernetes controller that identifies unused or unhealthy resources, helping you maintain a streamlined and efficient Kubernetes cluster. It provides flexible scheduling, label filtering, Lua-based selection criteria, resource removal or update and notifications via Slack, Webex and Discord. it can also automate clusters operations.

831 stars51 forksGoApache-2.0

At a glance

What is it?
k8s-cleaner is an Apache-2.0 licensed Kubernetes controller that identifies, removes or updates stale, orphaned and unhealthy resources, with cron scheduling, Lua based selection criteria and notifications for every change. The judgement that matters is what you point it at, since it deletes what it matches.
Who is it for?
k8s-cleaner suits platform teams that already have namespace labels and a habit of cron maintenance, and who want the deletion decisions to be recorded in a chat channel rather than executed by hand.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A controller for resources that outlived their purpose

k8s-cleaner is a Kubernetes controller that identifies, removes or updates stale, orphaned and unhealthy resources so that a cluster does not accumulate objects nobody owns any more.

Two design choices define its scope. First, it is not limited to built-in kinds. The documentation states it is designed to handle any Kubernetes resource types, including your own custom resources, so the same mechanism applies to a CRD your team wrote. Second, selection is not limited to a single mechanism either, because the controller offers label-based selection and custom Lua-based criteria.

The project sits inside the projectsveltos family. Its module path is gianlucam76/k8s-cleaner, it depends on the shared library projectsveltos/libsveltos, and the homepage points at the Sveltos documentation site. The container image is published under the same organisation, projectsveltos/k8s-cleaner, and the documentation for this project is hosted separately under the same GitHub Pages domain.

The effect of a cleaner controller is invisible until it goes wrong, which is why the rest of the design leans so heavily on scheduling, on a dry run mode and on notifications. Those three things are the difference between a tool that tidies a cluster and a tool that deletes something you needed.

Cron scheduling, a dry run mode and label filtering

The three features the documentation leads with answer three different operational questions.

Schedule answers when. You specify the frequency at which k8s-cleaner should scan the cluster and identify stale resources, using Cron syntax to define recurring schedules. The Go module depends on robfig/cron/v3, which is the standard Go cron implementation, so the schedules are the familiar five field form rather than something proprietary.

DryRun answers what would happen. It enables safe testing of the filtering logic without affecting actual resource configurations: resources matching the criteria get identified, but no changes get applied. This is the mode to spend the most time in, because the difference between a match and a deletion is one field.

Label filtering answers which. It is the ordinary Kubernetes mechanism, an object with a label selector, which means the same labels your team already uses for ownership, environment or tier can drive cleanup without a second labelling scheme.

Combined, they describe a normal workflow: write a selector, run it in DryRun, read what matched, enable the schedule, and let the notifications tell you what happened. The Cron schedule plus the dry run is also what makes the controller safe to run against a cluster that other people depend on, since both the timing and the effect are inspectable before they become permanent.

Lua criteria for what labels cannot describe

Label selection has a hard limit: it can only match on labels that exist on the object. Some cleanup rules are not label shaped at all. A ConfigMap is unused not because of what it is called but because nothing in the cluster references it. A pod is unhealthy because its last exit was an out-of-memory kill. Those are conditions, and the project answers them with a Lua based selection mechanism, backed by the github.com/yuin/gopher-lua dependency in the Go module.

The value of a scripting hook here is that the rule can be expressed against object state rather than against metadata. The project's examples for it are exactly the cases labels cannot reach: identifying resources from an annotation that records a maximum lifespan or an expiration date, detecting pods that are not using the most recent Secret data, and detecting pods stuck with repeated FailedMount events.

The cost is that a scripting hook moves policy into code that is harder to review than a YAML selector. A label selector fails loudly when it matches nothing, while a Lua expression that returns the wrong answer produces a confident, incorrect deletion. That is the main reason to treat DryRun as a mandatory step rather than an optional one, and the reason to keep criteria in version control next to the manifests they clean up.

Unhealthy is a separate question from unused

Most cleanup tooling asks whether an object is still referenced. k8s-cleaner also asks whether the things still running are actually working, and the documentation keeps the two apart with separate example sets.

The unused examples cover ConfigMaps and Secrets, ClusterRoles and Roles, ServiceAccounts, PersistentVolumes and PersistentVolumeClaims, Deployments and StatefulSets, plus the time based case driven by an annotation carrying a maximum lifespan or an expiration date.

The unhealthy examples are about pods. There is a case for pods mounting Secrets with old content, which detects pods not using the most recent Secret data. There is one for pods using expired certificates, which pins down pods running with expired security certificates. There is one for pods with repeated FailedMount events, which finds pods stuck unable to mount a volume. And there is one for pods with an OOM in their previous log, which identifies crash-looping pods whose last exit was an out-of-memory kill.

Read together, these cases describe a resource controller that watches runtime state rather than only the API objects. An expired certificate and a stale Secret mount are both invisible in a listing of the namespace, and both are the kind of thing that produces a support ticket rather than a cluster size complaint.

Selection that reads live Prometheus metrics

The strongest examples combine resource state with live Prometheus metrics, which is a different kind of rule again because the answer changes over time rather than over a policy.

One example scales Deployments down to zero when the HTTP error rate reported by Prometheus exceeds a threshold. Another deletes Pods when their memory working set exceeds 90 percent of the configured limit. Both are described as combining resource state with live metrics, and the metrics support comes from the github.com/prometheus/client_golang dependency.

This is where the tool becomes closest to a remediation controller rather than a garbage collector, and where the risk profile changes. A rule about unused ConfigMaps depends on what exists in the cluster and rarely changes between runs. A rule about memory saturation depends on what the workload is doing right now, so a bad threshold or a runaway application can turn a scheduled sweep into a repeated deletion of healthy pods.

If you adopt the metrics examples, the sane order is the same as for the rest: dry run first, watch the reported list over several cycles so that the pattern is stable rather than instantaneous, and only then let the schedule run unattended.

Notifications that list what was actually deleted

The controller keeps you in the loop, and the list of channels is long: Slack, Webex, Discord, Teams, Telegram, SMTP, a Kubernetes Event, and a Reports section in the documentation.

The important detail is not the channel list but the content. Each notification contains the list of all resources successfully deleted or modified by k8s-cleaner. That is a change record, not just an alert, and it changes how you can treat the tool.

With a per-run list of what was touched, a scheduled deletion is auditable after the fact, and a bad run can be diagnosed from the report rather than guessed at from the state of the cluster. The Kubernetes Event option keeps the evidence inside the cluster's own audit trail, and the Reports option keeps it in the project's documentation. The chat options exist for the case where someone has to act on it quickly.

The report is also the only thing standing between you and a silent mistake. The documented examples include deleting Pods on a memory threshold and scaling Deployments to zero, which are actions that change running workloads. If your notifications are not wired up before you enable a schedule, the deletions still happen and the evidence is gone.

Building the manager binary and packaging it distroless

The project is a Go module requiring Go 1.27.1, and its dependency list reads like the feature list: cron for scheduling, gopher-lua for criteria, the Prometheus client for metrics, and one client library per chat channel, slack-go/slack, discordgo, the Telegram bot API, go-teams-notify and the Cisco Webex teams client.

The container build is a three stage Dockerfile. The first stage builds the web interface from a node:24-alpine image. The second builds the manager binary from golang:1.27.1 with a static build:

dockerfile
FROM golang:1.27.1 AS builder

ARG BUILDOS
ARG TARGETARCH
dockerfile
RUN CGO_ENABLED=0 GOOS=$BUILDOS GOARCH=$TARGETARCH go build -a -o manager cmd/main.go

The final stage packages the binary into gcr.io/distroless/static:nonroot, runs it as user 65532 and sets the entrypoint:

dockerfile
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]

A distroless base means no shell in the running container, which is the right default for a component with delete permissions in your cluster, and it also means you cannot exec into the pod to poke around when something misbehaves.

For local work the Makefile drives the image name and version, with the registry, image name and tag all overridable:

bash
REGISTRY ?= projectsveltos
IMAGE_NAME ?= k8s-cleaner
TAG ?= v0.24.0

The repository also ships a charts directory, a manifest directory, a web directory and separate directories of example manifests for unused resources, unhealthy resources and automated cluster operations.

Recent releases, a permissive licence, and a controller that deletes

The project is actively shipping. Recent releases include v0.24.0 on 2026-09-22, v0.23.0 on 2026-08-19 and v0.22.0 on 2026-08-04, and the last push was on 2026-10-22. The version cadence of roughly one release a month across a 0.x line is what you expect from a controller that adds new resource kinds and new selection criteria over time.

The licence is Apache-2.0, with the licence file at the root of the repository and the badge in the project header. That is a permissive grant that suits both commercial use and contribution, and your counsel should confirm how it fits your own distribution.

The real cost of adoption is not the licence but the blast radius. k8s-cleaner is a controller with permission to delete and modify resources in your cluster, and every feature it has widens what it can act on: labels, Lua criteria, Prometheus metrics, and time based expiry. The mitigation is built in, in the form of DryRun and the per-run notification list, but only if you use them.

Compare it with the manual alternative. Running kubectl with a label selector by hand finds the same objects, but it happens once, it leaves no record, and it depends on someone remembering to do it. k8s-cleaner runs on a schedule, applies richer criteria than a selector allows, and writes down what it changed. That is the whole trade, and it is worth taking only if you are willing to treat the dry run as the real interface and the deletion as the automated part.

Editorial conclusion

k8s-cleaner suits platform teams that already have namespace labels and a habit of cron maintenance, and who want the deletion decisions to be recorded in a chat channel rather than executed by hand. It is the wrong choice for a cluster where resource ownership is genuinely ambiguous, since a miswritten label selector turns a cleanup job into a deletion incident, and the wrong choice for a team that wants a general purpose garbage collector for finished workloads, because this controller does not schedule workloads in the first place. Verify three things before enabling it on a real cluster: run it with DryRun first and read the reported list, confirm your Lua criteria return what you expect on a copy of production data, and check the notification channel receives the report, since that report is your only record of what was deleted.

Frequently asked questions

What does k8s-cleaner actually do in a Kubernetes cluster?

It is a controller that identifies, removes or updates stale, orphaned and unhealthy resources, and it is designed to handle any Kubernetes resource type including custom resources. Selection is by label, by custom Lua criteria, or by live Prometheus metrics.

How can I test k8s-cleaner without deleting anything?

Use the DryRun feature. It identifies resources matching the criteria and reports them, but no changes are applied, which is the mode to use first before enabling a schedule.

Which resources does k8s-cleaner consider unused?

The published examples cover ConfigMaps, Secrets, ClusterRoles, Roles, ServiceAccounts, PersistentVolumes, PersistentVolumeClaims, Deployments and StatefulSets. There is also a time based case that uses an annotation recording a maximum lifespan or an expiration date.

Can k8s-cleaner detect unhealthy pods?

Yes. Examples are given for pods mounting Secrets with old content, pods using expired certificates, pods with repeated FailedMount events, and pods with an OOM in their previous log, which identifies crash-looping pods killed for memory.

Which notification channels does k8s-cleaner support?

Slack, Webex, Discord, Teams, Telegram, SMTP, a Kubernetes Event and a reports page in the documentation. Each notification contains the list of all resources successfully deleted or modified by the controller.

Official sources

  1. gianlucam76/k8s-cleaner on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gianlucam76-k8s-cleaner.svg)](https://hysenlabs.com/projects/gianlucam76-k8s-cleaner)

Was this useful?