Argo CD Lesson 34 of 45

Argo CD HA & Disaster Recovery: Backup, Restore & Rebuilding the Control Plane

Most disaster-recovery lessons start by scaring you. This one starts by calming you down, because the single most important fact about recovering Argo CD is that it is far less scary than recovering a database — and understanding why changes every decision you make about HA and backups.

Here is the fact. Argo CD is, for practical purposes, stateless and rebuildable from Git. Its whole job is to reconcile a desired state that lives in your Git repositories into your clusters. That desired state — the manifests, the Helm values, the Kustomize overlays — is not inside Argo CD. It is in Git, versioned and replicated, already backed up by every clone and every mirror. And Argo CD’s control plane is not in the data path: it watches Git and nudges clusters toward it, but no user request to your running application ever passes through the application-controller. So if Argo CD falls over — or the entire cluster hosting it burns down — the Deployments it manages keep serving traffic. You lose reconciliation, drift correction, and the ability to ship new changes, but you do not lose your apps.

That does not make DR free. There is a small, specific set of state that is not in your app Git repos, and if you lose it your rebuilt Argo CD comes up blind — unable to reach its clusters, unable to authenticate a single engineer, unable to prove that the app it sees is the app that is running. This lesson is about exactly that: what that state is, how to keep Argo CD highly available so you rarely need to rebuild, how to back up the irreplaceable bits, and how to rebuild the entire control plane on a brand-new cluster and have it re-adopt every running app without redeploying a thing. If you have worked through cluster bootstrapping — where Terraform creates the cluster and installs Argo CD, then hands off — you already know the rebuild machinery; DR is that machinery pointed at a fresh cluster in anger.

Why this matters

Teams over-engineer Argo CD DR because they reason about it like a stateful application. A database is authoritative — lose it and its backups and the data is gone, because the database is the source of truth — so people carry that fear to Argo CD and imagine they must snapshot etcd, replicate Redis, and quiesce the controller before every backup. Almost none of that is true, and the mismatch wastes both money and confidence.

Argo CD inverts the database model. Git is authoritative; Argo CD is a cache and a reconciler on top of it. The desired state is in Git, the live state is in your clusters’ own etcd, and Argo CD’s Redis is a cache it regenerates by re-cloning the repos and re-listing the clusters. The only genuinely unique state it holds is a short list of connection and identity facts: how to reach each managed cluster, how to authenticate to each Git repo, who your users are, and how they map to permissions. Everything else it can rebuild.

The second load-bearing idea is the control plane / data path split, and it is the reason your on-call rotation can sleep. Argo CD’s components — the application-controller, repo-server, API server, ApplicationSet controller, whose roles you met in Argo CD Architecture: Components, the Repo-Server & the Controller — form a control plane that decides what should exist and makes it so. But a running Pod’s traffic flows through the cluster’s own kube-proxy, ingress, and mesh — never through Argo CD. Pull the entire Argo CD namespace and your checkout service still takes orders. That is categorically unlike an API gateway or a mesh sidecar, which is in the data path and whose failure is an immediate outage.

Property A database (e.g. Postgres) Argo CD
Source of truth The database itself Git (Argo CD only reconciles it)
In the data path of live traffic? Yes — queries hit it No — it is a control plane
If it dies, do running apps break? Yes, immediately No — workloads keep serving
Unique, irreplaceable state All your rows A short list: cluster/repo creds, RBAC/SSO, local accounts
Recovery model Restore a consistent snapshot Rebuild and re-point at Git
Realistic RPO target Seconds to minutes Minutes to hours (only the small state)

Hold that table in your head for the rest of the lesson. It tells you where to spend effort (HA so you rarely rebuild; a small, well-encrypted backup of the connection/identity state) and where not to (heroic etcd snapshots and Redis replication for data that is disposable). DR done right for Argo CD is cheap, fast, and boring — which is exactly what you want at 3 a.m.

The insight that shapes everything: Argo CD is (mostly) stateless

Let us make “mostly stateless” precise, because the “mostly” is where the whole lesson lives. Walk through everything Argo CD knows and ask one question of each — if I deleted the entire Argo CD namespace right now, could I get this back without a backup? — and the answers sort into three piles that come back for free, plus one short list that does not:

Pile What’s in it Recoverable without a backup?
In Git already Desired manifests; the App/AppSet/Project CRs if declarative Yes — Git is the copy
In the clusters Live state of every workload (each cluster’s own etcd) Yes — never Argo CD’s to lose
Pure cache Redis: computed manifests, resource trees, sync history Yes — regenerated on demand
The short list Cluster creds, repo creds, RBAC/SSO config, local accounts/tokens No — this is your backup scope

Every item in that last row is a credential or an access-control fact — the plumbing that lets a fresh Argo CD reach your repos and clusters and let the right humans in. That short list is your entire backup scope, and the next section enumerates it precisely.

Hence a best practice to adopt before you need DR: define your Application, ApplicationSet, and AppProject objects declaratively and commit them to Git. Apps created by clicking around the UI or one-off argocd app create commands exist only in the cluster’s etcd — lose the cluster, lose them. Living in a platform-gitops repo as an app-of-apps, the CRs are in Git too, and recovering them is just re-applying one root object. The declarative discipline you adopt for review and reproducibility also becomes your control-plane backup — not a coincidence, just the GitOps model doing its job.

Now the failure view, the one your incident commander cares about:

Failure What stops What keeps working Urgency
argocd-server down UI, CLI, API, SSO login Controller still reconciles; apps serve Low — cosmetic to end users
application-controller down Drift correction, self-heal, new syncs Every running workload; the API/UI Medium — you are “flying blind”
repo-server down New manifest generation Already-synced apps; live traffic Medium
Redis lost/wiped Nothing permanent Everything — cache repopulates Low — brief churn only
Entire Argo CD cluster gone All of the above at once Every managed workload on every spoke Rebuild, not outage

The bottom row is the disaster this lesson’s runbook targets, and the right-hand column is why it is survivable. When the hub is gone, your fleet is un-managed but up — you have minutes to an hour to rebuild before the lack of reconciliation bites (a drift no one corrects, a deploy no one can ship). A data-path component’s “gone” means “outage now”; Argo CD hands you a recovery window measured in coffee, not lost revenue per second.

The one way to squander this forgiveness is to put Argo CD in the data path by accident — say, having it host the only copy of a config a running app reads live, or coupling app health to the controller. Keep the boundary clean: Argo CD reconciles; it never serves. If a workload depends on Argo CD being up, you have designed a data-path dependency into a control plane and thrown away its single best property.

What state actually matters: the backup scope

If the backup scope is “credentials and access-control facts,” let us name every object precisely, because a DR plan that misses one of these is a DR plan that fails at the worst moment. Everything below lives in the argocd namespace as either a ConfigMap or a Secret, or as a custom resource.

The cluster Secrets — the critical, sensitive bit

When you register a spoke cluster (cluster registration has its own lesson), Argo CD stores the connection as a Secret labeled argocd.argoproj.io/secret-type: cluster — the only thing that lets the hub reach that spoke. It is a credential, so it is not in your app Git repos; lose it and the rebuilt hub sees the Application objects but cannot connect to the cluster they target, parking every one in Unknown/Failed with a connection error.

apiVersion: v1
kind: Secret
metadata:
  name: prod-eks-cluster
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: cluster   # this label is what makes it a "cluster"
type: Opaque
stringData:
  name: prod-eks
  server: https://ABCD1234.gr7.us-east-1.eks.amazonaws.com
  config: |
    {
      "execProviderConfig": {
        "apiVersion": "client.authentication.k8s.io/v1beta1",
        "command": "aws",
        "args": ["eks", "get-token", "--cluster-name", "prod-eks"]
      },
      "tlsClientConfig": { "caData": "<base64-CA>" }
    }

The config blob carries the auth method — an exec plugin, a bearer token, or client certs. That is genuine secret material, and it is per cloud: AKS spokes use kubelogin, EKS use aws eks get-token, GKE use gke-gcloud-auth-plugin. Back up these Secrets, and make sure the rebuilt hub image actually contains the exec-plugin binaries, or the restored Secret will still fail to authenticate.

The full backup scope, in one table

This is the centerpiece of the lesson — the “what to back up vs what’s already in Git vs what’s disposable” table. Print it; it is your DR checklist.

State Kubernetes object In your app Git? Back it up? How it comes back on rebuild
App / AppSet / Project CRs argoproj.io CRDs in argocd ns Yes, if declarative (ideal) Git is the backup; export as belt-and-braces Re-applied by the root app-of-apps
Cluster credentials Secret secret-type: cluster No — sensitive Yes — critical Restore (sealed / secret store / import)
Repo credentials Secret secret-type: repository / repo-creds No — sensitive Yes Restore or re-materialize via ESO
RBAC policy argocd-rbac-cm ConfigMap Should be (self-managed) Git + export Re-applied from Git / import
Server config, resource customizations argocd-cm ConfigMap Should be Git + export Re-applied from Git / import
Server signing key, OIDC client secret, TLS argocd-secret Secret No — sensitive Yes Restore, or regenerate + reconfigure SSO
Local accounts + bcrypt hashes argocd-cm + argocd-secret Partially Yes if relied on Restore or recreate
API tokens (accounts.*) argocd-secret No Yes if relied on Restore, or reissue (rotate)
Known hosts / TLS certs for repos argocd-ssh-known-hosts-cm, argocd-tls-certs-cm Should be Git + export Re-applied from Git / import
Redis cache (manifests, trees, app state) redis / redis-ha Pods No No — disposable Fresh Redis, warms from Git + clusters
Sync history / operation state Redis + object .status No No — disposable Lost; harmless

Three columns, one lesson: the middle column is what Git already protects, the right column is what comes back for free, and the rows where “In your app Git?” is No and “Back it up?” is Yes are your entire DR responsibility — a handful of objects, all in the argocd namespace, all captured by one command. Each maps to a preferred restore source:

Sensitive object Primary restore source Fallback
Cluster Secrets Sealed Secrets in Git, or ESO from the cloud secret store Encrypted argocd admin export
Repo credentials ESO / Sealed Secrets Encrypted export
argocd-secret (server key, OIDC secret) Cloud secret store / Sealed Secret Regenerate key + reconfigure SSO
argocd-cm / argocd-rbac-cm Git (self-managed config) Export snapshot

Why Redis is genuinely disposable

The last two rows deserve a word, because “disposable” makes people nervous. Redis stores only computed data — rendered manifests, the resource tree, cached diffs, some operation state — every byte of it derivable. Delete Redis and repo-server recomputes manifests, the controller re-lists clusters, and the cache refills. The official docs are blunt: Redis is “a disposable cache and can be safely rebuilt without service disruption.” So you never snapshot it, never restore it, and never panic when it is empty after a rebuild — losing it costs a brief reconcile churn, not one byte of data.

The nuance that trips people: argocd admin export does include the sensitive Secrets — the cluster creds, repo creds, and argocd-secret — in plaintext. That is what makes it a complete backup, and it is also what makes the export file itself a secret. A backup that is one kubectl apply away from restoring your whole control plane is also one leaked S3 object away from handing an attacker every spoke credential you own. Treat the export like the crown-jewel secret it is: encrypt it in transit and at rest, restrict who can read it, and never, ever commit it raw to Git.

HA topology: engineer it so you rarely rebuild

The best disaster recovery is the disaster that never escalates. HA keeps Argo CD serving through node failures, zone outages, and rolling upgrades, so a single dead Pod is a non-event instead of the start of a runbook — the same install from Installing Argo CD: Helm, Manifests, HA & First Login, wired for redundancy. The HA manifests live at manifests/ha/install.yaml (cluster-scoped) and manifests/ha/namespace-install.yaml (namespace-scoped), and the Helm chart’s redis-ha.enabled=true produces the same topology. The difference from the default install is redundancy on every tier plus a real, quorum-based Redis.

# Install HA Argo CD from the upstream manifests (pin to a release, not stable, in prod)
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/ha/install.yaml
# NOTE: the HA install schedules 3 redis + 3 haproxy + multi-replica components with
# pod anti-affinity, so it needs at least 3 schedulable nodes across zones.

What “HA” actually changes, component by component

Component Default HA Stateful? If one replica dies
application-controller 1 N shards (StatefulSet) via ARGOCD_CONTROLLER_REPLICAS No (state in Git + cache) Other shards cover; reconcile continues
argocd-server (API/UI) 1 ≥3 via ARGOCD_API_SERVER_REPLICAS Stateless UI/API stay up; no login blip
repo-server 1 ≥2 Stateless (ephemeral clones) Manifest generation load-balances
applicationset-controller 1 1-2 (leader-elected) Stateless Failover; generation pauses briefly
notifications-controller 1 1 Stateless Notifications pause; harmless
redis 1 standalone redis-ha: 3 servers + 3 Sentinels + 3 HAProxy Cache only (disposable) Sentinel promotes a new master
dex (SSO) 1 1 Stateless SSO login blips; local admin unaffected

The application-controller is the interesting one: it is a StatefulSet that shards, not a Deployment that merely replicates. Each replica owns a subset of your managed clusters, so scaling it both spreads load and removes the single-controller point of failure. Raise the replica count and set the matching env var so each shard knows the total:

# argocd-application-controller StatefulSet (excerpt) — HA + sharding
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: argocd-application-controller
          env:
            - name: ARGOCD_CONTROLLER_REPLICAS   # MUST match spec.replicas
              value: "3"
            # sharding strategy: legacy | round-robin | consistent-hashing
            - name: ARGOCD_CONTROLLER_SHARDING_ALGORITHM
              value: "round-robin"

The deeper treatment of why and how to shard — monorepo pressure, repo-server parallelism, the reconcile budget — is its own topic in Scaling Argo CD: Sharding, the Repo-Server & Monorepo Performance. For DR you need only the HA-relevant fact: a single controller replica is a single point of failure for reconciliation (not for your apps, which keep running), so production runs at least two shards.

--sharding-method / algorithm How clusters map to shards Status When to pick it
legacy Hash of cluster UID modulo replica count Default Small, stable fleets
round-robin Even index-based assignment Newer You want even shard load
consistent-hashing Bounded-load consistent hash Newer Fleets that add/remove clusters often (minimal reshuffle)

Redis HA: the one stateful-ish tier, and what losing it means

Redis is the only component with anything resembling a data plane, so HA treats it specially. The redis-ha topology runs three Redis servers (one master, two replicas) each paired with a Sentinel for automatic failover, fronted by three HAProxy Pods that always route clients to the current master. Argo CD’s components connect to HAProxy, not to Redis directly, so a master failover is transparent — Sentinel promotes a replica, HAProxy re-points, and the controller never notices.

Redis HA piece Count Role Failure behavior
redis-ha-server (StatefulSet) 3 Master + 2 replicas Sentinel promotes a replica on master loss
Sentinel (sidecar in each server Pod) 3 Quorum-based failover election Needs 2 of 3 for quorum
redis-ha-haproxy (Deployment) 3 Route clients to current master Any one can serve; clients retry

The crucial DR framing: losing Redis — even the whole redis-ha set — is not data loss, only cache loss. Worst case, the cold cache produces a short reconcile burst and slightly slower UI until it warms; apps do not restart and no backup is consulted. The only Redis failure that hurts is a quorum loss — if two of three Sentinels are unreachable (a bad two-node drain), failover cannot elect a master and reconciliation stalls until quorum returns. That is a HA availability bug, not a DR data-loss event; the fix is the quorum-aware disruption budget, next.

Redis event Impact What to do
One redis-ha-server Pod dies None — Sentinel keeps quorum, HAProxy re-routes Nothing; it reschedules
Master fails Sub-second failover, brief reconnect Nothing; Sentinel promotes a replica
Entire Redis wiped/lost Cold cache → brief reconcile churn Nothing to restore — it warms from Git + clusters
Quorum lost (2 of 3 down) Reconciliation stalls; no failover possible Restore a Pod fast; fix the PDB / zone spread

PodDisruptionBudgets and anti-affinity: surviving maintenance

HA replicas are worthless if a routine node drain schedules them all onto one node and then evicts that node. Two mechanisms prevent it. Pod anti-affinity (baked into the HA manifests) spreads replicas across nodes and zones. PodDisruptionBudgets cap how many replicas a voluntary disruption (a drain, a cluster upgrade) may take down at once. Without PDBs, kubectl drain will happily evict every argocd-server replica simultaneously and take your control plane down during a maintenance window you scheduled yourself.

# Representative PDBs — values illustrate intent; the HA manifests ship equivalents.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: argocd-server
  namespace: argocd
spec:
  minAvailable: 1                      # keep the API/UI reachable during a drain
  selector:
    matchLabels:
      app.kubernetes.io/name: argocd-server
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: argocd-redis-ha-server
  namespace: argocd
spec:
  minAvailable: 2                      # PRESERVE Redis Sentinel quorum (2 of 3)
  selector:
    matchLabels:
      app.kubernetes.io/name: argocd-redis-ha
Component PDB field Representative value Why this value
argocd-server minAvailable 1 (of ≥3) Keep the API/UI answering during a drain
repo-server maxUnavailable 1 Keep manifest-generation capacity
redis-ha-server minAvailable 2 (of 3) Never break Sentinel quorum
redis-ha-haproxy minAvailable 1 Always keep one routing proxy

The redis-ha-server PDB with minAvailable: 2 matters most and is the one people get wrong: set it to 1 (or omit it) and a two-node drain can take two Redis Pods, break quorum, and stall reconciliation cluster-wide — a self-inflicted incident during planned maintenance. It is the direct fix for the “HA Redis quorum lost” symptom below.

Multi-AZ and the cloud edges

HA within one zone survives a node failure; HA across zones survives a zone failure — the outage that actually shows up on a cloud status page. Spreading the control plane across zones is where the three clouds differ.

Concern AKS EKS GKE
Node spread across zones System node pool spanning Availability Zones Managed node group across 3 AZ subnets Regional cluster (nodes in 3 zones)
Satisfying pod anti-affinity ≥3 nodes across ≥2-3 zones ≥3 nodes across AZs Regional → 3 zones automatically
Managed control-plane HA AKS control plane (uptime SLA tier) EKS control plane is multi-AZ by default GKE regional control plane (3 replicas)
Zone-loss survival Pods reschedule in surviving zones Pods reschedule in surviving AZs Pods reschedule in surviving zones

The rule is identical everywhere — at least three nodes across two, ideally three, zones so anti-affinity places one replica per zone — only the knob differs: an AZ-aware node pool on AKS/EKS, a regional cluster on GKE. Put the hub on a regional/multi-AZ cluster and a zone outage becomes a rescheduling event, not a rebuild.

Backup: argocd admin export, the sensitive Secrets, and Git

You now know the scope. Backing it up is, satisfyingly, one command: argocd admin export walks the argocd namespace and writes a single YAML stream of the config ConfigMaps (argocd-cm, argocd-rbac-cm, known-hosts, TLS certs), the Argo CD Secrets (argocd-secret plus your cluster and repo creds), and every Application, ApplicationSet, and AppProject. One file, the entire control-plane state.

# Export ALL Argo CD state to a file. Talks to the Kubernetes API via your kubeconfig,
# NOT to argocd-server — so it works even when the API/UI is down.
argocd admin export -n argocd -o argocd-backup.yaml
# equivalently, the default out ("-") streams to stdout:
argocd admin export -n argocd > argocd-backup.yaml

# See what you captured — the kinds present prove the scope:
grep '^kind:' argocd-backup.yaml | sort | uniq -c
#   14 kind: Application
#    3 kind: AppProject
#    2 kind: ApplicationSet
#    6 kind: ConfigMap
#    9 kind: Secret          <-- cluster creds, repo creds, argocd-secret (PLAINTEXT)

Two properties make this command the backbone of Argo CD DR, and both are easy to miss:

Property Detail Why it matters for DR
Talks to the Kubernetes API, not argocd-server Uses your kubeconfig / in-cluster SA, reads objects directly Works when the Argo CD API/UI is down — exactly when you need a backup
Captures the sensitive Secrets isArgoCDSecret() matches cluster/repo/argocd-secret A complete restore is possible from this one file — and the file is a secret

The real flags you will use (current for 2.13+/3.x):

Flag Purpose
-o, --out Output file; default - (stdout). -o backup.yaml writes a file
-n, --namespace The Argo CD namespace. Always set it (see the silent-empty gotcha below)
--application-namespaces Also export apps from these namespace globs (apps-in-any-namespace mode)
--applicationset-namespaces Also export ApplicationSets from these namespace globs

The nastiest export gotcha, straight from the docs: argocd admin export will not fail if you run it against the wrong namespace. Point it at default instead of argocd and it cheerfully writes a nearly empty file and exits 0. Your backup “succeeds” every night and contains nothing. Always pass -n argocd explicitly, and — this is the important part — verify the output, e.g. assert a minimum kind: count, so an empty backup fails loudly instead of silently.

A backup CronJob

A backup you run by hand is a backup you forget. Schedule the export as an in-cluster CronJob that streams to encrypted object storage. Running inside the cluster, argocd admin export uses the ServiceAccount token as its kubeconfig automatically — you just need RBAC to read the objects.

# 1. A ServiceAccount + Role: read the ConfigMaps, Secrets, and Argo CD CRs.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: argocd-backup
  namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: argocd-backup
  namespace: argocd
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list"]
  - apiGroups: ["argoproj.io"]
    resources: ["applications", "applicationsets", "appprojects"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: argocd-backup
  namespace: argocd
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: argocd-backup
subjects:
  - kind: ServiceAccount
    name: argocd-backup
    namespace: argocd
# 2. The CronJob: export → gzip → encrypted object store, every 6 hours.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: argocd-backup
  namespace: argocd
spec:
  schedule: "0 */6 * * *"          # every 6h → 6h worst-case RPO for the small state
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          serviceAccountName: argocd-backup   # IRSA/WI-annotated for the object store
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: quay.io/argoproj/argocd:v2.13.3   # MATCH your Argo CD version
              command: ["/bin/sh", "-c"]
              args:
                - |
                  set -euo pipefail
                  ts=$(date +%Y-%m-%dT%H%M%SZ)
                  argocd admin export -n argocd \
                    | gzip \
                    | aws s3 cp - "s3://acme-argocd-backups/${ts}.yaml.gz" \
                        --sse aws:kms --sse-kms-key-id "$KMS_KEY_ID"

The --sse aws:kms is not optional decoration. The export contains plaintext cluster and repo credentials, so it must be encrypted at rest (KMS/CMK) as well as in transit. The CronJob’s ServiceAccount uses workload identity — never a static key — to reach the store, exactly as you set up when bootstrapping. Per cloud:

Concern AKS EKS GKE
Object store Azure Blob container S3 bucket GCS bucket
Upload command az storage blob upload aws s3 cp gcloud storage cp / gsutil cp
Encryption at rest SSE + optional CMK (Key Vault) SSE-KMS Google-managed or CMEK
CronJob identity (no static creds) Azure Workload Identity → Blob IRSA / Pod Identity → S3 Workload Identity → GCS
Cross-region durability GRS / RA-GRS Cross-Region Replication Multi-region bucket

Store the backup in a different region (or at least a different blast radius) from the Argo CD cluster — a backup that lives only in the hub’s region is one a regional outage takes with it. Cross-region replication is cheap for a few kilobytes of nightly YAML and removes the “the DR backup was in the datacenter that burned” failure mode.

Git is the primary backup; the export is the belt

Keep the hierarchy straight. Git is the primary backup of your app desired state and — if you are declarative — of the control-plane CRs. The argocd admin export is the belt-and-braces snapshot of the small sensitive state Git deliberately does not hold (cluster creds, argocd-secret). If you follow GitOps discipline fully, the only thing the export adds over Git is the credentials — which is why some teams skip full exports and instead back up just the sensitive Secrets (Sealed Secrets in Git, or the cloud secret store via ESO) and rebuild the rest from Git. Both are valid; pick by how much you value a one-command identical restore versus a fully-declarative rebuild.

Backup strategy What you store Restore path Trade-off
Full nightly export The whole argocd admin export YAML (encrypted) argocd admin import One-command identical restore; the file is a crown-jewel secret
Git + sealed secrets CRs & config in Git; creds as Sealed Secrets/ESO Apply root app + Sealed Secrets Fully declarative, nothing sensitive in plaintext; more moving parts
Hybrid (recommended) Git for everything declarative and an encrypted export as a snapshot Rebuild from Git; export as fallback Belt and braces; slightly more to manage

Restore: argocd admin import versus rebuilding from Git

There are two ways to bring the state back, and the choice is a philosophy, not a mechanic: import a snapshot, or rebuild from Git and re-adopt. Knowing both — and when each is right — is the core skill of Argo CD DR.

Here is the whole model in one picture. Read it left to right: Git holds the desired state (the primary backup) and a small backed-up state holds the sensitive credentials the app repos deliberately omit; a disaster destroys the hub while the workloads keep serving off the data path; you rebuild Argo CD on a fresh cluster and restore that small state; then you apply the one root Application and Argo CD re-adopts every running app from Git — the argocd app diff coming back empty is the proof that recovery caused no redeploy.

Left-to-right Argo CD disaster-recovery model: Git as the source of truth and primary backup, a small backed-up state of cluster Secrets and the argocd admin export, a disaster that destroys the hub cluster while workloads keep serving off the data path, a rebuild of Argo CD on a fresh cluster with the small state restored, and a final re-adopt step where applying the root app-of-apps makes argocd app diff show no changes and mark every app Synced and Healthy with no redeploy

The badges mark the decisions that make or break a recovery: Git is the real backup (1); the cluster Secrets are the sensitive bit that is not in your app repos (2); Redis is a disposable cache you never back up (3); the control plane is off the data path, which is why your apps keep serving through the disaster (4); you rebuild first and restore the small state before applying the root app (5); and you diff before you sync so recovery re-adopts instead of redeploying (6).

argocd admin import

Import is the mirror of export: it reads the YAML stream and creates/updates the objects in a target namespace. Like export, it talks to the Kubernetes API, not to argocd-server, so you can restore into a cluster whose Argo CD is freshly installed and not yet fully up.

# Restore everything from the encrypted backup into a fresh Argo CD.
# The "-" means "read the manifest stream from stdin".
gunzip -c argocd-backup.yaml.gz | argocd admin import -n argocd -

# What it does: upserts the ConfigMaps, Secrets, and CRs from the file. After this,
# the fresh Argo CD has your projects, apps, cluster creds, and RBAC — as of the backup.

Import restores an exact point-in-time snapshot: whatever apps, projects, and config existed when the export ran. That is its strength (a fast, identical rebuild) and its weakness (it can resurrect an app you deleted after the backup, and it is only as fresh as the last export — its RPO is your backup interval).

Rebuild-from-Git: the cleaner, GitOps-native path

The alternative is to not import the CRs at all. You install Argo CD, restore only the sensitive state Git does not hold (cluster Secrets, argocd-secret, repo creds), and apply your root app-of-apps — the same bootstrap seed Terraform plants when it hands a fresh cluster off. Argo CD reads the platform-gitops repo, recreates every child Application/ApplicationSet from Git, and reconciles. The control-plane CRs come back from Git — current, reviewed, identical to what you run day to day — not from a snapshot that may be hours stale.

# Rebuild-from-Git: restore ONLY the sensitive state, then let Git recreate the CRs.
# 1. Cluster + repo creds + argocd-secret (from Sealed Secrets, ESO, or a scoped import):
kubectl apply -n argocd -f sealed-cluster-secrets.yaml    # decrypts in-cluster
# 2. Seed the one root Application; it pulls the whole platform from Git:
kubectl apply -n argocd -f bootstrap/root-app.yaml
# 3. Argo CD recreates every child app FROM GIT and reconciles.
Dimension argocd admin import Rebuild-from-Git (app-of-apps)
What it restores Snapshot: apps, projects, appsets, config, and secrets Apps/projects/appsets from Git; you re-supply secrets
Source of truth on restore The export YAML (point-in-time) Git (current)
Staleness / RPO for app defs As old as the last export ~zero — Git is live
Secrets Included in the file Restored separately (Sealed/ESO)
Can resurrect stale/deleted apps? Yes — a footgun No — only what’s in Git now
Ties to bootstrapping Standalone Reuses your existing bootstrap seed
Best for Fast identical rebuild; forensic snapshot The recommended GitOps-native recovery

For most teams the answer is rebuild-from-Git for the CRs, plus a targeted restore of the sensitive Secrets — the best of both. Git gives current, reviewed app definitions with zero staleness; the Secret restore (or ESO) covers the one thing Git does not hold. Keep the full export as a forensic snapshot and break-glass fallback, not your primary path.

The re-adoption safety check: diff, then sync

Both paths converge on the single most important safety step in Argo CD DR — the one that separates a clean recovery from a self-inflicted fleet-wide outage. When the hub was lost but the spoke workloads kept running, your rebuilt Argo CD must re-adopt those live workloads — recognize that what Git says should exist already exists and is correct — and mark them Synced with nothing to change. It must not “restore” them by redeploying: redeploying every app across your fleet during an incident is its own incident.

The proof that re-adoption will be clean is argocd app diff returning empty before you let auto-sync run:

# BEFORE enabling auto-sync, diff the restored app against the live (surviving) cluster.
argocd app diff guestbook
# (no output) === an empty diff: Git matches live. Re-adoption is safe; nothing redeploys.

# If instead you see a full diff wanting to recreate everything, STOP — do not sync.
# Something is wrong: wrong targetRevision, wrong cluster Secret, or the live workloads
# are actually gone. Investigate before letting the controller act.

The operational discipline: install and restore with auto-sync disabled, run argocd app diff on a representative sample, confirm the diffs are empty (Git matches the surviving live state), then enable automated sync — turning “did my recovery just redeploy the fleet?” from a prayer into a checked precondition.

The DR runbook: the cluster hosting Argo CD is gone

Now assemble it into the procedure you run when the pager goes off. The scenario is the worst realistic one: the entire cluster hosting Argo CD is gone — a botched upgrade, a deleted cluster, a lost region — while the spoke clusters with your workloads stay up and serving. Your job: rebuild the control plane and re-adopt the fleet without touching the running apps.

Step Action Command / artifact Verify before proceeding
0 Declare scope. Confirm workloads still serving; this is a control-plane rebuild, not an app outage Check spoke ingress / synthetic probes Apps answering → you have time
1 Provision a fresh cluster (the substrate) terraform apply (your substrate/bootstrap module) kubectl get nodes → ≥3 across zones
2 Install Argo CD (HA), auto-sync OFF kubectl apply -k manifests/ha or Helm kubectl get pods -n argocd all Running
3 Restore the small state — cluster Secrets, repo creds, argocd-secret, config Sealed Secrets / ESO, or a scoped argocd admin import argocd cluster list shows the spokes
4 Apply the one root Application kubectl apply -f bootstrap/root-app.yaml root app appears
5 Diff before sync on a sample of apps argocd app diff <app> Empty diff = safe to adopt
6 Enable auto-sync; confirm re-adoption argocd app set <app> --sync-policy automated Synced/Healthy, pods not restarted
7 Restore backups + point DNS/ingress to the new hub UI Update the Argo CD ingress record Engineers can log in via SSO

The ordering is not cosmetic. Restore the cluster Secrets (step 3) before the root app (step 4), or the controller’s first reconcile hits every spoke Application with no credential and floods you with Failed/Unknown that masks whether recovery is working. And diff before sync (5 before 6) is the guardrail that keeps re-adoption from becoming redeployment. Run the steps in order and the fleet returns Synced/Healthy with not a single Pod restarted — the workloads never stopped; you just gave them their manager back.

# Steps 5-6 in practice: prove no redeploy, then hand control back to the controller.
argocd app list -o name | while read app; do
  echo "== $app =="; argocd app diff "$app" || true   # want: empty diffs
done
# All empty? Then re-enable automation fleet-wide:
argocd app list -o name | xargs -I{} argocd app set {} --sync-policy automated

RTO, RPO, and what DR does not cover

Set expectations honestly — “Argo CD DR” and “application DR” are different projects, and conflating them causes bad promises.

Metric Argo CD control plane Your app workloads
What an “outage” means No new deploys, no drift correction Actual user-facing downtime
Realistic RTO 15-60 min to rebuild + re-adopt (much of it Terraform) Separate — the app’s own DR plan
RPO ≈ backup cadence for the small state; ~0 for app defs (Git is live) The app’s data DR (DB snapshots, etc.)
Data-loss risk None if Git + the sensitive Secrets are recoverable Depends entirely on the app’s storage DR

Two honest caveats. First, Argo CD’s RTO is dominated by cluster provisioning, not by anything Argo-specific — argocd admin import takes seconds; standing up a fresh AKS/EKS/GKE cluster and its identity takes most of the window. How much you pre-build decides the number:

Standby posture What’s pre-built Rebuild time Idle cost
Cold Nothing — Terraform from scratch 30-60 min (provisioning dominates) ~zero
Pilot light Cluster module ready; backups staged 10-20 min low
Warm standby A second cluster with Argo CD already installed, idle 2-5 min (import + diff) a full idle cluster

Pre-baking a warm standby (or a fast, tested Terraform module) shrinks RTO — the same skill as bootstrapping. Second, and most important: DR for Argo CD is not DR for your applications. If a stateful app loses its database, Argo CD redeploys the app’s manifests but cannot restore its data — that is the app team’s backup/restore, separate from anything here. Argo CD guarantees your desired configuration comes back; it promises nothing about your application state. Keep the two runbooks separate.

Testing DR: the game day

A DR plan you have never executed is a hypothesis, not a plan. The only way to trust the runbook above is to run it on a schedule — a game day — against a throwaway environment.

Game-day drill What you inject What you are validating
Backup integrity Restore last night’s export into a scratch cluster The backup is non-empty and importable
Redis wipe kubectl delete pod -l app.kubernetes.io/name=argocd-redis-ha Cache rebuilds; no data loss; brief churn only
Controller loss Delete/scale the controller StatefulSet to 0 Apps keep serving; reconcile resumes on restore
Full hub loss Delete the entire argocd namespace, then run the runbook End-to-end rebuild + re-adopt with empty diffs
Secret loss Rebuild without restoring cluster Secrets You feel the “can’t reach spokes” failure safely

Run the “full hub loss” drill at least quarterly and time it — that measured number is your real RTO, not the one in a wiki. The most valuable game day is the one where you deliberately forget the cluster Secrets, watch every spoke app go Unknown, and burn “restore Secrets before the root app” into muscle memory before it happens for real.

Hands-on lab

You will back up a running Argo CD, inspect what the backup contains, schedule it, then simulate the disaster — destroy the control plane while the workload keeps running — and rebuild it, proving with argocd app diff that recovery re-adopts rather than redeploys. It runs on a free local kind cluster. The commands and manifests are real and schema-correct for Argo CD 2.13+/3.x; the outputs are representative — nothing was run against a live cluster, so treat them as the shape to expect, not a transcript.

This lab is free — kind runs in Docker on your laptop, no cloud spend. Prerequisites: kind, kubectl, and the argocd CLI. New to installing Argo CD? Start with Installing Argo CD: Helm, Manifests, HA & First Login.

Step 1 — A cluster with Argo CD and one managed app. Give yourself real state to back up: an app Argo CD manages, running in its own namespace.

kind create cluster --name argo-dr           # free, local
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/install.yaml
kubectl -n argocd rollout status deploy/argocd-server --timeout=180s

# A managed app with a distinct namespace so we can watch it survive the "disaster":
argocd login --core                          # talk to the API via kubeconfig (no server LB needed)
argocd app create guestbook \
  --repo https://github.com/argoproj/argocd-example-apps.git \
  --path guestbook --dest-server https://kubernetes.default.svc \
  --dest-namespace guestbook --sync-policy automated --auto-prune --self-heal
argocd app sync guestbook

What just happened: you have a real Argo CD managing guestbook, whose Pods run in the guestbook namespace. That namespace is your stand-in for a “surviving spoke” — it will keep running when we destroy the control plane.

Step 2 — Back up, and inspect the scope. Run the export and look at what it captured.

argocd admin export -n argocd -o argocd-backup.yaml
grep '^kind:' argocd-backup.yaml | sort | uniq -c
#    1 kind: Application        <-- guestbook is in the backup
#    6 kind: ConfigMap          <-- argocd-cm, argocd-rbac-cm, known-hosts, tls-certs, ...
#    5 kind: Secret             <-- argocd-secret + repo/cluster creds (PLAINTEXT)

What just happened: one command captured the control-plane state — the Application, config ConfigMaps, and Secrets. The Secret count is the tell: your backup holds plaintext credentials, which is why you encrypt it in production. The scope matches the “what to back up” table — apps, config, secrets; no Redis, because it is disposable.

Step 3 — Schedule it (and make an empty backup fail loudly). Apply the backup ServiceAccount/Role/CronJob from the Backup section. Add a guard so a wrong-namespace or empty export cannot pass silently.

# A verification you can bake into the CronJob's script — refuse to ship an empty backup:
n=$(grep -c '^kind:' argocd-backup.yaml)
if [ "$n" -lt 5 ]; then echo "BACKUP LOOKS EMPTY ($n objects) — failing"; exit 1; fi
echo "backup OK: $n objects"
# backup OK: 12 objects

What just happened: you turned the silent “export the wrong namespace and get an empty file” failure into a loud one. In the real CronJob this guard sits between argocd admin export and the upload, so a broken backup pages you instead of pretending to succeed.

Step 4 — Simulate the disaster. Destroy the control plane — delete the entire argocd namespace — but leave the workload running. This is the “hub is gone, spokes survive” scenario in miniature.

kubectl delete namespace argocd              # Argo CD is GONE
kubectl get pods -n guestbook                # ...but the workload is untouched:
# NAME                            READY   STATUS    RESTARTS   AGE
# guestbook-ui-6b5f...            1/1     Running   0          14m   <-- still serving

What just happened: you proved the thesis of the whole lesson on your own laptop. Argo CD is gone, yet guestbook keeps running with RESTARTS 0 — the control plane is not in the data path. You have lost reconciliation, not the app.

Step 5 — Rebuild the control plane. Reinstall Argo CD into the fresh (empty) namespace, restore the state from the backup, and — critically — do not sync yet.

# Reinstall Argo CD into the fresh namespace exactly as in Step 1, then restore:
argocd admin import -n argocd - < argocd-backup.yaml   # "-" reads the stream from stdin
kubectl get applications -n argocd
# NAME        SYNC STATUS   HEALTH STATUS
# guestbook   OutOfSync     Missing      <-- Argo CD sees the app spec but hasn't reconciled yet

What just happened: the rebuilt Argo CD has the guestbook Application back from the backup. It may briefly show OutOfSync/Unknown until it re-lists the live cluster — that is the controller catching up, not a problem. In a multi-cluster setup this is also where you would confirm the restored cluster Secrets are present so the hub can reach its spokes.

Step 6 — Prove re-adoption, not redeploy. This is the payoff. Diff the restored app against the still-running workload before allowing any sync.

argocd app diff guestbook
# (no output)   <-- EMPTY diff: Git matches the live, surviving workload.

argocd app get guestbook
# Name:  guestbook    Sync Status:  Synced      Health Status:  Healthy
kubectl get pods -n guestbook
# guestbook-ui-6b5f...  1/1  Running  0  20m     <-- RESTARTS still 0: never redeployed

What just happened: the empty diff proves Git matches what is already running, so Argo CD re-adopts the live Pods and marks the app Synced/Healthy without recreating anything. RESTARTS 0 and the unchanged age are the receipt: recovery restored management of the app, not the app itself — the entire promise of Argo CD DR, demonstrated end to end.

Step 7 — Teardown. Free the local resources.

kind delete cluster --name argo-dr           # removes the whole thing; zero cost anyway
rm -f argocd-backup.yaml

What just happened: kind clusters are local and ephemeral, so teardown is one command with nothing billable to leak — unlike a cloud DR drill, where you would also confirm no orphaned load balancers or disks remain.

Common mistakes and troubleshooting

Every row here is a real failure mode with a real Argo CD symptom. The nastiest three get extra prose after the table.

Symptom Cause Fix
Rebuilt hub: every spoke app Unknown/Failed, “connection refused”/auth error Lost the cluster Secrets — no credentials to reach the spokes Restore the secret-type: cluster Secrets before applying the root app; back them up in the first place
After a Redis restart, a burst of Progressing/reconcile activity Cold cache repopulating — expected, not data loss Wait it out; nothing to restore. Ensure redis-ha for fewer restarts
argocd admin import overwrote a newer app / resurrected a deleted one Import applies a point-in-time snapshot verbatim Prefer rebuild-from-Git for CRs; import only sensitive Secrets; keep exports fresh
Recovery redeployed every app across the fleet Synced before diffing; targetRevision or restored state wrong Install with auto-sync off, argocd app diff until empty, then enable automated
Reconciliation stalls cluster-wide; Redis “read-only”/no master Sentinel quorum lost (2 of 3 Redis Pods down) minAvailable: 2 PDB on redis-ha-server; spread across zones; restore quorum
kubectl drain during maintenance took the whole control plane down No PodDisruptionBudgets — all replicas evicted at once Apply PDBs (server minAvailable: 1, redis minAvailable: 2); anti-affinity
Restore succeeds but SSO login broken; apps fine The backup/restore missed argocd-secret (OIDC client secret, server key) Back up argocd-secret; or reconfigure SSO + regenerate the server key post-restore
Rebuild-from-Git: apps come back but can’t reach any cluster The cluster creds are (correctly) not in Git; you only restored Git Re-materialize cluster Secrets via Sealed Secrets/ESO alongside the root app
Single application-controller; a node loss froze all reconciliation One controller replica = SPOF for reconciliation Run HA: ≥2 shards, ARGOCD_CONTROLLER_REPLICAS set, StatefulSet
Nightly backup “succeeds” but the file is nearly empty argocd admin export ran against the wrong namespace (it does not error) Always pass -n argocd; assert a minimum object count or fail the job
Export uploaded but readable by too many principals Export contains plaintext creds; lax bucket/KMS policy Encrypt (KMS/CMK), lock the bucket policy, treat the file as a top secret

1. “Restore redeployed everything” is the incident-during-an-incident. The instinct is to install Argo CD with auto-sync on and let it “fix” everything — but your workloads are already running on the surviving spokes; there is nothing to fix, only to re-adopt. If the restored state has a wrong targetRevision, or a cluster Secret points at a cluster whose live state you can’t read, an eager controller reads “I can’t confirm this matches” as “recreate it” — a fleet-wide rollout in the middle of a disaster. The discipline is non-negotiable: auto-sync off, argocd app diff until empty, then automation on. An empty diff is your written permission to let the controller act; being in a hurry is exactly when it saves you.

2. Losing the cluster Secrets is the failure that hides in plain sight. Teams back up the Application CRs (or keep them in Git) and feel safe, then find during a rebuild that the hub sees every app but can reach no cluster — the one thing that was neither in Git nor in the “obvious” backup was the credential to the spokes. The cluster Secrets matter most precisely because GitOps discipline does not protect them: they are sensitive, so they are (correctly) never in your app repos. Back them up explicitly — Sealed Secrets in Git, the cloud secret store via ESO, or inside the encrypted export — and rehearse restoring them first, before the root app.

3. Redis panic during recovery wastes your window. When you rebuild and Redis comes up empty, the UI is slow, apps flicker Progressing, and the reconcile queue spikes — and an operator who does not know Redis is disposable starts hunting for a backup that does not and should not exist. There is nothing to restore. The cache is supposed to be cold after a rebuild; it warms within minutes as repo-server recomputes manifests and the controller re-lists clusters. Spend your recovery window on what actually needs you — the cluster Secrets, the diff check — not a phantom Redis restore.

Cheat-sheet

The backup scope as a decision rule you can apply to any piece of Argo CD state:

If the state is… It is… So you…
Desired manifests / (declarative) CRs In Git already Rely on Git; no separate backup
A cluster or repo credential Sensitive, not in app Git Back it up (Sealed/ESO/encrypted export)
argocd-cm / argocd-rbac-cm config Should be in Git (self-managed) Keep in Git; export as a snapshot
argocd-secret (server key, OIDC secret) Sensitive Back it up; or regenerate + reconfigure
Redis (any of it) A disposable cache Do nothing — never back up, never restore

The commands that do the work:

Command What it does
argocd admin export -n argocd -o backup.yaml Dump all Argo CD state (CRs + config + secrets) to a file
argocd admin export -n argocd | gzip | <upload> Stream an encrypted backup to object storage (the CronJob)
grep -c '^kind:' backup.yaml Sanity-check the backup is non-empty (guard against wrong-ns)
argocd admin import -n argocd - < backup.yaml Restore all state from the snapshot (reads stdin)
argocd app diff <app> The safety check — empty output = safe to re-adopt
argocd app set <app> --sync-policy automated Re-enable auto-sync after diffs confirm no redeploy
argocd cluster list Confirm the restored cluster Secrets can reach the spokes
kubectl apply -k manifests/ha Install the HA control plane (redis-ha, sharded controller, PDBs)

The HA + DR knobs:

Knob Purpose
ARGOCD_CONTROLLER_REPLICAS Number of controller shards (match StatefulSet replicas)
ARGOCD_API_SERVER_REPLICAS argocd-server replica count for API/UI HA
--sharding-method / ARGOCD_CONTROLLER_SHARDING_ALGORITHM legacy | round-robin | consistent-hashing
redis-ha PDB minAvailable: 2 Preserve Sentinel quorum through node drains
argocd-server PDB minAvailable: 1 Keep the API/UI up through maintenance

The DR runbook, condensed to what you type when the hub is gone:

# Do Prove
1 terraform apply a fresh cluster kubectl get nodes
2 Install Argo CD HA, auto-sync off pods Running
3 Restore cluster Secrets + argocd-secret argocd cluster list
4 kubectl apply the root app-of-apps root appears
5 argocd app diff <app> empty diff
6 Re-enable automated sync Synced/Healthy, no restarts

Interview and exam questions

Q: Why is disaster recovery for Argo CD fundamentally easier than for a database? A: Because Argo CD is not the source of truth and is not in the data path. Its desired state lives in Git (already backed up and replicated), the live state lives in the target clusters’ etcd, and its Redis is a disposable cache. The only unique state it holds is a small set of credentials and access-control config. So recovery is a rebuild (reinstall and re-point at Git), not a restore of authoritative data — and because the control plane isn’t in the request path, running workloads keep serving while you rebuild.

Q: If the entire cluster hosting Argo CD is destroyed, what happens to the applications it manages? A: They keep running. Argo CD is a control plane, not a data-plane component — no user traffic flows through it. When it’s gone you lose reconciliation, drift correction/self-heal, and the ability to ship new changes, but every already-running Deployment on every spoke keeps serving. You have a recovery window (typically minutes to an hour) before the lack of reconciliation actually bites.

Q: What state must you explicitly back up, given that Git already holds your manifests? A: The state that is not in your app Git repos, all of which is sensitive: the cluster Secrets (secret-type: cluster — credentials to reach the spokes), repo credentials (secret-type: repository), and argocd-secret (server signing key, OIDC client secret, TLS). If you keep your Application/ApplicationSet/AppProject CRs and argocd-cm/argocd-rbac-cm declaratively in Git, those are covered too; if not, back them up as well. Redis is never backed up — it’s disposable.

Q: What does argocd admin export capture, and why is the resulting file sensitive? A: It exports the whole Argo CD control-plane state from the namespace via the Kubernetes API: the config ConfigMaps, argocd-secret, the cluster/repo Secrets, and all Application/ApplicationSet/AppProject CRs. It’s sensitive because it includes those Secrets in plaintext — a complete backup is also a complete credential dump, so it must be encrypted in transit and at rest and never committed to Git raw.

Q: A colleague set up a nightly argocd admin export CronJob and it reports success every night, but the restore test found the backups empty. What likely happened? A: The export ran against the wrong namespace. argocd admin export does not error if it finds nothing in the target namespace — point it at default instead of argocd and it writes a near-empty file and exits 0. Fix: always pass -n argocd, and add a guard that asserts a minimum object count (grep -c '^kind:') so an empty backup fails the job loudly.

Q: Explain the difference between argocd admin import and rebuilding from Git, and when you’d choose each. A: import restores an exact point-in-time snapshot including secrets — fast and identical, but only as fresh as the last export and able to resurrect apps you deleted afterward. Rebuild-from-Git installs Argo CD, restores only the sensitive Secrets, and applies the root app-of-apps so the CRs come back from Git — current, reviewed, no staleness — but you must re-materialize the secrets separately. Most teams rebuild the CRs from Git and do a targeted restore of the sensitive Secrets, keeping a full export as a break-glass fallback.

Q: During recovery, how do you make sure Argo CD re-adopts running workloads instead of redeploying them? A: Install and restore with auto-sync disabled, then run argocd app diff on the restored apps against the surviving live clusters. An empty diff proves Git matches what’s already running, so Argo CD will mark the apps Synced and adopt the existing Pods with nothing to change. Only after diffs are empty do you re-enable automated sync. Skipping the diff risks a fleet-wide redeploy in the middle of an incident.

Q: What is the correct order of the DR runbook steps, and why does ordering matter? A: Provision the cluster → install Argo CD (HA, auto-sync off) → restore cluster Secrets/config → apply the root Application → diff → enable auto-sync. Restoring the cluster Secrets before the root app matters because otherwise the controller’s first reconcile hits every spoke app with no credential and floods you with failures. Diffing before enabling sync matters because it’s the guardrail that turns re-adoption into a checked precondition rather than a fleet-wide redeploy.

Q: What does losing Redis cost you, and what should you do about it? A: Nothing permanent. Redis is a disposable cache of computed manifests, resource trees, and app state that Argo CD regenerates from Git and the live clusters. Losing or wiping it causes a brief reconcile churn while the cache warms — not data loss — so you never back it up or restore it. The only Redis failure that hurts is a Sentinel quorum loss (2 of 3 down), which stalls reconciliation; prevent it with a minAvailable: 2 PDB and zone spread.

Q: How does the application-controller achieve HA, and what’s the single-replica risk? A: The controller runs as a StatefulSet that shards its managed clusters across replicas; you scale replicas and set ARGOCD_CONTROLLER_REPLICAS to match, choosing a --sharding-method (legacy/round-robin/consistent-hashing). A single controller replica is a single point of failure for reconciliation — if its node dies, drift correction and new syncs stop until it reschedules (your apps keep running regardless). Production runs at least two shards.

Q: What are realistic RTO and RPO for Argo CD, and what does its DR explicitly not cover? A: RTO is typically 15-60 minutes, dominated by provisioning the fresh cluster (the import/apply itself is seconds); shrink it with a warm standby or fast Terraform. RPO is roughly your backup cadence for the small sensitive state and near-zero for app definitions since Git is live. It does not cover application data — if a stateful app loses its database, Argo CD redeploys the app’s manifests but cannot restore its data; that’s the app team’s separate DR.

Key takeaways

argocdgitopskubernetesdisaster-recoveryhigh-availabilitybackuprestoreredis-haargocd-admin-exportrto-rposhardingcluster-secretsakseksgke
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments