Back to Distributed Systems & Kubernetes Series

Flux Track Part 3: HelmRelease & OCI Sources

June 6, 2026 Wasil Zafar 40 min read

Flux's helm-controller gives you full Helm lifecycle management through Kubernetes CRDs — install, upgrade, test, rollback, and uninstall — all driven by GitOps. Combine it with OCI-based chart repositories for a secure, immutable chart distribution pipeline.

Table of Contents

  1. HelmRelease Resource
  2. OCI Chart Repositories
  3. Cross-Namespace Patterns
  4. Complete grade-api Example
  5. Exercises
  6. Key Takeaways & Next Steps

HelmRelease Resource

Chart Source Options

# Option A: Chart from a HelmRepository
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: nginx-ingress
  namespace: flux-system
spec:
  interval: 1h
  chart:
    spec:
      chart: ingress-nginx              # Chart name in the repo
      version: "4.10.*"                 # Semver constraint
      sourceRef:
        kind: HelmRepository
        name: ingress-nginx
        namespace: flux-system
  targetNamespace: ingress-nginx
  install:
    createNamespace: true
  values:
    controller:
      replicaCount: 2
# Option B: Chart from an OCI HelmRepository
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 10m
  chart:
    spec:
      chart: podinfo
      version: ">=6.0.0"
      sourceRef:
        kind: HelmRepository
        name: podinfo             # OCI-type HelmRepository
        namespace: flux-system
  targetNamespace: default
# Option C: Chart from a GitRepository (chart stored in Git)
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: grade-api
  namespace: flux-system
spec:
  interval: 5m
  chart:
    spec:
      chart: ./charts/grade-api       # Path to chart within the repo
      sourceRef:
        kind: GitRepository
        name: grade-api               # GitRepository defined in Part 1
        namespace: flux-system
      reconcileStrategy: ChartVersion  # Or: Revision (use git SHA)
  targetNamespace: grade-api
  install:
    createNamespace: true

Injecting Values

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: grade-api
  namespace: flux-system
spec:
  interval: 5m
  chart:
    spec:
      chart: ./charts/grade-api
      sourceRef:
        kind: GitRepository
        name: grade-api

  # ── INLINE VALUES ─────────────────────────────────────────────
  values:
    replicaCount: 2
    image:
      repository: ghcr.io/your-org/grade-api
      tag: latest     # Image Updater will modify this (Part 4)
    service:
      type: ClusterIP
      port: 8080

  # ── VALUES FROM CONFIGMAPS / SECRETS ──────────────────────────
  # valuesFrom merges values in order (later entries win)
  valuesFrom:
    - kind: ConfigMap
      name: grade-api-values        # Environment-specific config
      valuesKey: values.yaml        # Key in the ConfigMap (default: values.yaml)
    - kind: Secret
      name: grade-api-secrets       # Sensitive values (DB password, etc.)
      valuesKey: values.yaml
      optional: true                # Don't fail if Secret doesn't exist
    - kind: ConfigMap
      name: grade-api-values
      valuesKey: db_host            # Reference a single key as a dot-path
      targetPath: postgresql.host   # Map it to a specific values path
# ConfigMap with environment values
apiVersion: v1
kind: ConfigMap
metadata:
  name: grade-api-values
  namespace: flux-system
data:
  values.yaml: |
    replicaCount: 3
    postgresql:
      host: postgres.grade-api.svc.cluster.local
      database: grade_api_prod
    ingress:
      enabled: true
      host: api.grade.company.com
  db_host: "postgres.grade-api.svc.cluster.local"

Upgrade & Rollback

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: grade-api
  namespace: flux-system
spec:
  interval: 5m
  chart:
    spec:
      chart: ./charts/grade-api
      sourceRef:
        kind: GitRepository
        name: grade-api

  # ── INSTALL BEHAVIOUR ─────────────────────────────────────────
  install:
    createNamespace: true
    remediation:
      retries: 3          # Retry install up to 3 times on failure

  # ── UPGRADE BEHAVIOUR ─────────────────────────────────────────
  upgrade:
    remediation:
      retries: 3
      remediateLastFailure: true   # Rollback on last retry failure
    cleanupOnFail: true            # Delete new resources if upgrade fails
    crds: CreateReplace            # Upgrade CRDs during helm upgrade

  # ── ROLLBACK BEHAVIOUR ────────────────────────────────────────
  rollback:
    timeout: 5m
    cleanupOnFail: true
    recreate: false               # Re-create pods after rollback

  # ── TEST ──────────────────────────────────────────────────────
  test:
    enable: true                  # Run helm test after install/upgrade
    ignoreFailures: false

  # ── DRIFT DETECTION ──────────────────────────────────────────
  driftDetection:
    mode: enabled                 # Detect and remediate drift
    ignore:
      - paths: ["/spec/replicas"] # Allow HPA to manage replicas
        target:
          kind: Deployment

  values:
    replicaCount: 2
# Trigger immediate reconciliation
flux reconcile helmrelease grade-api -n flux-system

# Check HelmRelease status
flux get helmreleases -n flux-system
# NAME       REVISION   SUSPENDED  READY  MESSAGE
# grade-api  1.2.3      False      True   Release reconciliation succeeded

# Debug a failed HelmRelease
flux describe helmrelease grade-api
kubectl describe helmrelease grade-api -n flux-system

# Manual rollback (suspend first)
flux suspend helmrelease grade-api
helm rollback grade-api 1 -n grade-api
flux resume helmrelease grade-api

# Force a full reinstall
flux suspend helmrelease grade-api
helm uninstall grade-api -n grade-api
flux resume helmrelease grade-api

OCI Chart Repositories

Pushing Charts to OCI

# Login to GHCR
echo $GITHUB_TOKEN | helm registry login ghcr.io -u $GITHUB_USER --password-stdin

# Package your chart
helm package ./charts/grade-api
# Successfully packaged chart and saved it to: grade-api-1.0.0.tgz

# Push to GHCR OCI registry
helm push grade-api-1.0.0.tgz oci://ghcr.io/your-org/charts

# Verify
helm show chart oci://ghcr.io/your-org/charts/grade-api --version 1.0.0
# In CI (GitHub Actions) — automate chart push on version bump
# .github/workflows/helm-publish.yaml
name: Helm Chart Publish
on:
  push:
    paths:
      - 'charts/**'
    tags:
      - 'chart-*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Login to GHCR
        run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Package and push
        run: |
          helm package charts/grade-api
          helm push grade-api-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts

Consuming OCI Charts

# OCI HelmRepository source
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: grade-api-charts
  namespace: flux-system
spec:
  type: oci
  interval: 5m
  url: oci://ghcr.io/your-org/charts
  secretRef:
    name: ghcr-credentials      # docker-registry type secret

---
# HelmRelease consuming OCI chart
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: grade-api
  namespace: flux-system
spec:
  interval: 5m
  chart:
    spec:
      chart: grade-api
      version: ">=1.0.0 <2.0.0"    # Semver range — picks latest matching
      sourceRef:
        kind: HelmRepository
        name: grade-api-charts
  targetNamespace: grade-api
  install:
    createNamespace: true
  values:
    replicaCount: 2

Cross-Namespace Patterns

# By default, sources and HelmReleases must be in the same namespace.
# To reference a source across namespaces, use a HelmChart CRD
# (helm-controller manages this automatically for you):

# Allow cross-namespace source references (set in helmrelease spec):
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: grade-api
  namespace: apps          # HelmRelease in 'apps' namespace
spec:
  interval: 5m
  chart:
    spec:
      chart: grade-api
      version: ">=1.0.0"
      sourceRef:
        kind: HelmRepository
        name: grade-api-charts
        namespace: flux-system    # Source in 'flux-system' namespace
# Cross-namespace references require allowing it in Flux's NetworkPolicy.
# Check if cross-namespace is enabled:
kubectl get configmap -n flux-system flux-system -o yaml | grep allow-cross-namespace

# Enable cross-namespace source references (in bootstrap or gotk-components):
flux install --allow-namespace-cross-references

Complete grade-api Example

# Full directory layout for grade-api with Flux + Helm
clusters/my-cluster/
├── flux-system/              # Bootstrap files (auto-generated)
└── apps/
    └── grade-api/
        ├── namespace.yaml
        ├── helmrepository.yaml
        └── helmrelease.yaml
# apps/grade-api/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: grade-api
  labels:
    app.kubernetes.io/managed-by: flux

---
# apps/grade-api/helmrepository.yaml
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: grade-api
  namespace: flux-system
spec:
  type: oci
  interval: 5m
  url: oci://ghcr.io/your-org/charts
  secretRef:
    name: ghcr-credentials

---
# apps/grade-api/helmrelease.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: grade-api
  namespace: flux-system
spec:
  interval: 5m
  dependsOn:
    - name: postgres           # Wait for Postgres HelmRelease to be Ready
  chart:
    spec:
      chart: grade-api
      version: ">=1.0.0 <2.0.0"
      sourceRef:
        kind: HelmRepository
        name: grade-api
  targetNamespace: grade-api
  install:
    createNamespace: true
    remediation:
      retries: 3
  upgrade:
    remediation:
      retries: 3
      remediateLastFailure: true
  driftDetection:
    mode: enabled
  valuesFrom:
    - kind: ConfigMap
      name: grade-api-values
    - kind: Secret
      name: grade-api-db-secret
      optional: true
  values:
    image:
      repository: ghcr.io/your-org/grade-api
      tag: latest              # Image Updater modifies this
    replicaCount: 2

Exercises

Exercise 1 — Deploy with HelmRelease: Create a HelmRelease for podinfo from its OCI repository (oci://ghcr.io/stefanprodan/charts). Set replicaCount: 2 inline. Watch Flux install it. Then change replicaCount to 3 in Git — watch Flux upgrade the release.
Exercise 2 — Values from ConfigMap: Create a ConfigMap with Helm values for the grade-api chart. Reference it via valuesFrom. Change a value in the ConfigMap (not the HelmRelease) — verify Flux detects the change and upgrades the release.
Exercise 3 — Intentional Failure: Set an invalid image tag in the HelmRelease values. Watch the install fail, the remediation retries, and the final rollback. Then fix the image tag — watch Flux recover.

Key Takeaways & Next Steps

Key Takeaways:
  • HelmRelease is a CRD that manages a Helm release declaratively — you describe the desired state, Flux reconciles it
  • Chart sources can be: HelmRepository (HTTP or OCI), GitRepository (chart in repo), or OCIRepository
  • valuesFrom lets you inject values from ConfigMaps and Secrets — keep sensitive values in Secrets, non-sensitive in ConfigMaps
  • Set install.remediation.retries and upgrade.remediation.remediateLastFailure: true to auto-rollback on failure
  • OCI registries (GHCR, ECR, ACR) are the modern, immutable way to distribute Helm charts
  • driftDetection.mode: enabled makes Flux reconcile manual helm upgrade commands back to the desired state

Next in This Track

In Part 4: Image Automation & Alerts, we complete the Flux loop — automatically update image tags in Git when new container images are pushed to a registry, and configure Flux to send Slack/Teams notifications on reconciliation events.