You have Flux installed and now have to deploy Helm charts the GitOps way — no helm install on a workstation, no values drift between environments, no surprise rollback because someone re-ran an old script. The HelmRelease CRD plus the helm-controller turn Helm into a fully declarative resource. This article is the operating procedure: define the chart source, version-pin it, write values in Git, and let the controller reconcile.
How to verify
The helm-controller should be running:
kubectl -n flux-system get deploy helm-controller
Helm sources should be registered:
flux get sources helm
HelmReleases should show their last applied revision:
flux get helmreleases -A
The underlying Helm release should match what Helm itself sees:
helm list -A
What’s happening
Flux splits Helm into two CRDs. HelmRepository is a source — it points at a Helm chart repo (HTTP, OCI registry, or git tree containing charts), and the source-controller polls it on an interval, pulling indexes and chart tarballs into the cluster. HelmRelease is a release — it references a HelmRepository (or a GitRepository if the chart lives in your gitops repo), pins a chart version, sets values, and tells the helm-controller to install/upgrade.
The helm-controller is the thing that actually talks to the Kubernetes API as if it were running helm install. It computes the release name (defaults to the HelmRelease name), runs the chart’s templates against the values, writes Secrets containing release state into the cluster, and applies the rendered manifests. Drift detection is built in — if you kubectl edit a Deployment that belongs to a HelmRelease, the next reconcile interval will revert your change.
The procedure
-
Add a HelmRepository. Point at the chart source — Bitnami in this example:
apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: bitnami namespace: flux-system spec: interval: 12h url: https://charts.bitnami.com/bitnamiCommit this to your
infrastructure/controllers/directory. The source-controller will index the repo and the index becomes available cluster-wide. -
Write a HelmRelease. This is the actual release:
apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: redis namespace: cache spec: interval: 15m timeout: 10m chart: spec: chart: redis version: '20.11.4' sourceRef: kind: HelmRepository name: bitnami namespace: flux-system interval: 1h install: remediation: retries: 3 upgrade: remediation: retries: 3 remediateLastFailure: true cleanupOnFail: true values: architecture: replication auth: existingSecret: redis-auth master: persistence: size: 8Gi replica: replicaCount: 2The
versionfield is a literal —20.11.4, not^20or~20.11. Pin it. If you want auto-update, use flux-image-automation on a separate watched range, not Helm semver matching. -
Layer values via valuesFrom. Secrets and large config blocks belong in their own resources:
spec: values: master: persistence: size: 8Gi valuesFrom: - kind: ConfigMap name: redis-extra-values - kind: Secret name: redis-auth-values optional: falseMultiple sources merge in order; later ones override earlier ones. Use this to keep secrets out of the main HelmRelease spec.
-
Pull from OCI registries. Modern charts publish to OCI:
apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmRepository metadata: name: ingress-nginx namespace: flux-system spec: type: oci interval: 12h url: oci://registry-1.docker.io/bitnamichartsThe OCI flow has the same UX as the HTTP flow — the source-controller pulls the chart tarball via OCI manifests.
-
Test the install/upgrade path. Apply your HelmRelease via Git push:
git add infrastructure/controllers/redis.yaml git commit -m "feat: add redis HelmRelease" git push flux reconcile kustomization infrastructure --with-source flux get helmreleases -n cacheThe HelmRelease should report
Release reconciliation succeeded. If it fails,flux events --for HelmRelease/redisshows the chart render error. -
Bump a chart version. Edit the
versionfield, push, observe. The helm-controller’supgrade.remediationblock decides what happens on failure —retries: 3plusremediateLastFailure: truemeans three upgrade attempts, then rollback to the last successful release.
Operational notes
spec.intervalis reconciliation cadence, not Helm poll cadence. The chart source is polled per the HelmRepository’s interval. A HelmRelease with a 15m interval still rolls within seconds of you pushing a values change becauseflux reconcileor a webhook (see notification-controller’s Receiver CRD) fires it manually.- The helm-controller stores release state in Secrets. Each HelmRelease produces a
sh.helm.release.v1.<name>.<rev>Secret per revision. WithhistoryMax: 10(default), ten revisions accumulate. Set lower if you have many small HelmReleases — these Secrets count against etcd size. cleanupOnFailis critical. Without it, a failed upgrade leaves the HelmRelease infailedstate and the partially-upgraded resources in the cluster. Future reconciles refuse to advance until you manually resolve.- HelmRelease deletion deletes the release. Removing the YAML from Git triggers
helm uninstallon the next reconcile. If the chart manages a PVC withRetainPolicy: Retain, the PVC stays; everything else is gone. Always confirm this is what you want. - Cross-namespace chart sources require an explicit
sourceRef.namespace. A HelmRelease inappreferencing a HelmRepository influx-systemworks only if the namespace is specified — the controller does not search.
Stack Harbor runs Flux-managed Helm releases for clients whose platform stack (ingress, cert-manager, monitoring, secrets operators) is exclusively chart-based — the HelmRepository catalogue, the chart-version pinning policy, the values-vs-valuesFrom split, and the upgrade remediation tuned per workload. See how we deliver that within managed Kubernetes operations.