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

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

The Flux v2 migration was "nearly done" two years ago. A handful of legacy v1 HelmReleases still run production, a few v2 files in git were never switched on, and half the repo still uses API versions the next Flux release will stop serving. The assistant works through all of it as small, reviewable MRs, 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, the installed Flux version.
Looks atThe v1 release spec and values, your chart sources, the running pods' image tags, the API versions the cluster serves, the Flux Kustomization tree.
You getA drop-in v2 HelmRelease per release, a follow-up MR that removes the v1 object once v2 is Ready, one bundled API-version MR per cluster, and Flux upgrade MRs.
01 · What is still on v1

Legacy releases, found in the cluster.

Every HelmRelease on the old helm.fluxcd.io/v1 API is listed from the live cluster, then matched 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 v1 spec is restructured into the v2 shape: chart.spec.chart, chart.spec.version, a sourceRef. The values block is carried over verbatim. A migration is not the moment to change behaviour.

When the model's diff is unusable, a deterministic template built from the v1 release produces the same result without it.

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. One is added only when that URL is not already declared in the repo, so ten releases from one repository share one source.

New files are wired into the directory's kustomization.yaml, or left to a Flux Kustomization that already walks the directory.

04 · A safe cut-over

Two controllers must never fight over one release.

The v2 HelmRelease is only activated when the v1 release is proven inactive, or proven equivalent and suspended in the same change. Anything the assistant cannot prove leaves the proposal held for review with the evidence, not shipped.

A new release landing under a Kustomization with wait: true gets a banner and high risk: 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 was never wired in, while the live v1 release kept being upgraded by hand. Copying git over the cluster would be a downgrade.

The running pod's image tag is compared with the chart's appVersion table to decide the direction. The result is a values-merge MR held for your confirmation, never shipped 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.

Two MRs, two risks: the migration can be reverted 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 it serves. Every manifest still on an older served version gets a one-line apiVersion bump, with comments and key order untouched. All of them ride one MR per cluster.

When a version is on Flux's published removal schedule, the MR says which Flux release will stop serving it.

08 · Flux itself

Upgrade the controllers, in the right order.

The Flux version in gotk-components.yaml is compared with the current flux CLI. When it is behind, the install manifest is regenerated with the same components and proposed as an MR.

If deprecated API versions are still in the repo, the upgrade MR lists them as prerequisites and carries high risk until the bundled bump MR is merged.

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
v1beta2 / v2beta2: still served, scheduled for removal in Flux 2.8
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.

Illustrative, shaped like 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

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; values are carried 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/, not declared in this repo 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. That comes as a separate MR 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 are not restarted when the rendered spec is unchanged.
  • The v1 HelmRelease shows as suspended and makes no further changes.
  • Within one scan cycle, a low-risk MR proposes removing the v1 object.
Rollback

Revert this MR: the v1 release is un-suspended and resumes ownership.

+ 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

Values block diffed against v1: identical. HelmRepository URL matches the v1 chart repository. Suspend and activation are in the same change. LGTM.

What keeps it honest.

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

Verbatim valuesA migration changes the owner, not the behaviour. Chart upgrades are a separate MR from the upgrade lane.
No silent empty MRsWhen a guard strips every diff, the proposal records which one did and why, so an empty result is never mistaken for "nothing to do".
No dead placementsIf the directory is actually reconciled from another repository, the release is held with a banner naming the real owner.
Direction from the clusterWhen git and the cluster disagree, the running image decides which side is newer. Unclear cases stay held for review.
Only Flux's own kindsThe API sweep touches only Flux CRDs whose storage migration Flux documents, and only versions the cluster still serves.
Refuses hand-edited FluxIf gotk-components.yaml contains a controller it does not recognise, no self-upgrade is proposed rather than clobbering 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; drifted-but-unwired direction check
  • Bundled deprecated-API MRs, 110+ files moved so far
  • Flux self-upgrade MRs, kept current from 2.6 to 2.9
In development
  • Server-side dry-run of the migrated objects (runs in observe mode, not yet in the MR body)
Roadmap
  • Flux feature-gate flags in the self-upgrade MR (a manual edit today: flux install cannot set them)
  • The same API sweep for non-Flux CRDs
← Previous storyAdoption into GitOps