Files
helmfile/docs/index.md
T
Dominik Schmidtandyxxhero 443cef0dc1 feat: dir= selector for path-based release filtering and traversal skip (#2609)
* feat: add dir= selector for path-based release filtering and traversal skip

Introduce a new selector key `dir=<path>` to the -l/--selector flag with two
effects: it filters releases by the directory of their defining helmfile
relative to the root helmfile (using directory-prefix matching, so
dir=apps/foo matches both apps/foo and apps/foo/sub), and it short-circuits
sub-helmfile traversal when paired with positive dir= constraints, so
non-matching branches in the helmfiles: tree are not parsed, templated, or
fetched.

The motivation is consuming aggregator-style upstreams that the operator
does not control (opendesk being the immediate example), where the user
cannot restructure the helmfile layout but still wants to act on a subset.

The dir label is auto-populated at filter time only; user-facing label
output is unchanged. The label key "dir" is reserved at state load.
Selectors that escape the root via .. or absolute paths, and the bare ".",
are rejected at parse time.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* refactor: use sync.Once for root dir resolution, extract skipForDirFilter

Add a unit test covering the per-entry skip decision: match descends,
sibling skipped, remote never short-circuited.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* cmd: shorten dir selector help text, link to docs

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* docs: highlight selectors inheritance caveat, add bases example for dir label

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* feat: warn instead of fail on user-defined dir label

Hard-failing on an existing dir label is a breaking change. Log a
deprecation warning for now and turn it into an error in a later release.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* refactor: apply go fix modernizers to dir selector code

Upstream CI now fails when go fix would rewrite code. Use maps.Copy in
injectLabel and drop the explicit embedded ReleaseSetSpec in test
literals.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* fix: tighten dir selector edge cases found in review

- Always shadow a user-defined dir label at match time, also when the
  release has no auto-populated value (remote or outside-root helmfiles).
  Previously the user value was matched there despite the warning.
- Skip sibling helmfiles entries that are directories, not only files.
- Warn about a dir label declared in a release template, and warn only
  once when it comes from a bases file.
- Share the root-relative path and at-or-below checks between the release
  filter and the traversal skip; a directory whose name merely starts with
  two dots is no longer treated as escaping the root.
- Log when the root directory cannot be resolved.
- Tests: require for setup steps, t.Chdir, shared app fixture.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* docs: sync dir selector CLI help, document anchor and traversal assumption

docs/cli.md still carried the long help text. Also spell out what the dir
value is relative to, that the traversal skip assumes nested helmfiles
stay below their parent's directory, and that user-defined dir labels are
ignored for releases without an auto-populated value as well.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* fix: complete dir selector values relative to the root helmfile

Completion listed directories relative to the working directory, while
dir values are relative to the root helmfile directory. Resolve the root
from -f or HELMFILE_FILE_PATH with the same logic the app uses, and offer
nothing for values the selector would reject (absolute paths, paths that
escape the root) or for a remote root helmfile.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* refactor(state): flatten nested conditionals in label filter parsing and matching

- LabelFilter.Match: extract labelMatches helper; positive/negative loops
  become single-level guards. Also removes the pre-existing empty
  then-branch (if !ok {} else if) in the negative-label loop and the
  redundant len()>0 guards around the loops
- positiveLabelsCompatibleWith: extract positiveLabelPairCompatible so the
  double loop holds one guard instead of a nested if chain
- ParseLabels: extract parseOneLabel (separator detection, dir= value
  validation, split) so the three-level regex/dir/error nesting becomes
  flat; drop the vestigial trailing 'var err error' return

No behavior change; state/app/cmd tests incl. -race and golangci-lint pass.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-06 08:21:35 +08:00

14 KiB

Helmfile

Tests Container Image Repository on GHCR Go Report Card Slack Community #helmfile Documentation

Deploy Kubernetes Helm Charts

Status

May 2025 Update

  • Helmfile v1.0 and v1.1 has been released. We recommend upgrading directly to v1.1 if you are still using v0.x.
  • If you haven't already upgraded, please go over this v1 proposal here to see a small list of breaking changes.

About

Helmfile is a declarative spec for deploying helm charts. It lets you...

  • Keep a directory of chart value files and maintain changes in version control.
  • Apply CI/CD to configuration changes.
  • Periodically sync to avoid skew in environments.

To avoid upgrades for each iteration of helm, the helmfile executable delegates to helm - as a result, helm must be installed.

NOTE: Helmfile supports both Helm 3.x and Helm 4.x.

Highlights

Declarative: Write, version-control, apply the desired state file for visibility and reproducibility.

Modules: Modularize common patterns of your infrastructure, distribute it via Git, S3, etc. to be reused across the entire company (See #648)

Versatility: Manage your cluster consisting of charts, kustomizations, and directories of Kubernetes resources, turning everything to Helm releases (See #673)

Patch: JSON/Strategic-Merge Patch Kubernetes resources before helm-installing, without forking upstream charts (See #673)

Observability: Opt-in OpenTelemetry traces and metrics for CI/CD runs — see where time goes, per release and per helm invocation (experimental)

Installation

  • download one of releases
  • run as a container
  • Archlinux: install via pacman -S helmfile
  • openSUSE: install via zypper in helmfile assuming you are on Tumbleweed; if you are on Leap you must add the kubic repo for your distribution version once before that command, e.g. zypper ar https://download.opensuse.org/repositories/devel:/kubic/openSUSE_Leap_\$releasever kubic
  • Windows (using scoop): scoop install helmfile
  • macOS (using homebrew): brew install helmfile

Running as a container

The Helmfile Docker images are available in GHCR. Make sure you pick the right tag for your version. Example:

$ docker run --rm --net=host -v "${HOME}/.kube:/helm/.kube" -v "${HOME}/.config/helm:/helm/.config/helm" -v "${PWD}:/wd" --workdir /wd ghcr.io/helmfile/helmfile:v1.1.0 helmfile sync

You can also use a shim to make calling the binary easier:

$ printf '%s\n' '#!/bin/sh' 'docker run --rm --net=host -v "${HOME}/.kube:/helm/.kube" -v "${HOME}/.config/helm:/helm/.config/helm" -v "${PWD}:/wd" --workdir /wd ghcr.io/helmfile/helmfile:v1.1.0 helmfile "$@"' |
    tee helmfile
$ chmod +x helmfile
$ ./helmfile sync

Getting Started

Prerequisites

  • A Kubernetes cluster (e.g., minikube, kind, or a cloud provider)
  • Helm 3+ installed (helm version to verify)

Step 1: Install Helmfile

Choose one of the following:

# macOS
brew install helmfile

# Linux - download from GitHub releases
curl -L https://github.com/helmfile/helmfile/releases/latest/download/helmfile_$(uname -s)_$(uname -m) -o /usr/local/bin/helmfile && chmod +x /usr/local/bin/helmfile

# Windows
scoop install helmfile

Verify: helmfile version

Step 2: Create your first helmfile.yaml

Use helmfile create to generate a project scaffold with best-practice directory structure:

# Scaffold a new project
helmfile create my-project
cd my-project

Or create a helmfile.yaml manually:

repositories:
  - name: prometheus-community
    url: https://prometheus-community.github.io/helm-charts

releases:
  - name: my-prometheus
    namespace: monitoring
    createNamespace: true
    chart: prometheus-community/prometheus
    values:
      - server:
          persistentVolume:
            enabled: false

What does this do?

  • repositories — tells Helm where to find charts (like a package registry)
  • releases — each entry is a Helm release to deploy
    • name — a unique name for this deployment
    • namespace — which Kubernetes namespace to deploy into
    • chart — which Helm chart to use (repository-name/chart-name)
    • values — customize the chart's default settings

Step 3: Initialize and deploy

# Initialize - checks helm and installs required plugins
helmfile init

# See what would be deployed (dry-run)
helmfile diff

# Deploy to your cluster
helmfile apply

Congratulations! You now have Prometheus running in your cluster.

Step 4: Make changes and re-apply

Edit helmfile.yaml to add another release:

repositories:
  - name: prometheus-community
    url: https://prometheus-community.github.io/helm-charts

releases:
  - name: my-prometheus
    namespace: monitoring
    createNamespace: true
    chart: prometheus-community/prometheus
    values:
      - server:
          persistentVolume:
            enabled: false

  - name: my-grafana
    namespace: monitoring
    chart: grafana/grafana

Run helmfile apply again — Helmfile will detect the new release and only deploy what changed.

Step 5: Clean up

# Remove everything
helmfile destroy

Next Steps

Now that you have the basics, explore these topics:

Core concepts (read in order):

  1. Writing Helmfile — patterns and best practices for structuring helmfiles
  2. Values and Merging — how Helmfile merges values from multiple sources
  3. Environments — manage dev/staging/production with a single helmfile

Reference material (look up as needed):

Labels Overview

A selector can be used to only target a subset of releases when running Helmfile. This is useful for large helmfiles with releases that are logically grouped together.

Labels are simple key value pairs that are an optional field of the release spec. When selecting by label, the search can be inverted. tier!=backend would match all releases that do NOT have the tier: backend label. tier=frontend would only match releases with the tier: frontend label.

Multiple labels can be specified using , as a separator. A release must match all selectors in order to be selected for the final helm command.

The selector parameter can be specified multiple times. Each parameter is resolved independently so a release that matches any parameter will be used.

--selector tier=frontend --selector tier=backend will select all the charts.

In addition to user supplied labels, the name, the namespace, and the chart are available to be used as selectors. The chart will just be the chart name excluding the repository (Example stable/filebeat would be selected using --selector chart=filebeat).

The dir selector

The dir= selector key matches releases by the directory of their defining helmfile, relative to the root directory: the directory of the file passed via -f (or that directory itself when -f points to one), or the current directory when no -f is given. Unlike other selectors, dir= uses directory-prefix matching: --selector dir=apps/opencloud matches both releases defined directly in apps/opencloud/ and those in any subdirectory below it. Values are normalized (trailing slashes and ./ prefixes are stripped). Absolute paths, .. segments that escape the root, and the bare . are rejected at parse time; omit the selector entirely to match every release.

The dir selector has a second, more important effect: it short-circuits sub-helmfile traversal. When dir= is set, helmfile inspects each helmfiles: entry's path before loading or templating it, and skips any branch whose location rules out a matching release. For projects that aggregate many services via nested helmfiles (e.g. opendesk's helmfile_generic.yaml.gotmpl lists ~13 services), running helmfile -l dir=apps/<single-service> sync becomes roughly as fast as targeting that single service directly, without the cost of parsing and templating the other branches.

The label key dir is reserved. If commonLabels, a release's labels or a release template declares a key named dir, helmfile logs a warning and the user-defined value is ignored for selector matching (the auto-populated value shadows it, also for releases that have no auto-populated value). Deprecation: this will become a hard error in a future release, so rename such labels now.

Interactions worth knowing

  • Releases loaded from remote (git::…) helmfiles do not receive a dir label and the optimization does not engage for them; they are always descended into. The same applies to sub-helmfiles located outside the root directory (e.g. helmfiles: [../shared/helmfile.yaml]).
  • The skip decision only looks at the location of the helmfiles: entry itself, so it assumes that nested helmfiles stay at or below their parent's directory. A branch that reaches the target via .. (e.g. stacks/prod/helmfile.yaml listing ../../apps/foo/helmfile.yaml) is skipped for -l dir=apps/foo, and its releases are not found.
  • Without -f, an auto-discovered helmfile.d/ is anchored at the current directory, so its releases match dir=helmfile.d. With -f helmfile.d the anchor is helmfile.d itself.
  • A nested helmfiles: entry that declares its own selectors: block replaces the CLI selectors for its sub-tree per the existing inheritance model, so a CLI -l dir=… neither filters releases in that branch nor skips traversal below it.
  • Releases declared in a file loaded via bases: are merged into the including helmfile, so their dir value reflects the including helmfile's location rather than the base file's. For example, with apps/web/helmfile.yaml containing bases: [../../common/releases.yaml], every release declared in common/releases.yaml gets dir=apps/web, not dir=common; -l dir=common matches nothing.
  • Globs in helmfiles: (e.g. apps/**/helmfile.yaml) are expanded before the dir-skip check, so a missing-file error in an entry that the dir filter would have skipped still fails the parent load. Use missingFileHandler: Warn if this is a problem.
  • Cross-branch transitive needs: are not auto-resurrected across pruned branches. If a release in apps/x depends on one in apps/y, run with a dir= covering both subtrees.

commonLabels can be used when you want to apply the same label to all releases and use templating based on that. For instance, you install a number of charts on every customer but need to provide different values file per customer.

templates/common.yaml:

templates:
  nginx: &nginx
    name: nginx
    chart: stable/nginx-ingress
    values:
    - ../values/common/{{ .Release.Name }}.yaml
    - ../values/{{ .Release.Labels.customer }}/{{ .Release.Name }}.yaml

  cert-manager: &cert-manager
    name: cert-manager
    chart: jetstack/cert-manager
    values:
    - ../values/common/{{ .Release.Name }}.yaml
    - ../values/{{ .Release.Labels.customer }}/{{ .Release.Name }}.yaml

helmfile.yaml:

{{ readFile "templates/common.yaml" }}

commonLabels:
  customer: company

releases:
- <<: *nginx
- <<: *cert-manager

Advanced

Community

Join our friendly slack community in the #helmfile channel to ask questions and get help. Check out our slack archive for good examples of how others are using it.

Experimental Features

Some experimental features may be available for testing in perspective of being (or not) included in a future release. Those features are set using the environment variable HELMFILE_EXPERIMENTAL. Here is the current experimental feature :

  • explicit-selector-inheritance : remove today implicit cli selectors inheritance for composed helmfiles, see composition selector

If you want to enable all experimental features set the env var to HELMFILE_EXPERIMENTAL=true

Examples

For more examples, see the examples/README.md or the helmfile distribution by Cloud Posse.