πŸš€ ArgoCD Complete Deep-Dive Guide

Master GitOps-based Continuous Delivery for Kubernetes β€” from zero to production, with real-world examples and best practices.

1. What is ArgoCD & Why It Exists

Definition

ArgoCD is a declarative, GitOps-based continuous delivery tool for Kubernetes. It watches a Git repository for changes to your Kubernetes manifests, and automatically (or manually) synchronizes those changes to your cluster β€” ensuring your live cluster always matches what's defined in Git.

The Problem ArgoCD Solves

Before ArgoCD, teams faced these challenges:

❌ Without ArgoCD
  • Manual kubectl apply deployments
  • No audit trail of who deployed what
  • "Works on my machine" cluster drift
  • CI pipeline has cluster credentials (security risk)
  • No easy rollback mechanism
  • No visibility into deployment state
βœ… With ArgoCD
  • Git commit = deployment (automated)
  • Full audit trail via git history
  • Drift detection & auto-healing
  • CI never touches the cluster directly
  • Rollback = git revert
  • Beautiful UI showing real-time state

Key Characteristics

FeatureDescription
DeclarativeYou describe the desired state; ArgoCD makes it happen
GitOps NativeGit is the single source of truth
Kubernetes NativeRuns as a set of controllers inside the cluster
Multi-ClusterCan manage many clusters from a single ArgoCD instance
Multi-TenancyProjects, RBAC, and SSO for team isolation
ExtensibleCustom health checks, resource actions, config management plugins

2. GitOps Principles

ArgoCD is built on four fundamental GitOps principles. Understanding these is essential before diving into the tool itself.

1
Declarative Configuration

Your entire system (infrastructure + applications) is described declaratively. In Kubernetes, this means YAML manifests, Helm charts, Kustomize overlays, etc. You never write imperative scripts like "run this command to create a pod."

2
Version Controlled (Git as Source of Truth)

All declarative configs are stored in Git. Git becomes the single source of truth. The desired state of the cluster IS whatever is in the Git repository. No more "let me SSH in and check what's running."

3
Automatically Applied

Once changes are approved (merged to the main branch), an agent (ArgoCD) automatically applies those changes to the cluster. Humans don't run kubectl apply β€” the agent does.

4
Continuously Reconciled (Self-Healing)

The agent continuously compares the desired state (Git) with the live state (cluster). If someone manually changes something in the cluster, ArgoCD detects the drift and corrects it. The cluster always converges to the Git state.

πŸ’‘ Pull vs Push Model

Traditional CI/CD (Push): Jenkins/GitHub Actions builds the image β†’ pushes to registry β†’ runs kubectl apply against the cluster. The CI server needs cluster credentials.

GitOps / ArgoCD (Pull): CI builds the image β†’ updates the Git repo with the new image tag β†’ ArgoCD (running inside the cluster) detects the change β†’ pulls the new state and applies it. The CI server NEVER touches the cluster.

Push Model vs Pull Model
⚠ Push Model (Traditional)
πŸ‘¨β€πŸ’»
Developer
git push
β†’
βš™οΈ
CI Server
build + deploy
β†’
☸️
K8s Cluster
kubectl apply

⚠ CI server has direct cluster credentials β€” security risk

βœ… Pull Model (GitOps / ArgoCD)
πŸ‘¨β€πŸ’»
Developer
git push
β†’
βš™οΈ
CI Server
build + push image
β†’
πŸ“¦
Git Repo
update YAML
←
πŸ”„
ArgoCD
pulls & applies
(inside K8s)

βœ… CI never touches the cluster β€” ArgoCD pulls from Git

3. ArgoCD Architecture

ArgoCD runs inside your Kubernetes cluster as a set of microservices. Let's look at every component:

ArgoCD Internal Architecture
☸ ArgoCD Namespace (argocd)
🌐
API Server
REST/gRPC API
Web UI & Auth/RBAC
CLI Gateway
πŸ“‚
Repo Server
Clones repos
Generates manifests
Helm / Kustomize
⚑
Redis
Caching layer
App state & repo info
 β–Ό
🧠
Application Controller
Watches Git repos (via Repo Server) • Compares desired vs live state
Executes sync operations • Runs health assessments
πŸ”‘
Dex Server
SSO / OIDC
πŸ””
Notifications
Slack / Email / Webhooks

Component Deep-Dive

πŸ”· API Server (argocd-server)

The front door to ArgoCD. It exposes a gRPC and REST API, serves the Web UI, and handles authentication/authorization. Everything flows through here β€” the CLI, the UI, and any external integrations.

  • Serves the beautiful web dashboard
  • Handles RBAC policy enforcement
  • Manages Git repository and cluster credentials
  • Processes webhook events from GitHub/GitLab
πŸ”· Repository Server (argocd-repo-server)

The brains behind manifest generation. When ArgoCD needs to know "what SHOULD the cluster look like?", it asks the Repo Server. This component clones Git repos, runs Helm template, Kustomize build, or plain YAML processing, and returns the final rendered manifests.

  • Clones and caches Git repositories
  • Runs helm template for Helm charts
  • Runs kustomize build for Kustomize overlays
  • Supports custom Config Management Plugins (CMP)
  • Stateless β€” can be horizontally scaled
πŸ”· Application Controller (argocd-application-controller)

The heart of ArgoCD. This is a Kubernetes controller that continuously monitors applications. It compares the desired state (from the Repo Server) with the live state (from the Kubernetes API) and determines if they match (Synced) or differ (OutOfSync).

  • Runs the reconciliation loop (default every 3 minutes)
  • Detects drift between Git and the live cluster
  • Executes sync (apply) operations
  • Runs health assessments on resources
  • Manages sync hooks (PreSync, PostSync, etc.)
πŸ”· Redis

An in-memory data store used for caching. Stores repo information, app state, and RBAC policy data to reduce load on the Kubernetes API and Git servers.

πŸ”· Dex Server (Optional)

An OpenID Connect (OIDC) identity provider. Enables SSO integration with providers like GitHub, GitLab, LDAP, SAML, Microsoft Entra ID (Azure AD), Okta, etc.

πŸ”· Notification Controller (Optional)

Sends notifications about application state changes to Slack, Email, Teams, PagerDuty, webhooks, and more.

How Data Flows

πŸ“¦
Git Repo
Source of truth
 β†’
πŸ“‚
Repo Server
Fetch & render
 β†’
🧠
Controller
Compare & sync
 β†’
☸️
K8s API
Live cluster

4. Installation β€” Step by Step

Prerequisites

  • A running Kubernetes cluster (minikube, kind, EKS, GKE, AKS β€” any)
  • kubectl configured and connected to the cluster
  • helm (optional, for Helm-based install)

Method 1: Plain YAML Install (Recommended for Learning)

1
Create the argocd namespace
kubectl create namespace argocd
2
Install ArgoCD
# Install the latest stable version
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Or a specific version (recommended for production)
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.0/manifests/install.yaml
3
Wait for pods to be ready
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s

# Check everything is running
kubectl get pods -n argocd

You should see these pods running:

NAME                                  READY   STATUS    AGE
argocd-application-controller-0       1/1     Running   2m
argocd-dex-server-xxxxx               1/1     Running   2m
argocd-redis-xxxxx                    1/1     Running   2m
argocd-repo-server-xxxxx              1/1     Running   2m
argocd-server-xxxxx                   1/1     Running   2m
argocd-notifications-controller-xxx   1/1     Running   2m
4
Access the Web UI
# Option A: Port forward (quick, local dev)
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Option B: Change service to LoadBalancer
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'

# Option C: Use an Ingress (production)

Open https://localhost:8080 in your browser.

5
Get the initial admin password
# The initial password is stored in a Kubernetes secret
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d

# Username: admin
# Password: (output from above command)
6
Install the ArgoCD CLI
# macOS
brew install argocd

# Linux
curl -sSL -o argocd-linux-amd64 \
  https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64

# Login
argocd login localhost:8080

# Change the default password (strongly recommended)
argocd account update-password

Method 2: Helm Install (Production)

# Add the ArgoCD Helm repo
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

# Install with custom values
helm install argocd argo/argo-cd \
  --namespace argocd \
  --create-namespace \
  --values values.yaml

Example values.yaml:

# values.yaml
server:
  replicas: 2
  ingress:
    enabled: true
    hostname: argocd.mycompany.com
    tls: true

controller:
  replicas: 1

repoServer:
  replicas: 2

redis-ha:
  enabled: true

configs:
  params:
    server.insecure: false
  rbac:
    policy.csv: |
      g, my-team, role:admin

5. Core Concepts

Application

The Application is the most important CRD in ArgoCD. It defines:

  • Source β€” Where the manifests live (Git repo + path + branch/tag)
  • Destination β€” Where to deploy (which cluster + namespace)
  • Sync Policy β€” How to sync (auto vs manual, prune, self-heal)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd        # Applications always live in the argocd namespace
spec:
  project: default         # Which ArgoCD project this belongs to

  source:
    repoURL: https://github.com/myorg/my-app-config.git
    targetRevision: main   # Branch, tag, or commit SHA
    path: k8s/overlays/production   # Path within the repo

  destination:
    server: https://kubernetes.default.svc   # Target cluster
    namespace: production                    # Target namespace

  syncPolicy:
    automated:             # Enable auto-sync
      prune: true          # Delete resources removed from Git
      selfHeal: true       # Revert manual changes in cluster
    syncOptions:
      - CreateNamespace=true
      - ApplyOutOfSyncOnly=true

Application States

StateMeaningIcon
SyncedLive state matches Git (desired state)βœ… Green
OutOfSyncLive state differs from Git🟑 Yellow
HealthyAll resources are running/readyπŸ’š Heart
DegradedSome resources are failingπŸ’› Warning
ProgressingResources are being rolled outπŸ”„ Spinning
MissingResource exists in Git but not in cluster❓ Question
UnknownHealth status cannot be determined❔ Gray

Project (AppProject)

Projects group applications and restrict what they can do. Think of them as "tenants" within ArgoCD.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-frontend
  namespace: argocd
spec:
  description: "Frontend team applications"

  # Which Git repos are allowed
  sourceRepos:
    - 'https://github.com/myorg/frontend-*'

  # Which clusters/namespaces are allowed
  destinations:
    - server: https://kubernetes.default.svc
      namespace: 'frontend-*'

  # Which Kubernetes resource types are allowed
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
  namespaceResourceWhitelist:
    - group: 'apps'
      kind: Deployment
    - group: ''
      kind: Service
    - group: 'networking.k8s.io'
      kind: Ingress

  # Role-based access for this project
  roles:
    - name: developer
      description: Frontend developers
      policies:
        - p, proj:team-frontend:developer, applications, get, team-frontend/*, allow
        - p, proj:team-frontend:developer, applications, sync, team-frontend/*, allow

Repository

ArgoCD needs to know how to access your Git repos. Repos can be configured via UI, CLI, or declaratively.

# Via CLI
argocd repo add https://github.com/myorg/my-repo.git \
  --username myuser \
  --password mytoken

# Via Secret (declarative)
apiVersion: v1
kind: Secret
metadata:
  name: my-repo
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  url: https://github.com/myorg/my-repo.git
  username: myuser
  password: ghp_xxxxxxxxxxxxxxxxxxxx
  type: git

Cluster

By default, ArgoCD can deploy to the cluster it runs in. For additional clusters:

# Add an external cluster
argocd cluster add my-eks-context

# This creates a ServiceAccount in the target cluster
# and stores the credentials in ArgoCD

6. Your First ArgoCD Application β€” Hands-On

Let's deploy a real application step by step. We'll deploy an Nginx web app using a Git repository.

Step 1: Create the Git Repository Structure

my-app-config/
β”œβ”€β”€ base/
β”‚   β”œβ”€β”€ deployment.yaml
β”‚   β”œβ”€β”€ service.yaml
β”‚   └── kustomization.yaml
└── overlays/
    β”œβ”€β”€ dev/
    β”‚   β”œβ”€β”€ kustomization.yaml
    β”‚   └── replica-patch.yaml
    └── prod/
        β”œβ”€β”€ kustomization.yaml
        └── replica-patch.yaml

base/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
  labels:
    app: nginx-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.25.3
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi

base/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx-app
spec:
  selector:
    app: nginx-app
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP

base/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml

overlays/dev/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
  - ../../base
patches:
  - path: replica-patch.yaml

overlays/dev/replica-patch.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 1

Step 2: Create the ArgoCD Application

Option A: Using CLI

argocd app create nginx-dev \
  --repo https://github.com/myorg/my-app-config.git \
  --path overlays/dev \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace dev \
  --sync-policy automated \
  --auto-prune \
  --self-heal

Option B: Using YAML (Recommended β€” it's GitOps all the way down!)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: nginx-dev
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app-config.git
    targetRevision: main
    path: overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Step 3: Verify Deployment

# Check via CLI
argocd app get nginx-dev

# Output:
# Name:               argocd/nginx-dev
# Sync Status:        Synced βœ…
# Health Status:      Healthy πŸ’š
#
# GROUP  KIND        NAME       STATUS  HEALTH
# apps   Deployment  nginx-app  Synced  Healthy
#        Service     nginx-app  Synced  Healthy

# Check via kubectl
kubectl get all -n dev

Step 4: Make a Change (See GitOps in Action)

# Update the image tag in your Git repo
# Edit base/deployment.yaml, change:
#   image: nginx:1.25.3  β†’  image: nginx:1.26.0

# Commit and push
git add . && git commit -m "Upgrade nginx to 1.26.0" && git push

# ArgoCD detects the change within 3 minutes (or instantly with webhooks)
# Watch it happen
argocd app get nginx-dev --refresh
πŸŽ‰ That's GitOps!

You changed a YAML file in Git, and your Kubernetes cluster automatically updated. No kubectl, no SSH, no CI pipeline touching the cluster. The full audit trail is in your git log.

7. Sync Policies, Strategies & Waves

Manual vs Automated Sync

Manual Sync

ArgoCD detects drift but waits for human approval.

syncPolicy: {}  # No "automated" key

# Trigger manually
argocd app sync my-app
Automated Sync

ArgoCD automatically applies changes when drift is detected.

syncPolicy:
  automated:
    prune: true
    selfHeal: true
    allowEmpty: false

Sync Options

OptionEffect
Prune=trueDelete resources no longer in Git
SelfHeal=trueRevert manual cluster changes
CreateNamespace=trueAuto-create the target namespace
ApplyOutOfSyncOnly=trueOnly apply changed resources (faster)
ServerSideApply=trueUse K8s server-side apply
Replace=trueUse kubectl replace (destructive)
PruneLast=trueDelete resources after all synced
RespectIgnoreDifferences=trueHonor ignoreDifferences during sync
FailOnSharedResource=trueFail if another App owns the resource

Sync Waves & Hooks

Sync waves control the order of resource creation. Lower wave numbers are synced first.

# Wave 0: Namespace (created first)
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "0"
---
# Wave 1: ConfigMap & Secrets
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  annotations:
    argocd.argoproj.io/sync-wave: "1"
---
# Wave 2: Database
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  annotations:
    argocd.argoproj.io/sync-wave: "2"
---
# Wave 3: Application (depends on database)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "3"
---
# Wave 4: Ingress (after app is healthy)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "4"
πŸ’‘ How Waves Work

ArgoCD processes waves sequentially: Wave 0 β†’ wait for healthy β†’ Wave 1 β†’ wait for healthy β†’ Wave 2 β†’ etc. If a resource in Wave 2 fails its health check, Wave 3 never starts. Negative waves (e.g., -1) run before wave 0.

Resource Hooks

HookWhen It RunsUse Case
PreSyncBefore sync startsDB migrations, backups
SyncDuring syncComplex deploy logic
PostSyncAfter all synced & healthySmoke tests, notifications
SyncFailWhen sync failsCleanup, alerts
SkipNever (skips the resource)Documentation resources
# Example: Database migration before deploy
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: myorg/migrate:latest
          command: ["./migrate", "up"]
      restartPolicy: Never

8. Helm, Kustomize & Jsonnet Integration

Helm Charts

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://prometheus-community.github.io/helm-charts
    chart: kube-prometheus-stack
    targetRevision: 55.5.0
    helm:
      releaseName: monitoring
      values: |
        grafana:
          enabled: true
          adminPassword: supersecret
        prometheus:
          prometheusSpec:
            retention: 30d
            storageSpec:
              volumeClaimTemplate:
                spec:
                  resources:
                    requests:
                      storage: 50Gi
      parameters:
        - name: alertmanager.enabled
          value: "true"
  destination:
    server: https://kubernetes.default.svc
    namespace: monitoring

Kustomize

spec:
  source:
    repoURL: https://github.com/myorg/my-app-config.git
    targetRevision: main
    path: overlays/production
    kustomize:
      images:
        - myorg/my-app:v2.1.0
      commonLabels:
        environment: production
      namePrefix: prod-

Plain YAML (Directory of Manifests)

spec:
  source:
    repoURL: https://github.com/myorg/my-app-config.git
    targetRevision: main
    path: manifests/
    directory:
      recurse: true
      include: '*.yaml'
      exclude: 'test-*'

Multiple Sources (ArgoCD 2.6+)

Combine a Helm chart with values from a different Git repo:

spec:
  sources:
    - repoURL: https://prometheus-community.github.io/helm-charts
      chart: kube-prometheus-stack
      targetRevision: 55.5.0
      helm:
        valueFiles:
          - $values/monitoring/values-production.yaml
    - repoURL: https://github.com/myorg/config-values.git
      targetRevision: main
      ref: values

9. Multi-Cluster & Multi-Tenancy

Managing Multiple Clusters

Multi-Cluster Management
Management Cluster
🎯
ArgoCD Instance
Central control plane
β–Ό
β–Ό
β–Ό
🟦
Dev Cluster
Amazon EKS
🟨
Staging Cluster
Google GKE
🟧
Prod Cluster
Azure AKS
# Add clusters via CLI
argocd cluster add dev-eks-context --name dev-cluster
argocd cluster add staging-gke-context --name staging-cluster
argocd cluster add prod-aks-context --name prod-cluster

# Deploy to a specific cluster
spec:
  destination:
    server: https://prod-aks.example.com
    namespace: production

Multi-Tenancy with Projects

# Team A: restricted to their namespaces
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-a
  namespace: argocd
spec:
  sourceRepos:
    - 'https://github.com/myorg/team-a-*'
  destinations:
    - server: '*'
      namespace: 'team-a-*'
  clusterResourceWhitelist: []

10. RBAC & SSO (Security)

RBAC Configuration

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    # Admins can do everything
    p, role:admin, applications, *, */*, allow
    p, role:admin, clusters, *, *, allow
    p, role:admin, repositories, *, *, allow
    p, role:admin, projects, *, *, allow

    # Developers can view and sync, but not delete
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, */*, allow
    p, role:developer, applications, action/*, */*, allow
    p, role:developer, logs, get, */*, allow

    # Frontend team β€” only their project
    p, role:frontend-dev, applications, *, team-frontend/*, allow

    # Map SSO groups to roles
    g, my-org:platform-team, role:admin
    g, my-org:developers, role:developer
    g, my-org:frontend, role:frontend-dev

SSO with GitHub

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  url: https://argocd.mycompany.com
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: $dex.github.clientID
          clientSecret: $dex.github.clientSecret
          orgs:
            - name: my-org
              teams:
                - platform-team
                - developers
                - frontend

SSO with Okta/OIDC

data:
  url: https://argocd.mycompany.com
  oidc.config: |
    name: Okta
    issuer: https://mycompany.okta.com/oauth2/default
    clientID: $oidc.okta.clientID
    clientSecret: $oidc.okta.clientSecret
    requestedScopes:
      - openid
      - profile
      - email
      - groups

11. ApplicationSets β€” Dynamic App Generation

ApplicationSets automatically generate ArgoCD Applications from templates. Instead of manually creating 50 Application manifests, you write ONE ApplicationSet.

Generator: Git Directory

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: microservices
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/myorg/microservices-config.git
        revision: main
        directories:
          - path: services/*
  template:
    metadata:
      name: '{{path.basename}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/microservices-config.git
        targetRevision: main
        path: '{{path}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{path.basename}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
πŸ’‘ How It Works

If your repo has services/auth, services/payments, services/orders, ArgoCD creates three Applications automatically. Add a new directory? A new Application appears.

Generator: List

spec:
  generators:
    - list:
        elements:
          - cluster: dev
            url: https://dev.k8s.example.com
            values:
              replicas: "1"
          - cluster: staging
            url: https://staging.k8s.example.com
            values:
              replicas: "2"
          - cluster: prod
            url: https://prod.k8s.example.com
            values:
              replicas: "5"
  template:
    metadata:
      name: 'my-app-{{cluster}}'
    spec:
      source:
        path: 'overlays/{{cluster}}'
      destination:
        server: '{{url}}'

Generator: Matrix (Combining)

Deploy every microservice to every cluster:

spec:
  generators:
    - matrix:
        generators:
          - git:
              repoURL: https://github.com/myorg/config.git
              directories:
                - path: services/*
          - list:
              elements:
                - cluster: dev
                  url: https://dev.k8s.example.com
                - cluster: prod
                  url: https://prod.k8s.example.com
  template:
    metadata:
      name: '{{path.basename}}-{{cluster}}'

Generator: Pull Request

Create preview environments for every open PR:

spec:
  generators:
    - pullRequest:
        github:
          owner: myorg
          repo: my-app
          tokenRef:
            secretName: github-token
            key: token
          labels:
            - preview
  template:
    metadata:
      name: 'preview-{{number}}'
    spec:
      source:
        repoURL: https://github.com/myorg/my-app.git
        targetRevision: '{{branch}}'
        path: k8s/preview
        kustomize:
          images:
            - 'myorg/my-app:pr-{{number}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: 'preview-{{number}}'

12. Notifications & Webhooks

Notifications to Slack

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  service.slack: |
    token: $slack-token

  template.app-sync-succeeded: |
    slack:
      attachments: |
        [{
          "title": "{{.app.metadata.name}} synced!",
          "color": "#18be52",
          "fields": [{
            "title": "Sync Status",
            "value": "{{.app.status.sync.status}}",
            "short": true
          }, {
            "title": "Repository",
            "value": "{{.app.spec.source.repoURL}}",
            "short": true
          }]
        }]

  template.app-health-degraded: |
    slack:
      attachments: |
        [{
          "title": "{{.app.metadata.name}} is DEGRADED!",
          "color": "#f4c030"
        }]

  trigger.on-sync-succeeded: |
    - when: app.status.operationState.phase in ['Succeeded']
      send: [app-sync-succeeded]

  trigger.on-health-degraded: |
    - when: app.status.health.status == 'Degraded'
      send: [app-health-degraded]

  subscriptions: |
    - recipients:
        - slack:platform-alerts
      triggers:
        - on-sync-succeeded
        - on-health-degraded

Git Webhooks (Faster Detection)

By default, ArgoCD polls Git every 3 minutes. Webhooks make detection instant:

# In GitHub repo β†’ Settings β†’ Webhooks:
# URL:    https://argocd.mycompany.com/api/webhook
# Secret: (your webhook secret)
# Events: Push events

# Configure in ArgoCD
apiVersion: v1
kind: Secret
metadata:
  name: argocd-secret
  namespace: argocd
stringData:
  webhook.github.secret: my-webhook-secret

13. Health Checks & Resource Hooks

Built-in Health Checks

ResourceHealthy When
DeploymentAll replicas available & updated
StatefulSetAll replicas ready & current revision
DaemonSetDesired = Ready nodes
ServiceAlways (unless LB has no IP)
IngressHas at least one IP/hostname
PVCStatus is "Bound"
Pod"Running" & all containers ready
JobCompleted successfully

Custom Health Check (Lua Script)

# argocd-cm ConfigMap
data:
  resource.customizations.health.certmanager.io_Certificate: |
    hs = {}
    if obj.status ~= nil then
      if obj.status.conditions ~= nil then
        for i, condition in ipairs(obj.status.conditions) do
          if condition.type == "Ready" and condition.status == "True" then
            hs.status = "Healthy"
            hs.message = condition.message
            return hs
          end
        end
      end
    end
    hs.status = "Progressing"
    hs.message = "Waiting for certificate to be issued"
    return hs

Custom Resource Actions

data:
  resource.customizations.actions.apps_Deployment: |
    discovery.lua: |
      actions = {}
      actions["restart"] = {["disabled"] = false}
      return actions
    definitions:
      - name: restart
        action.lua: |
          local os = require("os")
          if obj.spec.template.metadata == nil then
            obj.spec.template.metadata = {}
          end
          if obj.spec.template.metadata.annotations == nil then
            obj.spec.template.metadata.annotations = {}
          end
          obj.spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"] = os.date("!%Y-%m-%dT%H:%M:%SZ")
          return obj

14. Secrets Management

The biggest challenge in GitOps: you can't store secrets in Git! Here are the approaches:

Option 1: Sealed Secrets

Encrypt secrets client-side; only the cluster can decrypt.

helm install sealed-secrets \
  sealed-secrets/sealed-secrets \
  -n kube-system

kubeseal --format yaml \
  < secret.yaml \
  > sealed-secret.yaml
# Safe to commit!
Option 2: External Secrets

Sync from AWS Secrets Manager, Vault, GCP, Azure.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: my-secret
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: my-app-secret
  data:
    - secretKey: db-password
      remoteRef:
        key: prod/my-app/db
        property: password
Option 3: Vault + AVP

Inject secrets during rendering with ArgoCD Vault Plugin.

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  annotations:
    avp.kubernetes.io/path: "secret/data/myapp"
stringData:
  password: <password>
# AVP replaces at render time
Option 4: SOPS (Mozilla)

Encrypt YAML values in place. Decrypt at apply time.

sops --encrypt --in-place secret.yaml

# In Git:
data:
  password: ENC[AES256_GCM,...]

# KSOPS plugin decrypts
# before applying
Recommendation

For most teams: External Secrets Operator is the most production-friendly. It integrates cleanly with ArgoCD, supports all major cloud providers, and doesn't require custom plugins.

15. Diff Strategies & Ignore Differences

spec:
  ignoreDifferences:
    # Ignore replicas (managed by HPA)
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas

    # Ignore annotations added by webhooks
    - group: apps
      kind: Deployment
      jqPathExpressions:
        - .metadata.annotations["sidecar.istio.io/inject"]

    # Ignore on all Services
    - group: ""
      kind: Service
      jsonPointers:
        - /spec/clusterIP
        - /metadata/annotations

Global Ignore (All Applications)

# argocd-cm ConfigMap
data:
  resource.compareoptions: |
    ignoreAggregatedRoles: true
  resource.customizations.ignoreDifferences.all: |
    managedFieldsManagers:
      - kube-controller-manager
      - kube-scheduler
    jsonPointers:
      - /metadata/managedFields

16. Disaster Recovery & HA

HA Architecture

kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/ha/install.yaml
ComponentStandardHA Mode
API Server1 replica2+ replicas
Repo Server1 replica2+ replicas
Controller1 replica1 (leader election)
RedisSingleRedis HA (Sentinel)

Backup & Restore

# Export all ArgoCD resources
argocd admin export -n argocd > argocd-backup.yaml

# Restore from backup
argocd admin import -n argocd < argocd-backup.yaml

# Alternative: Velero
velero backup create argocd-backup --include-namespaces argocd
⚠️ What's NOT Backed Up

The backup does not include deployed applications β€” only ArgoCD's config. Since your app manifests are in Git, you can always re-create from Git (that's the beauty of GitOps!).

17. ArgoCD with CI/CD Pipelines

The Golden Pattern: Separate CI and CD

CI/CD Golden Pattern β€” Separated Pipeline
CI Pipeline (GitHub Actions / Jenkins / GitLab CI)
πŸ“
Code Push
β†’
πŸ§ͺ
Run Tests
Lint / SAST
β†’
🐳
Build Image
docker build
β†’
πŸ“€
Push Registry
ECR / GCR / Docker Hub
 β–Ό
πŸ“¦
Update Config Repo
Change image tag in YAML
CD β€” ArgoCD (runs inside K8s)
πŸ”„
ArgoCD Detects Change β†’ Syncs β†’ Deploys
Automatically reconciles cluster state with Git

GitHub Actions Example

# .github/workflows/ci.yaml
name: CI Pipeline
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: npm test
      - name: Build and push Docker image
        run: |
          docker build -t myorg/my-app:${{ github.sha }} .
          docker push myorg/my-app:${{ github.sha }}
      - name: Update config repo
        run: |
          git clone https://x-access-token:${{ secrets.TOKEN }}@github.com/myorg/my-app-config.git
          cd my-app-config/overlays/production
          kustomize edit set image myorg/my-app=myorg/my-app:${{ github.sha }}
          git config user.name "CI Bot"
          git config user.email "ci@myorg.com"
          git add . && git commit -m "Deploy ${{ github.sha }}" && git push

ArgoCD Image Updater (Alternative)

# Annotate your Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  annotations:
    argocd-image-updater.argoproj.io/image-list: myapp=myorg/my-app
    argocd-image-updater.argoproj.io/myapp.update-strategy: semver
    argocd-image-updater.argoproj.io/myapp.allow-tags: regexp:^v\d+\.\d+\.\d+$
    argocd-image-updater.argoproj.io/write-back-method: git

18. Monitoring & Observability

Prometheus Metrics

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: argocd-metrics
  namespace: argocd
spec:
  selector:
    matchLabels:
      app.kubernetes.io/part-of: argocd
  endpoints:
    - port: metrics

Key Metrics to Monitor

MetricWhat It Tells You
argocd_app_infoApp metadata (health, sync status)
argocd_app_sync_totalTotal sync operations count
argocd_app_reconcile_durationHow long reconciliation takes
argocd_git_request_totalGit requests (connectivity issues)
argocd_cluster_api_request_totalK8s API calls
argocd_repo_server_queue_depthPending manifest requests

Alerting Rules

groups:
  - name: argocd
    rules:
      - alert: ArgoCD_AppOutOfSync
        expr: argocd_app_info{sync_status="OutOfSync"} == 1
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "App {{ $labels.name }} out of sync for 30m"

      - alert: ArgoCD_AppDegraded
        expr: argocd_app_info{health_status="Degraded"} == 1
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "App {{ $labels.name }} is degraded"

      - alert: ArgoCD_SyncFailed
        expr: increase(argocd_app_sync_total{phase="Failed"}[1h]) > 3
        labels:
          severity: critical

Grafana: Import Dashboard ID 14584 for a complete ArgoCD overview.

19. Troubleshooting Common Issues

Issue: Application Stuck "OutOfSync"

Cause: Mutating webhooks modify resources after apply (e.g., Istio sidecar injection).

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/template/metadata/annotations
Issue: "ComparisonError"

Cause: Repo server cannot render manifests.

# Check repo server logs
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-repo-server --tail=100

# Test locally
helm template my-chart ./charts/my-chart -f values.yaml
kustomize build overlays/production
Issue: Sync Takes Too Long
repoServer:
  resources:
    limits:
      cpu: 2
      memory: 2Gi
# Plus:
syncOptions:
  - ApplyOutOfSyncOnly=true
Issue: "Permission Denied" on Sync
# Check ArgoCD RBAC
argocd admin settings rbac can role:developer sync applications '*/*'

# Check K8s RBAC
kubectl auth can-i create deployments \
  --as=system:serviceaccount:argocd:argocd-application-controller \
  -n target-namespace

General Debugging Commands

argocd app get my-app              # App details
argocd app diff my-app             # View sync diff
argocd app get my-app --refresh    # Force refresh from Git
argocd app get my-app --hard-refresh  # Invalidate cache

# Logs
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller -f
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-server -f
kubectl get events -n argocd --sort-by='.lastTimestamp'

20. Best Practices & Production Checklist

Repository Structure

βœ… Recommended: Separate App Code & Config Repos
# App repo (source code + Dockerfile)
myorg/my-app/
β”œβ”€β”€ src/
β”œβ”€β”€ Dockerfile
└── .github/workflows/ci.yaml   # CI only

# Config repo (Kubernetes manifests)
myorg/my-app-config/
β”œβ”€β”€ base/
β”‚   β”œβ”€β”€ deployment.yaml
β”‚   β”œβ”€β”€ service.yaml
β”‚   └── kustomization.yaml
└── overlays/
    β”œβ”€β”€ dev/
    β”œβ”€β”€ staging/
    └── production/

Production Checklist

CategoryAction
SecurityEnable SSO (disable local admin)
SecurityConfigure RBAC policies
SecurityUse Sealed Secrets or External Secrets
SecurityEnable audit logging
SecurityNetwork policies for argocd namespace
HADeploy in HA mode
HARedis HA (Sentinel)
HAMultiple repo-server replicas
MonitoringPrometheus metrics configured
MonitoringGrafana dashboard imported
MonitoringAlerts for OutOfSync & Degraded
NotificationsSlack/Teams notifications configured
AccessIngress with TLS configured
AccessGit webhooks for instant detection
BackupRegular backup of ArgoCD config
GitOpsArgoCD managed by ArgoCD (App of Apps)
ProjectsPer-team AppProjects configured
SyncProduction uses manual sync
SyncDev/staging uses automated sync

App of Apps Pattern

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/argocd-apps.git
    targetRevision: main
    path: apps/
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd

# apps/ directory:
# β”œβ”€β”€ nginx-dev.yaml
# β”œβ”€β”€ nginx-prod.yaml
# β”œβ”€β”€ prometheus.yaml
# β”œβ”€β”€ cert-manager.yaml
# └── external-secrets.yaml

21. ArgoCD vs Other CD Tools

FeatureArgoCDFlux CDJenkins XSpinnaker
GitOps Nativeβœ… Yesβœ… Yesβœ… Yes❌ No
Web UIβœ… Beautiful⚠️ Basic⚠️ Basicβœ… Rich
Multi-Clusterβœ… Built-inβœ… Built-in⚠️ Limitedβœ… Built-in
SSO/RBACβœ… Built-in⚠️ K8s RBAC⚠️ Basicβœ… Built-in
Helm Supportβœ… Nativeβœ… HelmReleaseβœ… Nativeβœ… Native
Drift Detectionβœ… Real-timeβœ… Periodic❌ No❌ No
ApplicationSetsβœ… Powerful⚠️ Kustomization❌ No❌ No
Learning CurveMediumMediumHighVery High
Community⭐ Largest⭐ LargeDecliningNetflix
When to Choose ArgoCD
  • You want a great UI for visualizing deployments
  • You need multi-cluster management from a single pane
  • You want built-in SSO and fine-grained RBAC
  • You need ApplicationSets for dynamic app generation
  • Your team includes non-CLI users who benefit from the UI

22. Real-World Scenario: Full Production Setup

Repository Layout

myorg/platform-config/
β”œβ”€β”€ argocd/                          # ArgoCD itself
β”‚   β”œβ”€β”€ base/
β”‚   └── overlays/production/
β”œβ”€β”€ apps/                            # App-of-Apps root
β”‚   β”œβ”€β”€ dev-apps.yaml
β”‚   β”œβ”€β”€ staging-apps.yaml
β”‚   └── prod-apps.yaml
β”œβ”€β”€ projects/                        # AppProjects
β”‚   β”œβ”€β”€ team-backend.yaml
β”‚   β”œβ”€β”€ team-frontend.yaml
β”‚   └── platform.yaml
└── services/                        # Service configs
    β”œβ”€β”€ auth-service/
    β”‚   β”œβ”€β”€ base/
    β”‚   └── overlays/ (dev/staging/production)
    β”œβ”€β”€ payment-service/
    └── order-service/

Production ApplicationSet

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: production-services
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/myorg/platform-config.git
        revision: main
        directories:
          - path: services/*/overlays/production
  template:
    metadata:
      name: 'prod-{{path[1]}}'
      labels:
        environment: production
    spec:
      project: platform
      source:
        repoURL: https://github.com/myorg/platform-config.git
        targetRevision: main
        path: '{{path}}'
      destination:
        server: https://prod.k8s.mycompany.com
        namespace: '{{path[1]}}'
      syncPolicy:
        syncOptions:
          - CreateNamespace=true
          - ApplyOutOfSyncOnly=true
          - PruneLast=true
      ignoreDifferences:
        - group: apps
          kind: Deployment
          jsonPointers:
            - /spec/replicas

Production Deployment Workflow

1
Developer pushes code

Merges a PR to auth-service main branch.

2
CI Pipeline runs

Tests, builds myorg/auth-service:abc123, pushes to ECR.

3
CI updates config repo

Opens PR on platform-config updating the image tag.

4
Team lead reviews & merges

Config PR = deployment approval gate.

5
ArgoCD detects OutOfSync

Via webhook, the app shows OutOfSync in the UI.

6
Platform engineer clicks Sync

Reviews diff in UI, clicks "Sync" (manual for production).

7
Rollout happens

Rolling update. ArgoCD monitors health until all pods ready.

8
Notification sent

Slack: "βœ… prod-auth-service synced successfully"

🎯 Result

Full audit trail in Git. Rollback = git revert. No one ran kubectl. CI never touched the cluster. Every change is peer-reviewed.

Rollback in Action

# Option 1: Git revert (recommended)
git revert HEAD && git push

# Option 2: ArgoCD rollback
argocd app rollback prod-auth-service

# Option 3: Sync to a specific commit
argocd app sync prod-auth-service --revision abc123def