Everything for Argo CD in one place — pick a section below. 39 reviewed items across 4 content types, plus a hands-on tutorial.
Quick Answer
Argo CD uses sync phases and waves to order resources, hooks to run lifecycle jobs, and health checks to decide whether reconciliation can progress. They prevent unsafe ordering when designed carefully, but failed hooks, unhealthy early waves, or selective sync behavior can leave applications stuck or partially deployed.
Detailed Answer
Think of a theater production. The stage crew must set the floor before actors enter, lights must be ready before the curtain opens, and cleanup happens after the show. If the lighting check fails, the performance should stop before customers see chaos. Argo CD sync phases and waves give Kubernetes deployments that same ordered choreography, but bad choreography can also stop the whole show.
Argo CD continuously compares desired state in Git with live state in clusters. During sync, hooks can run before, during, after, or on failure of the main operation. Sync waves add finer ordering with numeric annotations, where lower waves apply before higher waves. Health checks determine whether resources are ready enough for reconciliation to continue. This matters when namespaces, CRDs, RBAC, migrations, Deployments, and smoke tests depend on one another.
Internally, Argo CD orders work by phase, then wave, then kind, then name. PreSync hooks run first and can block everything if they fail. Normal resources default to wave zero unless annotated. PostSync hooks run after resources are synced and healthy. SyncFail hooks can perform cleanup. Argo CD also waits briefly between waves so other controllers have time to observe changes and update status.
In production, this is useful for database migrations, CRDs, admission policies, and smoke tests. Teams put CRDs and namespaces in early waves, workloads later, and validation jobs after workloads report healthy. They monitor application sync status, health status, operation duration, hook pods, repo-server errors, and controller queue depth. Progressive syncs and sync windows can reduce blast radius for fleets.
The gotcha is that hooks do not run during selective sync, and a resource that is unhealthy in an early wave can prevent later waves from applying. Another subtle failure is a migration hook that succeeds once but is not idempotent, then blocks rollback or retry. Senior engineers keep hooks small, observable, and retry-safe; avoid hiding critical dependencies in random waves; and document when manual intervention is required.
Code Example
argocd app get payments-api # Shows sync status, health status, current revision, and any stuck operation. argocd app diff payments-api # Compares live cluster resources against the Git desired state before syncing. argocd app sync payments-api --prune --timeout 600 # Applies desired state and prunes removed resources with a bounded timeout. kubectl get jobs -n payments -l app.kubernetes.io/instance=payments-api # Inspects hook jobs that may be blocking or failing the sync. kubectl describe application payments-api -n argocd # Reviews conditions and operation messages from the Argo CD Application resource.
Interview Tip
A junior engineer typically answers that Argo CD syncs Git to Kubernetes, but for a senior/architect role, the interviewer is actually looking for ordering and failure semantics. Explain phases, waves, hooks, health checks, and why early unhealthy resources can block later work. Mention selective sync bypassing hooks, idempotent migrations, and observability for hook jobs. That shows you know how GitOps behaves during messy production releases, not just during happy-path demos. Strong candidates also describe rollback ownership, sync windows, and fleet-level blast radius.
◈ Architecture Diagram
┌──────────┐
│ Git │
└────┬─────┘
↓
┌──────────┐
│ Diff │
└────┬─────┘
↓
┌──────────┐
│ PreSync │
└────┬─────┘
↓
┌──────────┐
│ Waves │
└────┬─────┘
↓
┌──────────┐
│ Healthy │
└────┬─────┘
↓
┌──────────┐
│ PostSync │
└──────────┘💬 Comments
Quick Answer
ArgoCD is a declarative GitOps continuous delivery tool for Kubernetes that continuously monitors Git repositories and reconciles the desired state defined in Git with the actual state running in the cluster, automatically detecting and optionally correcting drift.
Detailed Answer
Think of ArgoCD like a thermostat in your house. You set the desired temperature (your Git repository declares the desired cluster state), and the thermostat continuously monitors the current temperature (cluster state) and turns on heating or cooling (applies manifests) whenever there is a deviation. Just as the thermostat does not care how the temperature drifted — whether someone opened a window or the sun came out — ArgoCD does not care whether drift was caused by a kubectl edit, a rogue operator, or a failed rollout. It simply reconciles back to the declared truth in Git.
At its core, ArgoCD implements the GitOps pattern where Git serves as the single source of truth for declarative infrastructure and application definitions. When you register an Application resource with ArgoCD, you tell it which Git repository, branch, and path contain your Kubernetes manifests. ArgoCD then compares the rendered manifests from Git against the live objects in the target Kubernetes cluster. If they match, the application is considered Synced. If they differ, ArgoCD marks it as OutOfSync and can either alert you or automatically apply the changes depending on your sync policy configuration.
Internally, ArgoCD consists of three main components that power the reconciliation loop. The Repository Server clones Git repositories, caches them, and renders manifests using the appropriate tool such as Helm, Kustomize, or plain YAML. The Application Controller is the brain that runs the reconciliation loop on a configurable interval, defaulting to every three minutes. It fetches the desired state from the Repository Server, fetches the live state from the Kubernetes API server, performs a three-way diff using the last-applied-configuration annotation, and determines if the application is synced or drifted. The API Server exposes the gRPC and REST API consumed by the web UI and CLI, handles authentication via OIDC or DEX, and enforces RBAC policies for multi-tenant environments.
In production, you typically deploy ArgoCD in a dedicated namespace like argocd on a management cluster. Teams configure webhook notifications from GitHub or GitLab so that ArgoCD reacts to pushes within seconds rather than waiting for the next polling interval. You also integrate ArgoCD with notification controllers to send alerts to Slack or PagerDuty when an application goes OutOfSync or a sync operation fails. Resource tracking is configured using either the label-based or annotation-based method, and health checks are customized for CRDs that ArgoCD does not natively understand, ensuring the reconciliation loop accurately reflects the true health of your workloads.
A critical gotcha engineers overlook is the difference between the refresh interval and the sync interval. Refreshing means ArgoCD re-reads Git and re-computes the diff, while syncing means it actually applies changes to the cluster. Even with auto-sync enabled, ArgoCD will not sync if the previous sync failed — it enters a degraded state requiring manual intervention or a retry policy. Another pitfall is ignoring resource exclusions; by default ArgoCD tracks every resource it manages, and if an operator or admission controller mutates fields like timestamps or status subresources, ArgoCD may report perpetual OutOfSync status. You must configure ignoreDifferences in the Application spec to exclude those volatile fields from the diff computation.
Code Example
# argocd-application.yaml - Defines a GitOps-managed application
apiVersion: argoproj.io/v1alpha1 # ArgoCD custom resource API version
kind: Application # Declares this as an ArgoCD Application resource
metadata:
name: payments-api # Unique name for this application in ArgoCD
namespace: argocd # Must be deployed in the ArgoCD namespace
spec:
project: default # ArgoCD project for RBAC and resource restrictions
source:
repoURL: https://github.com/acme-corp/payments-api.git # Git repository URL containing manifests
targetRevision: main # Branch or tag to track for reconciliation
path: k8s/overlays/production # Directory path within the repo holding manifests
destination:
server: https://kubernetes.default.svc # Target cluster API server URL
namespace: payments # Target namespace where resources will be deployed
syncPolicy:
automated: # Enables the automatic reconciliation loop
prune: true # Deletes resources removed from Git automatically
selfHeal: true # Reverts manual changes made outside Git
syncOptions:
- CreateNamespace=true # Creates the target namespace if it does not exist
retry:
limit: 3 # Number of retry attempts on sync failure
backoff:
duration: 30s # Initial backoff delay between retries
factor: 2 # Multiplier applied to backoff on each retry
maxDuration: 3m # Maximum backoff duration capInterview Tip
A junior engineer typically describes ArgoCD as just a deployment tool, but interviewers want to hear you articulate the reconciliation loop as a continuous control loop — not a one-shot pipeline. Explain the three core components (Repository Server, Application Controller, API Server) and how each contributes to detecting drift and restoring desired state. Mention that the default polling interval is three minutes but webhooks reduce detection latency to seconds. Highlight the distinction between refresh (re-reading Git) and sync (applying changes), because confusing these two operations is a common mistake that signals shallow understanding. Demonstrate production awareness by discussing ignoreDifferences for volatile fields and retry backoff strategies for transient failures.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────┐ │ ArgoCD System │ │ │ │ ┌──────────────┐ ┌───────────────────┐ │ │ │ Git Repo │───→│ Repository Server │ │ │ │ (desired) │ │ (clone + render) │ │ │ └──────────────┘ └────────┬──────────┘ │ │ │ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ App Controller │ │ │ │ (reconcile loop) │ │ │ │ desired vs live │ │ │ └────────┬────────────┘ │ │ │ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ Kubernetes Cluster │ │ │ │ (actual state) │ │ │ └─────────────────────┘ │ │ │ │ ┌──────────────┐ ┌───────────────────┐ │ │ │ API Server │←──→│ Web UI / CLI │ │ │ └──────────────┘ └───────────────────┘ │ └─────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
An ArgoCD Application is a Kubernetes custom resource that defines the mapping between a source Git repository path and a destination cluster namespace, while sync policies control whether synchronization happens automatically or manually, whether pruning of deleted resources occurs, and how self-healing responds to configuration drift.
Detailed Answer
Imagine an ArgoCD Application as a contract between a landlord and a tenant. The contract specifies exactly what the apartment should look like (Git source), which building and unit it applies to (destination cluster and namespace), and the rules for maintenance (sync policy). The sync policy is like the maintenance clause — it determines whether the landlord can enter and fix things automatically when the tenant makes unauthorized modifications, or whether the landlord must first get approval before making any changes. Just as different tenants may have different maintenance agreements, different applications can have different sync policies depending on their criticality.
The Application custom resource is the fundamental building block of ArgoCD. It encapsulates four key pieces of information: the source (a Git repository URL, revision, and path), the destination (a cluster API server endpoint and namespace), the project (which governs RBAC and allowed resources), and the sync policy (which defines automation behavior). When ArgoCD processes an Application, it renders the manifests from the source using the detected or configured tool — Helm, Kustomize, or directory of plain YAML — and compares the rendered output against the live state in the destination cluster. The comparison result is the sync status, which can be Synced, OutOfSync, or Unknown.
Sync policies control the automation level through three primary mechanisms. First, automated sync enables ArgoCD to apply changes without human intervention whenever it detects that Git has diverged from the cluster state. Second, the prune option within automated sync controls whether ArgoCD deletes Kubernetes resources that were removed from Git; without prune enabled, ArgoCD will only create and update resources but never delete them, leading to orphaned objects. Third, selfHeal instructs ArgoCD to revert any manual changes made directly to the cluster, effectively enforcing Git as the immutable source of truth. Additionally, syncOptions provide granular control such as CreateNamespace, ApplyOutOfSyncOnly, ServerSideApply, and Replace, each addressing specific deployment scenarios.
In production environments, teams typically use automated sync with prune and selfHeal for non-critical services like internal tools and staging environments, while keeping manual sync for production-critical services like the payments-api or order-service to maintain a human approval gate. Sync windows add another layer of control by restricting when automatic syncs can occur — for example, blocking deployments during peak traffic hours or maintenance windows. ArgoCD also supports sync retries with configurable backoff strategies to handle transient failures like API server throttling or temporary network issues, preventing a single failed sync from requiring manual intervention during off-hours.
A common gotcha is enabling automated sync with prune without understanding its implications. If someone accidentally pushes a commit that removes a critical manifest file — say the production database StatefulSet — ArgoCD will dutifully delete that StatefulSet from the cluster within seconds. This is why many teams use the argocd.argoproj.io/sync-options annotation with Prune=false on critical resources as a safety net, preventing accidental deletion even when the global prune policy is enabled. Another frequently overlooked detail is that automated sync will not trigger if the Application is in a degraded or error health state from a previous failed sync. Engineers must configure proper alerting on sync failures and understand that selfHeal only applies to resources ArgoCD manages — it cannot detect or fix drift in resources outside its tracking scope.
Code Example
# payments-api-app.yaml - Application with detailed sync policies
apiVersion: argoproj.io/v1alpha1 # ArgoCD CRD API version
kind: Application # Kubernetes custom resource type
metadata:
name: payments-api # Application identifier in ArgoCD dashboard
namespace: argocd # ArgoCD watches this namespace for Application CRDs
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.slack: payments-alerts # Slack notification on sync failure
finalizers:
- resources-finalizer.argocd.argoproj.io # Cascade delete managed resources when Application is deleted
spec:
project: payments-team # Scoped project for RBAC isolation between teams
source:
repoURL: https://github.com/acme-corp/payments-api.git # Source Git repository for manifests
targetRevision: release-v2.3 # Pinned to release branch for stability
path: deploy/production # Subdirectory containing production manifests
destination:
server: https://kubernetes.default.svc # In-cluster deployment target
namespace: payments-prod # Production namespace for payments services
syncPolicy:
automated: # Enable automatic sync on Git changes
prune: true # Remove cluster resources deleted from Git
selfHeal: true # Revert manual kubectl edits automatically
allowEmpty: false # Prevent sync if rendered manifests are empty as a safety check
syncOptions:
- CreateNamespace=true # Auto-create namespace if missing
- PrunePropagationPolicy=foreground # Wait for dependents before pruning parent
- ApplyOutOfSyncOnly=true # Only apply resources that actually changed to reduce API calls
- ServerSideApply=true # Use server-side apply for conflict resolution
retry:
limit: 5 # Retry up to five times on transient sync failures
backoff:
duration: 10s # Start with ten second delay between retries
factor: 2 # Double the delay on each subsequent retry
maxDuration: 5m # Cap the maximum delay at five minutes
ignoreDifferences: # Ignore fields mutated by controllers to prevent false OutOfSync
- group: apps # Target the apps API group
kind: Deployment # Apply to Deployment resources specifically
jsonPointers:
- /spec/replicas # Ignore replicas managed by HPA autoscalerInterview Tip
A junior engineer typically just says 'ArgoCD syncs Git to the cluster' without explaining the nuances of sync policies. Interviewers want depth — explain the difference between automated sync, prune, and selfHeal as three independent toggles that compose together. Mention that prune without safeguards can accidentally delete critical resources and that the Prune=false annotation is a per-resource safety net. Discuss sync windows as a production control for blocking deployments during peak hours. Show you understand that selfHeal only covers tracked resources and that ignoreDifferences is essential when HPA or other controllers mutate fields. Mention the retry backoff strategy and explain why allowEmpty=false is a critical safety mechanism against misconfigured source paths that render zero manifests.
◈ Architecture Diagram
┌───────────────────────────────────────────────────────┐
│ ArgoCD Application Lifecycle │
│ │
│ ┌──────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ Git Push │────→│ Refresh │────→│ Compare │ │
│ │ (source) │ │ (poll/wh) │ │ (3-way diff│ │
│ └──────────┘ └─────────────┘ └─────┬──────┘ │
│ │ │
│ ↓ │
│ ┌────────────────┐ │
│ │ Sync Status │ │
│ │ Synced or │ │
│ │ OutOfSync │ │
│ └───────┬────────┘ │
│ │ │
│ ┌─────────────────┼────────┐ │
│ ↓ ↓ │ │
│ ┌────────────┐ ┌──────────────┐ │ │
│ │ Manual │ │ Automated │ │ │
│ │ Sync │ │ Sync+Prune │ │ │
│ │ (approval)│ │ +SelfHeal │ │ │
│ └─────┬──────┘ └──────┬───────┘ │ │
│ │ │ │ │
│ ↓ ↓ │ │
│ ┌──────────────────────────────┐ │ │
│ │ Apply to Cluster │ │ │
│ │ (create/update/prune) │ │ │
│ └──────────────────────────────┘ │ │
│ │ │
└─────────────────────────────────────────────────────┘ │
│
└───────────────────────────────────────────────────────┘💬 Comments
Quick Answer
ArgoCD ApplicationSets use a templating controller that generates multiple Application resources from a single template combined with generators like list, cluster, Git directory, Git file, merge, or matrix generators, enabling scalable multi-cluster and multi-tenant deployments without duplicating Application definitions.
Detailed Answer
Think of ApplicationSets like a mail merge in a word processor. You write one template letter (the Application template) and provide a spreadsheet of recipients with their unique details (the generator producing parameters). The mail merge engine produces one personalized letter for each row in the spreadsheet. Similarly, the ApplicationSet controller takes one Application template and one or more generators, then produces a unique ArgoCD Application for each set of parameters the generator yields — one per cluster, one per environment, or one per microservice, depending on your strategy.
ApplicationSets are a Kubernetes custom resource managed by the ApplicationSet controller, which runs alongside the core ArgoCD components. The resource specification consists of two main sections: generators and a template. Generators produce a list of parameter sets — key-value pairs that get substituted into the template. ArgoCD ships with several built-in generators including the list generator for explicit enumeration, the cluster generator that discovers clusters registered in ArgoCD, the Git directory generator that scans a repository for subdirectories, the Git file generator that reads JSON or YAML parameter files from a repository, the pull request generator for ephemeral preview environments, and the matrix and merge generators for combining multiple generators together. Each generator yields rows of parameters, and for each row, the controller renders the template with those parameters substituted in, creating a distinct Application resource.
Internally, the ApplicationSet controller watches for ApplicationSet resources and runs a reconciliation loop separate from the main ArgoCD Application Controller. When it detects a new or modified ApplicationSet, it evaluates the generators to produce the parameter matrix, renders the template for each parameter set, and then creates, updates, or deletes the corresponding Application resources. The controller uses owner references to track which Applications belong to which ApplicationSet, enabling garbage collection when parameters are removed from a generator. The matrix generator deserves special attention because it computes the Cartesian product of two child generators — for example, combining a cluster generator with a Git directory generator to deploy every microservice to every cluster, producing N times M Applications from just two generators.
In production, ApplicationSets shine for platform teams managing dozens or hundreds of clusters. A common pattern is to use the cluster generator combined with labels to target specific cluster groups — for example, deploying the payments-api to all clusters labeled region=us-east and tier=production. Another powerful pattern uses the Git file generator where each cluster has a configuration file in a dedicated repository, and the file contains cluster-specific parameters like replica counts, resource limits, and feature flags. When a new cluster is provisioned and registered with ArgoCD, the cluster generator automatically picks it up and creates Applications for it without any manual intervention, achieving true zero-touch multi-cluster deployment at scale.
A significant gotcha with ApplicationSets is the deletion policy. By default, when an ApplicationSet is deleted, all generated Applications and their managed resources are also deleted — this cascading behavior can be catastrophic if someone accidentally deletes the ApplicationSet in Git. The preserveResourcesOnDeletion policy prevents this by keeping the Applications alive even after the ApplicationSet is removed. Another common pitfall is generator parameter conflicts when using the matrix generator; if both child generators produce a parameter with the same key, the second generator's value silently overwrites the first, leading to subtle deployment errors. Teams should also be cautious with the pull request generator in production clusters — without proper RBAC scoping the ApplicationSet to a sandboxed project, a malicious pull request could deploy arbitrary workloads to production namespaces.
Code Example
# payments-applicationset.yaml - Multi-cluster deployment using ApplicationSet
apiVersion: argoproj.io/v1alpha1 # ApplicationSet API version matching ArgoCD
kind: ApplicationSet # Custom resource for templated multi-app generation
metadata:
name: payments-api-multi-cluster # Identifies this ApplicationSet in ArgoCD
namespace: argocd # Must reside in the ArgoCD namespace
spec:
generators: # Define how parameter sets are produced
- matrix: # Combines two generators via Cartesian product
generators: # Child generators whose outputs are cross-joined
- clusters: # First generator discovers registered ArgoCD clusters
selector: # Filter clusters by Kubernetes labels
matchLabels:
tier: production # Only target production-tier clusters
team: payments # Only clusters owned by the payments team
- git: # Second generator reads config from Git files
repoURL: https://github.com/acme-corp/cluster-configs.git # Repository holding per-cluster config files
revision: main # Track the main branch for cluster configurations
files: # Git file generator reads JSON parameter files
- path: "clusters/*/config.json" # Glob pattern matching each cluster config file
template: # Application template rendered for each parameter set
metadata:
name: "payments-api-{{name}}" # Dynamic name using cluster name from cluster generator
namespace: argocd # Generated Applications live in the argocd namespace
spec:
project: payments-team # RBAC project scoping for the payments team
source:
repoURL: https://github.com/acme-corp/payments-api.git # Application source repository
targetRevision: "{{branch}}" # Branch from the Git file config.json per cluster
path: k8s/overlays/production # Kustomize overlay path for production
kustomize: # Kustomize-specific rendering options
images: # Override container image tag per cluster
- "payments-api={{image_tag}}" # Image tag from config.json parameter
destination:
server: "{{server}}" # Cluster API server URL from cluster generator
namespace: payments-prod # Target namespace on each cluster
syncPolicy:
automated: # Enable automated sync for all generated Applications
prune: true # Prune resources deleted from Git
selfHeal: true # Revert manual changes on each cluster
syncPolicy: # ApplicationSet-level sync policy
preserveResourcesOnDeletion: true # Prevent cascading delete of Applications if ApplicationSet is removedInterview Tip
A junior engineer typically describes multi-cluster deployment as manually creating one Application per cluster, which reveals they have not worked at scale. Interviewers expect you to explain the generator-template model and name at least three generator types — cluster, Git file, and matrix — with concrete use cases for each. Emphasize the matrix generator as the Cartesian product of two generators and explain why this is powerful for deploying N services across M clusters. Mention the preserveResourcesOnDeletion policy as a critical safety mechanism and discuss how the pull request generator enables ephemeral preview environments. Show production maturity by explaining that parameter key collisions in matrix generators cause silent overwrites and require careful naming conventions across generator outputs.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ ApplicationSet Controller │ │ │ │ ┌────────────────┐ ┌────────────────────┐ │ │ │ Cluster Gen │ │ Git File Gen │ │ │ │ (3 clusters) │ │ (config.json) │ │ │ └───────┬────────┘ └────────┬───────────┘ │ │ │ │ │ │ └───────────┬───────────┘ │ │ ↓ │ │ ┌───────────────────┐ │ │ │ Matrix Generator │ │ │ │ (3 x N params) │ │ │ └────────┬──────────┘ │ │ ↓ │ │ ┌──────────────────────┐ │ │ │ Template Renderer │ │ │ └──────────┬───────────┘ │ │ │ │ │ ┌──────────────┼──────────────┐ │ │ ↓ ↓ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ App: │ │ App: │ │ App: │ │ │ │ us-east │ │ us-west │ │ eu-west │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ ↓ ↓ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Cluster │ │ Cluster │ │ Cluster │ │ │ │ us-east │ │ us-west │ │ eu-west │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Sync waves are integer annotations on Kubernetes resources that control the order in which ArgoCD applies them during a sync operation, while hooks are special resources that run at specific phases of the sync lifecycle such as PreSync, Sync, PostSync, and SyncFail, enabling ordered deployments like running database migrations before application rollouts.
Detailed Answer
Think of sync waves and hooks like staging a theater production. The stage crew sets up the backdrop first (wave -1 for config maps and secrets), then the orchestra pit is prepared (wave 0 for database migrations via a PreSync hook), the actors take their positions (wave 1 for the main deployments), and finally the curtain call sequence runs (wave 2 PostSync hooks for smoke tests and notifications). Each phase must complete successfully before the next begins, and if anything fails — say a prop malfunctions — there is a SyncFail hook that triggers the stage manager's emergency protocol. This ordered execution ensures that dependencies are satisfied before dependents are deployed.
Sync waves work through the argocd.argoproj.io/sync-wave annotation, which accepts an integer value. During a sync operation, ArgoCD groups all resources by their wave number and processes them in ascending order. Resources without a wave annotation default to wave 0. Within each wave, ArgoCD applies all resources simultaneously and waits for every resource in that wave to reach a healthy state before progressing to the next wave. If any resource in a wave fails to become healthy within the configured timeout, the sync operation is marked as failed and subsequent waves are not executed. Hooks, on the other hand, are resources annotated with argocd.argoproj.io/hook and are executed at specific phases: PreSync runs before any wave begins, Sync runs alongside wave 0, PostSync runs after all waves complete successfully, and SyncFail runs only when a sync operation fails.
Internally, the sync engine within the Application Controller orchestrates this process. When a sync is triggered, the controller first identifies all hook and wave annotations across the managed resources. It constructs an execution plan that orders PreSync hooks first, followed by each wave in numerical order with their contained resources, and finally PostSync hooks. For each phase, the controller applies the resources to the cluster and then enters a polling loop, checking the health status of each resource against ArgoCD's built-in health assessment logic. For Deployments, health means all replicas are available. For Jobs used as hooks, health means the Job completed successfully. The controller uses the argocd.argoproj.io/hook-delete-policy annotation to determine whether hook resources should be cleaned up before each sync, after successful completion, or after failure, preventing accumulation of completed Job pods.
In production, a typical deployment of the order-service might use wave -2 for namespace and RBAC resources, wave -1 for ConfigMaps and Secrets, a PreSync hook Job for database schema migrations, wave 0 for the core Deployment and Service, wave 1 for HorizontalPodAutoscaler and PodDisruptionBudget, and a PostSync hook for integration smoke tests that verify the API is responding correctly. This ensures the database schema is ready before the application starts, and the application is running before autoscaling policies are applied. Teams often combine waves with health checks on custom resources to gate progression — for example, waiting for a CertificateRequest to be issued before deploying the Ingress that references it.
The most dangerous gotcha with sync waves is circular health dependencies. If a resource in wave 0 depends on a resource in wave 1 to become healthy — for example, a Service in wave 0 that needs endpoints from a Deployment in wave 1 — the sync will deadlock because wave 0 will never become healthy and wave 1 will never execute. Engineers must carefully map dependency graphs before assigning wave numbers. Another common mistake is forgetting the hook-delete-policy annotation on PreSync Jobs; without it, the Job from the previous sync still exists and ArgoCD will not re-run it because Kubernetes prevents creating a Job with the same name. Setting HookSucceeded as the delete policy ensures the previous Job is cleaned up before the next sync creates a new one.
Code Example
# order-service-waves.yaml - Deployment with sync waves and hooks
---
apiVersion: v1 # Core Kubernetes API for ConfigMap
kind: ConfigMap # Configuration data deployed before the application
metadata:
name: order-service-config # ConfigMap name referenced by the Deployment
namespace: orders # Target namespace for the order service
annotations:
argocd.argoproj.io/sync-wave: "-1" # Deploy in wave -1 before the main application
---
apiVersion: batch/v1 # Batch API for Job resources
kind: Job # One-time execution resource for database migration
metadata:
name: order-service-db-migrate # Job name for the migration task
namespace: orders # Same namespace as the application
annotations:
argocd.argoproj.io/hook: PreSync # Execute before any sync wave begins
argocd.argoproj.io/hook-delete-policy: HookSucceeded # Delete Job after successful completion to allow re-creation
argocd.argoproj.io/sync-wave: "-2" # Explicit ordering among multiple PreSync hooks
spec:
backoffLimit: 3 # Retry the migration Job up to three times on failure
template: # Pod template for the migration Job
spec:
containers: # Container list for the migration pod
- name: migrate # Container name for the migration runner
image: acme-corp/order-service-migrate:v2.1.0 # Migration image with schema changes
command: ["./migrate"] # Entrypoint command to run migrations
args: ["--target", "latest"] # Migrate to the latest schema version
envFrom: # Load environment variables from external source
- secretRef: # Reference to secret containing database credentials
name: order-db-credentials # Secret with DB_HOST DB_USER DB_PASS
restartPolicy: Never # Do not restart the pod on failure let backoffLimit handle retries
---
apiVersion: apps/v1 # Apps API for Deployment resources
kind: Deployment # Main application deployment resource
metadata:
name: order-service # Deployment name for the order processing service
namespace: orders # Target namespace matching other resources
annotations:
argocd.argoproj.io/sync-wave: "0" # Deploy in the default wave after PreSync hooks
spec:
replicas: 3 # Run three replicas for high availability
selector: # Label selector to match pods managed by this Deployment
matchLabels:
app: order-service # Label used for pod selection
template: # Pod template specification
metadata:
labels: # Labels applied to each pod
app: order-service # Matching label for the selector
spec:
containers: # Container list for the application pod
- name: order-service # Primary container name
image: acme-corp/order-service:v2.1.0 # Application container image
ports: # Exposed container ports
- containerPort: 8080 # HTTP port for the order service API
---
apiVersion: batch/v1 # Batch API for the smoke test Job
kind: Job # Post-deployment verification job
metadata:
name: order-service-smoke-test # Job name for post-deployment validation
namespace: orders # Same namespace as the deployed application
annotations:
argocd.argoproj.io/hook: PostSync # Execute only after all sync waves succeed
argocd.argoproj.io/hook-delete-policy: HookSucceeded # Clean up after successful test run
spec:
backoffLimit: 1 # Only retry the smoke test once on failure
template: # Pod template for the smoke test
spec:
containers: # Container list for the test runner
- name: smoke-test # Container name for the test execution
image: acme-corp/api-test-runner:latest # Test runner image with curl and assertions
command: ["./run-tests"] # Execute the smoke test suite
args: ["--endpoint", "http://order-service:8080/health"] # Target the order service health endpoint
restartPolicy: Never # Do not restart on failure to surface errors clearlyInterview Tip
A junior engineer typically lumps all resources into a single sync without considering deployment order, which reveals inexperience with real-world dependency chains. Interviewers want you to explain the wave numbering system, the four hook phases (PreSync, Sync, PostSync, SyncFail), and the health-gating mechanism between waves. Give a concrete example: database migration as a PreSync hook before the Deployment in wave 0, followed by a smoke test PostSync hook. Critically, mention the hook-delete-policy annotation — forgetting HookSucceeded means Jobs accumulate and subsequent syncs silently skip hook execution because the Job already exists. Discuss the circular dependency deadlock risk where wave 0 resources depend on wave 1 resources. Show maturity by mentioning that SyncFail hooks are essential for automated rollback notifications or cleanup procedures.
◈ Architecture Diagram
┌──────────────────────────────────────────────────┐ │ ArgoCD Sync Wave Execution │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Phase: PreSync │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ Job: db-migrate (wave-2)│ │ │ │ │ └────────────┬────────────┘ │ │ │ │ ↓ wait healthy │ │ │ └─────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Wave -1: ConfigMaps + Secrets │ │ │ │ ┌────────────┐ ┌─────────────┐ │ │ │ │ │ ConfigMap │ │ Secret │ │ │ │ │ └─────┬──────┘ └──────┬──────┘ │ │ │ │ └────────┬───────┘ │ │ │ │ ↓ wait healthy │ │ │ └─────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Wave 0: Deployment + Service │ │ │ │ ┌────────────┐ ┌─────────────┐ │ │ │ │ │ Deployment │ │ Service │ │ │ │ │ └─────┬──────┘ └──────┬──────┘ │ │ │ │ └────────┬───────┘ │ │ │ │ ↓ wait healthy │ │ │ └─────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Phase: PostSync │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ Job: smoke-test │ │ │ │ │ └─────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Argo Rollouts is a Kubernetes controller that replaces the standard Deployment resource with a Rollout CRD, enabling progressive delivery strategies like canary releases with weighted traffic shifting and analysis-driven promotion, and blue-green deployments with instant traffic switching and automated rollback based on metrics from Prometheus, Datadog, or other providers.
Detailed Answer
Imagine deploying a new version of software like opening a new lane on a highway. With a standard Kubernetes Deployment, you instantly redirect all traffic to the new lane — if there is a pothole, every car hits it. A canary release is like opening the new lane to only five percent of traffic first, monitoring for incidents, then gradually increasing to twenty-five, fifty, and finally one hundred percent. A blue-green deployment is like building an entirely parallel highway, verifying it with test traffic, and then switching the exit ramp so all cars seamlessly move to the new road. Argo Rollouts provides the traffic controller that orchestrates these patterns with automated analysis and instant rollback capabilities.
Argo Rollouts extends Kubernetes with a Rollout custom resource that replaces the standard Deployment. The Rollout spec includes a strategy section where you define either a canary or blueGreen configuration. For canary deployments, you specify steps that control the rollout progression — setWeight to adjust traffic percentage, pause to wait for a duration or manual approval, and analysis to run automated metric checks. The Rollout controller manages two ReplicaSets: the stable version serving production traffic and the canary version receiving the configured percentage. Traffic splitting is achieved through integration with service mesh or ingress controllers including Istio, Linkerd, Nginx, ALB, SMI, Traefik, and Ambassador, each using their native traffic management primitives.
Internally, the Argo Rollouts controller watches Rollout resources and manages the rollout state machine. When a new image or spec change is detected, the controller creates a canary ReplicaSet and begins executing the defined steps sequentially. At each analysis step, the controller creates an AnalysisRun resource that queries configured metrics providers — Prometheus, Datadog, NewRelic, CloudWatch, or custom webhook providers — and evaluates the results against success criteria. If an AnalysisRun determines the canary is unhealthy based on error rate thresholds or latency percentiles, the controller automatically aborts the rollout and scales the canary ReplicaSet to zero, restoring all traffic to the stable version. For blue-green deployments, the controller maintains an active and preview Service, each pointing to different ReplicaSets. The preview Service receives the new version for testing, and upon promotion, the active Service selector is switched to point to the new ReplicaSet.
In production, teams typically integrate Argo Rollouts with ArgoCD by simply replacing the Deployment kind with Rollout in their Git manifests. ArgoCD recognizes the Rollout CRD and provides native health status tracking in its dashboard. A common pattern for the checkout-worker service is a canary strategy with five steps: set weight to ten percent, run a five-minute Prometheus analysis checking error rates and p99 latency, set weight to thirty percent, run another analysis for ten minutes, then set weight to one hundred percent. The AnalysisTemplate queries like rate(http_requests_total{status=~"5.*",service="checkout-worker"}[5m]) / rate(http_requests_total{service="checkout-worker"}[5m]) ensure the canary's error rate stays below the configured threshold before each promotion step.
The most critical gotcha is that Argo Rollouts traffic splitting only works if you have a supported traffic router configured. Without Istio VirtualServices, Nginx annotations, or another supported ingress controller, the weight-based traffic splitting has no effect and the canary just runs alongside the stable version without controlled traffic distribution — it falls back to basic replica-ratio-based splitting which is imprecise. Another common mistake is setting analysis intervals too short; a two-minute analysis window may not capture enough data points for statistically significant error rate calculations, leading to false positives that promote a bad canary. Engineers should also understand that argo rollouts differ from ArgoCD — they are separate projects with separate controllers, and you need both installed to get GitOps-driven progressive delivery.
Code Example
# checkout-worker-rollout.yaml - Canary deployment with automated analysis
apiVersion: argoproj.io/v1alpha1 # Argo Rollouts API version
kind: Rollout # Replaces standard Deployment for progressive delivery
metadata:
name: checkout-worker # Name of the rollout matching the service identity
namespace: checkout # Namespace for the checkout domain services
spec:
replicas: 5 # Total desired replicas across stable and canary
revisionHistoryLimit: 3 # Keep three previous ReplicaSets for instant rollback
selector: # Label selector for managing pods
matchLabels:
app: checkout-worker # Must match the pod template labels
template: # Pod template identical to a Deployment spec
metadata:
labels: # Labels applied to every pod
app: checkout-worker # App label for service discovery and selection
spec:
containers: # Container definitions for the worker
- name: checkout-worker # Primary container running the checkout logic
image: acme-corp/checkout-worker:v3.2.0 # Container image with version tag
ports: # Exposed ports for traffic routing
- containerPort: 8080 # HTTP port serving the checkout API
resources: # Resource requests and limits for scheduling
requests: # Minimum resources guaranteed to the container
cpu: 200m # Request 200 millicores of CPU
memory: 256Mi # Request 256 megabytes of memory
strategy: # Progressive delivery strategy configuration
canary: # Use canary deployment pattern with traffic shifting
canaryService: checkout-worker-canary # Service routing traffic to canary pods
stableService: checkout-worker-stable # Service routing traffic to stable pods
trafficRouting: # Configure the traffic router for weighted splitting
istio: # Use Istio service mesh for traffic management
virtualServices: # Istio VirtualService for traffic splitting rules
- name: checkout-worker-vsvc # VirtualService name managing traffic weights
routes: # Route definitions within the VirtualService
- primary # Route name to modify during canary progression
steps: # Ordered list of canary progression steps
- setWeight: 10 # Route 10 percent of traffic to the canary
- pause: {duration: 2m} # Wait two minutes to collect initial metrics
- analysis: # Run automated metric analysis at this step
templates: # Reference to AnalysisTemplate resources
- templateName: checkout-worker-success-rate # Template checking HTTP success rate
args: # Arguments passed to the AnalysisTemplate
- name: service-name # Parameter name expected by the template
value: checkout-worker # Actual service name for Prometheus queries
- setWeight: 30 # Increase canary traffic to 30 percent
- pause: {duration: 5m} # Wait five minutes for broader traffic validation
- setWeight: 60 # Increase canary traffic to 60 percent
- pause: {duration: 5m} # Wait five minutes before full promotion
- setWeight: 100 # Promote canary to receive all traffic
---
apiVersion: argoproj.io/v1alpha1 # Argo Rollouts API for analysis templates
kind: AnalysisTemplate # Defines metrics and success criteria for canary validation
metadata:
name: checkout-worker-success-rate # Template name referenced in Rollout steps
namespace: checkout # Same namespace as the Rollout resource
spec:
args: # Template parameters substituted at runtime
- name: service-name # Parameter placeholder for the service being analyzed
metrics: # List of metrics to evaluate during analysis
- name: success-rate # Human readable metric identifier
interval: 60s # Query Prometheus every 60 seconds
count: 5 # Collect five data points before making a decision
successCondition: result[0] >= 0.95 # Pass if success rate is 95 percent or higher
failureLimit: 2 # Allow up to two failed measurements before aborting
provider: # Metrics provider configuration
prometheus: # Use Prometheus as the metrics backend
address: http://prometheus.monitoring:9090 # Prometheus server endpoint
query: | # PromQL query calculating HTTP success rate
sum(rate(http_requests_total{status!~"5.*",service="{{args.service-name}}"}[5m])) / sum(rate(http_requests_total{service="{{args.service-name}}"}[5m])) # Ratio of non-5xx to total requestsInterview Tip
A junior engineer typically conflates Argo Rollouts with ArgoCD, but they are separate projects with separate controllers — interviewers will test this distinction. Explain that Rollouts replaces the Deployment kind with a Rollout CRD and requires a supported traffic router like Istio or Nginx for weighted traffic splitting. Walk through a concrete canary flow: setWeight to ten percent, pause, run AnalysisTemplate querying Prometheus for error rates, promote or abort based on the successCondition threshold. Mention that without a traffic router, weight-based splitting degrades to imprecise replica-ratio splitting. Discuss the AnalysisTemplate pattern and explain why short analysis intervals produce unreliable results due to insufficient data points. Demonstrate you understand blue-green as active and preview Services with instant selector switching.
◈ Architecture Diagram
┌───────────────────────────────────────────────────────┐ │ Argo Rollouts Canary Flow │ │ │ │ ┌────────────┐ ┌────────────┐ │ │ │ Stable │ │ Canary │ │ │ │ RS v3.1.0 │ │ RS v3.2.0 │ │ │ │ (90%) │ │ (10%) │ │ │ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ │ └──────────┬───────────┘ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ Istio VirtualSvc │ │ │ │ (traffic weights) │ │ │ └────────┬────────────┘ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ AnalysisRun │ │ │ │ (Prometheus query) │ │ │ └────────┬────────────┘ │ │ │ │ │ ┌──────┼──────┐ │ │ ↓ ↓ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Success │ │ Failure │ │ │ │ → Promote │ │ → Abort │ │ │ │ → 30%→60% │ │ → Scale to 0│ │ │ │ → 100% │ │ → Rollback │ │ │ └──────────────┘ └──────────────┘ │ └───────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
ArgoCD natively supports Helm charts, Kustomize overlays, and plain YAML directories as manifest sources, automatically detecting the tool based on repository contents and rendering manifests server-side through its Repository Server component before comparing the rendered output against the live cluster state during reconciliation.
Detailed Answer
Think of ArgoCD's manifest rendering like a universal translator at the United Nations. Delegates speak different languages (Helm charts with values, Kustomize with overlays, plain YAML), but the translator converts everything into one common language (rendered Kubernetes manifests) before the assembly (the cluster) acts on them. The translator does not care which language was spoken — it detects the language automatically and produces the same standard output. This means teams across an organization can each use their preferred templating tool while ArgoCD provides a consistent GitOps workflow for all of them.
ArgoCD detects the manifest source type automatically based on the repository contents. If it finds a Chart.yaml file, it treats the source as a Helm chart. If it finds a kustomization.yaml or kustomization.yml file, it uses Kustomize. If neither is present, it treats the directory as plain YAML manifests and applies all YAML and JSON files found in the specified path. You can also explicitly set the source type in the Application spec to override auto-detection. For Helm, you can specify values files, inline value overrides, release name, and chart version. For Kustomize, you can override images, add name prefixes and suffixes, and set common labels and annotations. For plain manifests, ArgoCD supports recursive directory traversal and file include or exclude patterns using the directory.recurse and directory.exclude options.
Internally, the Repository Server is the component responsible for all manifest rendering. When the Application Controller requests the desired state for an application, the Repository Server clones the Git repository (or fetches from a Helm chart repository), caches the result, and invokes the appropriate rendering tool. For Helm, it runs the equivalent of helm template with the configured values, producing raw Kubernetes manifests without installing a release into the cluster — this is a critical distinction because ArgoCD does not use Tiller or the Helm release secret mechanism. For Kustomize, it runs kustomize build on the specified path. The rendered output is then returned to the Application Controller for diffing against the live state. The Repository Server maintains a cache of cloned repositories and rendered manifests with configurable TTL, reducing Git API calls and rendering overhead for frequently polled applications.
In production, organizations commonly use a hybrid approach. Infrastructure components like cert-manager, ingress-nginx, and monitoring stacks are deployed as Helm charts sourced directly from public or private Helm repositories using the helm source type with a repoURL pointing to the chart repository. Application teams use Kustomize with a base-and-overlay pattern where the base directory contains the core Deployment, Service, and ConfigMap definitions, and overlays for staging and production add environment-specific patches like replica counts, resource limits, and ingress hostnames. Some teams use Helm charts as Kustomize bases by running helm template in a CI pipeline and committing the rendered output, giving them the parameterization power of Helm with the patching flexibility of Kustomize. This multi-tool strategy allows each team to use the approach that best fits their workflow while ArgoCD provides a unified deployment and observability layer.
A major gotcha with Helm integration is that ArgoCD runs helm template, not helm install, which means Helm hooks and lookup functions behave differently. The lookup function always returns empty results because there is no live cluster context during template rendering, causing templates that depend on runtime lookups to produce incorrect manifests. Helm release lifecycle hooks like pre-install and post-install are not natively supported — you must use ArgoCD sync hooks instead. Another common pitfall with Kustomize is version mismatches; ArgoCD bundles a specific version of Kustomize, and if your kustomization.yaml uses features from a newer version, rendering will fail silently or produce unexpected output. Teams should pin the Kustomize version in ArgoCD's configmap or use a custom plugin to ensure compatibility. For plain manifests, engineers frequently forget that ArgoCD applies all YAML files in the directory and accidentally include test fixtures or documentation YAML files in the deployment path.
Code Example
# helm-source-app.yaml - Application using Helm chart with value overrides
apiVersion: argoproj.io/v1alpha1 # ArgoCD CRD API version
kind: Application # ArgoCD Application resource
metadata:
name: payments-api-helm # Application name for the Helm-based deployment
namespace: argocd # ArgoCD namespace for Application resources
spec:
project: default # ArgoCD project for RBAC scoping
source:
repoURL: https://github.com/acme-corp/payments-api.git # Git repository containing the Helm chart
targetRevision: main # Branch to track for chart changes
path: charts/payments-api # Path to the Helm chart directory within the repo
helm: # Helm-specific rendering configuration
releaseName: payments-api # Helm release name used in resource naming
valueFiles: # List of values files applied in order
- values.yaml # Base values file with default configuration
- values-production.yaml # Production overrides for replicas and resources
values: | # Inline value overrides taking highest precedence
replicaCount: 5 # Override replica count for production scaling
image: # Container image configuration block
tag: v4.2.1 # Pin specific image tag for this deployment
resources: # Resource allocation overrides
requests: # Minimum guaranteed resources
cpu: 500m # Request 500 millicores of CPU
memory: 512Mi # Request 512 megabytes of memory
destination:
server: https://kubernetes.default.svc # Deploy to the local cluster
namespace: payments # Target namespace for rendered resources
syncPolicy:
automated: # Enable automated GitOps sync
prune: true # Remove resources deleted from the chart
selfHeal: true # Revert manual changes to chart-managed resources
---
# kustomize-source-app.yaml - Application using Kustomize overlays
apiVersion: argoproj.io/v1alpha1 # ArgoCD CRD API version
kind: Application # ArgoCD Application resource
metadata:
name: checkout-worker-kustomize # Application name for Kustomize-based deployment
namespace: argocd # ArgoCD namespace
spec:
project: default # ArgoCD project
source:
repoURL: https://github.com/acme-corp/checkout-worker.git # Git repository with Kustomize structure
targetRevision: main # Track the main branch
path: k8s/overlays/production # Path to the production Kustomize overlay
kustomize: # Kustomize-specific rendering options
images: # Override container images without editing kustomization.yaml
- acme-corp/checkout-worker=acme-corp/checkout-worker:v2.8.0 # Pin image to specific version
namePrefix: prod- # Add prefix to all resource names in this overlay
commonLabels: # Add labels to all resources rendered by Kustomize
env: production # Environment label for filtering and monitoring
team: checkout # Team ownership label for cost allocation
commonAnnotations: # Add annotations to all rendered resources
monitoring.acme.io/enabled: "true" # Enable monitoring sidecar injection
destination:
server: https://kubernetes.default.svc # Deploy to the local cluster
namespace: checkout # Target namespace
syncPolicy:
automated: # Enable automated sync
prune: true # Prune resources removed from overlays
selfHeal: true # Self-heal against manual drift
---
# plain-manifests-app.yaml - Application using plain YAML directory
apiVersion: argoproj.io/v1alpha1 # ArgoCD CRD API version
kind: Application # ArgoCD Application resource
metadata:
name: order-service-plain # Application name for plain manifest deployment
namespace: argocd # ArgoCD namespace
spec:
project: default # ArgoCD project
source:
repoURL: https://github.com/acme-corp/order-service.git # Git repository with plain YAML
targetRevision: main # Track the main branch
path: deploy/production # Directory containing plain Kubernetes YAML files
directory: # Directory-specific rendering options
recurse: true # Recursively include YAML files from subdirectories
exclude: "{test-*,*_test.yaml}" # Exclude test fixture files from deployment
destination:
server: https://kubernetes.default.svc # Deploy to the local cluster
namespace: orders # Target namespace
syncPolicy:
automated: # Enable automated sync for plain manifests
prune: true # Prune resources from deleted YAML files
selfHeal: true # Self-heal manual changesInterview Tip
A junior engineer typically only mentions one tool — usually Helm — without explaining how ArgoCD auto-detects the source type or how rendering works internally. Interviewers value breadth: explain that ArgoCD checks for Chart.yaml (Helm), kustomization.yaml (Kustomize), or falls back to plain YAML, and that all rendering happens in the Repository Server via helm template or kustomize build. Critically, emphasize that ArgoCD uses helm template and not helm install, which means Helm lookup functions return empty and Helm lifecycle hooks do not work — you must use ArgoCD sync hooks instead. Discuss the Kustomize version pinning issue and how rendering failures can be subtle. Mention the directory.exclude option to prevent accidental deployment of test fixtures from plain manifest directories. A strong answer includes a hybrid strategy where infrastructure uses Helm charts and applications use Kustomize overlays.
◈ Architecture Diagram
┌───────────────────────────────────────────────────────────┐ │ ArgoCD Manifest Rendering Pipeline │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Git Repository │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ │ │ Chart. │ │ kustomize. │ │ deploy/ │ │ │ │ │ │ yaml │ │ yaml │ │ *.yaml │ │ │ │ │ │ (Helm) │ │ (Kustomize) │ │ (Plain) │ │ │ │ │ └────┬─────┘ └──────┬───────┘ └───────┬───────┘ │ │ │ └───────┼───────────────┼──────────────────┼───────────┘ │ │ ↓ ↓ ↓ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Repository Server │ │ │ │ │ │ │ │ ┌────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │ │ │ helm │ │ kustomize │ │ yaml file │ │ │ │ │ │ template │ │ build │ │ reader │ │ │ │ │ └─────┬──────┘ └──────┬───────┘ └────────┬────────┘ │ │ │ │ └───────────────┼──────────────────┘ │ │ │ │ ↓ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ Rendered Manifests │ │ │ │ │ │ (standard K8s YAML) │ │ │ │ │ └──────────┬───────────┘ │ │ │ └───────────────────────┼───────────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────┐ │ │ │ Application Ctrl │ │ │ │ (diff + sync) │ │ │ └──────────────────────┘ │ └───────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
ApplicationSet generators automatically create and manage ArgoCD Application resources based on dynamic inputs such as cluster lists, git directory structures, or computed combinations. Matrix and merge generators compose simpler generators to produce complex multi-dimensional deployments, but each type carries distinct risks around uncontrolled Application proliferation, git polling load, and accidental deletion.
Detailed Answer
Think of a mail merge in a word processor. You have one template letter and a spreadsheet of recipients, and the system generates a personalized letter for each row. ApplicationSet generators work the same way: you write one Application template, and the generator produces a concrete ArgoCD Application for every entry it discovers, whether that entry is a cluster name, a git directory, or a computed combination of both.
ApplicationSet exists because manually creating and maintaining hundreds of Application resources across dozens of clusters is operationally unsustainable. The list generator is the simplest: you hardcode a JSON array of clusters and parameters, and each entry produces one Application. The git generator scans a repository for directories or files matching a pattern, so adding a new microservice directory automatically creates its Application. The matrix generator takes two child generators and computes every combination, like deploying every service to every cluster. The merge generator combines outputs from multiple generators by matching on a key field, allowing base parameters to be enriched with cluster-specific overrides.
Internally, the ApplicationSet controller runs inside the argocd-applicationset-controller pod and periodically evaluates each generator. For git generators, it clones or fetches the target repository at a configurable interval, walks the directory tree or parses files, and emits parameter sets. The matrix generator performs a Cartesian product of its child generator outputs. The merge generator performs a left-join on a specified key. Each resulting parameter set is rendered into the Application template, and the controller creates, updates, or deletes Application resources to match the computed set.
At production scale, teams running 200-plus clusters with 50-plus microservices can easily generate 10,000 Applications from a single ApplicationSet using matrix generators. This requires monitoring the ApplicationSet controller's memory consumption, git clone frequency (to avoid hammering the git server), reconciliation loop duration, and Application sync status across the fleet. The progressive sync feature with maxUpdate controls how many Applications sync simultaneously during a rollout, preventing a blast-radius scenario where every cluster updates at once.
The non-obvious gotcha is the delete behavior. By default, when a generator stops producing a parameter set, the corresponding Application is deleted, which triggers a cascade delete of the deployed resources. If a git directory is accidentally renamed or a list entry removed, the ApplicationSet controller can delete production workloads across multiple clusters in a single reconciliation loop. Architects must configure the preserveResourcesOnDeletion policy and use SCM-protected branches to prevent accidental mass deletion from a bad git commit.
Code Example
# ApplicationSet using matrix generator to deploy every service to every cluster
apiVersion: argoproj.io/v1alpha1 # ArgoCD ApplicationSet API version
kind: ApplicationSet # Generates multiple Application resources from a template
metadata:
name: platform-services-all-clusters # Descriptive name covering the scope
namespace: argocd # ApplicationSet controller watches this namespace
spec:
goTemplate: true # Enables Go template syntax for parameter rendering
goTemplateOptions: ['missingkey=error'] # Fails loudly if a parameter is missing
generators:
- matrix: # Computes Cartesian product of two child generators
generators:
- git: # First dimension: discovers services from git directories
repoURL: https://git.company.com/platform/k8s-manifests.git # Central manifests repo
revision: main # Tracks the main branch for production
directories:
- path: services/* # Each subdirectory is a service (payments-api, user-auth, etc.)
- list: # Second dimension: enumerates target clusters
elements:
- cluster: us-east-prod # US East production cluster
url: https://k8s-api.us-east.company.com # Cluster API server endpoint
- cluster: eu-west-prod # EU West production cluster
url: https://k8s-api.eu-west.company.com # Cluster API server endpoint
- cluster: ap-south-prod # Asia Pacific production cluster
url: https://k8s-api.ap-south.company.com # Cluster API server endpoint
strategy: # Controls rollout speed across generated Applications
type: RollingSync # Syncs Applications in controlled batches
rollingSync:
steps:
- matchExpressions: # First wave: canary cluster only
- key: cluster # Matches on the cluster parameter
operator: In # Selects specific values
values: [us-east-prod] # Deploys to US East first
maxUpdate: 2 # Syncs at most two Applications simultaneously
- matchExpressions: # Second wave: remaining clusters
- key: cluster # Matches on the cluster parameter
operator: NotIn # Selects everything else
values: [us-east-prod] # After canary succeeds
template: # Template for each generated Application
metadata:
name: '{{.path.basename}}-{{.cluster}}' # Unique name: service-cluster
spec:
project: platform-services # ArgoCD project for RBAC scoping
source:
repoURL: https://git.company.com/platform/k8s-manifests.git # Same manifests repo
targetRevision: main # Pins to main branch
path: '{{.path.path}}' # Service directory discovered by git generator
destination:
server: '{{.url}}' # Target cluster API endpoint from list generator
namespace: '{{.path.basename}}' # Namespace matches service name
syncPolicy:
automated: # Enables auto-sync for continuous deployment
prune: true # Removes resources no longer in git
selfHeal: true # Reverts manual drift in the cluster
preserveResourcesOnDeletion: true # Prevents accidental resource deletion when generator stops producing entriesInterview Tip
A junior engineer typically answers that ApplicationSets create multiple Applications from a template, but for a senior/architect role, the interviewer is actually looking for generator composition, blast-radius controls, and deletion safety. Explain how the matrix generator performs a Cartesian product, how the merge generator does key-based joins, what happens when a git directory disappears and the default delete policy kicks in, and how progressive sync with maxUpdate prevents fleet-wide outages. Strong answers include preserveResourcesOnDeletion, SCM branch protection, and monitoring the ApplicationSet controller's reconciliation performance at scale.
◈ Architecture Diagram
┌──────────┐
│ Git Gen │
│ services │
└────┬─────┘
│
↓
┌──────────┐
│ Matrix │──→ Cartesian
│ Generator│ Product
└────┬─────┘
│
↓
┌──────────┐
│ List Gen │
│ clusters │
└────┬─────┘
↓
┌──────────┐ ┌──────────┐
│ App: svc │ │ App: svc │
│ @east │ │ @west │
└──────────┘ └──────────┘💬 Comments
Quick Answer
ArgoCD registers external clusters as Kubernetes Secrets with connection credentials, distributes manifest rendering across sharded repo-server replicas, and uses Redis for caching and state sharing. Scaling requires tuning repo-server concurrency, partitioning clusters across application-controller shards, and configuring Redis with sentinel or cluster mode to avoid single-point bottlenecks.
Detailed Answer
Think of an air traffic control center managing flights across multiple airports. Each airport is a separate cluster, the control center is the ArgoCD installation, and the radar screens are the cached views of cluster state. If one radar screen handles all airports, it becomes overwhelmed. Sharding distributes airports across multiple controllers, and a shared communication backbone (Redis) keeps everyone coordinated.
ArgoCD was originally designed to manage applications on the cluster where it runs, but production enterprises routinely manage 50 to 300 clusters from a single ArgoCD instance. Each external cluster is registered as a Kubernetes Secret in the argocd namespace with the label argocd.argoproj.io/secret-type=cluster. This Secret contains the cluster API server URL, a bearer token or client certificate, TLS CA data, and optional configuration like connection limits and namespace restrictions. The application-controller watches these secrets and treats each registered cluster as a sync target.
Internally, three components must scale independently. The application-controller reconciles Application resources by comparing desired state (from git) with live state (from clusters). At scale, a single controller instance becomes CPU-bound when diffing thousands of resources across dozens of clusters. ArgoCD supports sharding the controller so that each shard handles a subset of clusters, assigned by round-robin or explicit annotation. The repo-server clones git repositories, renders Helm charts, runs Kustomize, and returns manifests. Multiple repo-server replicas share the load, and the parallelism limit per server controls concurrent rendering operations. Redis caches rendered manifests, application state, and RBAC data. Without proper Redis availability, every cache miss triggers a full git clone and render cycle.
At production scale with 100-plus clusters, architects must monitor application-controller queue depth, reconciliation duration per cluster, repo-server clone and render times, Redis memory usage and eviction rates, and git server request rates. The controller's ARGOCD_CONTROLLER_REPLICAS environment variable sets shard count, and each replica processes only its assigned clusters. Redis should run in sentinel mode or as a Redis cluster for high availability, because a Redis failure causes all repo-server cache lookups to fall back to git, which can overwhelm the git server and spike reconciliation times by an order of magnitude.
The non-obvious gotcha is cluster Secret rotation. When a service account token or client certificate in a cluster Secret expires, ArgoCD loses access to that cluster silently unless health checks are monitored. Applications appear OutOfSync or Unknown, but the root cause is a credential failure, not a drift issue. Architects should automate Secret rotation with external-secrets-operator or a custom controller and set up alerts on cluster connection errors in the application-controller logs before tokens approach expiry.
Code Example
# Register an external production cluster with ArgoCD CLI
argocd cluster add us-east-prod-context \
--name us-east-prod \
--kubeconfig /etc/kube/us-east-prod.config \
--system-namespace kube-system \
--server https://argocd.platform.company.com
# Inspect the generated cluster Secret
kubectl get secret -n argocd -l argocd.argoproj.io/secret-type=cluster -o name
# Configure application-controller sharding with 3 replicas
# In argocd-cmd-params-cm ConfigMap:
apiVersion: v1 # Standard Kubernetes ConfigMap
kind: ConfigMap # ArgoCD reads parameters from this ConfigMap
metadata:
name: argocd-cmd-params-cm # ArgoCD-specific configuration name
namespace: argocd # ArgoCD installation namespace
data:
controller.replicas: "3" # Runs three application-controller shards
controller.sharding.algorithm: round-robin # Distributes clusters evenly across shards
reposerver.parallelism.limit: "5" # Each repo-server handles five concurrent render operations
server.repo.server.timeout.seconds: "180" # Allows three minutes for large Helm chart rendering
# Scale the repo-server to handle rendering load from 100+ clusters
kubectl scale deployment argocd-repo-server -n argocd --replicas=5
# Check cluster connection health from the ArgoCD CLI
argocd cluster list --server https://argocd.platform.company.com
# Monitor application-controller shard assignment
kubectl get applications -n argocd -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.controllerNamespace}{"\n"}{end}'
# Redis sentinel configuration in argocd-redis-ha StatefulSet
# Ensures cache availability when a Redis primary fails
apiVersion: apps/v1 # StatefulSet for stable Redis pod identity
kind: StatefulSet # Maintains ordered pod creation for Redis HA
metadata:
name: argocd-redis-ha # High-availability Redis for ArgoCD
namespace: argocd # Same namespace as ArgoCD components
spec:
replicas: 3 # Three Redis nodes for sentinel quorum
serviceName: argocd-redis-ha # Headless service for peer discovery
selector:
matchLabels:
app: argocd-redis-ha # Selector for Redis HA pods
template:
metadata:
labels:
app: argocd-redis-ha # Label matching the StatefulSet selector
spec:
containers:
- name: redis # Redis server container
image: redis:7.2-alpine # Lightweight Redis image
ports:
- containerPort: 6379 # Redis default port
- containerPort: 26379 # Redis sentinel port for failover coordinationInterview Tip
A junior engineer typically answers that you add clusters with argocd cluster add, but for a senior/architect role, the interviewer is actually looking for control-plane scaling architecture. Explain how the application-controller shards work, why Redis availability directly affects reconciliation performance, how repo-server parallelism limits prevent git server overload, and what happens when cluster Secrets expire silently. A mature answer also covers how to monitor shard assignment balance, reconciliation queue depth per shard, and why the git server itself often becomes the true bottleneck before any ArgoCD component does.
◈ Architecture Diagram
┌──────────────────────────────┐
│ ArgoCD Control Plane │
│ ┌────────┐ ┌──────────────┐ │
│ │Ctrl S1 │ │ Ctrl S2 │ │
│ │Clust1-5│ │ Clust6-10 │ │
│ └────┬───┘ └──────┬───────┘ │
│ ↓ ↓ │
│ ┌──────────────────┐ │
│ │ Redis (HA) │ │
│ └──────────────────┘ │
│ ↓ │
│ ┌────────┐ ┌────────┐ │
│ │RepoSrv1│ │RepoSrv2│ │
│ └────────┘ └────────┘ │
└──────────────────────────────┘
↓ ↓
┌──────────┐ ┌──────────┐
│Cluster 1 │ │Cluster N │
└──────────┘ └──────────┘💬 Comments
Quick Answer
Sync waves assign integer ordering to resources so ArgoCD syncs lower-numbered waves first, waiting for each wave's resources to become healthy before proceeding. Hooks like PreSync, Sync, and PostSync run Jobs or Pods at specific lifecycle points. A hook failure in any wave halts the entire sync operation, leaving later waves unprocessed and requiring manual intervention or retry.
Detailed Answer
Think of a theater production with scene changes. The stagehands must set up the backdrop (wave 0) before the props (wave 1) before the actors enter (wave 2). If the backdrop collapses during setup, the director stops everything rather than sending actors onto a broken stage. ArgoCD sync waves work the same way: they enforce ordering so that infrastructure dependencies are ready before the services that depend on them.
Sync waves and hooks solve a fundamental problem in GitOps: not all resources should be applied simultaneously. A database migration must complete before the application deployment that depends on the new schema. A ConfigMap or Secret must exist before the Deployment that mounts it. A namespace must be created before resources are placed in it. Without ordering, ArgoCD would apply everything at once, leading to race conditions and transient failures.
Internally, ArgoCD processes sync waves by sorting all resources by their argocd.argoproj.io/sync-wave annotation (defaulting to 0). Within each wave, it applies resources and waits for them to reach a healthy state according to their resource health check. Hooks are special resources annotated with argocd.argoproj.io/hook, which can be PreSync (runs before any wave), Sync (runs during the sync phase), PostSync (runs after all waves complete), or SyncFail (runs when sync fails). Hook resources are typically Jobs or Pods. The hook-delete-policy annotation controls whether hooks are deleted before creation, after successful completion, or after failure.
At production scale, teams use wave 0 for namespaces and CRDs, wave 1 for ConfigMaps and Secrets, wave 2 for databases and stateful services, wave 3 for application Deployments, and wave 4 for ingress routes and monitoring. PreSync hooks commonly run database migrations, schema validators, or pre-deployment smoke tests. PostSync hooks run integration tests, cache warmers, or notification jobs. Monitoring should track sync duration per wave, hook execution time, hook pod resource consumption, and retry counts.
The non-obvious gotcha is partial sync state after a hook failure. If a PreSync migration Job fails at wave 1, ArgoCD stops the sync operation entirely. Wave 2 and beyond are never attempted, but wave 0 resources are already applied. The Application is now in a partially synced state that does not match either the previous git commit or the current one. Retrying the sync re-runs all hooks from the beginning, which means PreSync hooks must be idempotent. If a database migration is not idempotent and partially applied, retrying the sync can corrupt data. Architects must design every hook to be safely re-runnable and implement SyncFail hooks for cleanup or rollback notifications.
Code Example
# Deployment using sync waves and hooks for ordered rollout of payments platform
# Wave -1: PreSync hook runs database migration before any resources sync
apiVersion: batch/v1 # Kubernetes Job API for one-time execution
kind: Job # Runs the migration to completion then stops
metadata:
name: payments-db-migrate # Descriptive name for the migration Job
namespace: payments # Same namespace as the application
annotations:
argocd.argoproj.io/hook: PreSync # Runs before any sync wave begins
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation # Deletes previous Job before creating new one
argocd.argoproj.io/sync-wave: "-1" # Ensures migration runs before other PreSync resources
spec:
backoffLimit: 2 # Retries the migration twice on failure before marking failed
activeDeadlineSeconds: 300 # Kills the migration if it runs longer than five minutes
template:
spec:
restartPolicy: Never # Jobs should not restart; ArgoCD handles retry logic
containers:
- name: migrate # Container running the migration tool
image: registry.company.com/payments-db-migrator:3.7.2 # Versioned migration image
command: ['./migrate', '--direction=up', '--env=production'] # Applies pending migrations
env:
- name: DATABASE_URL # Connection string for the payments database
valueFrom:
secretKeyRef:
name: payments-db-credentials # Secret containing database connection details
key: url # Key within the Secret holding the full connection string
---
# Wave 0: ConfigMaps and Secrets that Deployments depend on
apiVersion: v1 # Core Kubernetes API
kind: ConfigMap # Application configuration deployed before the service
metadata:
name: payments-api-config # Configuration for the payments API
namespace: payments # Matches the application namespace
annotations:
argocd.argoproj.io/sync-wave: "0" # Deploys in wave 0 after PreSync hooks
data:
MAX_CONNECTIONS: "50" # Database connection pool size for production
CACHE_TTL: "300" # Cache time-to-live in seconds for payment lookups
---
# Wave 1: Main application Deployment depends on ConfigMap from wave 0
apiVersion: apps/v1 # Deployment API for rolling updates
kind: Deployment # Long-running service managed by the ReplicaSet controller
metadata:
name: payments-api # Primary payments API service
namespace: payments # Application namespace
annotations:
argocd.argoproj.io/sync-wave: "1" # Deploys after ConfigMaps are ready
spec:
replicas: 5 # Five replicas for production traffic handling
selector:
matchLabels:
app: payments-api # Connects Deployment to its Pods
template:
metadata:
labels:
app: payments-api # Pod label for Service discovery and monitoring
spec:
containers:
- name: api # Main API container
image: registry.company.com/payments-api:4.12.0 # Production release image
ports:
- containerPort: 8080 # HTTP API port
envFrom:
- configMapRef:
name: payments-api-config # Loads all keys from the wave-0 ConfigMap
---
# Wave 2: PostSync hook runs integration tests after all resources are healthy
apiVersion: batch/v1 # Job API for test execution
kind: Job # One-time test run
metadata:
name: payments-api-smoke-test # Post-deployment verification
namespace: payments # Same namespace as the tested service
annotations:
argocd.argoproj.io/hook: PostSync # Runs after all sync waves complete successfully
argocd.argoproj.io/hook-delete-policy: HookSucceeded # Cleans up Job after success
spec:
backoffLimit: 1 # Allows one retry for transient network issues
template:
spec:
restartPolicy: Never # Let ArgoCD handle retry decisions
containers:
- name: smoke-test # Container running integration tests
image: registry.company.com/payments-smoke-tests:2.1.0 # Test suite image
command: ['./run-tests', '--endpoint=http://payments-api:8080', '--suite=critical'] # Tests critical payment flowsInterview Tip
A junior engineer typically answers that sync waves order resource creation, but for a senior/architect role, the interviewer is actually looking for failure handling and idempotency design. Explain how ArgoCD halts at a failed wave, what partial sync state looks like, why hooks must be idempotent for safe retry, and how hook-delete-policy affects re-execution. A mature answer describes the production pattern of PreSync for migrations, Sync for application resources, PostSync for validation, and SyncFail for alerting. Bonus points for mentioning that long-running hooks block the entire sync and consume cluster resources until activeDeadlineSeconds terminates them.
◈ Architecture Diagram
┌──────────┐
│ PreSync │
│ Migrate │
└────┬─────┘
↓
┌──────────┐
│ Wave 0 │
│ ConfigMap│
└────┬─────┘
↓
┌──────────┐
│ Wave 1 │
│ Deploy │
└────┬─────┘
↓
┌──────────┐
│ PostSync │
│ Tests │
└──────────┘💬 Comments
Quick Answer
ArgoCD disaster recovery leverages git as the authoritative source by exporting Application definitions, AppProjects, and cluster Secrets to a backup repository. Recovery involves reinstalling ArgoCD, restoring these declarative resources, and letting ArgoCD reconcile desired state from git. This avoids complex etcd restore procedures because the actual workload state is defined in git, not in ArgoCD's internal database.
Detailed Answer
Think of rebuilding a house after a fire. If you have the blueprints (git) and the building permits (Application definitions), you can reconstruct everything without needing the original furniture (ArgoCD's internal state). The key insight is that ArgoCD is itself a reconciliation engine, so its own configuration should follow the same GitOps principles it enforces on workloads.
ArgoCD's disaster recovery philosophy is grounded in the principle that git contains the truth about what should be deployed. The ArgoCD control plane stores Application resources, AppProject definitions, repository credentials, cluster connection Secrets, and RBAC policies. If the ArgoCD installation is lost, these resources are what need to be reconstructed. The actual workload manifests, Helm charts, and Kustomize overlays remain safely in git and do not need separate backup.
The recommended backup strategy has three layers. First, all ArgoCD Application and AppProject resources should themselves be managed via GitOps, typically using an app-of-apps pattern or ApplicationSets stored in a dedicated bootstrap repository. Second, Secrets containing cluster credentials, repository SSH keys, and OIDC configurations should be backed up to a secure store like Vault or AWS Secrets Manager, since these cannot be safely stored in git. Third, the argocd-export command produces a YAML dump of all ArgoCD resources from its internal state, which serves as a point-in-time snapshot for comparison and audit.
During recovery, the team installs a fresh ArgoCD instance, restores Secrets from the secure store, applies the bootstrap repository containing Application and AppProject definitions, and ArgoCD automatically begins reconciling all managed workloads from git. The first sync cycle may take significant time as ArgoCD clones all repositories, renders manifests, and diffs against live cluster state, but no workload changes are applied because the clusters already contain the correct resources. Monitoring during recovery should track sync status, repository clone success rates, and cluster connection health.
The non-obvious gotcha is that argocd-export does not capture everything. Custom resource health checks defined in argocd-cm ConfigMap, notification templates in argocd-notifications-cm, and RBAC policies in argocd-rbac-cm are ConfigMaps that must be version-controlled separately. Teams that rely solely on argocd-export discover during recovery that their custom health assessments, notification routing, and RBAC rules are missing, leaving them with a functional but misconfigured ArgoCD instance that approves resources it should block or fails to notify teams about sync failures.
Code Example
# Export all ArgoCD resources for point-in-time backup
argocd admin export -n argocd > argocd-backup-2026-06-20.yaml
# Store the backup in a version-controlled disaster recovery repository
cp argocd-backup-2026-06-20.yaml /opt/dr-repo/argocd/
cd /opt/dr-repo && git add argocd/ && git commit -m 'ArgoCD backup 2026-06-20'
# Bootstrap Application that manages all other Applications (app-of-apps)
apiVersion: argoproj.io/v1alpha1 # ArgoCD Application API
kind: Application # Top-level bootstrap Application
metadata:
name: platform-bootstrap # The root Application that creates all others
namespace: argocd # Must be in the ArgoCD namespace
spec:
project: default # Uses the default project for bootstrap
source:
repoURL: https://git.company.com/platform/argocd-bootstrap.git # DR-safe bootstrap repo
targetRevision: main # Tracks the main branch
path: applications/ # Directory containing all Application YAML files
destination:
server: https://kubernetes.default.svc # Applies to the local cluster (ArgoCD's own cluster)
namespace: argocd # Creates Application resources in the argocd namespace
syncPolicy:
automated: # Bootstrap auto-syncs to restore all Applications
prune: true # Removes Applications deleted from the bootstrap repo
selfHeal: true # Re-creates Applications if manually deleted
---
# Recovery procedure script after ArgoCD control-plane loss
# Step 1: Install fresh ArgoCD
kubectl create namespace argocd # Create the ArgoCD namespace on the recovery cluster
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.14.0/manifests/ha/install.yaml # Install HA ArgoCD
# Step 2: Restore Secrets from Vault (cluster creds, repo keys, OIDC config)
vault kv get -format=json secret/argocd/cluster-secrets | jq -r '.data.data.manifests' | kubectl apply -n argocd -f -
# Step 3: Restore ConfigMaps with custom health checks, RBAC, and notifications
kubectl apply -n argocd -f /opt/dr-repo/argocd/configmaps/argocd-cm.yaml # Custom resource health checks
kubectl apply -n argocd -f /opt/dr-repo/argocd/configmaps/argocd-rbac-cm.yaml # RBAC policies
kubectl apply -n argocd -f /opt/dr-repo/argocd/configmaps/argocd-notifications-cm.yaml # Notification templates
# Step 4: Apply the bootstrap Application to trigger full reconciliation
kubectl apply -n argocd -f /opt/dr-repo/argocd/applications/platform-bootstrap.yaml
# Step 5: Monitor recovery progress across all Applications
argocd app list --server https://argocd.platform.company.com -o wide | grep -v SyncedInterview Tip
A junior engineer typically answers that you back up etcd to recover ArgoCD, but for a senior/architect role, the interviewer is actually looking for GitOps-native recovery design. Explain why git already contains workload truth, why Application and AppProject definitions should themselves be GitOps-managed via app-of-apps or ApplicationSets, where Secrets and ConfigMaps fit in the recovery plan since they cannot live in git, and what argocd-export does and does not capture. A strong answer describes the three-layer backup strategy (git for manifests, Vault for secrets, export for audit) and explains why the first post-recovery sync is safe because clusters already contain the correct resources.
◈ Architecture Diagram
┌──────────┐
│ Git Repo │
│ (Truth) │
└────┬─────┘
↓
┌──────────┐
│ Bootstrap│
│ App-of- │
│ Apps │
└────┬─────┘
↓
┌──────────┐ ┌──────────┐
│ ArgoCD │←────│ Vault │
│ (fresh) │ │ (Secrets)│
└────┬─────┘ └──────────┘
↓
┌──────────┐
│ Clusters │
│ (intact) │
└──────────┘💬 Comments
Quick Answer
ArgoCD uses a centralized control plane with a rich UI, built-in RBAC with OIDC integration, and a push-based sync model triggered by webhooks or polling. Flux uses a decentralized per-cluster agent model with pull-based reconciliation from source controllers, minimal UI (relying on third-party dashboards), and Kubernetes-native RBAC. The choice depends on organizational structure, multi-tenancy requirements, and operational maturity.
Detailed Answer
Think of two different approaches to managing a fleet of retail stores. ArgoCD is like a central headquarters with a control room, video feeds from every store, and managers who issue directives from HQ. Flux is like giving each store manager an autonomous playbook and letting them follow it independently, reporting status back when asked. Both keep stores running correctly, but the management overhead, visibility, and failure patterns differ fundamentally.
ArgoCD and Flux are both CNCF graduated GitOps tools, but they make different architectural choices. ArgoCD centralizes control: a single ArgoCD installation can manage hundreds of clusters through registered cluster connections, and operators see everything in a unified web UI with role-based access control, SSO integration, and audit logging. Flux decentralizes control: each cluster runs its own set of Flux controllers (source-controller, kustomize-controller, helm-controller, notification-controller), and each cluster independently reconciles from git without needing a central management plane.
The reconciliation models differ in important ways. ArgoCD's application-controller actively compares live cluster state against rendered manifests and reports drift. Sync can be automatic or manual, and webhooks from git providers can trigger immediate reconciliation. Flux's source-controller polls git repositories or OCI registries at a configurable interval, downloads the latest artifacts, and the kustomize-controller or helm-controller applies them. Flux's model is strictly pull-based: the cluster reaches out to git, not the other way around. This means Flux clusters behind restrictive firewalls need only egress access to git, while ArgoCD needs both egress to git and ingress credentials to target clusters.
At production scale, the UI and RBAC differences become decisive. ArgoCD provides a full web dashboard showing application topology, sync status, resource health, logs, and diff views. Its RBAC system supports project-based access control with OIDC/SAML integration, allowing platform teams to grant developers read-only access to specific projects. Flux has no built-in UI; teams use Weave GitOps (now a commercial product), Capacitor, or custom dashboards. Flux's RBAC relies on Kubernetes-native RBAC applied to the service accounts used by Flux controllers, which is simpler but less granular for multi-tenant developer self-service.
The non-obvious gotcha is that neither tool is universally superior. ArgoCD's centralized model creates a single point of failure and a scaling challenge: the control plane must handle all clusters, and an ArgoCD outage blocks deployments everywhere. Flux's decentralized model means no single point of failure for deployments, but troubleshooting requires accessing each cluster individually, and ensuring consistent configuration across hundreds of Flux installations requires its own automation layer (often Terraform or Crossplane). Architects should choose ArgoCD when developer self-service UI, centralized audit logging, and granular RBAC are requirements, and Flux when cluster autonomy, firewall restrictions, or minimal central infrastructure are priorities.
Code Example
# ArgoCD: Create an Application with CLI showing centralized management
argocd app create payments-api \
--repo https://git.company.com/platform/payments.git \
--path deploy/production \
--dest-server https://k8s-api.us-east.company.com \
--dest-namespace payments \
--project platform-services \
--sync-policy automated \
--self-heal \
--auto-prune
# ArgoCD: Check sync status from the central control plane
argocd app get payments-api --refresh
# ArgoCD: RBAC policy granting dev team read-only access to their project
# In argocd-rbac-cm ConfigMap:
# policy.csv: |
# p, role:payments-dev, applications, get, platform-services/payments-*, allow
# p, role:payments-dev, applications, sync, platform-services/payments-*, deny
# g, payments-team, role:payments-dev
# Flux equivalent: GitRepository and Kustomization per cluster (decentralized)
apiVersion: source.toolkit.fluxcd.io/v1 # Flux source controller API
kind: GitRepository # Tells Flux where to pull manifests from
metadata:
name: payments-repo # Source name referenced by Kustomizations
namespace: flux-system # Flux controllers watch this namespace
spec:
interval: 5m # Polls git every five minutes for changes
url: https://git.company.com/platform/payments.git # Same repository as ArgoCD
ref:
branch: main # Tracks main branch for production
secretRef:
name: git-credentials # Kubernetes Secret containing git access token
---
apiVersion: kustomize.toolkit.fluxcd.io/v1 # Flux kustomize controller API
kind: Kustomization # Applies manifests from the GitRepository source
metadata:
name: payments-api # Application-level reconciliation unit
namespace: flux-system # Flux controllers namespace
spec:
interval: 10m # Re-applies every ten minutes to correct drift
sourceRef:
kind: GitRepository # References the source defined above
name: payments-repo # Links to the git repository source
path: ./deploy/production # Directory within the repo containing manifests
targetNamespace: payments # Overrides namespace for all resources
prune: true # Removes resources deleted from git (equivalent to ArgoCD auto-prune)
healthChecks: # Waits for resources to become healthy after apply
- apiVersion: apps/v1 # Checks Deployment health
kind: Deployment # Watches the payments-api Deployment
name: payments-api # Specific resource to health-check
namespace: payments # Namespace of the health-checked resource
timeout: 5m # Marks unhealthy if resources are not ready in five minutesInterview Tip
A junior engineer typically answers that ArgoCD has a UI and Flux does not, but for a senior/architect role, the interviewer is actually looking for architectural reasoning about centralized vs decentralized reconciliation. Explain the security implications of pull vs push (Flux clusters need only egress; ArgoCD needs cluster credentials), the operational cost of maintaining a central ArgoCD control plane vs hundreds of independent Flux installations, how RBAC models differ (ArgoCD's project-based OIDC vs Flux's Kubernetes-native service account RBAC), and why neither is universally better. Strong answers map organizational structure to tool choice: ArgoCD for platform teams offering developer self-service, Flux for autonomous teams with strong Kubernetes RBAC maturity.
◈ Architecture Diagram
┌──────────────────────────┐ │ ArgoCD Model │ │ ┌──────────┐ │ │ │ Central │──→ Cluster1 │ │ │ Ctrl+UI │──→ Cluster2 │ │ └──────────┘──→ Cluster3 │ └──────────────────────────┘ ┌──────────────────────────┐ │ Flux Model │ │ ┌────────┐ ┌────────┐ │ │ │Flux C1 │ │Flux C2 │ │ │ │Pull←Git│ │Pull←Git│ │ │ └────────┘ └────────┘ │ └──────────────────────────┘
💬 Comments
Quick Answer
Common causes: resource hooks, server-side field defaults, helm template differences, last-applied-annotation drift, or CRD schema changes. Debug with `argocd app diff` and check for ignored differences.
Detailed Answer
1. Server-side defaults: Kubernetes API server adds default fields (e.g., strategy.rollingUpdate, resources: {}, terminationGracePeriodSeconds: 30) that don't exist in your Git manifests. ArgoCD sees these as differences.
2. Helm template non-determinism: Helm functions like randAlphaNum, now, or lookup produce different output each time, causing perpetual drift.
3. Mutating admission webhooks: Webhooks (Istio sidecar injector, Vault agent injector) modify resources after apply, adding fields not in Git.
4. Resource hooks: ArgoCD resource hooks (argocd.argoproj.io/hook) may leave resources in unexpected states.
5. CRD schema changes: CRD upgrade changes field defaults or removes deprecated fields.
6. Last-applied-configuration annotation: kubectl apply sets this annotation; ArgoCD uses server-side diff, so the annotation itself can cause drift.
1. argocd app diff my-app --local ./manifests — shows exact diff 2. Check ArgoCD UI diff view — highlights which fields differ 3. kubectl get <resource> -o yaml vs your Git manifest — compare manually 4. Check for mutating webhooks: kubectl get mutatingwebhookconfigurations
1. Ignore known diffs in ArgoCD Application spec: ``yaml spec: ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/template/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration ``
2. Use server-side apply: argocd app set my-app --server-side-diff=true (ArgoCD 2.10+) 3. Pin Helm values explicitly: Don't rely on Helm chart defaults 4. Normalize manifests: Use argocd app set --self-heal to auto-sync
Code Example
# Debug: see exact diff
argocd app diff checkout-service
# Ignore known diffs in Application spec
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Managed by HPA
- group: ""
kind: Service
jqPathExpressions:
- .spec.clusterIP # Assigned by k8s
# Enable server-side diff (ArgoCD 2.10+)
argocd app set checkout-service --server-side-diff=true
# Check mutating webhooks
kubectl get mutatingwebhookconfigurations -o nameInterview Tip
This is a very common ArgoCD interview question because it happens constantly in production. Mention server-side defaults and mutating webhooks as the top two causes. The ignoreDifferences config and server-side diff are the production solutions.
💬 Comments
Quick Answer
Drift is any difference between the live state of resources in the cluster and the desired state defined in Git. Argo CD detects drift continuously and can respond in three ways depending on configuration: report it as OutOfSync and wait for a manual sync, automatically sync it back to match Git (auto-sync), or automatically sync AND revert any manual changes that weren't made through Git (auto-sync with self-heal).
Detailed Answer
Argo CD's core reconciliation loop continuously compares the live manifests in the cluster against what's rendered from the target Git revision, and any difference is drift — this could come from someone running kubectl edit directly against a resource, another controller mutating a field Argo CD also manages, or simply Git having moved forward since the last sync.
By default (manual sync), Argo CD only reports drift as an 'OutOfSync' status in the UI/CLI and does nothing further until an operator explicitly runs argocd app sync. With automated sync policy enabled, Argo CD will automatically apply a sync whenever it detects the live state differs from Git — but note this only triggers when Argo CD's own reconciliation notices drift on its polling interval, not instantaneously. Adding selfHeal: true to the automated sync policy makes Argo CD actively revert manual changes back to the Git-defined state as soon as it detects them, treating any out-of-band kubectl edit as something to be corrected rather than tolerated — this is what makes Git genuinely the single source of truth rather than just a convenient deployment starting point.
A common gotcha: without selfHeal, a well-meaning but manual kubectl scale during an incident will silently persist until the next sync (which might reset it, surprising the responder), and with selfHeal it might get reverted almost immediately, which is exactly the desired GitOps behavior but can confuse an engineer who doesn't realize self-healing is enabled and expects their manual fix to stick.
Code Example
syncPolicy:
automated:
prune: true
selfHeal: trueInterview Tip
Distinguish the three levels precisely — report only, auto-sync-on-drift, auto-sync-with-selfHeal — since interviewers often use this to check whether you understand selfHeal isn't just 'automated sync turned up.'
💬 Comments
Quick Answer
App of Apps is a pattern where a single 'parent' Argo CD Application's manifests are themselves just a set of other Argo CD Application resources — syncing the parent causes Argo CD to create all the child Applications, which then independently sync their own actual workloads. It solves the problem of bootstrapping and managing many related Applications (e.g. one per microservice, or one per cluster/environment) as a single Git-tracked unit instead of manually creating each Application by hand.
Detailed Answer
An ordinary Argo CD Application points at a Git path containing Kubernetes manifests for a real workload (Deployments, Services, etc.). In App of Apps, the parent Application's Git path instead contains a set of Argo CD Application custom resource manifests themselves — so when Argo CD syncs the parent, what it 'applies' is the creation of several child Application objects in the argocd namespace, and each of THOSE then independently reconciles against its own target Git path/cluster/namespace, syncing the actual workload manifests.
This is commonly combined with ApplicationSets for even more dynamic generation (e.g. templating one child Application per entry in a list of clusters or a Git directory structure), but the basic App of Apps pattern alone already solves a real operational problem: onboarding a new microservice or a new cluster becomes 'add one more Application manifest to the parent's Git repo' rather than manually running argocd app create by hand for every new service, which doesn't scale past a handful of applications and isn't itself tracked in Git.
A nuance worth knowing: because each child Application syncs independently once created, ordering/dependencies between them (e.g. a shared CRD needing to exist before a dependent Application's resources can be applied) require explicit handling — commonly via sync waves (argocd.argoproj.io/sync-wave annotations) rather than assuming the App of Apps parent enforces any particular child ordering by default.
Interview Tip
Mention sync waves specifically when discussing ordering — that's the mechanism that resolves the natural follow-up question 'but what if child A depends on child B existing first?'
💬 Comments
Quick Answer
Start with `argocd app diff` to see exactly what Argo CD believes differs between live and desired state, then check for common causes in order: a controller or admission webhook mutating fields after Argo CD applies them (causing perpetual drift), a Helm/Kustomize rendering difference between what's committed and what's actually generated, RBAC preventing Argo CD's service account from reading or applying specific resources, or a resource that's intentionally excluded/ignored via diff customizations that are misconfigured.
Detailed Answer
argocd app get <app> and argocd app diff <app> are the first stops — diff shows a live-vs-target manifest comparison so you can see exactly which fields Argo CD considers out of sync, rather than guessing from the UI's summary badge alone.
Common root causes, roughly in likelihood order for a persistent/confusing OutOfSync: (1) a mutating admission webhook or another controller (e.g. a HorizontalPodAutoscaler adjusting replica count, or a service mesh sidecar injector adding fields) modifies the resource after Argo CD applies it, and Argo CD's next reconciliation sees that as drift from Git — the fix is usually adding an ignoreDifferences rule for that specific field path in the Application spec, rather than fighting the other controller; (2) templating differences — what helm template or kustomize build renders locally doesn't exactly match what Argo CD renders server-side (different chart/kustomize version, different values precedence), which argocd app diff combined with running the same render command locally can confirm; (3) RBAC — the Argo CD Application controller's service account lacking permission on a specific resource type/namespace will cause sync failures on just those resources, visible in the Application's conditions/events rather than the diff itself; (4) resources explicitly excluded via syncOptions or the Application's ignoreDifferences block that were configured for a different reason and are now masking something real.
The systematic approach: reproduce the render locally (helm template/kustomize build) to rule out a templating mismatch first, since that's usually fastest to confirm or rule out, then check argocd app diff for the specific field-level differences, then check RBAC/events only if the diff shows a resource that never even got attempted.
Code Example
argocd app diff my-app argocd app get my-app -o wide helm template ./chart -f values.yaml # compare against what's actually rendered
Interview Tip
Naming 'another controller mutating fields after Argo CD applies them' as the top cause (versus assuming Argo CD itself is buggy) is the answer that signals real production troubleshooting experience.
💬 Comments
Quick Answer
Without prune, Argo CD only creates/updates resources that are present in Git — if a resource is removed from Git, Argo CD leaves the corresponding live resource untouched in the cluster (orphaned). With prune enabled, Argo CD actively deletes live resources that are no longer present in the target Git revision, which keeps the cluster clean but means a mistaken deletion in Git (or a bad merge) can delete real cluster resources automatically on the next sync.
Detailed Answer
Argo CD's default behavior (prune: false, or not set) is conservative: it applies whatever's in Git, but if a manifest is removed from the tracked Git path, Argo CD doesn't infer that means 'delete this from the cluster too' — the corresponding resource is simply no longer managed/tracked but stays running, becoming what's often called an orphaned resource. This avoids accidental deletions but means cluster cleanliness degrades over time unless someone manually cleans up orphaned resources.
prune: true in the sync policy makes Argo CD's reconciliation symmetric: anything present in the live cluster that Argo CD manages but that's NOT present in the current target Git revision gets deleted on the next sync. This keeps the cluster's actual state exactly matching Git, which is the more 'complete' GitOps promise, but it also means a mistake in Git — someone accidentally deleting a manifest file, a bad rebase/merge that drops a resource, a misconfigured Kustomize overlay that stops including something — now automatically deletes real running resources on the very next automated sync, potentially including things like PersistentVolumeClaims if not explicitly protected.
Because of this, teams often combine prune with additional safety nets: PrunePropagationPolicy to control cascade behavior, explicit preserveResourcesOnDeletion for stateful resources, requiring manual sync (not fully automated) for particularly sensitive Applications even if prune is enabled, and strong Git branch protection/review requirements specifically because a bad merge is now a production deletion risk, not just a drift risk.
Interview Tip
Point out that prune turns 'a bad Git merge' into 'a production deletion' — that's the concrete risk framing that shows you understand why teams are cautious about enabling it everywhere.
💬 Comments
Quick Answer
Argo CD itself doesn't solve secret encryption — the common patterns layer a separate tool underneath: Sealed Secrets (encrypt secrets client-side so only the cluster's controller can decrypt them, safe to commit encrypted), or an external secrets operator (like External Secrets Operator) that syncs real secret values from a vault/KMS into Kubernetes Secrets at runtime, with Argo CD only ever tracking the reference/policy, not the actual secret value, in Git.
Detailed Answer
GitOps's core premise — Git as the single source of truth, fully readable and diffable — is fundamentally in tension with secrets, which must never be plaintext-readable in Git. Argo CD doesn't natively solve this; it just applies whatever manifests are in Git, so teams need a complementary mechanism.
Sealed Secrets (Bitnami/community project) lets you encrypt a Secret client-side using a public key, producing a SealedSecret custom resource that's safe to commit to Git in plaintext-looking-but-actually-encrypted form; a cluster-side controller holding the matching private key decrypts it into a real Secret only inside the cluster. Argo CD just syncs the SealedSecret manifest like anything else — the encryption/decryption is transparent to it.
External Secrets Operator (and similar tools) take a different approach: Git only contains a reference/policy manifest (which secret path in Vault/AWS Secrets Manager/etc. to pull, and which Kubernetes Secret name to populate), and a controller running in-cluster fetches the actual secret value from the external secret store and creates/updates the real Kubernetes Secret — Argo CD tracks and syncs the reference manifest, never the actual secret value, which never touches Git at all.
Both patterns keep Argo CD's job unchanged (sync manifests from Git) while solving the secret-material problem underneath it; the choice between them usually comes down to whether the org already has a central secrets manager (favoring External Secrets Operator) or wants a simpler, self-contained encrypt-and-commit workflow (favoring Sealed Secrets).
Interview Tip
Be explicit that Argo CD doesn't solve this itself — naming the two concrete patterns (Sealed Secrets vs. External Secrets Operator) and the distinction between them is what the question is really testing.
💬 Comments
Quick Answer
GitOps makes Git the source of truth; Argo CD continuously reconciles the cluster to match the manifests in a repo.
Detailed Answer
Instead of pushing changes with kubectl from CI, you commit desired state to Git and Argo CD pulls and applies it, detecting and optionally correcting drift. Benefits: every change is reviewed and auditable, rollback is git revert, and the cluster self-documents. It's declarative and continuous rather than imperative and one-shot.
Interview Tip
Define GitOps precisely: declarative, versioned, pulled, continuously reconciled.
💬 Comments
Quick Answer
An Application CR maps a repo path (source) to a cluster/namespace (destination) and defines how to sync them.
Detailed Answer
It specifies repoURL, targetRevision, and path for the source, the destination cluster and namespace, and a syncPolicy. Argo CD renders the manifests (plain YAML, Kustomize, or Helm) and keeps the destination matching. ApplicationSets can generate many Applications automatically.
Code Example
spec:
source:
repoURL: https://github.com/acme/deploy
path: overlays/prod
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: prodInterview Tip
Know the three source fields: repoURL, targetRevision, path.
💬 Comments
Quick Answer
Sync status says whether the cluster matches Git (Synced/OutOfSync); health status says whether resources actually work (Healthy/Degraded/Progressing/Missing).
Detailed Answer
The two are independent: an app can be Synced but Degraded (matches Git but pods crashloop) or OutOfSync but Healthy (someone changed the cluster manually). Argo CD surfaces both per resource in a live tree, which is key to diagnosing incidents.
Interview Tip
Give an example of each independent combination.
💬 Comments
Quick Answer
prune deletes resources removed from Git; selfHeal reverts manual cluster changes back to Git.
Detailed Answer
Automated sync applies Git changes without a manual click. prune: true removes orphans when you delete a manifest — without it, deleted resources linger. selfHeal: true undoes drift like a manual kubectl scale within seconds, enforcing that Git always wins. Together they make the cluster fully declarative.
Code Example
syncPolicy:
automated:
prune: true
selfHeal: trueInterview Tip
Explain why prune matters: otherwise deletions never propagate.
💬 Comments
Quick Answer
Use the App-of-Apps pattern or an ApplicationSet to generate one Application per folder, cluster, or Git branch.
Detailed Answer
An ApplicationSet with a git directory generator creates an Application for every services/* folder automatically — add a folder, get an app. Cluster and list generators fan out across environments. App-of-Apps is a simpler variant where one root Application deploys child Applications.
Interview Tip
Name the generator types: git, cluster, list, matrix.
💬 Comments
Quick Answer
Prefer git revert so the change is reviewed and re-synced; use argocd app rollback as a break-glass tool.
Detailed Answer
Because Git is the source of truth, reverting the offending commit flows through review and Argo CD applies it. argocd app history and app rollback <rev> revert to a previously synced revision instantly, but bypass Git, so treat it as emergency-only to avoid drift between Git and cluster.
Interview Tip
Stress that git revert is the normal path; CLI rollback is break-glass.
💬 Comments
Quick Answer
AppProjects restrict which repos, clusters, and namespaces a set of Applications may use, plus allowed resource kinds.
Detailed Answer
The default project allows everything, which is unsafe for multi-team clusters. Custom projects scope source repos, destination clusters/namespaces, and permitted resource kinds, and integrate with RBAC/SSO. They're the primary multi-tenancy guardrail in Argo CD.
Interview Tip
Mention that 'default' allows everything — a red flag in prod.
💬 Comments
Quick Answer
Annotations order resources into waves and run Job hooks at PreSync/Sync/PostSync phases.
Detailed Answer
argocd.argoproj.io/sync-wave orders resources (lower waves first), so a database migration or CRD can apply before the workload that needs it. Sync hooks (PreSync/PostSync) run Jobs at defined phases — e.g. a PreSync migration or a PostSync smoke test — enabling safe, ordered rollouts.
Interview Tip
Give a concrete case: run a DB migration before the Deployment.
💬 Comments
Context
A platform team needed to onboard onto GitOps a rapidly growing set of microservices (starting around 50 and expected to keep growing), each needing its own Argo CD Application across multiple environments (dev, staging, prod) and, for some, multiple regional clusters.
Problem
Manually running `argocd app create` for every service/environment/cluster combination doesn't scale past a handful of services, isn't itself version-controlled, and makes onboarding a new service or a new cluster a manual, error-prone, undocumented process.
Solution
Adopt the App of Apps pattern with a single parent Application whose Git path contains ApplicationSet resources rather than individual Application manifests. Each ApplicationSet uses a generator (Git directory generator for per-service directories, or a cluster generator for per-cluster fan-out) to dynamically produce one child Application per matching directory/cluster combination, templated from a shared Application spec.
Commands
argocd app create platform-parent --repo https://git.example.com/gitops.git --path applicationsets --dest-server https://kubernetes.default.svc --dest-namespace argocd
kubectl apply -f service-applicationset.yaml
Outcome
Onboarding a new microservice became 'add a new directory with a values file to the monorepo,' automatically producing a fully-configured Argo CD Application with no manual argocd CLI invocation. Adding a new target cluster is similarly a one-line addition to the cluster generator's list, and it retroactively applies existing service definitions to the new cluster where applicable.
Lessons Learned
Sync-wave ordering became essential once volume grew — some child Applications (shared CRDs, a namespace-provisioning Application) needed to exist before others could successfully sync, and this had to be handled explicitly via sync-wave annotations rather than assumed. The team also learned to keep the ApplicationSet generator templates simple and push complexity into the per-service values files, since debugging a templating issue inside the generator itself across 50+ generated Applications is much harder than debugging one service's own values.
💬 Comments
Context
A team migrating to Argo CD-driven GitOps needed a way to manage Kubernetes Secrets (database credentials, API keys) without ever committing plaintext secret values to their Git repository, while still wanting the full audit trail and review process Git provides for every other manifest change.
Problem
GitOps's core value proposition — every change to the cluster is a reviewable, auditable Git commit — breaks down for secrets, since committing plaintext secret values to Git (even a private repo) is unacceptable, but managing secrets entirely outside the GitOps flow (e.g. applying them manually) loses that same audit trail and review process for exactly the resources that most need it.
Solution
Deploy the Sealed Secrets controller into the target clusters and adopt a workflow where engineers encrypt a Secret client-side using `kubeseal` and the cluster's public key before committing, producing a SealedSecret custom resource that's safe to commit in a normal pull request. Argo CD syncs the SealedSecret manifest exactly like any other resource; the in-cluster Sealed Secrets controller decrypts it into a real Kubernetes Secret using its private key, which never leaves the cluster.
Commands
kubeseal --cert prod-cert.pem -o yaml < secret.yaml > sealed-secret.yaml
kubectl apply -f sealed-secret.yaml
Outcome
Secret changes now go through the same PR review, Git history, and Argo CD sync process as every other manifest, with the actual secret VALUE never appearing in plaintext anywhere in Git history, while still being fully version-controlled and auditable at the level of 'when did this secret change and who approved it.'
Lessons Learned
SealedSecrets are encrypted per-cluster (tied to that cluster's controller key), so promoting the same secret across environments (dev to staging to prod) requires re-sealing for each target cluster's public key rather than copying one SealedSecret manifest everywhere — this needs to be built into the promotion pipeline rather than assumed, or engineers will be confused when a SealedSecret that decrypts fine in staging fails to decrypt in prod.
💬 Comments
Symptom
A sync operation removed ConfigMaps used by another team, causing two services to restart with missing configuration.
Error Message
comparison result: one or more synchronization tasks are not valid
Root Cause
The Application path included a shared namespace directory and automated prune was enabled. Argo CD correctly reconciled the declared desired state for that Application, but the ownership boundary was wrong. GitOps tools are powerful because they remove unmanaged drift; that same power is dangerous when ownership scopes overlap.
Diagnosis Steps
Solution
Disable automated prune temporarily, restore deleted ConfigMaps from Git or backup, and split Applications so each owns only its resources. Add resource exclusions or project restrictions where shared objects are unavoidable.
Commands
argocd app set platform-shared --sync-policy none
kubectl apply -f restored-configmaps.yaml
argocd app sync platform-shared --prune=false
Prevention
Define Application ownership boundaries narrowly. Require diff review for prune changes. Use AppProjects to restrict namespaces and resource kinds.
◈ Architecture Diagram
┌──────────┐
│ Trigger │
└────┬─────┘
↓
┌──────────┐
│ Incident │
└────┬─────┘
↓
┌──────────┐
│ Verify │
└──────────┘💬 Comments
Symptom
ArgoCD shows one or more applications as perpetually 'OutOfSync' even immediately after a successful sync operation completes. The sync status flips from Synced to OutOfSync within seconds. The ArgoCD UI diff view shows differences in fields that are not defined in the Git manifests, such as added annotations, modified resource limits, injected sidecar containers, or defaulted fields. The continuous sync attempts generate excessive API server load and flood the ArgoCD notification channels with sync-started/sync-completed events every 30-60 seconds.
Error Message
Status: OutOfSync Sync Status: ComparisonError Live state differs from desired state: metadata.annotations: map[sidecar.istio.io/inject:true] != null spec.template.spec.containers: length 2 != 1
Root Cause
Kubernetes mutating admission webhooks modify resources after they are submitted to the API server but before they are persisted to etcd. ArgoCD compares the desired state (from Git) against the live state (from the API server), and any fields added or modified by admission webhooks create a permanent diff. In this incident, three separate mutating webhooks were contributing to the sync loop. First, the Istio sidecar injector webhook added a sidecar container to every pod spec and added the annotation sidecar.istio.io/inject: true, making the live deployment have 2 containers when Git defined only 1. Second, a custom OPA webhook enforced resource limits by injecting default CPU and memory limits into any container spec that did not explicitly define them, adding fields like resources.limits.cpu: 500m that were not in the Git manifest. Third, the AWS VPC CNI webhook added an annotation for ENI allocation. Each sync cycle, ArgoCD applied the Git manifest (which lacked these fields), the webhooks immediately modified the applied resources, and ArgoCD's next comparison detected the differences and marked the application as OutOfSync. With auto-sync enabled, this created an infinite loop: sync → webhook modifies → detected as OutOfSync → sync again. The loop generated 40-60 sync operations per hour per affected application, putting significant load on both the Kubernetes API server and the ArgoCD repo-server which had to render manifests for each sync attempt. With 30 affected applications, this amounted to over 1,200 unnecessary sync operations per hour.
Diagnosis Steps
Solution
Configure ArgoCD resource customizations to ignore fields that are managed by admission webhooks rather than by Git. In the argocd-cm ConfigMap, add ignoreDifferences entries for each field pattern that webhooks modify. For the Istio sidecar injector, ignore the sidecar container and related annotations. For the OPA resource limits webhook, ignore the resources.limits and resources.requests fields if your organization's policy is to let the webhook manage these. Use both application-level and system-level ignore configurations depending on scope. At the application level, add ignoreDifferences in the Application spec for targeted overrides. At the system level, configure resource.customizations.ignoreDifferences.all in argocd-cm for patterns that apply across all applications. For Istio specifically, configure the jqPathExpressions format which provides more precise field matching than jsonPointers: use .spec.template.spec.containers[] | select(.name == "istio-proxy") to ignore only the injected sidecar container without ignoring changes to application containers. After configuring ignore rules, force a single sync to establish the baseline, then verify the application remains in Synced status. Monitor the ArgoCD application controller metrics for sync frequency to confirm the loop has stopped. For a more permanent solution, consider including the webhook-managed fields explicitly in your Git manifests so the desired state matches the live state, eliminating the diff entirely. This approach is preferred for fields like Istio annotations where you want explicit control over sidecar injection behavior per application.
Commands
kubectl edit configmap argocd-cm -n argocd
argocd app sync payments-api --prune --force
argocd app get payments-api -o json | jq '.status.sync.status'
Prevention
Audit all mutating admission webhooks in the cluster and document which fields they modify. Configure ArgoCD ignoreDifferences for webhook-managed fields during initial ArgoCD setup. When adding new webhooks to a cluster, always update ArgoCD ignore rules in the same change. Include webhook-injected fields explicitly in Git manifests where possible to maintain a single source of truth. Monitor ArgoCD sync frequency metrics and alert when any application exceeds 5 syncs per hour.
◈ Architecture Diagram
Sync Loop:
┌──────────┐ apply ┌──────────────┐
│ Git │────────► │ K8s API │
│ (1 ctnr) │ │ Server │
└──────────┘ └──────┬───────┘
▲ │ mutating
│ │ webhook
│ ┌──────▼───────┐
│ OutOfSync! │ Istio injects│
│ (2 != 1) │ sidecar │
│ │ (2 ctnrs) │
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
└───────────────│ ArgoCD │
re-sync │ compares │
│ Git vs Live │
└──────────────┘
Fix: ignoreDifferences → breaks the loop💬 Comments
Symptom
All ArgoCD applications across every project simultaneously transition to a 'SyncFailed' status. The ArgoCD UI shows a red error banner on every application. No new syncs can complete, and auto-sync applications that were previously healthy are now stuck. The error consistently references Git repository access failure. The issue affects all applications regardless of the target cluster or namespace.
Error Message
ComparisonError: rpc error: code = Unknown desc = authentication required Failed to load repository: ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain repository not accessible: authentication required
Root Cause
The SSH deploy key used by ArgoCD to authenticate with the organization's Git server (GitHub Enterprise) had expired. The key was created 12 months ago during the initial ArgoCD setup with a 1-year expiration date set by the organization's security policy. The same SSH key was configured as a repository credential in ArgoCD for the organization-level repository pattern (ssh://[email protected]/*), meaning every application that used any repository under that organization inherited this single credential. When the key expired, all 85 ArgoCD applications lost Git access simultaneously. The ArgoCD repo-server component, which is responsible for cloning Git repositories and rendering manifests, could not authenticate to fetch the latest manifests. This meant ArgoCD could not determine the desired state for any application, so it could not perform sync operations, drift detection, or health assessments. The impact was total: no deployments could proceed through the GitOps workflow, and any application that drifted from its desired state during this window could not be automatically corrected. The issue was compounded by the fact that the SSH key expiration was not monitored. The key had been generated by a platform engineer who had since left the organization, and the only documentation of the key's existence was in the ArgoCD repository credentials screen. No calendar reminder, monitoring alert, or automated rotation process existed for this critical credential. The team first noticed the issue when a developer tried to deploy a hotfix and the ArgoCD sync failed, triggering a P1 incident during business hours.
Diagnosis Steps
Solution
Generate a new SSH key pair and update the ArgoCD repository credentials immediately. Generate a new ED25519 SSH key with ssh-keygen -t ed25519 -C 'argocd-repo-access' -f argocd-deploy-key. Add the public key as a deploy key in the Git server (GitHub Enterprise, GitLab, or Bitbucket) with read-only access. Update the ArgoCD repository credential via the CLI: argocd repo update ssh://[email protected]/org/ --ssh-private-key-path ./argocd-deploy-key. Alternatively, update the Kubernetes secret directly: kubectl create secret generic repo-creds -n argocd --from-file=sshPrivateKey=./argocd-deploy-key --dry-run=client -o yaml | kubectl apply -f -. After updating the credential, trigger a repository connection test: argocd repo get ssh://[email protected]/org/payments-api.git should show successful connection. Then trigger a sync for critical applications first. For long-term remediation, implement automated SSH key rotation using a secrets manager. Store the SSH private key in HashiCorp Vault or AWS Secrets Manager and use the External Secrets Operator to sync it to the ArgoCD repository secret with automatic refresh. Set the key rotation interval to 90 days. Configure a monitoring check that tests Git repository connectivity from ArgoCD daily and alerts 30 days before credential expiration. For GitHub, use GitHub App authentication instead of SSH deploy keys — GitHub Apps support token refresh and provide better audit logging. ArgoCD natively supports GitHub App credentials which automatically handle token generation and refresh.
Commands
ssh-keygen -t ed25519 -C 'argocd-repo-access' -f /tmp/argocd-deploy-key -N ''
argocd repo update ssh://[email protected]/org/ --ssh-private-key-path /tmp/argocd-deploy-key
argocd repo get ssh://[email protected]/org/payments-api.git
Prevention
Implement automated credential rotation for ArgoCD repository access using a secrets manager with automatic refresh via External Secrets Operator. Switch from SSH deploy keys to GitHub App authentication which handles token lifecycle automatically. Create monitoring checks that test ArgoCD Git connectivity daily and alert 30 days before credential expiration. Document all ArgoCD credentials in a runbook with expiration dates, responsible owners, and rotation procedures.
◈ Architecture Diagram
Timeline:
Month 0 Month 11 Month 12
│ │ │
▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐
│ Key │ │ Key │ │ Key │
│ OK │──────────│ OK │──────────│EXPRD│
└─────┘ └─────┘ └──┬──┘
│
┌──────▼──────┐
│ ALL 85 apps │
│ SyncFailed │
└──────┬──────┘
│
┌──────▼──────┐
│ No deploys │
│ No drift │
│ detection │
│ P1 INCIDENT │
└─────────────┘
Fix: rotate key + add monitoring + use GitHub App💬 Comments
Symptom
ArgoCD reports specific resources as OutOfSync even though the live manifest in the cluster matches the Git manifest exactly when compared manually with kubectl diff. The ArgoCD diff view shows differences in metadata.managedFields or in fields that appear identical but differ in field ordering, empty vs null values, or default values applied by the API server. The issue affects only certain resource types (Services, Deployments with specific annotations) and started appearing after a Kubernetes version upgrade.
Error Message
Status: OutOfSync Diff: spec.selector: map[app:payments-api] (live) vs map[app:payments-api] (desired) metadata.managedFields: differs spec.template.spec.terminationGracePeriodSeconds: 30 (live) vs null (desired)
Root Cause
After upgrading the Kubernetes cluster from 1.27 to 1.29, the API server's Server-Side Apply (SSA) behavior changed how it tracks field ownership through the managedFields metadata. ArgoCD uses a comparison algorithm that normalizes manifests before diffing, but certain edge cases in the normalization logic produced false positives after the Kubernetes upgrade. Specifically, three patterns triggered false diffs. First, the API server now explicitly sets default values for fields like terminationGracePeriodSeconds: 30, dnsPolicy: ClusterFirst, and restartPolicy: Always in the stored manifest even when they are not specified in the submitted manifest. ArgoCD's desired state (from Git) has these fields as null/absent, while the live state has them as explicit defaults, creating a semantic match but a structural diff. Second, the managedFields metadata changed format between Kubernetes versions — the field manager name for kubectl apply changed, and SSA introduced additional field managers for system components. ArgoCD's default comparison includes managedFields in certain configurations. Third, map field ordering in labels and annotations differed between the Git-rendered YAML and the API server's JSON-to-YAML conversion, causing byte-level differences in fields that were semantically identical. This affected 15 out of 85 applications, all of which used resource types where the API server applied the most defaults. The false OutOfSync status was not just cosmetically annoying — it triggered auto-sync operations that performed unnecessary rollout restarts, caused alert fatigue in the on-call team who received OutOfSync notifications, and masked real configuration drift that occurred simultaneously in two other applications.
Diagnosis Steps
Solution
Configure ArgoCD to use Server-Side Apply diff strategy and normalize fields that the API server defaults. First, update the ArgoCD application to use server-side diff by setting the annotation argocd.argoproj.io/compare-options: ServerSideDiff=true on affected applications or globally in the argocd-cm ConfigMap. Server-side diff sends the desired manifest to the API server's dry-run endpoint and compares the result against the live state, which accounts for defaulting and admission webhook modifications. This eliminates false positives from API server defaulting because both sides go through the same defaulting logic. Second, configure ignoreDifferences in argocd-cm for metadata.managedFields globally since this field is never meaningful for GitOps comparison: add an entry with jsonPointers: ["/metadata/managedFields"] for all resource types. Third, for specific defaulted fields that still cause false diffs, either explicitly set them in your Git manifests to match the API server defaults (preferred for documentation clarity) or add them to ignoreDifferences. For terminationGracePeriodSeconds, adding terminationGracePeriodSeconds: 30 explicitly in your deployment manifest makes the intent clear and eliminates the diff. After making these changes, force a refresh on all affected applications with argocd app get <app> --hard-refresh to clear the cached desired state. Monitor the sync status dashboard for 24 hours to confirm all false positives are resolved.
Commands
kubectl annotate application payments-api -n argocd argocd.argoproj.io/compare-options=ServerSideDiff=true
argocd app get payments-api --hard-refresh
argocd app diff payments-api
Prevention
Enable Server-Side Diff as the default comparison strategy in ArgoCD for all applications. When upgrading Kubernetes versions, test ArgoCD sync behavior in a staging cluster first and review the ArgoCD release notes for known compatibility issues. Explicitly set common defaulted fields in Git manifests rather than relying on API server defaults. Configure global ignoreDifferences for metadata.managedFields which is never relevant for GitOps comparison. Run ArgoCD's built-in drift detection tests after cluster upgrades.
◈ Architecture Diagram
False Positive Diff: Git Manifest (desired): Live State (API server): ┌──────────────────────┐ ┌──────────────────────┐ │ spec: │ │ spec: │ │ replicas: 3 │ == │ replicas: 3 │ │ template: │ │ template: │ │ spec: │ │ spec: │ │ containers: │ == │ containers: │ │ ... │ │ ... │ │ │ DIFF! │ termGrace: 30 │ ← defaulted │ │ DIFF! │ dnsPolicy: │ ← defaulted │ │ │ ClusterFirst │ │ │ DIFF! │ managedFields: │ ← SSA │ │ │ - manager: kubectl│ └──────────────────────┘ └──────────────────────┘ Fix: ServerSideDiff=true normalizes both sides
💬 Comments
Symptom
ArgoCD application syncs for the checkout-worker service timeout after 90 seconds and eventually fail. The ArgoCD repo-server pods are repeatedly OOM killed and restarted by Kubernetes. Other applications using simple Kustomize or plain YAML manifests continue to sync normally. The issue started after the checkout-worker Helm chart added three new subchart dependencies.
Error Message
rpc error: code = DeadlineExceeded desc = context deadline exceeded repo-server: OOMKilled (exit code 137) Failed to generate manifests: rpc error: code = Unknown desc = manifest generation error
Root Cause
The ArgoCD repo-server is responsible for cloning Git repositories, running Helm template rendering, Kustomize builds, and other manifest generation operations. The checkout-worker Helm chart had grown to include 8 subchart dependencies: postgresql, redis, kafka, elasticsearch, cert-manager CRDs, istio gateway configuration, prometheus-operator ServiceMonitor definitions, and a shared company library chart. When ArgoCD's repo-server ran helm template on this chart, it needed to download and extract all 8 subchart archives, load all templates from all charts into memory, evaluate all Go template functions, resolve all values overrides and merge them across parent and subchart values, and render the final manifests. The total rendered output was approximately 15,000 lines of YAML across 120 Kubernetes resources. The helm template process consumed 2.1GB of memory during rendering, but the repo-server container had a memory limit of 1Gi (the ArgoCD default). The rendering process was killed by the OOM killer before it could complete. The problem was intermittent initially because the repo-server handles requests for all applications and the memory spike only occurred when the checkout-worker chart was being processed. During periods of high sync activity, the baseline memory usage from other chart renderings left less headroom, making the OOM more likely. The ArgoCD repo-server runs manifest generation in forked processes, and each concurrent generation consumes memory independently. With the default parallelism limit of 0 (unlimited), multiple large chart renderings could run simultaneously, compounding the memory pressure. Additionally, the repo-server had no caching configured for Helm chart dependencies, so each sync operation re-downloaded all subchart archives from the chart repository.
Diagnosis Steps
Solution
Increase the repo-server memory limits and configure concurrency controls to prevent memory exhaustion. First, increase the repo-server container memory limit to 4Gi and set the request to 2Gi. In the ArgoCD Helm chart values or the repo-server deployment, update the resource limits. This provides sufficient headroom for large chart rendering while Kubernetes scheduling can still bin-pack pods efficiently. Second, configure the repo-server parallelism limit to restrict concurrent manifest generation operations. Set the environment variable ARGOCD_REPO_SERVER_PARALLELISM_LIMIT=5 to limit concurrent Helm template operations to 5, preventing multiple large chart renderings from compounding memory usage. Third, enable Helm dependency caching by configuring a persistent volume for the repo-server's Helm cache directory (/helm-working-dir). This prevents re-downloading subchart archives on every sync operation, reducing both memory churn and network latency. Fourth, increase the repo-server timeout from the default 90 seconds to 180 seconds for large chart rendering by setting server.repo.server.timeout.seconds=180 in argocd-cm. Fifth, consider breaking the monolithic checkout-worker Helm chart into an ApplicationSet with separate ArgoCD Applications for infrastructure dependencies (database, cache, messaging) and the application itself. This distributes the rendering load across multiple smaller operations. For charts that consistently require high memory, configure a dedicated repo-server pod with higher resource limits using the sidecar repo-server pattern or ArgoCD's repository-specific configuration.
Commands
kubectl patch deployment argocd-repo-server -n argocd -p '{"spec":{"template":{"spec":{"containers":[{"name":"argocd-repo-server","resources":{"limits":{"memory":"4Gi","cpu":"2"},"requests":{"memory":"2Gi","cpu":"1"}},"env":[{"name":"ARGOCD_REPO_SERVER_PARALLELISM_LIMIT","value":"5"}]}]}}}}'kubectl rollout status deployment/argocd-repo-server -n argocd
argocd app sync checkout-worker --timeout 180
Prevention
Set ArgoCD repo-server memory limits based on the largest Helm chart in the cluster with 2x headroom. Configure parallelism limits to prevent concurrent large renderings from compounding memory usage. Monitor repo-server memory usage with Prometheus and alert at 70% of the limit. Break monolithic Helm charts with many dependencies into smaller, focused charts managed by ApplicationSets. Enable persistent Helm dependency caching to reduce memory churn from repeated downloads.
◈ Architecture Diagram
Helm Chart Dependency Tree: ┌───────────────────────────┐ │ checkout-worker │ │ (parent chart) │ ├───────┬───────┬───────────┤ │ │ │ │ ▼ ▼ ▼ ▼ postgre redis kafka elasticsearch sql ▼ ▼ ▼ ▼ cert- istio prom- shared-lib manager gateway operator Total: 8 subcharts → 15K lines YAML Repo-server memory during render: ┌────────────────────────────────┐ │████████████████████████ 2.1GB │ ← render │▓▓▓▓▓▓▓▓▓▓▓▓ 1.0GB limit │ ← OOM! └────────────────────────────────┘ Fix: increase limit to 4Gi + limit parallelism
💬 Comments
Symptom
A developer deleted an ArgoCD Application resource intending to reconfigure it, and Kubernetes immediately began terminating all pods, services, configmaps, secrets, and other resources that the Application had managed in the production namespace. The payments-api service went down completely as its Deployment, Service, Ingress, HPA, and PDB were all deleted within seconds. The production outage lasted 25 minutes until the resources were recreated.
Error Message
application 'payments-api' was deleted deployment.apps "payments-api" deleted service "payments-api" deleted ingress.networking.k8s.io "payments-api" deleted horizontalpodautoscaler.autoscaling "payments-api" deleted
Root Cause
ArgoCD Applications have a finalizer called resources-finalizer.argocd.argoproj.io that is added by default when an Application is created. This finalizer instructs the Kubernetes garbage collector to delete all resources managed by the Application when the Application itself is deleted — this is known as a cascade delete. The developer intended to delete the ArgoCD Application object to recreate it with a different project and source configuration, expecting that only the ArgoCD tracking metadata would be removed while the actual Kubernetes workloads would remain running. Instead, the cascade delete finalizer triggered deletion of every resource that ArgoCD had labeled as managed by that Application. This included the Deployment (which terminated all pods), the Service (which removed the ClusterIP endpoint), the Ingress (which removed the external routing), the HPA (which removed autoscaling), the PDB (which removed disruption protection), and 12 ConfigMaps and Secrets containing application configuration and TLS certificates. The deletion was nearly instantaneous because Kubernetes processes finalizers synchronously. The developer had used kubectl delete application payments-api -n argocd without understanding the cascade delete behavior. In the ArgoCD UI, the delete confirmation dialog does show a warning about cascade deletion and provides a checkbox to disable it, but the CLI kubectl delete command bypasses ArgoCD's UI protections entirely. The lack of any deletion protection policy (such as requiring approval for production application deletion) meant a single developer action caused a complete production service outage. The recovery required re-syncing the Application from Git, which took 5 minutes, plus 20 minutes for all pods to become ready, DNS to propagate, and load balancer health checks to pass.
Diagnosis Steps
Solution
Implement multiple layers of protection against accidental cascade deletion. First, for the immediate recovery, recreate the ArgoCD Application resource from Git or a backup and trigger a sync to restore all managed resources. Use argocd app create with the same parameters or apply the Application manifest from the infrastructure repository. Second, configure the cascade delete prevention by setting the propagation policy on critical Applications. Add the annotation argocd.argoproj.io/sync-options: Delete=false to the Application or include the Foreground deletion policy. The most effective protection is to remove the resources-finalizer from production Applications: edit the Application and remove resources-finalizer.argocd.argoproj.io from the metadata.finalizers list. Without this finalizer, deleting the Application only removes the ArgoCD tracking object, not the managed resources. However, this means you must manually clean up resources when genuinely decommissioning a service. Third, implement RBAC controls in ArgoCD to restrict who can delete Applications in production projects. Configure ArgoCD RBAC to deny delete permissions on Applications for all users except a small group of platform administrators. Use the ArgoCD AppProject spec to add a deny rule: p, role:developer, applications, delete, production/*, deny. Fourth, add a Kubernetes admission webhook or OPA Gatekeeper policy that prevents deletion of Application resources in the argocd namespace that target production namespaces, requiring an explicit bypass annotation for intentional deletions.
Commands
kubectl patch application payments-api -n argocd --type json -p '[{"op":"remove","path":"/metadata/finalizers","value":["resources-finalizer.argocd.argoproj.io"]}]'argocd app create payments-api --repo ssh://[email protected]/org/payments-api.git --path charts/payments-api --dest-server https://kubernetes.default.svc --dest-namespace production
argocd app sync payments-api --prune=false
Prevention
Remove the cascade delete finalizer from all production ArgoCD Applications to prevent accidental resource deletion. Implement ArgoCD RBAC that denies Application delete permission for non-admin users on production projects. Add a Gatekeeper/Kyverno policy that blocks deletion of ArgoCD Applications targeting production namespaces without an explicit bypass annotation. Train all developers on the distinction between deleting an ArgoCD Application (which cascade-deletes resources) and disabling auto-sync (which preserves resources).
◈ Architecture Diagram
Delete with finalizer (DANGEROUS): Delete without finalizer (SAFE):
┌──────────────────┐ ┌──────────────────┐
│ ArgoCD App │ │ ArgoCD App │
│ (has finalizer) │ │ (no finalizer) │
└────────┬─────────┘ └────────┬─────────┘
│ kubectl delete │ kubectl delete
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ CASCADE DELETE │ │ App removed │
│ Deployment ✗ │ │ Resources STAY │
│ Service ✗ │ │ Deployment ✓ │
│ Ingress ✗ │ │ Service ✓ │
│ HPA ✗ │ │ Ingress ✓ │
│ ConfigMaps ✗ │ │ HPA ✓ │
│ Secrets ✗ │ │ Pods running ✓ │
│ OUTAGE! │ │ No impact │
└──────────────────┘ └──────────────────┘💬 Comments
Symptom
Production applications managed by ArgoCD showed Synced and Healthy status for weeks, but a manual audit revealed that several resources had drifted significantly from their Git-defined desired state. Resource limits had been changed via kubectl edit, environment variables had been manually added, and replica counts had been overridden outside of Git. ArgoCD did not detect or report any of these changes, creating a false sense that GitOps was being enforced.
Root Cause
During the initial ArgoCD deployment, the platform team configured broad ignoreDifferences rules in the argocd-cm ConfigMap to resolve sync loop issues caused by mutating webhooks and API server defaulting. However, the ignore rules were overly broad. Instead of targeting specific fields modified by webhooks, the rules used wildcard JSON pointers that excluded entire subtrees of the resource spec from comparison. One rule used jsonPointers: ["/spec"] on Deployment resources to suppress a diff caused by the API server defaulting spec.revisionHistoryLimit. This single overly broad rule meant that ArgoCD ignored all changes to the entire Deployment spec — including replicas, template, strategy, selector, and every other field. An operator who ran kubectl scale deployment payments-api --replicas=1 in production during an incident forgot to revert it afterward, and ArgoCD never detected the change because /spec was ignored. Similarly, a developer who ran kubectl set env deployment/checkout-worker DEBUG=true for troubleshooting left the debug flag enabled for three weeks. Another rule ignored all metadata.annotations changes to suppress a webhook-added annotation, which meant manually added annotations (including those controlling ingress behavior and cert-manager TLS configuration) were invisible to ArgoCD drift detection. The platform team had copy-pasted these ignore rules from a community forum post without understanding the scope of each rule. The rules were added in a single PR six months ago and never reviewed or tested for unintended side effects. There was no monitoring or alerting on the breadth of ignore rules, and no periodic audit of whether ignored fields had actually drifted.
Diagnosis Steps
Solution
Audit and tighten all ignoreDifferences rules to use the most specific JSON pointers possible. Replace the overly broad /spec ignore rule with precise pointers targeting only the fields that actually need to be ignored. For the API server defaulting issue that caused spec.revisionHistoryLimit diffs, use jsonPointers: ["/spec/revisionHistoryLimit"] instead of ["/spec"]. For webhook-added annotations, use a jqPathExpression that targets only the specific annotation key: jqPathExpressions: [".metadata.annotations.\"sidecar.istio.io/inject\""]. Remove the blanket metadata.annotations ignore and replace it with specific annotation key ignores. After tightening the rules, perform a hard refresh on all applications with argocd app get <app> --hard-refresh to force ArgoCD to re-evaluate drift against the updated ignore configuration. Review the diff output for every application and remediate any discovered drift — either update Git to match the desired live state or sync to restore the Git-defined state. Implement a quarterly review process for all ignoreDifferences rules where the platform team verifies each rule is still necessary and appropriately scoped. Add a CI check on the argocd-cm ConfigMap that rejects overly broad ignore patterns like /spec, /metadata, or /status. Create a Prometheus metric or custom health check that counts the number of ignored fields per application and alerts when the count exceeds a threshold, indicating possible over-ignoring.
Commands
kubectl get configmap argocd-cm -n argocd -o yaml > /tmp/argocd-cm-backup.yaml
argocd app list -o json | jq '.[].metadata.name' | xargs -I{} argocd app get {} --hard-refreshargocd app diff payments-api --server-side
Prevention
Review all ignoreDifferences rules quarterly and verify each rule uses the narrowest possible field path. Never use broad JSON pointers like /spec or /metadata in ignore rules. Add CI validation on the argocd-cm ConfigMap that rejects overly broad patterns. Enable ArgoCD notifications for any application that has more than 5 ignored field paths as a warning indicator. Implement a drift audit job that compares live state to Git state using kubectl diff independent of ArgoCD ignore rules to catch drift that ArgoCD is configured to ignore. Train the platform team on the security implications of overly broad ignore rules.
◈ Architecture Diagram
Overly Broad Ignore Rule: Precise Ignore Rule:
┌──────────────────────┐ ┌──────────────────────┐
│ ignoreDifferences: │ │ ignoreDifferences: │
│ jsonPointers: │ │ jsonPointers: │
│ - /spec ← BAD │ │ - /spec/revision │
│ │ │ HistoryLimit │
│ Ignores ALL of: │ │ │
│ ✗ replicas │ │ Only ignores: │
│ ✗ template │ │ ✗ revisionHistory │
│ ✗ strategy │ │ │
│ ✗ resources │ │ Still detects: │
│ ✗ env vars │ │ ✓ replicas changes │
│ ✗ EVERYTHING │ │ ✓ env var changes │
│ │ │ ✓ resource changes │
│ Drift = INVISIBLE │ │ ✓ template changes │
│ GitOps = BROKEN │ │ │
└──────────────────────┘ │ Drift = DETECTED │
│ GitOps = ENFORCED │
└──────────────────────┘💬 Comments
Symptom
During a traffic spike incident, an on-call engineer ran `kubectl scale` to manually increase replica count as an immediate mitigation. Within about a minute, the replica count silently dropped back down, undoing the mitigation without any error or obvious explanation, prolonging the incident.
Error Message
No error surfaced to the engineer — kubectl scale succeeded, replicas increased briefly, then Argo CD's next reconciliation logged a normal sync reverting the replica count back to the Git-defined value
Root Cause
The Application had `syncPolicy.automated.selfHeal: true` configured, which by design treats any manual, out-of-band change (including a well-intentioned incident mitigation) as drift to be corrected back to Git's declared state. The on-call engineer either wasn't aware self-healing was enabled for this Application or forgot about it under incident pressure, and there was no in-the-moment indicator distinguishing this Application from others without self-heal.
Diagnosis Steps
Solution
For the immediate incident, update the replica count in Git and let Argo CD sync it properly (the correct GitOps way to make the change stick), rather than fighting self-heal with repeated manual kubectl commands.
Commands
argocd app get my-app -o yaml | grep -A5 automated
argocd app history my-app
kubectl scale deployment my-app --replicas=10
Prevention
Document clearly, in the on-call runbook and ideally via an Argo CD UI annotation/label convention, which Applications have selfHeal enabled, since it changes the correct incident-response procedure (edit Git and let it sync, rather than kubectl edit directly). Consider whether truly critical, frequently-scaled Applications are good candidates for selfHeal at all, or whether manual sync with monitoring is a better fit for services that need frequent manual intervention.
💬 Comments
Symptom
A routine merge to the main branch, intended to refactor an unrelated part of the Kustomize overlay, accidentally dropped the PersistentVolumeClaim manifest from the rendered output. The Application had automated sync with prune enabled, and on the next reconciliation, Argo CD deleted the live PVC along with its underlying volume, causing data loss for that service.
Error Message
Argo CD sync operation log showing the PVC as pruned; application logs immediately after showing failure to mount expected persistent storage
Root Cause
A Kustomize overlay refactor unintentionally excluded the PVC resource from the rendered manifest set (a patch/reference restructuring silently dropped it rather than any deliberate removal). Because the Application's sync policy had prune: true and automated sync enabled, Argo CD correctly followed its configured behavior — treating the PVC's absence from Git as an instruction to delete it from the cluster — with no distinction made for stateful, hard-to-recover resources versus stateless ones.
Diagnosis Steps
Solution
Restore from the most recent volume snapshot/backup if available (data loss is otherwise permanent once the underlying volume is reclaimed); revert the Git commit that dropped the PVC manifest and re-sync to recreate it going forward.
Commands
argocd app history my-app
kustomize build overlays/prod | diff - <(git show main~1:overlays/prod | kustomize build -)
kubectl get volumesnapshot -n my-namespace
Prevention
Set `preserveResourcesOnDeletion` or explicit resource-level annotations to exclude stateful/data-bearing resources (PVCs especially) from automatic pruning, require volume snapshots on a regular schedule regardless of GitOps configuration, and add a pre-merge CI check that diffs rendered manifests between branches specifically to flag when a previously-present resource would disappear from the sync target.
💬 Comments
Symptom
An Application remained perpetually OutOfSync despite no actual functional problems — every reconciliation cycle re-triggered a sync, generating continuous sync operation noise in the Argo CD UI and notifications, and making it hard to tell when a genuinely meaningful drift occurred versus this background noise.
Error Message
argocd app diff showing a persistent, recurring difference on a specific annotation/field that Argo CD kept trying to correct, only for it to reappear on the next reconciliation
Root Cause
A service mesh sidecar-injection mutating admission webhook was adding an annotation/field to the resource immediately after Argo CD applied it. Argo CD's next reconciliation saw this as drift from the Git-defined manifest (which didn't include that field) and synced again to 'correct' it, which triggered the webhook to re-add the field again — an infinite, low-severity but noisy loop.
Diagnosis Steps
Solution
Add an `ignoreDifferences` rule to the Application spec for the specific field path the webhook manages, telling Argo CD to stop treating that field as meaningful drift.
Commands
argocd app diff my-app
kubectl get mutatingwebhookconfigurations
argocd app set my-app --ignore-differences '{"group":"apps","kind":"Deployment","jsonPointers":["/spec/template/metadata/annotations"]}'Prevention
When onboarding any cluster-wide mutating webhook (service mesh injection, policy engines, etc.), proactively audit which Argo CD-managed fields it touches and pre-configure ignoreDifferences for those paths as part of the webhook's own rollout checklist, rather than discovering the conflict reactively per-Application.
💬 Comments