Helmfile: Declarative Management for Helm Charts
Back to Blog

Helmfile: Declarative Management for Helm Charts

6 min read
Read in Deutsch

When Helm Alone No Longer Scales

Helm has made a lot of things easier. Instead of raw Kubernetes manifests, you work with parameterized charts that can be configured per environment. But anyone managing a project with twenty or more Helm releases quickly realizes: the complexity just shifts.

In one of our current client projects, roughly thirty services run per cluster -- custom Spring Boot applications, databases, ingress controllers, monitoring stacks, message brokers. Each release requires its own helm upgrade command with the right combination of chart version, values file, and namespace. Multiplied by three environments -- development, staging, production -- that's close to a hundred individual Helm commands that need to be executed in the right order with the right parameters.

Shell scripts help at first but quickly become fragile. The question is: why do we describe our infrastructure declaratively in Kubernetes manifests but manage deployments imperatively with individual Helm commands?

This is exactly where Helmfile comes in.

What Helmfile Does

Helmfile is a declarative wrapper around Helm. Instead of installing releases one by one via the command line, you describe the desired state of all releases in a single YAML file -- the helmfile.yaml. Helmfile reads this file, compares the desired state with the actual state in the cluster, and executes the necessary changes.

The concept is comparable to Terraform for cloud infrastructure: you define what you want, not how to get there. The key difference from Kustomize or plain Helm is that Helmfile specializes in orchestrating multiple Helm releases rather than manipulating individual manifests.

Structure of a helmfile.yaml

The structure is intentionally kept simple. A helmfile.yaml consists of three core sections: repositories, releases, and environments.

repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami
  - name: ingress-nginx
    url: https://kubernetes.github.io/ingress-nginx
  - name: prometheus-community
    url: https://prometheus-community.github.io/helm-charts

releases:
  - name: my-spring-app
    namespace: application
    chart: ./charts/spring-boot-app
    version: 1.4.0
    values:
      - values/{{ .Environment.Name }}/app.yaml

  - name: postgresql
    namespace: database
    chart: bitnami/postgresql
    version: 9.8.4
    values:
      - values/{{ .Environment.Name }}/database.yaml

  - name: ingress-nginx
    namespace: ingress
    chart: ingress-nginx/ingress-nginx
    version: 3.7.1
    values:
      - values/{{ .Environment.Name }}/ingress.yaml

  - name: prometheus
    namespace: monitoring
    chart: prometheus-community/kube-prometheus-stack
    version: 10.1.0
    values:
      - values/{{ .Environment.Name }}/monitoring.yaml

environments:
  dev:
    values:
      - environments/dev.yaml
  staging:
    values:
      - environments/staging.yaml
  production:
    values:
      - environments/production.yaml

In the repositories block, you define all Helm repositories from which charts are sourced. In the releases block, you list each Helm release with its name, namespace, chart source, and values files. The environments block enables environment-specific configurations.

The expression {{ .Environment.Name }} is a Go template that gets replaced at runtime with the name of the active environment. This way, Helmfile automatically loads the appropriate values files without needing to maintain separate configuration files.

Directory Structure for Values

In practice, the following directory structure has proven effective:

helmfile/
  helmfile.yaml
  environments/
    dev.yaml
    staging.yaml
    production.yaml
  values/
    dev/
      app.yaml
      database.yaml
      ingress.yaml
      monitoring.yaml
    staging/
      app.yaml
      database.yaml
      ingress.yaml
      monitoring.yaml
    production/
      app.yaml
      database.yaml
      ingress.yaml
      monitoring.yaml

Each environment has its own values files per release. Global environment variables -- such as cluster domain, registry URL, or resource defaults -- reside in the respective environments/*.yaml. These values are then available to all releases as template variables.

The Key Commands

Helmfile offers a handful of commands that cover the entire lifecycle:

# Apply desired state -- installs new releases, updates existing ones
helmfile -e production sync

# Show changes without executing them (dry-run with diff)
helmfile -e production diff

# Update only changed releases (more efficient than sync)
helmfile -e production apply

# Remove all releases for the environment
helmfile -e production destroy

# Render templates locally (for review)
helmfile -e production template

The -e parameter selects the environment. helmfile sync brings the cluster to the defined state -- new releases are installed, existing ones updated. helmfile diff shows the changes without executing them, similar to terraform plan. helmfile apply combines both: it shows the diff and only executes the releases that actually changed.

In our CI/CD pipelines, we use helmfile diff as a validation step and helmfile apply for the actual deployment. This way, you can see exactly what will change in the pipeline log before the changes take effect.

Imperative vs. Declarative: The Difference in Practice

Without Helmfile, a deployment script for four releases looks roughly like this:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm upgrade --install postgresql bitnami/postgresql \
  --namespace database --version 9.8.4 \
  -f values/production/database.yaml

helm upgrade --install my-spring-app ./charts/spring-boot-app \
  --namespace application --version 1.4.0 \
  -f values/production/app.yaml

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress --version 3.7.1 \
  -f values/production/ingress.yaml

helm upgrade --install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring --version 10.1.0 \
  -f values/production/monitoring.yaml

That works, but it has drawbacks: the order is implicit, errors in one command don't necessarily abort the entire deployment, and the desired state is scattered across multiple commands. With Helmfile, it reduces to:

helmfile -e production apply

A single command that reconciles the entire state. Declarative instead of imperative.

Helmfile and GitOps

Helmfile fits seamlessly into GitOps workflows. The helmfile.yaml and all values files reside in the Git repository. Changes go through the normal pull request process with code review. The CI/CD pipeline runs helmfile apply on a merge to the main branch and brings the cluster to the desired state.

This means: Git is the single source of truth for the entire cluster state. Every change is traceable, every deployment reproducible. If you want to know the current state of a cluster, you look at the repository, not the cluster.

Environment variables can be included via the environments section or through Helmfile's built-in support for environment variables. Secrets can be stored encrypted in the repository via the helm-secrets plugin, so that sensitive values are versioned without being visible in plaintext.

Comparison with Other Tools

Helmfile is not the only option for declarative Kubernetes management. Kustomize works directly on Kubernetes manifests without Helm and is a good fit for teams that want to avoid the Helm overhead. Terraform with the Helm provider offers state management and is suitable when you want to manage cloud infrastructure and Kubernetes deployments in the same tool.

Helmfile positions itself in between: it uses Helm as a foundation but extends it with declarative orchestration of multiple releases. If you've already invested in Helm charts and want to continue using them, Helmfile is the most flexible tool for managing those charts across environments.

Conclusion

Helm solves the problem of parameterizing individual deployments. Helmfile solves the problem of orchestrating many deployments. As soon as a cluster encompasses more than a handful of releases -- which is the norm in microservice architectures -- the use of Helmfile quickly pays for itself.

Getting started is straightforward: create a helmfile.yaml with your existing releases, move values files into the appropriate directory structure, and run helmfile sync. From there, you can gradually extend the configuration with environments, templates, and secrets.

In our projects, Helmfile has largely replaced deployment scripts. The cluster state is documented in the repository, changes are traceable, and a new deployment is a single command. If you're already using Helm and struggling with managing many releases, you should give Helmfile a try.

Patrick Hütter

Written by

Patrick Hütter

Founder & Software Architect

Software architect, engineer and entrepreneur. Patrick has been building products and platforms for over a decade — from enterprise backends and cloud-native infrastructure to AI-powered applications. As founder of encircle360, he combines deep technical expertise with entrepreneurial vision, driving open source projects that create real impact.