Argo CD: GitOps Continuous Delivery for Kubernetes
Argo CD is a declarative GitOps controller for Kubernetes: it watches a Git repository and ensures the cluster always matches what's in Git. When a commit changes a Helm chart values file or a raw Kubernetes manifest, Argo CD detects the drift and syncs it — without a CI pipeline pushing to the cluster. This covers Argo CD installation, Application and ApplicationSet configuration, sync waves and hooks for ordered deployments, RBAC for multi-team clusters, App of Apps pattern for bootstrapping, and production operational concerns (high availability, notifications, image updater).

A CI pipeline builds and pushes a Docker image. A CD system deploys it to Kubernetes. In the traditional model, the CD system runs kubectl apply or helm upgrade imperatively — a push model. GitOps inverts this: the cluster pulls its desired state from Git, and a controller (Argo CD) continuously reconciles the cluster toward that state. The result is that Git becomes the single source of truth for cluster state. If someone runs kubectl edit and changes a Deployment manually, Argo CD detects the drift and reverts it. If a deployment causes problems, the rollback is a git revert.
Argo CD is the most widely adopted GitOps tool for Kubernetes. It manages Helm charts, Kustomize overlays, raw YAML, and Jsonnet, and provides a UI that shows exactly which resources are out of sync and why.
Installation
kubectl create namespace argocd
# Check https://github.com/argoproj/argo-cd/releases for latest
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.0/manifests/install.yamlFor production, use the Helm chart which supports HA configuration:
1helm repo add argo https://argoproj.github.io/argo-helm
2helm repo update
3
4helm install argocd argo/argo-cd \
5 --namespace argocd \
6 --create-namespace \
7 --version 7.7.11 \
8 --values argocd-values.yamlMinimal argocd-values.yaml for a single-cluster production deployment:
1global:
2 domain: argocd.internal.example.com
3
4server:
5 replicas: 2 # HA: 2 server replicas
6
7applicationSet:
8 replicas: 2
9
10controller:
11 replicas: 1 # Application controller is a StatefulSet — 1 replica for standard; sharding for 1000+ apps
12
13configs:
14 params:
15 server.insecure: true # Terminate TLS at the ingress/ALB; don't double-encrypt
16
17 cm:
18 # Allow tracking Helm chart versions by OCI image tag (useful with image updater)
19 application.resourceTrackingMethod: annotationRetrieve the initial admin password:
argocd admin initial-password -n argocd
# Or:
kubectl get secret argocd-initial-admin-secret -n argocd \
-o jsonpath='{.data.password}' | base64 -dApplication: Deploying a Helm Chart
The Application CRD is the core Argo CD resource. It defines a source (Git repo + path/chart) and a destination (cluster + namespace):
1apiVersion: argoproj.io/v1alpha1
2kind: Application
3metadata:
4 name: payments-api
5 namespace: argocd # Applications always live in the argocd namespace
6 finalizers:
7 - resources-finalizer.argocd.argoproj.io # Cascading delete: removing the Application deletes deployed resources
8spec:
9 project: payments # Argo CD Project for RBAC isolation
10
11 source:
12 repoURL: https://github.com/codingprotocols/k8s-apps
13 targetRevision: main # Branch, tag, or commit SHA
14 path: apps/payments-api/helm # Path within the repo containing the Helm chart
15
16 helm:
17 releaseName: payments-api
18 valueFiles:
19 - values.yaml
20 - values-production.yaml # Environment overlay
21
22 destination:
23 server: https://kubernetes.default.svc # The cluster Argo CD itself runs in
24 namespace: payments
25
26 syncPolicy:
27 automated:
28 prune: true # Delete resources that were removed from Git
29 selfHeal: true # Revert manual changes (kubectl edit, etc.)
30 allowEmpty: false # Never sync an empty application (safety guard)
31 syncOptions:
32 - CreateNamespace=true
33 - ServerSideApply=true # Use SSA instead of CSA (handles large objects, prevents annotation truncation)
34 retry:
35 limit: 5
36 backoff:
37 duration: 5s
38 factor: 2
39 maxDuration: 3mCheck sync status:
1argocd app get payments-api
2# Name: payments-api
3# Project: payments
4# Server: https://kubernetes.default.svc
5# Namespace: payments
6# URL: https://argocd.internal.example.com/applications/payments-api
7# Repo: https://github.com/codingprotocols/k8s-apps
8# Target: main
9# Path: apps/payments-api/helm
10# SyncWindow: Sync Allowed
11# Sync Policy: Automated
12# Sync Status: Synced to main (abc1234)
13# Health Status: Healthy
14
15argocd app sync payments-api # Manual sync when neededKustomize Applications
For applications managed with Kustomize overlays:
1source:
2 repoURL: https://github.com/codingprotocols/k8s-apps
3 targetRevision: main
4 path: apps/payments-api/overlays/production # Kustomize overlay path
5
6 kustomize:
7 images:
8 - payments-api=012345678901.dkr.ecr.us-east-1.amazonaws.com/payments-api:v2.1.0Argo CD auto-detects Kustomize when it finds a kustomization.yaml in the path — no explicit kustomize: block needed if not overriding images.
Projects: Multi-Team RBAC
AppProject resources define boundaries: which repos a team can deploy from, which clusters and namespaces they can deploy to, and which resource kinds are allowed or denied.
1apiVersion: argoproj.io/v1alpha1
2kind: AppProject
3metadata:
4 name: payments
5 namespace: argocd
6spec:
7 description: "Payments team applications"
8
9 # Only allow deploying from these repos
10 sourceRepos:
11 - https://github.com/codingprotocols/payments-*
12 - https://charts.bitnami.com/bitnami # Bitnami charts for PostgreSQL, Redis
13
14 # Only allow deploying to the payments namespace
15 destinations:
16 - server: https://kubernetes.default.svc
17 namespace: payments
18
19 # Empty whitelist = deny all cluster-scoped resources (safer than blacklist)
20 # A blacklist has blind spots for new resource kinds added by operators or CRDs;
21 # an empty whitelist explicitly denies everything cluster-scoped by default.
22 clusterResourceWhitelist: [] # Empty whitelist = deny all cluster-scoped resources (safer than blacklist)
23
24 # Allow these namespace-level resources
25 namespaceResourceWhitelist:
26 - group: apps
27 kind: Deployment
28 - group: ""
29 kind: Service
30 - group: networking.k8s.io
31 kind: Ingress
32 - group: external-secrets.io
33 kind: ExternalSecret
34 # ... add all resource kinds the team usesRBAC policy in the argocd-rbac-cm ConfigMap:
1# argocd-rbac-cm ConfigMap
2data:
3 policy.default: role:readonly # Default role for authenticated users
4 policy.csv: |
5 # payments team gets full access to their project
6 # Note: `applications, *` does NOT include `sync` in Argo CD < 2.4 — keep the explicit sync grant for compatibility
7 p, role:payments-dev, applications, *, payments/*, allow
8 p, role:payments-dev, applications, sync, payments/*, allow
9 g, payments-team, role:payments-dev # Map GitHub team to role
10
11 # Platform team can manage everything
12 g, platform-team, role:adminCLI Operations
The argocd CLI is the primary operational interface for day-to-day tasks:
1# Login
2argocd login argocd.internal.example.com
3
4# List applications
5argocd app list
6
7# Check sync status and resource details
8argocd app get payments-api
9
10# Sync manually (trigger immediate sync)
11argocd app sync payments-api
12
13# Diff — what would change if we sync?
14argocd app diff payments-api
15
16# Rollback to a previous sync revision
17argocd app history payments-api
18argocd app rollback payments-api 3 # Rollback to revision 3
19
20# Force sync even if application is healthy (for hook re-execution)
21argocd app sync payments-api --force
22
23# Sync only specific resources
24argocd app sync payments-api \
25 --resource apps:Deployment:payments-apiAppProject JWT Tokens for CI/CD Service Accounts
For CI pipelines that need to trigger syncs or check status without a user login, generate a JWT token scoped to a project role:
1apiVersion: argoproj.io/v1alpha1
2kind: AppProject
3metadata:
4 name: payments
5 namespace: argocd
6spec:
7 roles:
8 - name: deployer
9 description: "CI/CD pipeline access"
10 policies:
11 - p, proj:payments:deployer, applications, get, payments/*, allow
12 - p, proj:payments:deployer, applications, sync, payments/*, allow
13 - p, proj:payments:deployer, applications, override, payments/*, allow
14 jwtTokens: [] # Tokens are generated via CLI and appended hereGenerate the token:
1# Generate a JWT token for the deployer role (never expires by default — add --expires-in for rotation)
2argocd proj role create-token payments deployer
3
4# Use the token in CI (store in GitHub Secrets / CI secret store)
5argocd login $ARGOCD_SERVER \
6 --auth-token $ARGOCD_TOKEN \
7 --grpc-web
8argocd app sync payments-apiThe JWT token is scoped to the project role's policies — the CI pipeline can only sync applications in the payments project, not manage RBAC or cluster resources.
Sync Waves and Hooks: Ordered Deployments
Argo CD syncs all resources in an application at once by default. Sync waves and hooks control order: database migrations before application deployment, CRDs before custom resources, secrets before pods that mount them.
Sync Waves
Annotate resources with a sync wave number. Lower waves sync first. Default is wave 0.
1# Wave -1: provision database (Crossplane Claim)
2apiVersion: platform.codingprotocols.com/v1alpha1
3kind: PostgreSQLInstance
4metadata:
5 name: payments-db
6 annotations:
7 argocd.argoproj.io/sync-wave: "-1"
8
9---
10# Wave 0 (default): deploy the application
11apiVersion: apps/v1
12kind: Deployment
13metadata:
14 name: payments-api
15 # No annotation = wave 0Argo CD waits for all resources in wave N to become healthy before proceeding to wave N+1. For the Crossplane claim, "healthy" means the database's READY=True condition — Argo CD reads the resource's .status.conditions to determine health.
Sync Hooks
Hooks run at specific sync lifecycle points: PreSync (before resources are applied), Sync (alongside resources, with wave ordering), PostSync (after all resources are healthy), SyncFail (if the sync fails).
1# Database migration Job — runs after DB is ready, before application starts
2apiVersion: batch/v1
3kind: Job
4metadata:
5 name: payments-migrate
6 annotations:
7 argocd.argoproj.io/hook: PreSync # Run before sync
8 argocd.argoproj.io/hook-delete-policy: BeforeHookCreation # Delete previous job before creating
9spec:
10 template:
11 spec:
12 restartPolicy: Never
13 containers:
14 - name: migrate
15 image: 012345678901.dkr.ecr.us-east-1.amazonaws.com/payments-api:v2.1.0
16 command: ["python", "manage.py", "migrate"]
17 env:
18 - name: DATABASE_URL
19 valueFrom:
20 secretKeyRef:
21 name: payments-db-conn
22 key: DATABASE_URLHook delete policies (there is no default — if no policy is set, the hook resource is never deleted):
HookSucceeded— delete after successful completionHookFailed— delete after failureBeforeHookCreation— delete the previous run before creating a new one (prevents job accumulation on repeated syncs; the most useful policy for migration jobs)
ApplicationSet: Multi-Environment and Multi-Cluster Deployments
ApplicationSet generates multiple Application resources from a template. The most common generators are list, git (directory or file), and matrix. For a comprehensive guide covering all generators (Cluster, SCM Provider, Matrix, Pull Request), see Argo CD ApplicationSet: Multi-Cluster Deployment and Generator Patterns.
Environment Promotions with List Generator
1apiVersion: argoproj.io/v1alpha1
2kind: ApplicationSet
3metadata:
4 name: payments-api-envs
5 namespace: argocd
6spec:
7 generators:
8 - list:
9 elements:
10 - env: staging
11 cluster: https://kubernetes.default.svc
12 namespace: payments-staging
13 valuesFile: values-staging.yaml
14 - env: production
15 cluster: https://kubernetes.default.svc
16 namespace: payments
17 valuesFile: values-production.yaml
18
19 template:
20 metadata:
21 name: "payments-api-{{env}}"
22 spec:
23 project: payments
24 source:
25 repoURL: https://github.com/codingprotocols/k8s-apps
26 targetRevision: main
27 path: apps/payments-api/helm
28 helm:
29 valueFiles:
30 - values.yaml
31 - "{{valuesFile}}"
32 destination:
33 server: "{{cluster}}"
34 namespace: "{{namespace}}"
35 syncPolicy:
36 automated:
37 prune: true
38 selfHeal: trueGit Directory Generator: One Application per Team
generators:
- git:
repoURL: https://github.com/codingprotocols/k8s-apps
revision: main
directories:
- path: "teams/*/apps/*" # Match any path like teams/payments/apps/apiEach directory that matches the glob becomes an Application. The path components are available as template variables: {{path.basename}}, {{path[0]}}, etc.
App of Apps Pattern
The App of Apps pattern bootstraps a cluster by creating a single "root" Application that manages other Application resources. The root app points to a directory of Application CRD YAML files:
k8s-infra/
├── apps/
│ ├── argocd-apps/ # Root app watches this directory
│ │ ├── payments.yaml # Application for payments team
│ │ ├── orders.yaml # Application for orders team
│ │ └── platform.yaml # Application for platform services
│ └── ...
Root app:
1apiVersion: argoproj.io/v1alpha1
2kind: Application
3metadata:
4 name: root
5 namespace: argocd
6spec:
7 project: default
8 source:
9 repoURL: https://github.com/codingprotocols/k8s-infra
10 targetRevision: main
11 path: apps/argocd-apps # Contains Application CRD YAML files
12 destination:
13 server: https://kubernetes.default.svc
14 namespace: argocd
15 syncPolicy:
16 automated:
17 prune: true
18 selfHeal: trueWhen apps/argocd-apps/payments.yaml is added to Git, the root app syncs and creates the payments Application. Argo CD then syncs the payments Application, deploying the payments service. Bootstrapping a new cluster is: install Argo CD, apply the root app.
GitOps for Infrastructure: Argo CD and Crossplane
In 2026, GitOps is increasingly used to manage infrastructure (RDS, S3, IAM) alongside applications. Crossplane turns your Kubernetes cluster into a control plane for cloud resources, and Argo CD manages those resources as if they were standard Kubernetes objects:
1# Uses the Upbound provider-aws (upjet-based, actively maintained)
2# Legacy crossplane-contrib/provider-aws used database.aws.crossplane.io/v1beta1
3apiVersion: rds.aws.upbound.io/v1beta1
4kind: Instance
5metadata:
6 name: payments-db
7 annotations:
8 argocd.argoproj.io/sync-wave: "1" # Deploy DB before the app
9spec:
10 forProvider:
11 region: us-east-1
12 dbInstanceClass: db.t3.medium
13 engine: postgres
14 engineVersion: "15.4"
15 allocatedStorage: 20By combining Argo CD sync waves with Crossplane, you can orchestrate the creation of a database, its IAM roles, and the application itself in a single GitOps transaction. Argo CD waits for the Crossplane claim's READY=True condition before advancing to the next sync wave.
For ApplicationSet fleet management patterns across multiple clusters, see Argo CD ApplicationSet: Multi-Cluster Deployments.
Notifications: Slack and PagerDuty Alerts
Argo CD Notifications (bundled since v2.3; the standalone deployment was removed in v2.6) sends alerts when applications go out of sync, fail to sync, or become unhealthy.
1# argocd-notifications-cm ConfigMap
2apiVersion: v1
3kind: ConfigMap
4metadata:
5 name: argocd-notifications-cm
6 namespace: argocd
7data:
8 # Templates
9 template.app-sync-failed: |
10 slack:
11 attachments: |
12 [{
13 "title": "{{.app.metadata.name}}",
14 "color": "#E96D76",
15 "fields": [
16 {"title": "Sync Status", "value": "{{.app.status.sync.status}}", "short": true},
17 {"title": "Repository", "value": "{{.app.spec.source.repoURL}}", "short": true}
18 ]
19 }]
20
21 trigger.on-sync-failed: |
22 - when: app.status.operationState.phase in ['Error', 'Failed']
23 send: [app-sync-failed]
24
25 # Subscribe all apps to sync-failed notifications
26 subscriptions: |
27 - recipients:
28 - slack:platform-alerts
29 triggers:
30 - on-sync-failedThe argocd-notifications-secret Secret holds the Slack token:
1apiVersion: v1
2kind: Secret
3metadata:
4 name: argocd-notifications-secret
5 namespace: argocd
6type: Opaque
7stringData:
8 slack-token: xoxb-...Image Updater: Automated Image Tag Updates
Argo CD Image Updater watches a container registry and commits updated image tags to Git when new images are pushed — closing the loop between CI (build) and CD (deploy).
helm install argocd-image-updater argo/argocd-image-updater \
--namespace argocd \
--set config.argocd.serverAddress=argocd-server.argocd.svc:443Annotate the Application:
1metadata:
2 annotations:
3 argocd-image-updater.argoproj.io/image-list: api=012345678901.dkr.ecr.us-east-1.amazonaws.com/payments-api
4 argocd-image-updater.argoproj.io/api.update-strategy: semver # Track semver tags; options: semver, latest, digest, name
5 argocd-image-updater.argoproj.io/api.allow-tags: "^v[0-9]+\.[0-9]+\.[0-9]+$" # Only release tags
6 argocd-image-updater.argoproj.io/write-back-method: git # Commit updated tag to Git
7 argocd-image-updater.argoproj.io/git-repository: https://github.com/codingprotocols/k8s-appsWith write-back-method: git, Image Updater commits a .argocd-source-<app-name>.yaml file into the application's source path in the repo. Argo CD picks up this file on the next sync. The pipeline becomes: CI builds and pushes payments-api:v2.1.0 → Image Updater detects it → commits v2.1.0 to the app source path in Git → Argo CD syncs → pods updated.
High Availability
For production clusters where Argo CD itself can't go down:
1# argocd-values.yaml for HA
2server:
3 replicas: 2
4 autoscaling:
5 enabled: true
6 minReplicas: 2
7 maxReplicas: 5
8
9applicationSet:
10 replicas: 2
11
12controller:
13 replicas: 1 # Single shard (no sharding) — sufficient for most clusters
14
15redis-ha:
16 enabled: true # Use redis-ha subchart instead of single Redis
17 haproxy:
18 enabled: trueThe application controller is stateful (maintains a Kubernetes informer cache per managed cluster). Multiple replicas are supported via sharding — required for clusters with 1000+ applications:
controller:
replicas: 3 # 3 shards — the Helm chart sets ARGOCD_CONTROLLER_REPLICAS automaticallyWith sharding, applications are distributed across controller replicas by a consistent hash of the application name.
Frequently Asked Questions
How does Argo CD handle secrets in Git?
Argo CD doesn't solve the secrets-in-Git problem on its own. The recommended approach is to store only secret references in Git (ExternalSecret manifests pointing to AWS Secrets Manager or Vault) and use External Secrets Operator to sync the actual values. The ApplicationSet or Application manifests contain no secret values — they contain ExternalSecret resources that ESO resolves. See External Secrets Operator: Syncing AWS, GCP, and Vault Secrets to Kubernetes.
What's the difference between sync and health status?
Sync Status tracks whether the live cluster state matches Git. Health Status tracks whether the deployed resources are actually healthy (all pods running, no CrashLoopBackOff, etc.). An application can be Synced but Degraded — the deployment was applied from Git, but pods are failing. An application can be OutOfSync but Healthy — someone manually changed a resource but it's still running fine. Both dimensions matter for production monitoring.
How do I handle multiple clusters?
Register clusters with the Argo CD CLI:
1# Login first
2argocd login argocd.internal.example.com
3
4# Add a remote cluster (uses current kubeconfig context)
5argocd cluster add production-us-east-1
6# This creates a ServiceAccount + ClusterRoleBinding in the remote cluster
7# and stores the credentials in argocd-secretThen reference the cluster by server URL in Application destinations. Use ApplicationSet with a clusters generator to automatically create an Application for each registered cluster.
What's the impact of prune: true in automated sync?
With prune: true, Argo CD deletes resources from the cluster when they're removed from Git. This is what makes Git the source of truth — a deleted manifest results in a deleted resource. Without prune: true, resources stay in the cluster even after their manifests are removed from Git, creating configuration drift. The risk: accidentally deleting a manifest in Git would delete the resource. Mitigate with branch protection rules and PR reviews on the GitOps repo.
For External Secrets Operator that manages secret references in the GitOps repo (the recommended pattern for keeping secret values out of Git), see External Secrets Operator: Syncing AWS, GCP, and Vault Secrets to Kubernetes. For Crossplane Claims deployed via Argo CD sync waves (provisioning infrastructure before the application), see Crossplane: Cloud Infrastructure as Kubernetes Resources. For the production Argo CD setup guide covering App of Apps, RBAC, and SSO configuration, see GitOps with Argo CD: Production Setup Guide. For Flux CD as the alternative GitOps controller (Kubernetes-native, CRD-only, no UI), see Flux CD: GitOps for Kubernetes.
Migrating from push-based CI/CD to GitOps on EKS, or designing Argo CD RBAC for a multi-team cluster? Talk to us at Coding Protocols — we help platform teams implement GitOps workflows that give developers fast deployments without direct cluster access.


