Home / Stories / Adoption into GitOps
Story · Lifecycle Live

Taking ownership of what nobody declared, without touching it.

Somewhere in your cluster runs a Deployment that someone applied by hand in 2021. Another workload is a Helm 2 release whose Tiller (the Helm 2 server component) is long gone. Both still serve traffic. Neither lives in git. The assistant finds them and rebuilds the declaration from what is actually installed. Then it proposes a draft merge request (MR). Its first reconcile, the first sync from git to the cluster, should change nothing at all.

Starts fromA running workload that no GitOps controller owns. Examples are hand-applied manifests, Helm 3 releases and Helm 2 / Tiller releases.
Looks atOwnership labels and annotations, the decoded Helm or Tiller release record, the live object, your Flux Kustomizations, chart sources.
You getA draft MR with the manifests or HelmRelease. It sits in a directory that Flux really reconciles. Flux is the tool that applies git to your cluster. You also get the checks the assistant ran and what to expect at cut-over.
01 · Who owns this?

Ownership is decided per object, not per namespace.

The assistant sorts every workload by its own metadata. The groups are: reconciled by a Flux v2 HelmRelease or Kustomization; still on a legacy Flux v1 release; installed by Helm 3 by hand; installed by Tiller; owned by an operator; or applied with kubectl and forgotten.

Helm 3, Tiller and kubectl-applied objects are adoption candidates. Objects owned by an operator get an informational issue instead, because a raw copy would fight the operator for ownership.

02 · Read the release record

A Tiller release still remembers what it installed.

Helm 2 kept each release as a ConfigMap in the Tiller namespace. Helm 3 keeps a Secret for each revision. The assistant decodes the latest deployed revision. It reads the chart name, the chart version and the values used at install.

Here: shop/orders-api, chart [email protected], deployed five years ago. Its controller is gone, so the workload is frozen at its last apply.

03 · Or read the live object

For hand-applied manifests, the cluster is the source.

A Deployment applied with kubectl has no release record. So the assistant rebuilds the declaration from the live object itself. It strips the fields that the API server writes (status, managedFields, uid, resourceVersion, timestamps, the last-applied annotation). What remains is what someone asked for.

04 · A home Flux actually reads

A file in git is not the same as a file Flux applies.

The assistant puts the new file in a directory that one of your Flux Kustomizations reconciles. If that directory's kustomization.yaml lists resources explicitly, the assistant adds the new file to the list.

Sometimes the only covering Kustomization turns out to reconcile a different repository. Then the assistant holds the whole proposal for review. A manifest that nobody applies is worse than none, because it looks done.

05 · No second copy

Adopting must not mean installing twice.

Before it proposes anything, the assistant renders the chart with helm template. It compares the result with what already runs elsewhere in the cluster. The checks cover the names of CRDs (custom resource definitions), cluster-wide RBAC on the same resources, webhook configurations and the services they call.

On any collision, the assistant drops the diff. It turns the finding into an issue with the conflict report instead. This check exists because two copies of one operator that fight over the same CRDs can cause a real outage.

06 · Can Flux fetch it?

The chart source must resolve, and the version must not go backwards.

The HelmRelease's sourceRef must point at a HelmRepository, GitRepository or OCIRepository that exists in git (or the same MR creates it, once). The pinned version must be published. It must not be older than what runs live. A provable dead end blocks the MR.

The assistant moves credential-shaped values from the old release into an ExternalSecret. It does not commit them.

07 · Prove the first reconcile is a no-op In development

Compare the proposal with what is running, field by field.

A release record can drift from the cluster. Someone may scale a replica count by hand, or patch a memory limit up during an incident. Adopting the stored values would quietly undo those fixes.

A detector compares the adoption's values with the live workloads (replicas, resources, images) and lists every divergence. It is built, but it ships switched off and only reports. The next steps are to turn it on, then to make it a gate.

08 · Safe to hand over

The HelmRelease must not be able to uninstall what it adopts.

On an adoption, Flux's default install remediation (retry, then uninstall on failure) would delete the running workload after a slow rollout. The assistant flags proposals that combine install retries with a default timeout over multi-replica workloads. It also marks as divergent any remediation that would delete live objects.

Roadmap The assistant will write the Helm 3 ownership annotations into the MR as numbered pre-merge steps. Tiller-era objects need these annotations before Flux can take them over.

ownership · namespace shop
  • deploy/checkout-web Flux v2 HelmRelease · managed
  • deploy/cart-api Flux Kustomization · managed
  • sts/search-index operator-owned · info issue only
  • deploy/orders-api heritage: Tiller · controller gone
  • deploy/label-printer kubectl apply · no owner
2 adoption candidates · 1 operator-owned · 2 already managed
kube-system/configmap orders-api.v7 · OWNER=TILLER · decoded
name: orders-api
namespace: shop
status: DEPLOYED
chart: { name: orders-api, version: 2.4.1 }
config:            # the values it was installed with
  replicaCount: 3
  image: { repository: registry.example.com/shop/orders-api, tag: v1.18.4 }
  resources: { limits: { memory: 512Mi } }
  db: { host: orders-db, password: •••• (credential-shaped) }
no Flux HelmRelease for shop/orders-api · last Tiller apply 5 years ago
live: deploy/label-printer -o yaml
metadata:
  name: label-printer
  namespace: shop
  uid: 3f1c…                          server-set
  resourceVersion: "88142031"         server-set
  creationTimestamp: 2021-03-02T…     server-set
  managedFields: [ … 64 lines … ]     server-set
  annotations: { kubectl.kubernetes.io/last-applied-configuration: … }
spec:
  replicas: 1
  template: { … containers, probes, volumes kept verbatim … }
status: { availableReplicas: 1, … }  server-set
0 status / managedFields leftovers · spec kept verbatim
Kustomization flux-system/appspath ./apps · source: this repo
apps/shop/kustomization.yamlexplicit resources list · entry appended
apps/shop/helmrelease_orders-api.yamlnew file · reconciled
placement: covered by a Kustomization that reconciles this repository
double-install check · helm template vs cluster
  • CRD names chart renders none
  • ClusterRole rules no overlap on shared API groups
  • Webhook configurations none rendered
  • Webhook service DNS no existing config points at it
  • Same app in another namespace none found
no conflict · adoption, not a second installation
postcondition · source, version, direction
  • sourceRef HelmRepository/acme-charts created once in this MR
  • orders-api 2.4.1 in index.yaml published
  • 2.4.1 vs live 2.4.1 no downgrade
  • db.password moved to valuesFrom + ExternalSecret
valuesFrom:
  - kind: Secret
    name: orders-api-externalized
adoption values vs live workloads · report-only
  • image tag v1.18.4 = live v1.18.4
  • replicaCount release 3 · live 2 (scaled by hand)
  • memory limit release 512Mi · live 768Mi (patched)
  • probes, ports, env match
apps/shop/helmrelease_orders-api.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata: { name: orders-api, namespace: shop }
spec:
  interval: 10m
  timeout: 15m
  install:
    remediation: { retries: 0 }   # set in review: flagged otherwise
  chart:
    spec: { chart: orders-api, version: 2.4.1,
            sourceRef: { kind: HelmRepository, name: acme-charts } }
category:upgradesource:flux_migrationrisk:mediumxrev::LGTM
↓ the full merge request is below

The merge request you get.

Illustrative. The assistant uses this title format, these checks and these labels. It does not yet produce the numbered pre-merge ownership steps or the release-vs-live divergence listing (roadmap and in development). In real adoptions so far, people wrote them by hand.

Draft

Adopt manual-Helm release as Flux v2 HelmRelease: shop/orders-api

from gitops-ai/adopt-shop-orders-api into main · 4 files · +58 −0
category:upgradesource:flux_migrationrisk:mediumxrev::LGTM
OverviewChecksCut-overChanges
Summary

Live Tiller Helm release shop/orders-api (chart [email protected]) has no Ready Flux v2 HelmRelease. Its Helm/Tiller controller is gone. Flux does not manage the workload, and it is frozen at its last apply. This MR adopts it as a Flux v2 HelmRelease so GitOps owns it again.

What is in this MR
  • A HelmRelease pinned to the same chart version that runs today (2.4.1). Upgrading is a separate, later MR.
  • The acme-charts HelmRepository, which did not exist in git yet.
  • An ExternalSecret for the one credential found in the release values. The value itself is not in this MR.
  • One line in apps/shop/kustomization.yaml, so Flux applies the new file.
What does not change

Image, probes, ports and environment match what runs today. Two values in the release record differ from the running Deployment. They are replicaCount (3 vs live 2) and the memory limit (512Mi vs live 768Mi). Merging as-is applies the release values. Set them to the live values if those hand changes should stay.

CheckResult
Ownershipheritage: Tiller, no Flux or operator owner.
Placementflux-system/apps reconciles apps/shop/. It sources this repository.
Double installNo CRD, ClusterRole, webhook or same-app collision.
Chart sourceThe MR creates the HelmRepository once. Version 2.4.1 is published. It is not a downgrade.
Credentials1 value moved to valuesFrom. Populate the referenced Vault path before merging.
RemediationInstall retries with a default timeout over 2 replicas: flagged. The reviewer set retries to 0 and an explicit 15m timeout.
After merge, expect
  • flux get hr -n shop orders-api shows 2.4.1, Ready, revision 1.
  • No pod restarts, once the two values above match what runs.
  • The ExternalSecret reports SecretSynced before the HelmRelease reconciles.
  • Watch the helm-controller log for the release name, not only the Ready condition.
Rollback

Suspend the HelmRelease, then revert this MR. With install remediation off, Flux will not remove the running workload.

+ apps/shop/helmrelease_orders-api.yaml            (new, 31 lines)
+ infrastructure/repositories/helmrepository_acme-charts.yaml   (new)
+ apps/shop/externalsecret_orders-api-externalized.yaml        (new)
  apps/shop/kustomization.yaml
    resources:
      - helmrelease_checkout-web.yaml
+     - helmrelease_orders-api.yaml
+     - externalsecret_orders-api-externalized.yaml
R
independent reviewer second opinion, no pipeline context

Checked the placement against the Flux Kustomizations and the replica pin against the live Deployment. Values carry no plaintext credential. LGTM once someone populates the Vault path.

What keeps it honest.

Adoption touches workloads that already serve traffic, so most of the work is refusing to propose the wrong thing.

Live beats storedA release record is what someone installed years ago. Where it disagrees with the running object, the assistant lists the difference for the reviewer (detector built, report-only, not yet switched on).
No dead placementsA new file must land where a Flux Kustomization from this repository will apply it. Otherwise the assistant withholds every diff and holds the finding for review.
No double installsThe assistant does not propose a chart as an MR when it would recreate CRDs, cluster-wide RBAC or webhooks that another installation already serves.
No uninstall on timeoutThe assistant flags remediation that could delete live workloads. This check exists because of one real incident with exactly this shape.
Operators keep what they ownThe assistant does not touch objects that an operator owns, or anything you mark with the skip annotation. It never adopts system namespaces.
Secrets stay out of gitCredential-shaped values become ExternalSecret references. The assistant scans every diff for literal credentials before it can become an MR.

Where this stands.

Live
  • Per-object ownership: Flux v1/v2, Helm 3, Tiller, operator, unmanaged
  • Helm 3 and Tiller releases adopted as Flux v2 HelmReleases; hand-applied workloads as manifests
  • Placement, double-install, source and version checks; remediation safety flags
  • As of October 2026: 24 Tiller-era releases, databases included, adopted into Flux v2 across staging and production, with operator-led cut-over
In development
  • Adoption values vs live workloads: detector built, report-only, not yet enabled
  • Re-generating a stored adoption proposal on request
Roadmap
  • Helm ownership annotation steps written into the MR
  • Follow-up MR to remove the old Tiller or Helm release records once Flux is Ready
  • Argo CD Applications as an adoption target
← Previous storyVersion upgrades