
Most Argo CD tutorials start with an empty cluster. Real platforms rarely do. The lakehouse from the storage exercise in “When Storage Is the Bottleneck” was already running 28 applications as Helm releases and raw manifests, on storage where every restart was expensive.
The goal was to make Git the only way to change the cluster, without redeploying anything. A careless first sync could have restarted databases and reintroduced the failures that exercise had just documented, so the migration needed a strict no-restart standard.
The result: 205 resources adopted, no pods replaced, and automated sync with prune and self-heal enabled at the end. Four problems had to be solved first.
The approach
- Install Argo CD and Sealed Secrets.
- Generate one
Applicationper component, using the same charts, versions and values files as the deploy scripts, and commit them to a branch Argo CD tracks. - Create every Application with sync disabled. Then, for each one: – review the diff; – change Git until the diff is empty, or shows only Argo CD’s tracking annotation; – sync; – confirm that no pod restarted; – move to the next application.
- Enable automation only when every application is clean.
The diff drives the adoption at every step, so nothing depends on assumptions about what the cluster contains.
Problem 1: Secrets that existed only on the command line
The deploy scripts passed secrets to Helm with --set, so the values files in Git held placeholders such as password: "set-at-install". That works for scripts, but Argo CD renders only what is in Git. Its first sync would have replaced six real passwords with placeholders.
This risk was already proven. During an earlier manual upgrade, helm upgrade --reuse-values -f values.yaml let the file’s placeholder override the reused secret, and JupyterHub’s password silently became set-at-install. Nothing crashed. Logins simply started failing.
Every secret moved out of Helm values into a Kubernetes Secret that the chart references, in the style of existingSecret. Each Secret was then sealed and committed:
- A SealedSecret is encrypted with the cluster’s public key, so it is safe in Git, and only the in-cluster controller can decrypt it.
- The sealing script copies only
typeanddata, pipes directly intokubeseal, and never writes plaintext to disk. - 30 Secrets were sealed with byte-identical data, verified by hash before adoption.
- The deploy scripts changed from “create or update” to “create only if missing”, so a script run can no longer rotate a password outside Git.
Back up the sealing key. If the cluster is rebuilt without it, every SealedSecret in Git becomes undecryptable.
Problem 2: Values that are random by design
Some charts generate secrets at install time with Helm’s lookup function: reuse the existing Secret if there is one, otherwise generate a value. JupyterHub’s proxy token and cookie secret and Bitnami’s postgres-password both work this way.
Argo CD renders charts with helm template, which has no cluster access. lookup always returns nothing, so the chart generates a new random value on every render. That creates a permanent diff and, with self-heal enabled, a password change on every sync.
The fixes, in order of preference:
- If the chart supports an existing Secret, create one and seal it.
- Otherwise, use
ignoreDifferencesfor exactly those fields, including any checksum annotation derived from them, and setRespectIgnoreDifferences=trueso a sync never writes them either.
The second option was tested directly: JupyterHub’s keys were unchanged after a sync, and the hub did not restart. The cost is that ignored fields disappear from diffs, which is acceptable for random tokens.
Problem 3: Helm features Argo CD does not have
- Post-renderers. Opik needed a post-renderer to add startup probes that its MySQL sub-chart does not expose. Argo CD has no post-renderer hook, so Opik became a Kustomize application:
helmCharts:inflates the chart and JSON patches add the probes. The script path keeps the same patches, so the two renderings have to stay in step. - Helm hooks. Argo CD honours
helm.sh/hookonly for Helm sources. A chart inflated through Kustomize turned its MinIO bucket job into an ordinary resource, which went missing once its TTL deleted it and left the application permanently OutOfSync. Argo CD’s own annotation,argocd.argoproj.io/hook: PostSync, fixed this, and also worked for charts with TTL-managed migration Jobs such as Airflow. - Large CRDs. The Spark Operator’s CRD exceeds the 256 KB annotation limit that client-side apply uses for
last-applied-configuration. SettingServerSideApply=truefor that application, and for Strimzi, KServe and Opik, avoids the annotation entirely.
Problem 4: IPv6 on an IPv4-only pod network
The repo-server could not fetch the repository, failing with git fetch ... timeout after 1m40s. The pods have IPv4 only, and the Git host answered on IPv6 first. An SSH configuration with AddressFamily inet fixed it. The networking article in this series covers the root cause.
The adoption
With those problems solved, the diffs across all 205 resources were uneventful. The only differences were Argo CD’s tracking annotation and one Helm label. Each application was synced individually, with kubectl get pods checked for restarts after each one.
Automation then went on for all 28 applications: automated: {prune: true, selfHeal: true}.
The final test was deliberate drift. A Deployment changed by hand was reverted by Argo CD in 10 seconds. From that point, kubectl edit is only a temporary experiment, and Git is the write path.
What changed in daily operations
- A change is a commit. Values, manifests and Sealed Secrets go to
main, and Argo CD applies them within 5 minutes. It polls, because the cluster does not accept inbound webhooks from the Git host. - The scripts remain for bootstrap and break-glass use. They never overwrite existing Secrets, so they cannot conflict with Argo CD.
- Drift is visible. OutOfSync now means someone changed the cluster outside Git, and with self-heal that change has already been undone.
Recommendations
- Inventory every
--set. Each one is a future Argo CD regression. - Find every
lookupin your charts by comparing twohelm templateruns for values that change. - Adopt with sync off and work through the diffs one application at a time.
- Seal secrets from the live cluster, not from a
.envfile that may have drifted, and keep a backup of the sealing key outside the cluster. - Plan to translate post-renderers and hooks for Argo CD.
- Enable self-heal last, after an empty diff, and test it.
Get the next one in your inbox
Enjoyed this? I write about local AI, self-hosting, and cloud cost ninjutsu. Join the Ninja Dispatch for occasional, high-signal notes. No fluff.

