← All runbooks in Git, CI/CD & GitOps
Runbook: Recover a GitOps Controller
1 · Prerequisites
Confirm every item is in place before any state change.
- git-cicd-gitops-rb-18-troubleshoot-argocd-reconcile
- kubectl configured against the affected cluster context
- For Argo CD HA: knowledge of the Redis/Postgres backend (and credentials)
- Access to the GitOps repo for manifest recovery
- Helm installed locally (
helm version) for re-installation scenarios
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Identify the controller type and version. Argo CD:
kubectl get statefulset -n argocd -l app.kubernetes.io/name=argocd-application-controller -o jsonpath="{.items[0].spec.template.spec.containers[0].image}". Flux:flux --version(run from a workstation with cluster access) orkubectl get deploy -n flux-system -o jsonpath="{.items[*].spec.template.spec.containers[*].image}". The version determines the recovery procedure (HA vs non-HA) - · Capture the current state of the controller namespace. Argo CD:
kubectl get all,cm,secret -n argocd -o yaml > /tmp/argocd-pre-recovery.yaml. Flux:kubectl get all,cm,secret -n flux-system -o yaml > /tmp/flux-pre-recovery.yaml. The capture is the baseline for the recovery - · Capture the controller logs. Argo CD:
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --previous --tail=300. Flux:kubectl logs -n flux-system -l app=kustomize-controller --previous --tail=300. The "previous" logs are from the crashed instance and contain the failure cause - · Check the controller's events.
kubectl get events -n argocd --sort-by=.lastTimestamp | tail -50(Argo CD) orkubectl get events -n flux-system --sort-by=.lastTimestamp | tail -50(Flux). Events name the failure (image pull failure, OOMKilled, probe failure) - · Check the controller's database (Argo CD only). Argo CD HA uses Postgres or Redis; non-HA uses in-memory state.
kubectl get pods -n argocd -l app=redis -o wideandkubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server. The database must be healthy for the controller to recover - · Verify the cluster's connectivity to the Git repository. The controller's failure may be unrelated to the controller itself; the Git provider may be unreachable. See
git-cicd-gitops-rb-17-troubleshoot-argocd-authandgit-cicd-gitops-rb-21-recover-git-hosting-dependency
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1STEP 1 - Classify the controller failure. Five classes: (a) single-pod CrashLoopBackOff; (b) all-pod CrashLoopBackOff; (c) controller pods running but not reconciling; (d) database backend (Redis/Postgres) down; (e) controller namespace lost (cluster recovery scenario)
- 2STEP 2 - For class (a) single-pod CrashLoopBackOff: the StatefulSet is healthy but a single pod is bad.
kubectl delete pod -n argocd -l app.kubernetes.io/name=argocd-application-controller(replace the label for Flux). Kubernetes will create a new pod. If the new pod also crashes, escalate to class (b) - 3STEP 3 - For class (b) all-pod CrashLoopBackOff: the StatefulSet is broken. Inspect the controller logs from every pod:
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --previous --tail=100 --all-containers. Common causes: image pull failure (registry outage, image missing), RBAC denied (ServiceAccount misconfigured), config map invalid (argocd-cm corruption), resource limit hit (OOMKilled) - 4STEP 4 - For class (b) image pull failure:
kubectl describe pod -n argocd -l app.kubernetes.io/name=argocd-application-controller | grep -A5 "Failed\\|ImagePull". If the registry is down, seegit-cicd-gitops-rb-22-recover-registry-outage. If the image tag is wrong, patch the StatefulSet:kubectl set image statefulset/argocd-application-controller -n argocd argocd-application-controller=<registry>/argocd:<correct-tag> - 5STEP 5 - For class (b) OOMKilled: the controller ran out of memory.
kubectl describe pod -n argocd -l app.kubernetes.io/name=argocd-application-controller | grep -A2 "Last State". Increase the memory limit in the StatefulSet:kubectl set resources statefulset/argocd-application-controller -n argocd --limits=memory=2Gi. The default limit is too low for large clusters (1000+ Applications) - 6STEP 6 - For class (b) config map invalid: the argocd-cm or argocd-cmd-params-cm is corrupted. Diff against a known-good version:
kubectl get cm -n argocd argocd-cm -o yaml > /tmp/argocd-cm-broken.yaml. Restore from the GitOps repo (where the manifest is the source of truth) or from a backup.kubectl apply -f /path/to/gitops-repo/argocd-cm.yaml - 7STEP 7 - For class (c) pods running but not reconciling: the controller is alive but not doing work. Check the controller's metrics endpoint (
kubectl port-forward -n argocd svc/argocd-metrics 8082:8082then curl:8082/metrics). A controller withargocd_app_reconcile_countstuck at 0 is deadlocked. Common causes: shard/replica misconfiguration (HA), Git source unreachable, RBAC denied on managed namespaces - 8STEP 8 - For class (c) shard/replica misconfiguration: in HA mode, the controller shards managed clusters across StatefulSet replicas — if the replica count and the shard count disagree, Applications on some clusters never reconcile. Compare
kubectl get statefulset argocd-application-controller -n argocd -o jsonpath="{.spec.replicas}"with theARGOCD_CONTROLLER_REPLICASenv var, and checkkubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --tail=100 | grep -i shardfor the shard assignment. Make the two values match, thenkubectl rollout restart statefulset/argocd-application-controller -n argocd - 9STEP 9 - For class (d) database backend down (Argo CD HA): the controller cannot start without its database. Argo CD HA uses Redis (cache) and Postgres (source of truth).
kubectl get pods -n argocd -l app=redis. If Redis is down, seekubernetesrunbooks for StatefulSet recovery. If Postgres is down, see the database runbook. The controller cannot function without its database; restore the database first - 10STEP 10 - For class (e) controller namespace lost (cluster recovery): the entire argocd/flux-system namespace is gone. This is the worst case. Reinstall the controller via Helm: Argo CD
helm install argocd argo-cd/argo-cd --namespace argocd --create-namespace; Fluxflux install --namespace flux-system. Then re-import every Application or re-apply every Kustomization/HelmRelease from the GitOps repo - 11STEP 11 - For Argo CD re-import after reinstall:
argocd app create <app> --repo <url> --path <path> --dest-server <server> --dest-namespace <ns> --revision <branch>for every Application. The Applications must be re-imported from the GitOps repo; they cannot be recovered from the cluster - 12STEP 12 - For Flux re-apply after reinstall: every Kustomization and HelmRelease CR must be re-applied from the GitOps repo.
kubectl apply -f <path-to-cr>for each. The source-controller will re-establish the Git connection; the kustomize-controller/helm-controller will reconcile - 13STEP 13 - Verify the controller recovers.
argocd app listshows all Applications (re-imported, Synced, Healthy).flux get kustomization -Ashows all kustomizations (Ready=True). A forced refresh produces a new sync:argocd app get <app> --refresh. The controller is recovered
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-application-controllershows all pods Running (HA: 3, non-HA: 1). No CrashLoopBackOff. No OOMKilled inkubectl describe - ✓
kubectl get pods -n argocd -l app=redisshows Redis running (HA mode only) - ✓
argocd app listreturns the same Applications that were registered before the failure - ✓
argocd app listreports every Application as Synced + Healthy - ✓
flux get kustomization -Areports Ready=True for every kustomization - ✓A forced refresh (
argocd app get <app> --refresh) produces a new sync attempt and succeeds - ✓The controller metrics endpoint shows
argocd_app_reconcile_countincreasing (controller is doing work) - ✓End-to-end: a test manifest change in Git results in the cluster being updated within the sync interval
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶If the recovery fails and the controller is in a worse state than before: revert to the previous known-good state. For Helm-installed controllers:
helm rollback argocd <previous-revision> -n argocd. For Flux: re-apply the previous known-good set of CRs - ↶If the database was restored from a backup but the controller is using a newer schema: the controller will fail to start. Run the database migrations: Argo CD
argocd-db-migrationjob orkubectl create job --from=cronjob/argocd-db-migration argocd-db-migration-once -n argocd. The job upgrades the database schema to match the controller version - ↶If Applications cannot be re-imported because the GitOps repo is unavailable: see
git-cicd-gitops-rb-21-recover-git-hosting-dependency. The recovery is blocked on Git access - ↶If the controller is recovered but Applications remain OutOfSync: the manifest sync is independent of the controller recovery. See
git-cicd-gitops-rb-18-troubleshoot-argocd-reconcilefor the application-side reconciliation - ↶If the controller's HA setup was non-HA and the single-pod failure was caused by a node failure: the recovery is temporary until the node is fixed. Restart the controller on a different node with
kubectl delete pod -n argocd -l app.kubernetes.io/name=argocd-application-controllerand anodeSelectorpatch. The node must be investigated separately - ↶If the recovery required re-importing every Application and the GitOps repo is large (>500 Applications): the recovery is slow. Scale out the repo server for manifest-generation throughput (
kubectl scale deploy/argocd-repo-server -n argocd --replicas=3) and driveargocd app createfrom a script. Document the recovery time in the incident postmortem - ↶If the controller is recovered but the cluster was the failure cause (etcd corruption, network partition): the controller recovery is not enough. See
kubernetesrunbooks for cluster recovery
6 · Escalation
When the runbook isn't enough, contact:
- · The controller is recovered but applications are still Degraded in a way that suggests a cluster-wide issue (etcd, CNI, ingress): the cluster itself is broken. Engage the platform team immediately
- · The controller database (Postgres) is down and cannot be recovered from a backup: the controller's source of truth is lost. Reinstall the controller and re-import every Application from the GitOps repo. This is a multi-hour recovery; engage the platform team and inform the application owners
- · The controller namespace was lost and the GitOps repo is also lost: this is the worst case. The cluster has no GitOps coverage. Engage the platform team to re-establish GitOps and re-bootstrap the cluster from any remaining infrastructure-as-code
- · The controller is recovered but the cluster is still in an unmanaged state (no Applications registered, no resources reconciled): the GitOps onboarding for this cluster must be re-done. This is a multi-day recovery; engage the platform team and the application owners
- · The controller recovery triggered a mass reconciliation that is overwhelming the cluster: scale the controller back (
kubectl scale statefulset/argocd-application-controller -n argocd --replicas=1, keeping theARGOCD_CONTROLLER_REPLICASenv var in step with the replica count whenever you scale), set--status-processors=1and--kubectl-parallelism-limit=1, and let the reconciliation drain. Do not increase parallelism to "fix" the slowness - · The controller is recovering but the GitOps repo is being actively rewritten (a force-push incident): see
git-cicd-gitops-rb-06-respond-to-force-push-incident. The controller recovery must wait for Git to stabilize
The GitOps controller is the cluster”s brain. When the controller is down, the cluster stops reconciling — Applications stay in the state they were in when the controller died. The recovery depends on the failure class: a single-pod crash is a delete-and-recreate; a full controller outage is a database/restore problem; a namespace loss is a full reinstall.
1. Capture pre-recovery state
$ echo "--- Argo CD namespace state ---"
kubectl get all,cm,secret -n argocd -o yaml > /tmp/argocd-pre-recovery.yaml
kubectl get pods -n argocd -o wide
echo "--- controller logs (previous instance) ---"
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --previous --tail=200
echo "--- events ---"
kubectl get events -n argocd --sort-by=.lastTimestamp | tail -30The pre-recovery capture is the baseline. The previous-instance logs contain the failure cause. Events name the failure class (image pull, OOMKilled, probe failure).
2. Single-pod recovery
$ NS="argocd"
LABEL="app.kubernetes.io/name=argocd-application-controller"
kubectl delete pod -n "$NS" -l "$LABEL"
kubectl rollout status statefulset/argocd-application-controller -n "$NS" --timeout=120s
echo "--- new pod ---"
kubectl get pods -n "$NS" -l "$LABEL"
echo "--- new pod logs ---"
kubectl logs -n "$NS" -l "$LABEL" --tail=100A single-pod CrashLoopBackOff is usually a transient resource contention. Deleting the pod forces Kubernetes to create a new one on a different node (or with a new PID). If the new pod also crashes, escalate.
3. All-pod recovery: fix the cause, not the symptom
$ NS="argocd"
LABEL="app.kubernetes.io/name=argocd-application-controller"
echo "--- common causes ---"
kubectl describe pod -n "$NS" -l "$LABEL" | grep -E 'Failed|ImagePull|OOMKilled|BackOff' | head -20
echo "--- recent logs (all pods) ---"
kubectl logs -n "$NS" -l "$LABEL" --previous --tail=100 --all-containers | head -100
echo "--- events ---"
kubectl get events -n "$NS" --sort-by=.lastTimestamp | tail -30The describe output names the failure class. The previous-instance logs give the stack trace. Events give the kubelet”s perspective. All three together identify the cause.
4. Fix common causes
$ NS="argocd"
echo "--- image pull failure: check the image ---"
kubectl get statefulset argocd-application-controller -n "$NS" -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
echo "--- OOMKilled: bump the memory limit ---"
kubectl set resources statefulset/argocd-application-controller -n "$NS" --limits=memory=2Gi,cpu=2 --requests=memory=1Gi,cpu=500m
echo "--- RBAC: verify the SA ---"
kubectl get statefulset argocd-application-controller -n "$NS" -o jsonpath='{.spec.template.spec.serviceAccountName}{"\n"}'
kubectl auth can-i list applications --as=system:serviceaccount:argocd:argocd-application-controller -n argocd
echo "--- rollout restart after fix ---"
kubectl rollout restart statefulset/argocd-application-controller -n "$NS"
kubectl rollout status statefulset/argocd-application-controller -n "$NS" --timeout=300sEach cause has a different fix. Image pull: patch the image. OOMKilled: bump the limit. RBAC: patch the ClusterRole. After the fix, rollout restart and wait for healthy.
5. Recover the database backend (Argo CD HA)
$ NS="argocd"
echo "--- redis ---"
kubectl get pods -n "$NS" -l app=redis
echo "--- postgres ---"
kubectl get pods -n "$NS" -l app.kubernetes.io/name=argocd-server,role=server -o jsonpath='{.items[*].metadata.labels.app}'
echo "--- database migrations if needed ---"
kubectl create job --from=cronjob/argocd-db-migration argocd-db-migration-once -n "$NS" --dry-run=client -o yaml | kubectl apply -f -
kubectl wait --for=condition=complete job/argocd-db-migration-once -n "$NS" --timeout=600sArgo CD HA uses Redis (cache) and Postgres (source of truth). The controller cannot start without both. Restore the database first; the controller will recover automatically once the database is healthy.
6. Reinstall (namespace-loss scenario)
$ echo "--- Argo CD ---"
helm repo add argo-cd https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo-cd/argo-cd --namespace argocd --create-namespace --version REPLACE_WITH_CHART_VERSION
echo "--- wait for controller ---"
kubectl rollout status statefulset/argocd-application-controller -n argocd --timeout=300s
echo "--- Flux ---"
flux install --namespace flux-system --export > /tmp/flux-install.yaml
kubectl apply -f /tmp/flux-install.yamlA namespace loss requires a full reinstall. The Helm chart (Argo CD)
or flux install (Flux) re-creates the controllers. After
reinstall, re-import every Application or re-apply every
Kustomization/HelmRelease.
7. Re-import applications (Argo CD)
$ GITOPS_REPO="/path/to/gitops-prod"
cd "$GITOPS_REPO"
echo "--- enumerate Applications in Git ---"
find . -name '*.yaml' | xargs grep -l 'kind: Application' | head
echo "--- re-import each Application ---"
for app_file in $(find . -name '*.yaml' | xargs grep -l 'kind: Application'); do
argocd app create -f "$app_file"
done
echo "--- verify ---"
argocd app listApplications are not recovered from the cluster — they are recovered
from the GitOps repo. Iterate over every Application manifest and
argocd app create -f.
Verification
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-application-controller
shows all pods Running with no CrashLoopBackOff. Redis running (HA
mode). argocd app list returns the same Applications that were
registered before the failure and reports Synced + Healthy. A forced
refresh produces a new sync attempt and succeeds. The controller
metrics endpoint shows argocd_app_reconcile_count increasing.
Rollback
If the recovery fails and the controller is in a worse state, revert
to the previous known-good Helm revision. If the database was
restored from a backup but the controller is using a newer schema,
run the database migrations job. If Applications cannot be re-imported
because the GitOps repo is unavailable, the recovery is blocked on
Git access. If Applications remain OutOfSync, see
git-cicd-gitops-rb-18-troubleshoot-argocd-reconcile. If the
controller”s HA setup was non-HA and the single-pod failure was
caused by a node failure, restart the controller on a different
node. If the recovery required re-importing every Application and the
GitOps repo is large, expect a slow recovery — document the time.