24.6 Helm Hooks: pre-install, post-upgrade, pre-delete
Now, let’s talk about Helm Hooks, the Swiss Army knife of Helm chart automation and, occasionally, the source of your most baffling “why is that pod just sitting there?” debugging sessions. You use these when you need something to happen around your main deployment process—not as part of the main application’s lifecycle, but as a supporting act. Think of them as the stagehands that set up the scenery before the play, adjust things between acts, and clean up after the final curtain.
The core idea is simple: you annotate a Kubernetes manifest in your templates/ directory to tell Helm, “Hey, don’t treat this like a normal resource. Do this special thing at a specific time.” Without a hook, Helm just sends all your manifests to Kubernetes and hopes for the best. Hooks let you sequence things.
The Hook Annotations: Your Control Panel
Every hook is defined by two, and sometimes three, annotations. Here’s the blueprint you’ll slap onto a Job, Pod, or any other resource:
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-database-migrate"
annotations:
"helm.sh/hook": post-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
The main event is helm.sh/hook. The most common hooks you’ll use are:
pre-install: Runs after templates are rendered, but before any resources are created. Perfect for setting up stuff like custom CRDs or namespaces that your main app depends on.post-install: Runs after all resources are created. This is your “okay, the main app is up, now go seed the database” hook.pre-upgrade/post-upgrade: These are the workhorses.pre-upgraderuns before the upgrade and is great for backing up data.post-upgrade(like above) is infamous for running database migrations. This is where you’ll live 90% of your hook-driven life.pre-delete/post-delete: These run when youhelm uninstall.pre-deleteis your last chance to gracefully extract data or de-register from a service catalog before things get nuked.post-deleteis less common because, well, what’s left to run on?
Why You Need Hook Weights
Here’s the first “questionable choice” we must address: by default, Helm runs all hooks of a given type in a non-deterministic order. Let that sink in. If you have two post-upgrade jobs, Helm might run Job B before Job A. This is a fantastic way to break your application if Job A is a prerequisite for Job B.
The solution is the helm.sh/hook-weight annotation. This can be any integer, positive or negative, and Helm sorts them in ascending numerical order. So a hook with weight -5 runs before a hook with weight 5. I often use negative weights for “pre-” steps and positive for “post-” steps within the same hook type, but it’s entirely up to you. The important thing is that you define the sequence. Always use weights. No excuses.
Cleaning Up After Yourself: Hook Delete Policies
A hook resource, like a Job, is created by Helm to do its thing. But afterwards, you’re left with a completed Job resource sitting in your namespace. By default, it just stays there forever. This is clutter. You need to tell Helm how to clean it up using helm.sh/hook-delete-policy.
hook-succeeded: Delete the resource if the hook succeeded (e.g., the Job completed with exit code 0).hook-failed: Delete the resource if the hook failed. Be careful with this one—if you delete the failed job immediately, you have no logs to debug why it failed!before-hook-creation: This is the most important one. It says “before you run this hook again, delete the previous instance.” This is critical forpost-upgradejobs. Without it, if you runhelm upgradea second time, Helm will try to create a new Job with the same name as the existing one, and it will fail with a “already exists” error. You almost always wantbefore-hook-creation.
My standard, no-nonsense policy is to use before-hook-creation,hook-succeeded. This cleans up the old job before running a new one and then cleans up the new one if it succeeds. If it fails, the resource is left behind for me to inspect.
# A full example of a reliable database migration job
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
annotations:
"helm.sh/hook": post-upgrade
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrator
image: "my-app-db-migrator:{{ .Values.imageTag }}"
command: ["rake", "db:migrate"]
The Biggest Pitfall: Hooks and Helm History
Here’s the real trench wisdom. Helm tracks the resources it manages. A hook is a managed resource. When you run helm uninstall, by default, it tries to delete everything it ever created, including your pre-delete hook. This creates a race condition: the pre-delete hook is supposed to run to back up your data, but Helm might try to delete it before it completes.
The solution is to use the "helm.sh/hook-delete-policy": hook-succeeded for your pre-delete hook and let it delete itself upon success. This way, by the time Helm goes to clean up the rest, the hook resource is already gone and won’t cause a conflict. It’s a bit of a hack, but it works.
Hooks are powerful, but they inject imperative, procedural logic into your declarative Helm charts. Use them sparingly, test them relentlessly (especially the cleanup policies), and always, always assign a weight. Your future self, staring at a broken deployment at 2 AM, will thank you.