Skip to main content

FluxCD HelmRelease Reference

HelmRelease (helm.toolkit.fluxcd.io/v2) is managed by helm-controller. It installs, upgrades, tests, and garbage-collects Helm releases with drift detection and structured remediation.


Minimal example​

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cert-manager
namespace: cert-manager
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: cert-manager
namespace: flux-system
values:
installCRDs: true
install:
strategy:
name: RetryOnFailure # always use this

Chart source — two mutually exclusive approaches​

ApproachWhen to useNotes
spec.chartRefOCI chart sources (recommended)References an existing OCIRepository or HelmChart; supports Cosign verification
spec.chart.specHTTPS Helm repos / GitRepositoryAuto-creates a HelmChart object; cannot be used with spec.chartRef
spec:
chartRef:
kind: OCIRepository
name: cert-manager
namespace: flux-system

Using spec.chart.spec (HTTPS repo)​

spec:
chart:
spec:
chart: cert-manager
version: ">=1.13.0"
sourceRef:
kind: HelmRepository
name: jetstack
namespace: flux-system

Do not set both — they are mutually exclusive.


Values merging​

Sources are applied in sequence, each layer overriding the previous:

spec:
valuesFrom:
- kind: ConfigMap
name: cert-manager-values # base values
valuesKey: values.yaml
- kind: Secret
name: cert-manager-secrets # sensitive overrides
valuesKey: values.yaml
optional: true
values:
# inline values — applied last, highest precedence
replicaCount: 2

ConfigMaps/Secrets used in valuesFrom should carry:

labels:
reconcile.fluxcd.io/watch: Enabled # triggers immediate HelmRelease reconciliation on change

Install / upgrade strategy​

Always use RetryOnFailure — it retries without uninstalling or rolling back, avoiding downtime and data loss:

spec:
install:
strategy:
name: RetryOnFailure
retryInterval: 3m # how long to wait between retries (default: interval)
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 3m
cleanupOnFail: true
force: false # set true only for immutable field changes

Set retryInterval shorter than spec.interval for faster recovery from transient failures. Without it, Flux waits the full reconciliation interval between retries.

Do not mix modern install.strategy with legacy install.remediation.retries — they are mutually exclusive.


Drift detection​

ModeBehaviour
disabledNo detection (default)
warnDetects and reports but does not correct
enabledDetects and corrects on every reconciliation
spec:
driftDetection:
mode: enabled
ignore:
- paths:
- /spec/replicas # exclude HPA-managed field
target:
kind: Deployment

CRD lifecycle​

ValueInstallUpgrade
Create (default)Installs CRDsSkips — does not upgrade existing CRDs
SkipSkips CRDsSkips CRDs
CreateReplaceInstalls CRDsReplaces CRDs — use for breaking CRD upgrades
spec:
install:
crds: CreateReplace
upgrade:
crds: CreateReplace

Dependencies​

Wait until listed HelmReleases or Kustomizations are Ready before proceeding:

spec:
dependsOn:
- name: cert-manager
namespace: cert-manager
- name: envoy-gateway
namespace: envoy-gateway-system

Health check expressions​

CEL expressions for custom resource readiness, evaluated when wait: true is active:

spec:
healthCheckExprs:
# Gateway has no Ready condition, so kstatus alone reports it healthy before
# it has an address. Programmed=True means the data plane is serving.
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
failed: status.conditions.exists(e, e.type == 'Accepted' && e.status == 'False')
current: status.conditions.exists(e, e.type == 'Programmed' && e.status == 'True')

Use expressions only for custom resources whose readiness kstatus cannot read. Built-in kinds such as Deployment already get kstatus's rollout-aware check; do not redefine them. A hand-written Deployment rule is easy to get wrong: Flux evaluates inProgress before current, and a finished Deployment keeps Progressing=True, so an inProgress on that condition never lets it become healthy. Expressions evaluate against the resource itself: status.…, not object.status.….


Post-renderers​

Kustomize patches applied after Helm templating, before apply. Useful for injecting annotations, sidecars, or overriding fields the chart doesn't expose as values:

spec:
postRenderers:
- kustomize:
patches:
- target:
kind: Deployment
name: ".*"
patch: |
- op: add
path: /spec/template/metadata/annotations/prometheus.io~1scrape
value: "true"
- target:
kind: Ingress
name: ".*"
patch: |
- op: add
path: /metadata/annotations/nginx.ingress.kubernetes.io~1ssl-redirect
value: "true"

Multiple post-renderers execute sequentially — each renderer's output feeds into the next.


Remote cluster deployment​

spec:
kubeConfig:
secretRef:
name: remote-cluster-kubeconfig
key: value

Status tracking​

status:
conditions:
- type: Ready
status: "True"
history:
- chartVersion: "1.13.3"
digest: sha256:abc123...
firstDeployed: "2024-01-15T10:00:00Z"
lastDeployed: "2024-01-20T14:30:00Z"
lastAppliedRevision: "1.13.3+sha256:abc123"
lastAttemptedRevision: "1.13.3+sha256:abc123"

Validation​

# Status overview
flux get helmreleases -A

# Detailed reconciliation trace
kubectl describe helmrelease cert-manager -n cert-manager
flux logs --kind=HelmRelease --name=cert-manager --namespace=cert-manager

# Force immediate reconciliation
flux reconcile helmrelease cert-manager -n cert-manager --with-source

# Check rendered values
kubectl get secret -n cert-manager sh.helm.release.v1.cert-manager.v1 -o jsonpath='{.data.release}' | base64 -d | base64 -d | gunzip | jq '.config'

Namespace management​

Never use targetNamespace or createNamespace on a HelmRelease. These bypass proper namespace lifecycle management — the namespace is created outside GitOps control and won't be pruned correctly.

Instead, create the target namespace in the parent Kustomization or ResourceSet that deploys the component:

# ❌ Wrong — namespace created outside lifecycle control
spec:
targetNamespace: cert-manager
install:
createNamespace: true

# ✅ Correct — namespace owned by the Kustomization
# In the Kustomization that owns this HelmRelease, include a Namespace manifest:
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager

The namespace must exist before the HelmRelease runs — create it in the parent Kustomization or ResourceSet, never in the HelmRelease itself.


Common mistakes​

MistakeCorrect approach
spec.chart.spec with spec.chartRef setMutually exclusive — use one only
install.remediation.retries (legacy)Use install.strategy.name: RetryOnFailure
valuesFrom ConfigMap without watch labelAdd reconcile.fluxcd.io/watch: Enabled or changes won't trigger reconciliation
driftDetection.mode: enabled without ignore rulesHPA-managed replicas will fight with Flux — always ignore /spec/replicas
targetNamespace + createNamespace: true on HelmReleaseCreate the namespace in the parent Kustomization/ResourceSet instead
No retryInterval on strategyFlux waits the full spec.interval between retries — set strategy.retryInterval: 3m for faster recovery