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

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.