ArgoCD and GitOps: Continuous Delivery for Kubernetes (2026)

GitOps has become the de facto deployment model for Kubernetes in production. Rather than pushing changes imperatively via kubectl apply, GitOps treats your Git repository as the single source of truth — a reconciliation loop continuously compares desired state (Git) against actual state (cluster) and corrects any drift. ArgoCD is the most widely deployed GitOps controller, used by organizations ranging from startups to Fortune 500 companies. This guide covers everything from first principles to production-hardened patterns including multi-cluster ApplicationSets, RBAC, and automated rollback strategies.

GitOps Principles and Why They Matter

GitOps rests on four foundational principles defined by the OpenGitOps project:

  1. Declarative — The entire system is described declaratively (Kubernetes manifests, Helm values, Kustomize overlays).
  2. Versioned and immutable — The desired state is stored in a version-controlled system (Git), providing history and auditability.
  3. Pulled automatically — Approved changes are applied automatically by a software agent running inside the cluster.
  4. Continuously reconciled — Software agents continuously observe actual state and attempt to match the desired state.

The practical benefits over push-based CI/CD are significant. Drift detection catches manual kubectl changes that bypass your pipeline. Rollback is a git revert. Audit logs come free from git history. Cluster credentials never leave the cluster boundary — your CI system only writes to Git, not to Kubernetes.

Tip: Separate your application source repository (code + Dockerfile) from your config repository (Kubernetes manifests). Your CI pipeline builds and pushes an image, then updates the image tag in the config repo. ArgoCD watches the config repo and deploys. This prevents feedback loops and keeps deployment history clean.

ArgoCD Architecture Deep Dive

ArgoCD runs as several components inside your cluster:

  • argocd-server — The API server and web UI. Handles all user interactions via gRPC and REST.
  • argocd-repo-server — Clones Git repositories and renders manifests (Helm, Kustomize, plain YAML). Isolated for security — does not have cluster access.
  • argocd-application-controller — The heart of ArgoCD. Watches Application resources, calls repo-server for desired state, calls Kubernetes API for live state, compares them, and triggers syncs.
  • argocd-dex-server — OpenID Connect provider for SSO integration (GitHub, Okta, LDAP).
  • argocd-redis — Caches rendered manifests and live cluster state.
  • applicationset-controller — Generates Application resources from templates based on generators (Git, cluster list, matrix, etc.).

Install ArgoCD into its own namespace:

kubectl create namespace argocd
kubectl apply -n argocd -f \
  https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for all pods to be ready
kubectl wait --for=condition=Available deployment --all -n argocd --timeout=300s

# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d

The Application CRD: Core Configuration

Every deployment in ArgoCD is represented by an Application custom resource. Here is a production-ready example deploying a Helm chart:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payments-service
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io  # cascade delete
  annotations:
    notifications.argoproj.io/subscribe.on-sync-succeeded.slack: deployments
    notifications.argoproj.io/subscribe.on-sync-failed.slack: alerts
spec:
  project: production
  source:
    repoURL: https://github.com/myorg/k8s-config
    targetRevision: main
    path: apps/payments-service/overlays/production
    # For Helm charts:
    # chart: payments-service
    # helm:
    #   releaseName: payments
    #   values: |
    #     replicaCount: 3
    #     image:
    #       tag: "v2.4.1"
  destination:
    server: https://kubernetes.default.svc
    namespace: payments
  syncPolicy:
    automated:
      prune: true       # Delete resources removed from Git
      selfHeal: true    # Revert manual kubectl changes
      allowEmpty: false # Never sync to empty state
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - ApplyOutOfSyncOnly=true  # Only apply changed resources
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
  revisionHistoryLimit: 10
Note: The resources-finalizer.argocd.argoproj.io finalizer ensures that when you delete an Application resource, ArgoCD also deletes all the Kubernetes resources it created. Omit it if you want to delete the Application without affecting the deployed resources (useful for migrating apps between ArgoCD instances).

Sync Policies: Auto vs Manual

The choice between automated and manual sync involves trade-offs that depend on your deployment risk tolerance:

FeatureManual SyncAuto Sync
Deploy triggerHuman approval in UI/CLIAny Git commit to tracked path
Drift correctionShows diff, requires manual actionAutomatically reverts in seconds
Safe for productionYes (more control)Yes with proper Git branch protection
Emergency rollbackManual sync to previous revisiongit revert + auto-applied
Recommended forStateful apps, databasesStateless services, microservices

A common pattern is to use auto-sync for staging and manual sync for production, with Git branch protection rules requiring PR reviews before merging to the production branch. Configure this per project:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
  namespace: argocd
spec:
  description: Production workloads
  sourceRepos:
    - 'https://github.com/myorg/k8s-config'
  destinations:
    - namespace: '*'
      server: https://prod-cluster.example.com
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
  namespaceResourceBlacklist:
    - group: ''
      kind: ResourceQuota  # Prevent apps from modifying quotas
  roles:
    - name: deployer
      description: Can sync but not delete apps
      policies:
        - p, proj:production:deployer, applications, sync, production/*, allow
        - p, proj:production:deployer, applications, get, production/*, allow
      groups:
        - myorg:sre-team

App of Apps Pattern

Managing dozens or hundreds of Application resources manually does not scale. The App of Apps pattern solves this: one root Application watches a directory of other Application manifests, bootstrapping the entire cluster from a single command.

# Root application — points to a directory of Application CRDs
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cluster-bootstrap
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/k8s-config
    targetRevision: main
    path: clusters/production/apps  # contains Application YAML files
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

The clusters/production/apps/ directory then contains individual Application manifests:

clusters/production/apps/
├── ingress-nginx.yaml
├── cert-manager.yaml
├── prometheus-stack.yaml
├── payments-service.yaml
├── user-service.yaml
└── api-gateway.yaml
Tip: Use Kustomize overlays within App of Apps. Your base/ directory holds common Application templates, and each cluster's overlay patches in cluster-specific values (server URL, namespace, image tags). This avoids copy-paste across staging and production.

ApplicationSet for Multi-Cluster Deployments

ApplicationSet is the evolution of App of Apps for multi-cluster and multi-tenant scenarios. A single ApplicationSet resource generates many Application resources using generators:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: payments-service-all-clusters
  namespace: argocd
spec:
  generators:
    - matrix:
        generators:
          - clusters:
              selector:
                matchLabels:
                  environment: production
          - git:
              repoURL: https://github.com/myorg/k8s-config
              revision: main
              directories:
                - path: apps/payments-service/overlays/*
  template:
    metadata:
      name: '{{name}}-payments'
      annotations:
        notifications.argoproj.io/subscribe.on-sync-failed.pagerduty: ""
    spec:
      project: production
      source:
        repoURL: https://github.com/myorg/k8s-config
        targetRevision: main
        path: '{{path}}'
        kustomize:
          images:
            - 'payments:{{metadata.annotations.image-tag}}'
      destination:
        server: '{{server}}'
        namespace: payments
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

The Matrix generator combines the cluster list with the Git directory list, producing one Application per cluster per environment overlay. When you add a new cluster with the environment: production label, ApplicationSet automatically creates all the Application resources for it.

RBAC and SSO in ArgoCD

ArgoCD's RBAC is configured via ConfigMaps in the argocd namespace. The default policy uses Casbin syntax:

# kubectl edit configmap argocd-rbac-cm -n argocd
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    # Admins can do everything
    g, myorg:platform-team, role:admin

    # SRE can sync and view but not create/delete apps
    p, role:sre, applications, sync, */*, allow
    p, role:sre, applications, get, */*, allow
    p, role:sre, applications, action/*, */*, allow
    g, myorg:sre-team, role:sre

    # Developers can only view their own project
    p, role:dev-payments, applications, get, payments/*, allow
    p, role:dev-payments, applications, sync, payments/staging-*, allow
    g, myorg:payments-team, role:dev-payments

  scopes: '[groups]'

For GitHub SSO via Dex, add to argocd-cm:

data:
  url: https://argocd.example.com
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: $dex.github.clientID
          clientSecret: $dex.github.clientSecret
          orgs:
            - name: myorg
              teams:
                - platform-team
                - sre-team
                - payments-team

Rollback Strategy and Notifications

ArgoCD maintains a revision history for each Application. Rolling back is straightforward:

# Via CLI — list history
argocd app history payments-service

ID  DATE                           REVISION
0   2026-06-01 10:00:00 +0000 UTC  abc1234
1   2026-06-03 14:30:00 +0000 UTC  def5678
2   2026-06-05 09:15:00 +0000 UTC  ghi9012

# Roll back to previous revision
argocd app rollback payments-service 1

# Roll back and disable auto-sync temporarily
argocd app rollback payments-service 1 --prune
Note: Rollback via argocd app rollback puts the application into a degraded state relative to HEAD. Auto-sync will try to re-apply the latest Git state. Always fix the root cause in Git and merge a fix commit rather than relying on rollback as a permanent solution.

Configure notifications using the ArgoCD Notifications controller:

# argocd-notifications-cm ConfigMap trigger configuration
data:
  trigger.on-sync-failed: |
    - when: app.status.operationState.phase in ['Error', 'Failed']
      send: [slack-message, pagerduty-create-incident]
  trigger.on-health-degraded: |
    - when: app.status.health.status == 'Degraded'
      oncePer: app.status.sync.revision
      send: [slack-message]
  template.slack-message: |
    message: |
      Application {{.app.metadata.name}} sync {{.app.status.operationState.phase}}
      Revision: {{.app.status.sync.revision}}
      Details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}
  service.slack: |
    token: $slack-token
    username: ArgoCD

Frequently Asked Questions

What is the difference between ArgoCD and Flux?

Both are CNCF-graduated GitOps controllers. ArgoCD provides a rich web UI, multi-cluster management from a single control plane, and fine-grained RBAC out of the box. Flux is more lightweight and follows a purely controller-per-concern architecture (source controller, kustomize controller, helm controller). ArgoCD is generally preferred when you need a centralized multi-cluster dashboard; Flux is preferred when running decentralized GitOps where each cluster manages itself independently.

How do I handle secrets in GitOps without committing them to Git?

The standard approaches are: (1) Sealed Secrets — encrypt secrets with a cluster-specific public key so only your cluster can decrypt them; safe to commit to Git. (2) External Secrets Operator — reference secrets from AWS Secrets Manager, Vault, or GCP Secret Manager; the operator syncs them into Kubernetes Secrets. (3) SOPS + age/GPG — encrypt secret values in YAML files using Mozilla SOPS; ArgoCD has a SOPS plugin. Most enterprises use External Secrets Operator for centralized secret governance.

Can ArgoCD deploy to clusters outside the one it's installed in?

Yes. ArgoCD supports managing external clusters by registering them with argocd cluster add <context-name>. This creates a ServiceAccount in the target cluster and stores the kubeconfig in an ArgoCD Secret. The Application destination server field then references the external cluster URL. This is the standard multi-cluster model — one ArgoCD instance (often in a management cluster) controls all application clusters.

How do I prevent ArgoCD from overwriting manual hotfixes during an incident?

The cleanest approach is to temporarily disable auto-sync on the affected Application: argocd app set payments-service --sync-policy none. Apply your hotfix manually. Then create a Git commit with the same change, merge it, re-enable auto-sync, and let ArgoCD reconcile. Disabling auto-sync ensures the reconciliation loop does not undo your emergency change during the incident window.

What is the recommended resource structure for a large organization with 50+ microservices?

Use one AppProject per team, one ApplicationSet per service family, and the App of Apps pattern per cluster. Store all configuration in a monorepo under clusters/<env>/apps/ with Kustomize overlays per environment. Apply branch protection on the production branch requiring two reviewers. Use ArgoCD's syncWindows in AppProject to restrict auto-sync to business hours for production, giving your on-call team control during off-hours.