Helmfile: Declarative Management for Helm Charts
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.
Written by
Patrick HütterFounder & 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.
You might also like
CI Runners in a microVM: Docker Builds with Kata Containers on Kubernetes
Aug 31, 2026 · 10 min read
GitOps with Helmfile and Kyverno: Our Deployment Workflow
Mar 10, 2025 · 7 min read
K3s and KubeVirt: Converged Infrastructure on Bare Metal
Jan 20, 2025 · 6 min read