Helmfile: Or How I Learned to Stop Worrying and Love Kubernetes Deployments

Have you ever tried to deploy multiple Helm charts and felt like you were juggling flaming torches while riding a unicycle? Yeah, me too. That's where Helmfile comes in.
What Even Is This Thing?
Helmfile is basically what happens when someone looks at Helm and says "this is nice, but what if we could make it even more declarative?". It's a tool that wraps around Helm and lets you define your entire deployment state in a single YAML file. Or multiple files. Or a combination of both that will make you question your life choices.
Think of it as the package manager for your package manager. Because apparently, we needed to go deeper.
The "Why" Behind the Madness
So why would you want to use Helmfile instead of just running helm install
commands until your fingers bleed? Good question.
Helmfile solves the problem of managing multiple environments without wanting to throw your laptop out the window. You know that feeling when you have dev, staging, and production environments, and each one needs slightly different values? Helmfile says "hold my beer" and lets you template your way out of that nightmare.
Here's the thing though. Helm is great for packaging Kubernetes applications, but when you need to deploy a full stack with dependencies, it gets messy fast. Like, "explaining to your boss why the entire production cluster is down" messy.
The Declarative Dream
Helmfile takes the GitOps approach seriously. You write what you want, version control it, and apply it. Simple, right? Well, mostly.
repositories:
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
releases:
- name: prom-norbac-ubuntu
namespace: prometheus
chart: prometheus-community/prometheus
set:
- name: rbac.create
value: false
This is basically Helmfile saying "I want Prometheus, and I want it now". The beauty is in the simplicity. Or the complexity, depending on how deep you go.
Environment Templating: Where Things Get Spicy
Now here's where Helmfile starts to show its real power. You can define different environments and have your releases behave differently based on which environment you're targeting.
environments:
dev:
values:
- env: dev
- replicas: 1
production:
values:
- env: prod
- replicas: 5
releases:
- name: myapp
chart: ./charts/myapp
values:
- replicas: {{ .Values.replicas }}
Is this over-engineering? Probably. Will it save you from manually updating configs across environments? Absolutely.
The Dependencies Dance
Remember when I mentioned dependencies? This is where Helmfile gets interesting. You can define which releases need to be installed before others using the needs
keyword.
releases:
- name: database
chart: stable/postgresql
- name: backend
chart: ./charts/backend
needs:
- database
- name: frontend
chart: ./charts/frontend
needs:
- backend
Helmfile will install these in order: database, then backend, then frontend. Revolutionary stuff, right? Well, actually, yes.
But here's where it gets fun. There's this delightful bug where cross-file dependencies don't work properly. You split your releases across multiple files for organization, and suddenly Helmfile can't find dependencies that exist in other files. It's like having a conversation with someone who can only remember what happened in the current room.
The Multi-File Headache
Speaking of multiple files, let's talk about the elephant in the room. You'd think splitting your Helmfile into multiple files would make things more organized. And it does, until you try to use dependencies across files.
# helmfile-split.yaml
helmfiles:
- helmfile-release-1.yaml
releases:
- name: release-2
needs:
- release-1 # This will fail spectacularly
The error message is beautifully unhelpful: "release(s) 'default/release-2' depend(s) on an undefined release 'default/release-1'". Even though release-1 clearly exists in the other file.
This has been a known issue for years. It's like Helmfile has selective amnesia when it comes to cross-file references.
Environment Variables: The Good, The Bad, The Ugly
Helmfile supports environment variables, which is great until you realize how many there are. There's HELMFILE_DISABLE_INSECURE_FEATURES
, HELMFILE_EXPERIMENTAL
, and my personal favorite, HELMFILE_GO_YAML_V3
.
Why do we need a flag to choose between YAML parsers? Because apparently, the world of YAML parsing is more controversial than pineapple on pizza.
Templating Within Templates
Here's where Helmfile gets really meta. You're using Go templates to generate Helm templates, which generate Kubernetes manifests. It's templates all the way down.
releases:
- name: {{ .Values.appName }}-{{ .Environment.Name }}
chart: {{ .Values.chartName }}
values:
- image:
tag: {{ .Values.imageTag | default "latest" }}
This is either brilliant or insane, depending on your perspective. Maybe both.
The Sync vs Apply Debate
Helmfile gives you two main commands: sync
and apply
. The difference? sync
will always run, while apply
only runs when there are changes.
Sounds simple, right? Wrong. The apply
command uses the helm-diff plugin to detect changes, and this plugin has its own opinions about what constitutes a "change". Sometimes it thinks everything changed, sometimes it misses actual changes.
It's like having a security guard who either lets everyone in or locks out the building owner.
Best Practices That Actually Work
After years of battle scars, here are some patterns that don't make you want to quit your job:
Use release templates to avoid repetition. Instead of copying the same namespace and values configuration everywhere, define it once:
templates:
default: &default
namespace: my-namespace
values:
- values.yaml
releases:
- name: app1
<<: *default
chart: ./charts/app1
- name: app2
<<: *default
chart: ./charts/app2
This is YAML anchors doing the heavy lifting, and it actually works.
When Things Go Wrong
And they will go wrong. The most common error you'll see is the dreaded "key already set in map" error. This happens when you use YAML anchors and Helmfile's templating at the same time, creating a perfect storm of confusion.
The fix? Set HELMFILE_GOCCY_GOYAML=true
or use the inherit
functionality instead of YAML anchors. Because apparently, we needed yet another environment variable to make YAML work properly.
The Learning Curve
Helmfile has a learning curve steeper than a ski jump. You need to understand Helm, Kubernetes, Go templating, YAML, and whatever arcane knowledge is required to debug cross-file dependencies.
But once you get it working, it's actually pretty powerful. You can manage complex deployments across multiple environments with a single command. That's worth something, right?
Production War Stories
In the real world, Helmfile is used to manage entire infrastructure stacks. People deploy monitoring, logging, ingress controllers, and applications all from a single Helmfile configuration.
The key is starting simple and gradually adding complexity. Don't try to template everything on day one. Start with basic releases and add environments and dependencies as you need them.
Tools and Ecosystem
Helmfile plays well with CI/CD systems, though you'll want to be careful about those environment variables. Many teams use it with ArgoCD for GitOps workflows, creating a deployment pipeline that's both declarative and repeatable.
The plugin ecosystem is decent, with helm-diff being the most important one. Without it, the apply
command doesn't work, which kind of defeats the point.
Real-World Usage Patterns
Most teams end up with a structure like this:
helmfile.d/
├── environments/
│ ├── dev.yaml
│ ├── staging.yaml
│ └── production.yaml
├── releases/
│ ├── monitoring.yaml
│ ├── ingress.yaml
│ └── apps.yaml
└── helmfile.yaml
This gives you organization without hitting the cross-file dependency bugs. Mostly.
The Bottom Line
Helmfile is a tool that solves real problems, even if it creates a few of its own along the way. It's not perfect, but then again, what is in the Kubernetes ecosystem?
The key to success with Helmfile is understanding its limitations and working around them. Don't try to be too clever with cross-file dependencies. Do use environment templating for managing multiple deployment targets. And always, always test your changes in a dev environment first.
Is it worth learning? If you're managing more than a handful of Helm charts across multiple environments, probably yes. Just be prepared for some debugging sessions that will test your patience and your understanding of YAML parsing edge cases.
After all, someone has to manage all those microservices, and Helmfile beats doing it by hand. Most of the time.