Home / Stories / Adoption into GitOps
Story · Lifecycle Live

Taking ownership of what nobody declared, without touching it.

Somewhere in your cluster is a Deployment that was applied by hand in 2021, and a Helm 2 release whose Tiller has long gone. Both still serve traffic. Neither lives in git. The assistant finds them, rebuilds the declaration from what is actually installed, and proposes an MR whose first reconcile should change nothing at all.

Starts fromA workload running in the cluster that no GitOps controller owns: hand-applied manifests, Helm 3 releases, 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, wired into a directory Flux really reconciles, plus the checks that were run and what to expect at cut-over.
01 · Who owns this?

Ownership is decided per object, not per namespace.

Every workload in the cluster is classified from its own metadata: 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: 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 per revision. The assistant decodes the latest deployed revision: chart name, chart version, and the values it was installed with.

Here: shop/orders-api, chart orders-api@2.4.1, deployed five years ago, controller gone. 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 declaration is rebuilt from the live object itself. Fields the API server writes (status, managedFields, uid, resourceVersion, timestamps, the last-applied annotation) are stripped. 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 new file is placed in a directory that one of your Flux Kustomizations reconciles, and added to that directory's kustomization.yaml when it lists resources explicitly.

If the only covering Kustomization turns out to reconcile a different repository, the whole proposal is held for review: a manifest nobody applies is worse than none, because it looks done.

05 · No second copy

Adopting must not mean installing twice.

Before emitting, the chart is rendered with helm template and compared with what already runs elsewhere in the cluster: CRD names, cluster-wide RBAC on the same resources, webhook configurations and the services they call.

Any collision drops the diff and turns the finding into an issue with the conflict report. This check exists because two copies of one operator competing on the same CRDs is a real outage shape.

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 is created in the same MR, once), the pinned version must be published, and it must not be older than what runs live. A provable dead end blocks the MR.

Credential-shaped values found in the old release are moved out to an ExternalSecret instead of being committed.

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: a replica count scaled by hand, a memory limit patched down 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 and ships switched off, report-only; turning it on, then making it a gate, are the next steps.

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. Proposals that combine install retries with a default timeout over multi-replica workloads are flagged, and a remediation that would delete live objects is called out as divergent.

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

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 title format, the checks and the labels are the ones the assistant uses; the numbered pre-merge ownership steps and the release-vs-live divergence listing are not emitted yet (roadmap and in development), and were written by hand in real adoptions so far.

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 orders-api@2.4.1) has no Ready Flux v2 HelmRelease. Its Helm/Tiller controller is gone, so the workload is unmanaged by Flux and 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: 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.
Placementapps/shop/ is reconciled by flux-system/apps, which sources this repository.
Double installNo CRD, ClusterRole, webhook or same-app collision.
Chart sourceHelmRepository created once; 2.4.1 is published; 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. Set in review to retries 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 the Vault path is populated.

What keeps it honest.

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

Live beats storedA release record is what someone installed years ago. Where it disagrees with the running object, the difference is listed 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 every diff is withheld and the finding is held for review.
No double installsA chart that would recreate CRDs, cluster-wide RBAC or webhooks already served by another installation is not proposed as an MR.
No uninstall on timeoutRemediation that could delete live workloads is flagged. This check exists because of one real incident with exactly this shape.
Operators keep what they ownObjects owned by an operator, and anything you mark with the skip annotation, are left alone. System namespaces are never adopted.
Secrets stay out of gitCredential-shaped values become ExternalSecret references. Every diff is scanned 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
  • 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