Right, so you’ve got your cluster bootstrapped with Flux. It’s happily syncing your Kustomization resources, and you feel like you’ve got a handle on this whole GitOps thing. But let’s be real: you’re probably not just deploying raw YAML. You’re almost certainly using Helm charts, either your own or, more likely, a bunch from the community. Manually running helm install or helm upgrade is a hard no-go in our new GitOps utopia. It’s a imperative speck in our declarative masterpiece. This is where the HelmRelease comes in to save the day, and your sanity.

The HelmRelease is a Flux custom resource (kind: HelmRelease) that tells the Helm Controller, “Hey, see this Helm chart? I want this specific version of it, deployed with these exact values, and I want you to keep it that way forever until I say otherwise.” You define the desired state in a YAML file, commit it, and Flux makes it happen. It’s the declarative API helm always wished it had.

The Basic Anatomy of a HelmRelease

Let’s break down a simple but functional example. We’ll deploy the trusty ingress-nginx chart, because everyone needs ingress.

apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: ingress-nginx
  namespace: infra
spec:
  interval: 5m0s
  chart:
    spec:
      chart: ingress-nginx
      version: "4.8.2"
      sourceRef:
        kind: HelmRepository
        name: ingress-nginx
        namespace: flux-system
      interval: 1m0s
  values:
    controller:
      service:
        type: LoadBalancer
        annotations:
          service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

There’s a lot to unpack here. The interval in the main spec is how often Flux will check if this release is actually in the state you defined. The chart.spec is where the magic happens: it points to a HelmRepository source (which you should have already defined) that tells Flux where to pull the chart from. Notice the separate interval here? That’s how often Flux will check the repository itself for a new version of the chart. This separation is brilliant—you can check for new chart versions every minute without necessarily reconciling the release itself every 60 seconds.

Sourcing Charts: The Big Three Ways

You’re not limited to public repositories. Flux is wonderfully flexible here, which is code for “there are a few ways to do this, and you need to know the trade-offs.”

1. From a HelmRepository (Public or Private): This is the standard way, as shown above. You define a HelmRepository resource that points to a URL. It works with any classic chart museum, including private ones that need credentials.

2. From a GitRepository (Charts in Git): This is my personal favorite for internal charts. You store your chart in a Git repo (e.g., alongside the application code it deploys). Flux can pull the chart directly from Git, which feels truly GitOps-native.

chart:
  spec:
    chart: ./charts/my-app
    sourceRef:
      kind: GitRepository
      name: my-app-repo
      namespace: flux-system
    interval: 1m0s

3. From an OCI Repository: This is the future, and it’s quickly becoming the present. You can store Helm charts in OCI registries like Docker Hub, GHCR, ECR, or GAR. It’s more efficient and often simpler for CI/CD pipelines.

chart:
  spec:
    chart: podinfo
    version: "6.4.0"
    sourceRef:
      kind: HelmRepository
      name: podinfo-oci
      namespace: flux-system
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: podinfo-oci
  namespace: flux-system
spec:
  interval: 5m
  url: oci://ghcr.io/stefanprodan/charts

The Art of the values.yaml Dance

You wouldn’t run helm install without a custom values.yaml, and you shouldn’t with HelmRelease either. You have two primary options, and which one you choose says a lot about how you organize your GitOps repo.

Option A: Inline everything. Just stick your values right in the HelmRelease under the values: key. It’s simple, and everything is in one file. This is great for simple configurations or if you’re a masochist who enjoys 1000-line YAML files.

Option B: External values files. This is the clean way. You can point to a separate values file stored in the same Git repository. This keeps your HelmRelease manifest tidy and lets you potentially share values files between releases.

spec:
  ...
  valuesFrom:
  - kind: ConfigMap
    name: my-app-common-values
  - kind: Secret
    name: my-app-secret-values
  - kind: ConfigMap
    name: env-specific-values

Wait, what? ConfigMap and Secret? Yes. This is Flux’s superpower. Your values don’t have to be flat files in Git. They can come from external sources like ConfigMaps and Secrets that Flux itself manages through other Kustomization resources. This is how you handle environment-specific or sensitive values without hardcoding them into your application’s deployment manifest. It’s a bit more setup, but the separation of concerns is worth its weight in gold.

Drift Detection and Remediation: The Whole Point

Here’s the killer feature: true drift detection. If someone goes into the cluster and does a kubectl edit on a resource managed by your HelmRelease, Flux will notice. On its next interval tick, it will compute a diff and revert the change to match what’s defined in your values. This is the “Ops” in GitOps. It enforces immutability and makes your cluster state predictable. You can see this in action by looking at the kustomization-controller logs after you make a rogue edit. It’s a thing of beauty.

Common Pitfalls and “Oh, C’mon” Moments

  • The Chart Source Ref Gotcha: The sourceRef in your chart.spec must point to a HelmRepository or GitRepository that exists and is ready. If you apply your HelmRelease before the source is ready, the release will just sit there with a 'Chart' not found error. Check kubectl get helmrepositories first.
  • The API Version Trap: You’ll mostly use apiVersion: helm.toolkit.fluxcd.io/v2beta1. If you find older documentation using v2alpha1 or especially v1, run away. The API changed significantly.
  • Values Merging Logic: When you use multiple valuesFrom sources, they are merged in order. Later keys override earlier ones. This is powerful but can be confusing if you’re not careful. The inline values: are applied last and have the highest precedence. Draw a map if you have to.
  • The Install vs. Upgrade Quirk: Unlike the helm CLI, a HelmRelease doesn’t have an explicit install command. You just define the release. If it doesn’t exist, Flux installs it. If it does, Flux upgrades it. This is the declarative way, but it means you need to be extra sure your values are correct on the first apply, as there’s no --dry-run flag on the resource itself (though you can use kubectl diff if you have it configured).

The HelmRelease is the workhorse that makes GitOps practical for real-world applications. It takes the wild west of helm commands and turns it into a disciplined, auditable, and repeatable process. Master this, and you’ve mastered the most common pattern for deploying software with Flux.