Home / Stories / Flux v1 → v2 migration
Story · Lifecycle Live

Migrate first, delete second, and never both in one MR.

Flux is the tool that applies your git state to the cluster. Your migration from Flux v1 to v2 has been "nearly done" for years. A handful of legacy v1 HelmReleases still run production. A few v2 files in git were never switched on. Half the repository still uses API versions that a coming Flux release will stop serving. The assistant works through all of it as small draft merge requests (MRs) that you can review. It works in an order that never leaves a release without an owner.

Starts fromLive helm.fluxcd.io/v1 HelmReleases, stale v2 files, Flux manifests on deprecated API versions, and the installed Flux version.
Looks atThe v1 release spec and values, your chart sources, the image tags of the running pods, the API versions that the cluster serves, and the Flux Kustomization tree.
You getA drop-in v2 HelmRelease for each release. A follow-up MR that removes the v1 object once v2 is Ready. One bundled API-version MR for each cluster. Flux upgrade MRs.
01 · What is still on v1

Legacy releases, found in the cluster.

The assistant lists every HelmRelease on the old helm.fluxcd.io/v1 API from the live cluster. A HelmRelease is the Flux object that installs a Helm chart (a packaged application). The assistant then matches each one to its file in git. A v1 release with no Ready v2 counterpart is a migration candidate.

Here: payments/invoice-renderer, still reconciled by the Flux v1 Helm operator.

02 · Translate, don't rewrite

Same chart, same version, same values.

The assistant restructures the v1 spec into the v2 shape: chart.spec.chart, chart.spec.version and a sourceRef. It copies the values block verbatim. A migration is not the moment to change behaviour.

Sometimes the AI model (LLM) writes a diff that nobody can use. Then a rule-based (deterministic) template, built from the v1 release, produces the same result without the model.

03 · Sources and wiring

One HelmRepository per chart source, and a file Flux will apply.

v1 embedded the chart repository URL in each release. v2 needs a HelmRepository object. The assistant adds one only when the repository does not yet declare that URL. So ten releases from one chart repository share one source.

If a Flux Kustomization already walks the directory, the assistant leaves new files to it. Otherwise it adds them to the directory's kustomization.yaml.

04 · A safe cut-over

Two controllers must never fight over one release.

The assistant activates the v2 HelmRelease only in two cases. In the first, it has proven that the v1 release is inactive. In the second, it has proven that v1 is equivalent and suspended in the same change. If the assistant cannot prove either case, it holds the proposal for review with the evidence. It does not ship the proposal.

A new release can land under a Kustomization with wait: true. Then it gets a banner and high risk, because one stuck release there stalls every sibling.

05 · The v2 file nobody switched on

Which way should the drift be fixed?

Sometimes a v2 file exists in git but nobody wired it in. Meanwhile people kept upgrading the live v1 release by hand. Copying git over the cluster would then be a downgrade.

The assistant compares the running pod's image tag with the chart's appVersion table to find the direction of the fix. The result is a values-merge MR that the assistant holds for your confirmation. It never ships it on its own.

06 · Delete second

The v1 object goes only after v2 reports Ready.

Once the v2 HelmRelease for the same release is Ready in the cluster, the old v1 object is strictly redundant. A separate, low-risk MR removes it.

So two MRs carry two separate risks. You can revert the migration without resurrecting anything, and the deletion never races the cut-over.

07 · Deprecated Flux APIs

One bundled MR per cluster, one line per file.

The cluster reports which versions of each Flux CRD (a custom resource type) it serves. Every manifest that still uses an older served version gets a one-line apiVersion bump. The bump leaves comments and key order untouched. All bumps go into one MR for each cluster.

Flux publishes a removal schedule. When a version is on that schedule, the MR says which Flux release will stop serving it.

08 · Flux itself

Upgrade the controllers, in the right order.

The assistant compares the Flux version in gotk-components.yaml with the current flux CLI. When the file is behind, the assistant regenerates the install manifest with the same components. It proposes the result as an MR.

If deprecated API versions are still in the repository, the upgrade MR lists them as prerequisites. It carries high risk until you merge the bundled bump MR.

releases/payments/invoice-renderer.yaml
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
  name: invoice-renderer
  namespace: payments
spec:
  releaseName: invoice-renderer
  chart:
    repository: https://charts.example.com/
    name: invoice-renderer
    version: 3.2.0
  values:
    replicaCount: 2
    pdf: { fonts: [ noto, dejavu ], timeoutSeconds: 45 }
live: v1 HelmRelease · no v2 HelmRelease for payments/invoice-renderer
apps/payments/helmrelease_invoice-renderer.yaml (v2)
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata: { name: invoice-renderer, namespace: payments }
spec:
  interval: 10m
  chart:
    spec:
      chart: invoice-renderer
      version: 3.2.0
      sourceRef: { kind: HelmRepository, name: example-charts }
  values:                       # verbatim from v1
    replicaCount: 2
    pdf: { fonts: [ noto, dejavu ], timeoutSeconds: 45 }
sources and wiring
  • https://charts.example.com/ not yet declared · HelmRepository added
  • same URL in 3 other migrations reuse, no duplicate
  • apps/payments/kustomization.yaml explicit list · entry appended
  • covering Kustomization reconciles this repository
placement proven · path follows apps/<ns>/<kind>_<name>.yaml
v1 invoice-rendererequivalent spec · suspended in this MR
cut-over checkchart, version, values match
v2 invoice-rendereractive
cut-over outcomes
  • v1 proven inactive activate v2
  • v1 equivalent, suspended in the same change activate v2
  • no editable v1 manifest v2 staged suspended
  • anything unproven held for review, no activation diff
directionality · edge/ingress-controller
4.7.0git v2 file
4.9.x
4.11.1live v1
  • running image tag matches appVersion of 4.11.1
  • copy git over cluster would be a downgrade
v2 HelmRelease payments/invoice-renderer · Ready · revision 1
Draft · Remove redundant Flux v1 HelmRelease: payments/invoice-renderer
- releases/payments/invoice-renderer.yaml      (deleted)
  releases/kustomization.yaml
-   - payments/invoice-renderer.yaml
category:upgradesource:flux_migrationrisk:low
Bump Flux CRD apiVersions to canonical in 63 files
apps/payments/helmrelease_invoice-renderer.yaml
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
infrastructure/sources/helmrepository_example-charts.yaml
-apiVersion: source.toolkit.fluxcd.io/v1beta2
+apiVersion: source.toolkit.fluxcd.io/v1
clusters/prod/apps.yaml
-apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+apiVersion: kustomize.toolkit.fluxcd.io/v1
… 60 more files, one line each
v2beta1 / v1beta2: deprecated, still served by this cluster's Flux v2.6.4
flux self-upgrade
# Flux Version: v2.6.4           (clusters/prod/flux-system/gotk-components.yaml)
$ flux --version
flux version 2.7.2
$ flux install --components=… --components-extra=… --export
✓ regenerated with the same components
! 4 files still on deprecated apiVersions → listed as prerequisites

The merge request you get.

This MR is illustrative. It has the shape of the real migration MR: same title format, same labels, same cut-over rule.

Draft

Migrate Flux v1 HelmRelease to v2: payments/invoice-renderer

from gitops-ai/migrate-payments-invoice-renderer into main · 4 files · +27 −1
category:upgradesource:flux_migrationrisk:mediumxrev::LGTM
OverviewCut-overAfter mergeChanges
Summary

This MR migrates payments/invoice-renderer from the legacy Flux v1 Helm operator to a Flux v2 HelmRelease. Chart invoice-renderer 3.2.0 stays on the same version. The values carry over verbatim.

What is in this MR
  • A v2 HelmRelease at apps/payments/helmrelease_invoice-renderer.yaml.
  • A HelmRepository example-charts for https://charts.example.com/. This repository did not declare it before.
  • spec.suspend: true on the v1 HelmRelease, so only one controller acts on the release.
  • One line in apps/payments/kustomization.yaml.
What this MR does not do

It does not delete the v1 object. A separate MR does that once the v2 HelmRelease reports Ready.

CheckResult
Chart name and versionv1 and v2 identical (3.2.0).
ValuesIdentical, byte for byte.
v1 stateEquivalent; suspended in this change.
Covering Kustomizationapps, sources this repository, wait: false.
DispositionSafe to activate.
After merge, expect
  • flux get hr -n payments invoice-renderer: Ready, chart 3.2.0.
  • Helm adopts the existing objects. Pods do not restart when the rendered spec is unchanged.
  • The v1 HelmRelease shows as suspended and makes no further changes.
  • Within one scan cycle, the assistant opens a low-risk MR that removes the v1 object.
Rollback

Revert this MR. Then the v1 release is no longer suspended and owns the release again.

+ apps/payments/helmrelease_invoice-renderer.yaml       (new, 19 lines)
+ infrastructure/sources/helmrepository_example-charts.yaml   (new)
  apps/payments/kustomization.yaml
+   - helmrelease_invoice-renderer.yaml
  releases/payments/invoice-renderer.yaml
    spec:
+     suspend: true
      releaseName: invoice-renderer
R
independent reviewer second opinion, no pipeline context

I diffed the values block against v1: identical. The HelmRepository URL matches the v1 chart repository. The suspend and the activation are in the same change. LGTM.

What keeps it honest.

A migration is finished only when every release has exactly one owner and Flux applies every file.

Verbatim valuesA migration changes the owner, not the behaviour. The upgrade lane makes chart upgrades in a separate MR.
No silent empty MRsSometimes a guard strips every diff. Then the proposal records which guard did it, and why. So nobody mistakes an empty result for "nothing to do".
No dead placementsIf another repository reconciles the directory, the assistant holds the release with a banner that names the real owner.
Direction from the clusterWhen git and the cluster disagree, the running image decides which side is newer. The assistant holds unclear cases for review.
Only Flux's own kindsThe API sweep touches only Flux CRDs whose storage migration Flux documents. It touches only versions that the cluster still serves.
Refuses hand-edited FluxIf gotk-components.yaml contains a controller that the assistant does not recognise, it proposes no self-upgrade. It does not overwrite your customisation.

Where this stands.

Live
  • v1 → v2 HelmRelease migration with HelmRepository dedup and cut-over proof
  • Redundant-v1 removal MRs once v2 is Ready, and a direction check for drifted but unwired files
  • Bundled deprecated-API MRs, 110+ files moved as of October 2026
  • Flux self-upgrade MRs when the installed version falls behind the current Flux CLI
In development
  • Server-side dry-run of the migrated objects (it runs in observe mode and is not yet in the MR body)
Roadmap
  • Flux feature-gate flags in the self-upgrade MR (still a manual edit, because flux install cannot set them)
  • The same API sweep for non-Flux CRDs
← Previous storyAdoption into GitOps