Platform Engineering
15 min readMay 5, 2026

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).

AJ
Ajeet Yadav
Platform & Cloud Engineer
Argo CD: GitOps Continuous Delivery for Kubernetes

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

bash
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.yaml

For production, use the Helm chart which supports HA configuration:

bash
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.yaml

Minimal argocd-values.yaml for a single-cluster production deployment:

yaml
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: annotation

Retrieve the initial admin password:

bash
argocd admin initial-password -n argocd
# Or:
kubectl get secret argocd-initial-admin-secret -n argocd \
  -o jsonpath='{.data.password}' | base64 -d

Application: 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):

yaml
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: 3m

Check sync status:

bash
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 needed

Kustomize Applications

For applications managed with Kustomize overlays:

yaml
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.0

Argo 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.

yaml
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 uses

RBAC policy in the argocd-rbac-cm ConfigMap:

yaml
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:admin

CLI Operations

The argocd CLI is the primary operational interface for day-to-day tasks:

bash
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-api

AppProject 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:

yaml
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 here

Generate the token:

bash
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-api

The 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.

yaml
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 0

Argo 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).

yaml
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_URL

Hook delete policies (there is no default — if no policy is set, the hook resource is never deleted):

  • HookSucceeded — delete after successful completion
  • HookFailed — delete after failure
  • BeforeHookCreation — 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

yaml
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: true

Git Directory Generator: One Application per Team

yaml
generators:
  - git:
      repoURL: https://github.com/codingprotocols/k8s-apps
      revision: main
      directories:
        - path: "teams/*/apps/*"    # Match any path like teams/payments/apps/api

Each 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:

yaml
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: true

When 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:

yaml
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: 20

By 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.

yaml
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-failed

The argocd-notifications-secret Secret holds the Slack token:

yaml
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).

bash
helm install argocd-image-updater argo/argocd-image-updater \
  --namespace argocd \
  --set config.argocd.serverAddress=argocd-server.argocd.svc:443

Annotate the Application:

yaml
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-apps

With 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:

yaml
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: true

The application controller is stateful (maintains a Kubernetes informer cache per managed cluster). Multiple replicas are supported via sharding — required for clusters with 1000+ applications:

yaml
controller:
  replicas: 3    # 3 shards — the Helm chart sets ARGOCD_CONTROLLER_REPLICAS automatically

With 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:

bash
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-secret

Then 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.

Related Topics

Argo CD
GitOps
Kubernetes
CI/CD
Helm
Platform Engineering
EKS
Continuous Delivery

Read Next