{"id":54339,"date":"2025-12-03T07:17:41","date_gmt":"2025-12-03T07:17:41","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=54339"},"modified":"2025-12-03T07:17:41","modified_gmt":"2025-12-03T07:17:41","slug":"kubernetes-what-are-finalizers","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/kubernetes-what-are-finalizers\/","title":{"rendered":"Kubernetes: What are finalizers?"},"content":{"rendered":"\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. What are finalizers, really?<\/h2>\n\n\n\n<p>When you delete a Kubernetes object (CRD, Namespace, Pod, etc.), Kubernetes <strong>does not delete it immediately<\/strong>.<\/p>\n\n\n\n<p>Instead it:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Sets <code>metadata.deletionTimestamp<\/code><\/li>\n\n\n\n<li>Leaves the object in place<\/li>\n\n\n\n<li>Waits for <strong>all finalizers<\/strong> to be removed from <code>metadata.finalizers<\/code><\/li>\n<\/ol>\n\n\n\n<p>A <strong>finalizer<\/strong> is just a string tag like:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>\"kubernetes\"<\/code> (for namespaces)<\/li>\n\n\n\n<li><code>\"customresourcecleanup.apiextensions.k8s.io\"<\/code> (for CRDs)<\/li>\n\n\n\n<li><code>\"finalizer.keda.sh\"<\/code> (for KEDA)<\/li>\n\n\n\n<li><code>\"foregroundDeletion\"<\/code> (for some resources)<\/li>\n<\/ul>\n\n\n\n<p>It means:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cBefore you remove this object from etcd, call the controller that owns this finalizer so it can clean stuff up (external state, dependent resources, DNS, volumes, etc). Once it\u2019s done, it will remove its finalizer, and then K8s can truly delete the object.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>So if the responsible controller <strong>never does its job<\/strong>, or is gone, or is misconfigured \u2192 finalizer never gets removed \u2192 resource stays <strong>Terminating forever<\/strong>.<\/p>\n\n\n\n<p>That\u2019s exactly what you saw with:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>customresourcecleanup.apiextensions.k8s.io<\/code> on your CRDs<\/li>\n\n\n\n<li><code>kubernetes<\/code> finalizer on the <code>keda<\/code> namespace<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Why does it sometimes take SO long (or never complete)?<\/h2>\n\n\n\n<p>Common reasons:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Controller is gone or broken<\/strong>\n<ul class=\"wp-block-list\">\n<li>You deleted the operator\/Helm release <strong>before<\/strong> deleting the CRD or its instances<\/li>\n\n\n\n<li>Now the CRD has finalizers, but the controller that should remove them no longer exists<\/li>\n\n\n\n<li>Kubernetes waits\u2026 forever.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Controller can\u2019t reach its backend<\/strong>\n<ul class=\"wp-block-list\">\n<li>For example, deletion wants to clean something in AWS, but AWS creds are broken<\/li>\n\n\n\n<li>Cleanup fails, finalizer stays, resource never finishes deleting.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Namespace-level finalizer (<code>kubernetes<\/code>)<\/strong>\n<ul class=\"wp-block-list\">\n<li>When you delete a namespace, K8s tries to clean <strong>everything inside it<\/strong><\/li>\n\n\n\n<li>If <em>any<\/em> object is stuck (webhook, CRD instance, PVC, etc.), the namespace stays <code>Terminating<\/code> forever.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Buggy or over-eager operators<\/strong>\n<ul class=\"wp-block-list\">\n<li>Some operators add finalizers everywhere but don\u2019t handle edge cases well.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p>So the long waits \/ hangs are <strong>by design<\/strong>: Kubernetes is saying<br>\u201cBefore I forget this object, I must give controllers a chance to clean up external stuff.\u201d<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. Is there a \u201cbetter\u201d way? (In practice)<\/h2>\n\n\n\n<p>There\u2019s no magic global flag like \u201cignore all finalizers\u201d, but you <em>can<\/em> make this much less painful by following some practices:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">a) Always uninstall the app\/operator correctly<\/h3>\n\n\n\n<p>For things like KEDA, Prometheus, cert-manager, etc:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Prefer <strong>Helm uninstall<\/strong> or vendor\u2019s documented uninstall procedure.<\/li>\n\n\n\n<li>This gives the operator time to:\n<ul class=\"wp-block-list\">\n<li>Clean its CR instances<\/li>\n\n\n\n<li>Remove finalizers from them<\/li>\n\n\n\n<li>Let CRDs\/namespace delete without hanging<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Deleting CRDs or namespaces first and operators later is the most common way to get into trouble.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">b) Only patch finalizers as a <strong>last resort<\/strong><\/h3>\n\n\n\n<p>What you did (patching <code>finalizers: []<\/code>) is the right <em>last step<\/em>, but it comes with trade-offs:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You\u2019re telling K8s: \u201cDon\u2019t wait for cleanup, just forget this resource.\u201d<\/li>\n\n\n\n<li>If the controller was supposed to delete something external (buckets, DNS, etc.), that cleanup <strong>may never happen<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p>For <strong>dev\/sandbox clusters<\/strong> \u2192 totally fine.<br>For <strong>prod<\/strong> \u2192 should be done carefully, knowing what might be left behind.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">c) How to quickly diagnose \u201cwhy is this stuck?\u201d<\/h3>\n\n\n\n<p>When something is Terminating forever, my standard steps are:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Check finalizers:<\/strong> <code>kubectl get &lt;kind> &lt;name> -n &lt;ns> -o jsonpath='{.metadata.finalizers}'<\/code> That tells you <strong>who is holding the deletion<\/strong>.<\/li>\n\n\n\n<li><strong>Check events:<\/strong><code>kubectl describe &lt;kind> &lt;name> -n &lt;ns><\/code> Sometimes you\u2019ll see helpful errors like:\n<ul class=\"wp-block-list\">\n<li>\u201ccannot contact webhook \u2026\u201d<\/li>\n\n\n\n<li>\u201cfailed to clean up custom resources \u2026\u201d<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Check controller logs<\/strong> for the finalizer owner\n<ul class=\"wp-block-list\">\n<li>For KEDA finalizer \u2192 <code>kubectl logs -n keda deploy\/keda-operator<\/code><\/li>\n\n\n\n<li>For CRD cleanup \u2192 <code>kube-apiserver<\/code> \/ <code>apiextensions-apiserver<\/code> logs (harder on managed clusters)<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">d) How to avoid this pain in future?<\/h3>\n\n\n\n<p>For your use case (EKS + addons like KEDA, Datadog, etc.):<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Use Helm (or GitOps) as the source of truth<\/strong>\n<ul class=\"wp-block-list\">\n<li>Install\/upgrade\/uninstall via Helm.<\/li>\n\n\n\n<li>When decommissioning: <code>helm uninstall &lt;release><\/code> first, <em>then<\/em> delete CRDs if needed.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Don\u2019t nuke CRDs and namespaces first<\/strong>\n<ul class=\"wp-block-list\">\n<li>If you need to remove KEDA:\n<ul class=\"wp-block-list\">\n<li><code>helm uninstall keda -n keda<\/code><\/li>\n\n\n\n<li>Wait for CRs to disappear.<\/li>\n\n\n\n<li>Then remove CRDs if you really want.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Keep operators running until cleanup is finished<\/strong>\n<ul class=\"wp-block-list\">\n<li>Don\u2019t delete operator deployments before their resources are gone.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Accept that in dev, patching is normal<\/strong>\n<ul class=\"wp-block-list\">\n<li>In dev\/sandbox clusters, patching finalizers (<code>kubectl patch ... finalizers: []<\/code>) is a perfectly OK escape hatch.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. TL;DR in human language<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Finalizers are \u201chooks\u201d that block deletion until cleanup is done.<\/li>\n\n\n\n<li>They\u2019re good for correctness, but <strong>awful for UX<\/strong> if the responsible controller is gone or broken.<\/li>\n\n\n\n<li>That\u2019s why your CRDs and namespace took ages \/ got stuck.<\/li>\n\n\n\n<li>Best you can do:\n<ul class=\"wp-block-list\">\n<li>Uninstall apps the <em>clean<\/em> way (Helm uninstall, not CRD delete first).<\/li>\n\n\n\n<li>Only patch-out finalizers when you know what you\u2019re skipping.<\/li>\n\n\n\n<li>In dev: patching is fine. In prod: be deliberate.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>1. What are finalizers, really? When you delete a Kubernetes object (CRD, Namespace, Pod, etc.), Kubernetes does not delete it immediately. Instead it: A finalizer is just a string tag like: It means: \u201cBefore you remove this object from etcd, call the controller that owns this finalizer so it can clean stuff up (external state,&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-54339","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/54339","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=54339"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/54339\/revisions"}],"predecessor-version":[{"id":54340,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/54339\/revisions\/54340"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=54339"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=54339"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=54339"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}