Kubernetes-namespace blijft hangen in Terminating: zo vind je de blokkerende finalizer

kubectl delete namespace komt direct terug, maar de namespace blijft daarna eindeloos in Terminating staan. De oorzaak is bijna altijd een finalizer die geen enkele controller verwijdert: of op een resource binnen de namespace, of op de eigen kubernetes-finalizer van de namespace omdat een APIService niet beschikbaar is. Dit artikel legt het finalizer-mechanisme uit, laat zien hoe je de exacte resource vindt die deletion blokkeert, en hoe je de finalizer weghaalt zonder de cleanup over te slaan die hij beschermde.

Symptoom: kubectl delete namespace blijft eeuwig hangen

Je draaide kubectl delete namespace team-payments en het commando gaf direct namespace "team-payments" deleted terug. De namespace is weg. Behalve dat hij dat niet is:

$ kubectl get namespace team-payments
NAME            STATUS        AGE
team-payments   Terminating   42h

Dit duurt al uren, soms dagen. kubectl get pods -n team-payments geeft niets of een paar vastzittende pods terug. kubectl delete namespace team-payments --force --grace-period=0 werkt zonder fout maar verandert helemaal niks. De namespace blijft in Terminating staan, ook na een control-plane-restart en bij elke retry.

Dit is de klassieke vastlopende namespace, en er is precies één mechanische oorzaak: er staat nog minstens één finalizer op de namespace zelf of op een resource erin, en geen enkele controller haalt hem weg.

Waarom namespaces vastlopen: het finalizer-mechanisme

Een finalizer is een namespaced sleutel in metadata.finalizers die Kubernetes vertelt te wachten met definitieve verwijdering tot bepaalde voorwaarden zijn vervuld. Op het moment dat je een namespace verwijdert gebeuren er tegelijk twee dingen. Eerst krijgt de namespace een deletionTimestamp en gaat hij naar de Terminating-fase. Daarna start de namespace-controller met het langslopen van elke API-resource binnen de namespace en verwijdert wat hij tegenkomt. De namespace verdwijnt pas uit etcd als die opruiming af is én elke finalizer-sleutel weg is.

Twee verschillende plekken waar finalizers wonen zijn relevant voor namespaces, en ze door elkaar halen is dé grootste bron van fixes die niets oplossen:

  • metadata.finalizers staat op losse resources binnen de namespace (PVCs, custom resources, soms pods). Elke sleutel benoemt een controller die nog opruimwerk te doen heeft. Die controller is verantwoordelijk voor het afronden van het werk en het weghalen van de sleutel.
  • spec.finalizers staat op de namespace zelf en bevat op een gezond cluster precies één entry: kubernetes. Dit is de eigen finalizer van de namespace, die door de namespace-controller via het aparte /finalize-subresource wordt geleegd, en alleen pas nadat alles binnen de namespace verwijderd is.

De namespace blijft Terminating tot beide lijsten leeg zijn. Crasht een controller, wordt hij gedeïnstalleerd, of valt zijn API-endpoint uit, dan wordt de finalizer-sleutel nooit verwijderd en stagneert de hele deletion.

Je kunt deze toestand gewoon uitlezen. De namespace-status draagt vier vaste condities die precies vertellen welke fase vastzit:

Conditie Betekenis
NamespaceDeletionDiscoveryFailure De namespace-controller kon één of meer API-resources niet listen om met cleanup te beginnen
NamespaceDeletionContentFailure Een API-call tijdens cleanup faalde (vaak een onbereikbare APIService)
NamespaceContentRemaining Er staan nog resources in de namespace
NamespaceFinalizersRemaining Op minstens één resource in de namespace staat nog een finalizer

De diagnostische flow hieronder gebruikt deze condities als startpunt.

Veelvoorkomende oorzaken, in volgorde van waarschijnlijkheid

  1. Een verweesde finalizer op een custom resource. Een operator of CRD-controller is gedeïnstalleerd terwijl er nog een CR bestond. Op die CR staat in metadata.finalizers iets als cert-manager.io/finalizer of tackle.tackle.io/finalizer, en de controller die hem zou verwijderen draait niet meer.
  2. Een onbeschikbare APIService. Een metrics- of extensie-APIService (v1beta1.metrics.k8s.io, v1beta1.custom.metrics.k8s.io, een admission-webhook-backend) staat op Available: False. De namespace-controller kan resources niet enumereren voor cleanup, logt NamespaceDeletionDiscoveryFailure en stopt.
  3. Ingebouwde PV- of PVC-protection-finalizers. Een pod verwijst nog naar een PVC, of een PVC heeft nog een gebonden PV, dus de kubernetes.io/pvc-protection of kubernetes.io/pv-protection-finalizer wordt nooit gewist. De namespace wacht tot die resources vrijkomen.
  4. Een vastzittende pod met een eigen finalizer. Op een pod staat een finalizer van een admission-controller of sidecar (Istio, Linkerd, KEDA scaledobjects) die niet meer draait.
  5. Een falende admission-webhook. Een ValidatingWebhookConfiguration of MutatingWebhookConfiguration met failurePolicy: Fail roept een backend aan die weg is, dus de namespace-controller kan resources niet verwijderen om de webhook tevreden te stellen.

De diagnose hieronder controleert deze oorzaken in deze volgorde.

Vind de resource die de namespace blokkeert

Begin met de condities op de namespace zelf. Die noemen het type fout voor je:

kubectl get namespace team-payments -o jsonpath='{.status.conditions}' | jq

Een typische output bij een vastzittende namespace ziet er zo uit:

[
  {
    "lastTransitionTime": "2026-04-22T14:02:11Z",
    "message": "All content successfully deleted, may be waiting on finalization",
    "reason": "ContentDeletionTermination",
    "status": "False",
    "type": "NamespaceDeletionContentFailure"
  },
  {
    "lastTransitionTime": "2026-04-22T14:02:11Z",
    "message": "Some content in the namespace has finalizers remaining: cert-manager.io/finalizer in 1 resource instances",
    "reason": "SomeFinalizersRemain",
    "status": "True",
    "type": "NamespaceFinalizersRemaining"
  }
]

Lees elke message aandachtig. De NamespaceFinalizersRemaining-message benoemt de exacte finalizer-sleutel (cert-manager.io/finalizer) en het aantal resources dat hem nog draagt. Dat is de nuttigste informatie in deze hele workflow. Zie je alleen NamespaceContentRemaining zonder NamespaceFinalizersRemaining, dan zijn de resources nog niet aangeraakt en heb je waarschijnlijk een discovery- of content-failure.

Staat NamespaceDeletionDiscoveryFailure op True, controleer dan eerst de APIServices:

kubectl get apiservice | grep -v True

Elke APIService met Available=False blokkeert discovery. De output ziet er zo uit:

NAME                                   SERVICE                              AVAILABLE                  AGE
v1beta1.custom.metrics.k8s.io          monitoring/custom-metrics-apiserver  False (ServiceNotFound)    63d
v1beta1.metrics.k8s.io                 kube-system/metrics-server           True                       63d

Een reason als ServiceNotFound, MissingEndpoints of FailedDiscoveryCheck betekent dat de bijbehorende Service of pods weg zijn of niet werken. De namespace-controller belt elke geregistreerde APIService op om uit te vinden wat er opgeruimd moet worden; één kapotte APIService is genoeg om de hele deletion stil te leggen.

Wijzen de condities naar een finalizer (NamespaceFinalizersRemaining), zoek dan de exacte resource:

# Lijst elk namespaced API-resourcetype
kubectl api-resources --verbs=list --namespaced -o name |
  xargs -n 1 kubectl get --show-kind --ignore-not-found -n team-payments

Dit is de door GKE aanbevolen sweep. Het bevraagt elk namespaced API-kind en print wat er nog staat. Alles wat een normale namespace-deletion overleeft, draagt per definitie een finalizer.

Om de finalizer-sleutels op een specifieke resource te bekijken:

kubectl get <kind>/<name> -n team-payments -o jsonpath='{.metadata.finalizers}'

Om in één commando elke namespaced resource te zien die nog een finalizer draagt:

kubectl api-resources --verbs=list --namespaced -o name |
  xargs -I {} kubectl get {} -n team-payments \
    -o jsonpath='{range .items[?(@.metadata.finalizers)]}{.kind}/{.metadata.name}: {.metadata.finalizers}{"\n"}{end}'

De output noemt elk kind, elke naam en de exacte finalizer-sleutels die er nog op staan. Aan de hand van die lijst bepaal je welke fix hieronder van toepassing is.

Fix A: verwijder een finalizer van een vastzittende namespaced resource

Dit is de juiste route als de diagnose een specifieke resource (een custom resource, een PVC, een pod) heeft genoemd waar een finalizer op staat die geen controller weghaalt.

Bedenk eerst of de controller misschien terugkomt voordat je iets gaat patchen. Heb je per ongeluk de controller van een CRD weggegooid en kun je hem herinstalleren, doe dat dan eerst. De controller pikt de resource weer op, draait zijn cleanup en haalt de finalizer netjes weg. Haal je hem handmatig weg, dan sla je die cleanup over, wat externe resources (DNS-records, cloud-volumes, certificaten, IAM-bindings) wees kan achterlaten in de systemen die de controller beheerde.

Is de controller definitief weg (de operator is uitgefaseerd, de CRD is verwijderd, de upstream-service is afgeschakeld), patch dan de finalizer van die specifieke resource af. Het garbage-collection-model van Kubernetes ziet finalizers als cleanup-signalen; de sleutel weghalen vertelt de API-server "stop met wachten op opruiming, gooi het object gewoon weg":

kubectl patch certificate api-tls -n team-payments \
  --type=json \
  -p='[{"op": "remove", "path": "/metadata/finalizers"}]'

Je weet dat het werkte als: de resource binnen een seconde of twee verdwijnt uit kubectl get <kind> -n team-payments, en kubectl get namespace team-payments ofwel NotFound geeft (deletion is klaar) ofwel doorschuift naar een volgende blokkerende finalizer als er nog meer staan.

Voor PVC-protection-finalizers is wegpatchen meestal niet de juiste fix. De kubernetes.io/pvc-protection-finalizer verdwijnt automatisch zodra geen pod meer naar de PVC verwijst. Vind de pod die nog refereert:

kubectl get pods -n team-payments -o json |
  jq -r '.items[] | select(.spec.volumes[]?.persistentVolumeClaim) |
    "\(.metadata.name): \([.spec.volumes[].persistentVolumeClaim.claimName] | join(","))"'

Verwijder die pod netjes (of haal eerst zijn eigen finalizer weg als de pod zelf vastzit), en de PVC-protection-finalizer wist zichzelf.

Fix B: verwijder of repareer een onbeschikbare APIService

Staat NamespaceDeletionDiscoveryFailure op True en laat kubectl get apiservice | grep -v True iets met Available=False zien, dan kan de namespace-controller geen resources opsommen om op te ruimen. Tot de APIService weer gezond is óf weg is, gaat geen enkele namespace in het cluster netjes terminating afronden.

Controleer welke kapot is:

kubectl describe apiservice v1beta1.custom.metrics.k8s.io

De Status-sectie noemt de conditie. ServiceNotFound en MissingEndpoints betekenen dat de achterliggende Service of zijn pods weg zijn. FailedDiscoveryCheck betekent dat de backend draait maar het OpenAPI-endpoint niet correct serveert.

Twee routes:

1. Herinstalleer de ontbrekende controller. Hoort de APIService bij een metrics-server of custom-metrics-adapter die zou moeten draaien, redeploy hem dan. Zodra kubectl get apiservice weer Available=True toont, hervat namespace-deletion automatisch; je hoeft de delete niet opnieuw te draaien.

2. Verwijder de verweesde APIService. Is de controller voorgoed weg en is de APIService alleen nog leftover-registratie:

kubectl delete apiservice v1beta1.custom.metrics.k8s.io

Hiermee verwijder je de registratie zodat de namespace-controller geen service meer aanroept die niet bestaat. Na verwijdering klapt NamespaceDeletionDiscoveryFailure binnen seconden naar False en gaat de controller verder met content-cleanup.

Hetzelfde patroon geldt voor kapotte admission-webhooks. Een MutatingWebhookConfiguration met failurePolicy: Fail en een dode backend blokkeert resource-verwijdering op dezelfde manier. Herstel de backend, zet de webhook op failurePolicy: Ignore voor de geraakte operations, of verwijder de webhook-configuratie helemaal als de controller erachter weg is.

Fix C (laatste redmiddel): leeg de eigen finalizer van de namespace via /finalize

Als elke namespaced resource is verwijderd maar de namespace zelf in Terminating blijft staan, is de eigen spec.finalizers: [kubernetes]-entry van de namespace de laatste blokkade. Normaal leegt de namespace-controller die entry via het aparte /finalize-subresource zodra content-cleanup klaar is. Heeft de controller een permanente fout (een content-failure die hij niet kan retryen), dan blijft de entry staan.

Dit is de situatie waarin je de controller omzeilt en zelf naar /finalize schrijft. Het is een laatste redmiddel omdat je de API-server vertelt: "ik neem op me dat er niets in deze namespace nog opruiming nodig heeft". Was er nog iets dat op de controller rekende om externe resources vrij te geven, dan blijft dat werk voorgoed liggen.

De netste oneliner met jq en kubectl replace --raw:

NS=team-payments
kubectl get namespace "$NS" -o json |
  jq 'del(.spec.finalizers)' |
  kubectl replace --raw "/api/v1/namespaces/$NS/finalize" -f -

kubectl replace --raw stuurt de aangepaste JSON direct naar het /finalize-subresource. De API-server controleert of je alleen spec.finalizers aanpast (het subresource staat verder niets toe) en accepteert de lege lijst. De namespace verdwijnt binnen een seconde.

Je weet dat het werkte als: kubectl get namespace team-payments Error from server (NotFound) retourneert. Krijg je de namespace nog steeds terug, dan heeft de call /finalize niet geraakt (vaak een rechtenprobleem: dit vereist update op namespaces/finalize).

Sommige oudere guides raden kubectl proxy plus een curl PUT naar http://127.0.0.1:8001/api/v1/namespaces/$NS/finalize aan met de aangepaste JSON in een tempfile. Dat werkt ook en is functioneel identiek, maar kubectl replace --raw schakelt de proxy-stap uit en is de richting waar het project naartoe is bewogen.

Waarom kubectl delete --force --grace-period=0 hier niet werkt

Een veelvoorkomende reflex is de delete opnieuw proberen met --force --grace-period=0. Het lijkt iets dat alles overschrijft. Dat doet het niet.

Volgens de kubectl-referentie doet --force --grace-period=0 twee dingen: het slaat graceful pod-termination over, en het verwijdert de resource direct uit de API voor resources waar dat ondersteund wordt. Het verwijdert geen finalizers. Bij pods slaat het de SIGTERM-grace-period over en vraagt het de kubelet de pod uit etcd te halen; bij namespaces betekent het feitelijk niets behalve "wacht niet op graceful termination", iets wat de namespace-controller toch al niet doet.

Zit een namespace vast op een finalizer, dan geeft elke --force-retry hetzelfde namespace deleted-bericht en verandert er niets. De delete-aanvraag was al geaccepteerd bij de eerste delete; de API blijft hem alleen bijhouden. Het enige mechanisme dat de toestand echt opheldert, is finalizer-verwijdering, of door de verantwoordelijke controller of via Fix A, B of C hierboven.

Daarom doen retry-loops, scripted re-deletes en kubectl replace van een yaml-manifest met een gewiste metadata.deletionTimestamp allemaal niets. Geen van deze is de operatie die de API-server behandelt als "verwijder de finalizer".

Wat dit NIET is

Een paar aangrenzende fouttypen lijken erop maar zijn een ander probleem:

  • Een pod die in Terminating blijft hangen is niet hetzelfde als een namespace die in Terminating blijft hangen. Een pod kan vasthangen op zijn eigen finalizer of op een onbereikbare kubelet. Met de namespace kan niets aan de hand zijn; alleen één resource zit vast. Diagnosticeer pods eerst als losse workloads het symptoom zijn.
  • Trage deletion is geen vastgelopen deletion. Een namespace met honderden pods kan minuten kosten om volledig te termineren, zeker met lange terminationGracePeriodSeconds. Tonen de condities voortgang (resourcecount daalt, finalizers verdwijnen één voor één), wacht dan af. Vastgelopen betekent geen voortgang over vele minuten, niet traag.
  • Een namespace met Active-status die je niet weg krijgt is een ander probleem (RBAC, admission, finalizer toegevoegd vóór deletion). Dat is niet het Terminating-probleem dat dit artikel behandelt.
  • CRD-niveau termination-issues zien er soms uit als namespace-issues maar zijn CRD-breed. Een CRD die zelf in Terminating blijft hangen omdat instances over meerdere namespaces leven heeft een eigen diagnose.

Waarschuwing: wat je mogelijk achterlaat

Een finalizer verwijderen is alleen een metadata-aanpassing. De finalizer was een markering dat een controller nog werk te doen had; hem weghalen vertelt de API-server "dat werk is al gedaan of overgeslagen". Overgeslagen is meestal het juistere woord: het werk gebeurt simpelweg niet.

Wat dat in de praktijk betekent hangt af van de finalizer:

  • cert-manager.io/finalizer op een Certificate: opruiming van uitgegeven certificaten bij de issuer (Let's Encrypt-account, ACME-challenge-records, interne CA) wordt overgeslagen. Het Certificate-object is weg; de upstream-record niet.
  • Een CSI-driver-finalizer op een PV: het volume bij de cloud-provider (EBS, GCE PD, Azure Disk) wordt niet losgekoppeld of verwijderd. Je krijgt verweesde volumes die kosten blijven maken.
  • Een external-dns-finalizer: DNS-records die de controller in Route 53 of Cloudflare aanmaakte worden niet weggehaald.
  • Een operator-finalizer op een Kafka- of Redis-CR: cluster-niveau cleanup (topics droppen, externe systemen deregistreren, IAM-rollen vrijgeven) blijft liggen.

Voor je Fix A of Fix C gebruikt, controleer dus of de controller echt weg is of slechts tijdelijk niet gezond. Komt hij misschien terug, dan is de goedkoopste route om hem te herstellen en zijn werk te laten afmaken. Is hij voorgoed weg, draai dan achteraf een handmatige sweep van het externe systeem (cloud-console, DNS-provider, IAM) om op te ruimen wat de finalizer zou hebben afgehandeld.

Wanneer escaleren

Als je hulp gaat vragen (interne SRE-channel, support, GitHub-issues van de controller), verzamel dan:

  • De volledige output van kubectl get namespace <name> -o yaml, inclusief status.conditions.
  • De output van kubectl get apiservice (de hele tabel, niet alleen False-rijen).
  • De volledige lijst van overgebleven resources uit kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get --show-kind --ignore-not-found -n <name>.
  • De exacte finalizer-sleutels die je hebt geïdentificeerd (cert-manager.io/finalizer, kubernetes.io/pvc-protection, etc.) en welke resources ze dragen.
  • De Kubernetes-versie (kubectl version --short) en provider (EKS, GKE, AKS, on-prem met de versie van de gebruikte CNI).
  • Een notitie of de verantwoordelijke controller hoort te draaien en, zo nee, wanneer en waarom hij weg is.

Zonder dat vraagt iedereen die je helpt er sowieso eerst om. Het van tevoren verzamelen scheelt je een rondje heen en weer.

Voorkom dat het opnieuw gebeurt

  • Deïnstalleer nooit een operator zolang er nog CRs van bestaan. Verwijder eerst de CRs, wacht tot de cleanup klaar is, en deïnstalleer dan pas de operator. Was de operator al weg, ga er dan niet vanuit dat de overgebleven CRs vanzelf verdwijnen; ze blokkeren elke namespace waar ze in zitten.
  • Gebruik helm uninstall met --wait voor charts die zowel CRDs als CRs beheren, of volg de uninstall-volgorde uit de chart-documentatie (CRs, dan operator, dan CRDs).
  • Audit regelmatig op onbeschikbare APIServices. Een geplande job die alert op kubectl get apiservice | grep -v True vangt verweesde registraties op voor ze pijn gaan doen.
  • Zet failurePolicy: Ignore op optionele admission-webhooks die niet security-kritisch zijn. Een Fail-policy op een backend die wegvalt blokkeert meer dan alleen de namespace waar de webhook op gericht is; het kan elke cluster-brede cleanup op datzelfde resource-type stilleggen.
  • Documenteer welke finalizers je platform legitiem gebruikt. Loopt iets vast, dan weet je in seconden welke finalizers eigen zijn aan je platform en welke leftover, zonder dat je de bredere Kubernetes-ecosystem moet uitkammen.

Voor bredere Kubernetes-troubleshooting, zie Pod blijft in Pending: waarom Kubernetes je workload niet kan schedulen voor resource-blokkades, OOMKilled: Kubernetes out-of-memory-fouten uitgelegd voor geheugen-gerelateerde pod-failures, en Kubernetes multi-tenancy: namespace-isolatie, ResourceQuota en LimitRange voor hoe de namespace zelf wordt ingericht en welke finalizers daar typisch in komen wonen.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.