What the starting point usually looks like
Few teams begin from a blank page. The typical estate has some services deployed by a pipeline running helm upgrade --install, a few applied by hand with kubectl apply from a laptop folder, an operator or two installed years ago following a README, and a handful of objects nobody can explain. Staging and production have drifted apart. Deployments depend on who is on shift.
Moving to GitOps is how that ends, and it is the capstone of our GitOps, Helm, ArgoCD and Flux service. The work is less about installing a controller and more about getting an honest, reviewable description of what you run. This page lays out how we do it safely, in stages, without a big-bang cutover.
Why not just export everything
The tempting shortcut is kubectl get -o yaml for every resource, commit the result and point a controller at it. It goes wrong in predictable ways:
- Live objects carry generated fields such as status, resource versions and managed field data, which cause permanent diffs.
- Secrets would land in git in readable form.
- Defaults added by admission webhooks and operators get committed as if you wrote them.
- It preserves every accidental difference between environments, so promotion stays painful.
- Nobody reviews thousands of lines, so errors move into git unnoticed.
An export is a useful input to the audit. It is not the deliverable. The deliverable is clean, minimal declarations that humans understand.
What good looks like at the end
- Every workload in the cluster is declared in git or is explicitly listed as out of scope.
- The controller shows the clusters as in sync, with no unexplained ignore rules.
- CI has no cluster credentials, and engineers have read access, not routine write.
- Environments differ only by documented overlays or values, as in multi-environment promotion.
- A new service is added by a pull request using a known pattern.
How the migration runs
Phase 1: Audit
We compare three sources for each namespace: what is in your repositories, what Helm believes it installed, and what is actually live. Differences are the interesting part. Typical findings include a manually scaled Deployment, an image tag patched in place, a ConfigMap edited during an incident, and a CronJob that exists only in the cluster. Each is classified and a decision recorded.
Phase 2: Design the target
The repository structure, tool choice and installation happen before workloads move, so each migrated service lands in its final home. Secrets are solved first, using the approach in Sealed Secrets or External Secrets, because they block everything else.
Phase 3: Adopt in waves
Wave one is low-risk: an internal tool, a cron job, a staging service. We write the declarations, register the application with sync set to manual, and read the diff. The goal is an empty diff, or a diff you fully explain and accept. Only then do we sync, and later enable automation.
For Helm releases, the safest path is to keep the same chart, release name and values, so the objects are the same and nothing is recreated. A short check before the first sync:
# Compare what the chart would render with what is live
helm template payments ./charts/payments-api -f values-prod.yaml \
| kubectl diff -f -
# In ArgoCD, review the diff without changing anything
argocd app diff payments-prod
Pay attention to selectors and immutable fields. A changed Deployment selector, or a StatefulSet volume claim template, cannot be updated in place and will force a recreation, which is the one thing a zero-downtime migration must avoid. We catch those in the diff and plan them separately.
Phase 4: Critical services
Customer-facing and stateful services go last, with a change window agreed, a pre-checked rollback and a second pair of eyes. Prune stays off until we are confident every managed object is correctly labelled and owned. Where progressive delivery fits, it is added after the plain GitOps deploy is stable, not during the migration itself.
Phase 5: Close the doors
The last step decides whether migration sticks. CI jobs lose their kubeconfig, humans lose routine write access under the model in GitOps RBAC, and self-heal is turned on in the environments where it belongs. If any person can still kubectl edit production, drift will return within months.
Decisions and trade-offs
Big-bang or waves. Waves take longer calendar time but each is reversible. Big-bang suits only very small clusters.
Clean rewrite or faithful adoption. A faithful copy gets you running quickly but preserves mess. A rewrite is cleaner but riskier. We usually adopt faithfully, then refactor in git, where changes are reviewable and reversible.
Helm only or Kustomize too. If you already have working charts, keep them. If you have raw manifests, Kustomize is often the lighter first step, with charts added later where reuse justifies them. See writing charts.
When not to migrate yet
If you have one cluster, two services and one engineer, a disciplined pipeline may do for now. If the platform itself is about to change, such as a move to a new cloud, it can be better to land GitOps with that cloud migration rather than twice. If your infrastructure is still hand-built, start with Terraform, covered in our guide on moving from ClickOps to Terraform.
Common failure modes
- Pruning something important because it lacked the right labels. Prune is enabled last.
- Fighting controllers, where an HPA, operator or admission webhook changes fields the GitOps tool keeps reverting.
- Secrets forgotten, discovered only when a pod restarts with empty variables.
- Half-migrated estates where nobody knows which workloads are managed. We keep a status table per namespace.
- No communication, so an engineer applies a hotfix by hand and the controller reverts it mid-incident.
Delivery and handover
You receive the audit report, the target repository, migrated workloads, updated pipelines, runbooks and a coached first release by your own engineers. The migration is finished when manual deploy paths are closed, not when the last application turns green.
How to tell it is working
The controller shows a clean state. Rollback is a revert in git. Environment differences are explained. New hires deploy through pull requests on their first week. To scope a migration for your estate, book a consultation.