Back to Modern DevOps & Platform Engineering Series

Part 6: Argo CD & GitOps Deployment

May 14, 2026 Wasil Zafar 35 min read

Master the de facto GitOps controller — from installation and architecture to progressive delivery with Argo Rollouts.

Table of Contents

  1. Introduction
  2. Getting Started
  3. Sync & Drift
  4. Scaling
  5. Security & Delivery
  6. Operations

Introduction — Why Argo CD Is the De Facto GitOps Controller

In Part 5, we established the GitOps philosophy — Git as the single source of truth for infrastructure and application state. Now we turn theory into practice with Argo CD, the CNCF-graduated project that has become the industry standard for Kubernetes-native continuous delivery.

Argo CD implements the GitOps pattern by continuously monitoring Git repositories and reconciling the desired state declared in manifests against the live state running in your clusters. When drift is detected — whether from manual changes, failed deployments, or configuration errors — Argo CD can automatically remediate, ensuring your production environment always matches what's committed in Git.

Key Insight: Argo CD doesn't just deploy — it continuously reconciles. Unlike CI-driven push-based deployment, Argo CD pulls desired state from Git and actively converges your cluster toward it, detecting and optionally correcting drift in real time.

By the end of this article, you'll be able to install Argo CD, deploy applications declaratively, configure sync policies and drift remediation, manage multi-cluster deployments with ApplicationSets, and implement progressive delivery strategies with Argo Rollouts.

Installing Argo CD

Argo CD runs entirely within Kubernetes. The recommended installation uses the official manifests or Helm chart. Let's cover both approaches.

Method 1: Plain Manifests

# Create the argocd namespace
kubectl create namespace argocd

# Install Argo CD (stable release)
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Verify all pods are running
kubectl get pods -n argocd
# NAME                                               READY   STATUS    RESTARTS   AGE
# argocd-application-controller-0                    1/1     Running   0          45s
# argocd-dex-server-6dcf645b6b-x2j4k                1/1     Running   0          45s
# argocd-redis-5b6967fdfc-7g8n2                      1/1     Running   0          45s
# argocd-repo-server-7598bf5999-4kl8m                1/1     Running   0          45s
# argocd-server-7d56c5bf4d-9mn3p                     1/1     Running   0          45s

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

Method 2: Helm Chart

# Add the Argo Helm repository
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 \
  --set server.service.type=LoadBalancer \
  --set configs.params."server\.insecure"=true \
  --set controller.replicas=2

# Install the Argo CD CLI
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 via CLI
argocd login localhost:8080 --username admin --password $(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
Security Note: Always change the initial admin password immediately after installation. Delete the argocd-initial-admin-secret once you've configured SSO or updated credentials: kubectl -n argocd delete secret argocd-initial-admin-secret

Argo CD Architecture

Argo CD consists of five core components that work together to implement the GitOps reconciliation loop:

Argo CD Internal Architecture
flowchart TB
    subgraph External["External Systems"]
        GIT[("Git Repository")]
        K8S[("Kubernetes Cluster")]
        IDP["Identity Provider
(OIDC/SAML)"] end subgraph ArgoCD["Argo CD Components"] API["API Server
(argocd-server)"] REPO["Repo Server
(argocd-repo-server)"] CTRL["Application Controller
(argocd-application-controller)"] REDIS[("Redis
Cache")] DEX["Dex Server
(SSO)"] end subgraph Clients["Clients"] UI["Web UI"] CLI["argocd CLI"] CICD["CI/CD Webhooks"] end UI --> API CLI --> API CICD --> API API --> REDIS API --> REPO API --> DEX DEX --> IDP CTRL --> REPO CTRL --> K8S CTRL --> REDIS REPO --> GIT

Component Responsibilities

Component Role Scaling
API Server gRPC/REST API, Web UI serving, RBAC enforcement, webhook handling Horizontal (multiple replicas)
Repo Server Clones Git repos, renders manifests (Helm, Kustomize, Jsonnet, plain YAML) Horizontal (CPU-intensive)
Application Controller Reconciliation loop — compares desired vs live state, triggers sync Sharding (by cluster)
Redis Caching layer for manifest generation, app state, repository credentials Single instance (HA with Sentinel)
Dex OpenID Connect provider for SSO (GitHub, LDAP, SAML, OIDC) Horizontal

Accessing the Dashboard

The Argo CD Web UI provides a visual representation of your application topology, sync status, and deployment history.

Port-Forwarding (Development)

# Forward the argocd-server service to localhost:8080
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Open browser at https://localhost:8080
# Login: admin / (password from initial secret)

Ingress Setup (Production)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - argocd.example.com
      secretName: argocd-server-tls
  rules:
    - host: argocd.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 443

Core Concepts

Argo CD introduces several domain-specific concepts that map directly to the GitOps workflow:

Argo CD Core Concepts & Relationships
flowchart LR
    subgraph Source["Source of Truth"]
        REPO["Git Repository"]
        PATH["Path / Directory"]
        REV["Revision / Branch"]
    end

    subgraph ArgoApp["Argo CD Application"]
        APP["Application CR"]
        PROJ["AppProject"]
        SYNC["Sync Policy"]
    end

    subgraph Target["Destination"]
        CLUSTER["K8s Cluster"]
        NS["Namespace"]
    end

    REPO --> APP
    PATH --> APP
    REV --> APP
    APP --> PROJ
    APP --> SYNC
    APP --> CLUSTER
    APP --> NS
                            
Concept Reference GitOps Fundamentals
Argo CD Terminology
  • Application — A Kubernetes Custom Resource that defines the source (Git repo + path + revision) and destination (cluster + namespace) for a set of manifests
  • Project — A logical grouping of Applications with RBAC boundaries, restricting which repos, clusters, and resources an Application can access
  • Sync — The process of applying the desired state from Git to the target cluster
  • Health — The runtime health of deployed resources (Healthy, Progressing, Degraded, Suspended, Missing)
  • Refresh — Re-reading the Git repository to detect changes (hard refresh clears cache)
Application CR AppProject Sync Status Health Status

Creating Your First Application

Applications can be created declaratively via YAML, through the Web UI, or using the CLI. The declarative approach is preferred — it allows you to manage Argo CD itself via GitOps.

Declarative Application Manifest

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: guestbook
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: HEAD
    path: guestbook
  destination:
    server: https://kubernetes.default.svc
    namespace: guestbook
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
# Apply the Application manifest
kubectl apply -f guestbook-app.yaml

# Check sync status via CLI
argocd app get guestbook
# Name:               argocd/guestbook
# Project:            default
# Server:             https://kubernetes.default.svc
# Namespace:          guestbook
# URL:                https://argocd.example.com/applications/guestbook
# Repo:               https://github.com/argoproj/argocd-example-apps.git
# Target:             HEAD
# Path:               guestbook
# SyncWindow:         Sync Allowed
# Sync Policy:        Automated (Prune)
# Sync Status:        Synced
# Health Status:      Healthy

# Manually trigger sync (if auto-sync is disabled)
argocd app sync guestbook

# View application resource tree
argocd app resources guestbook
Best Practice: Always include resources-finalizer.argocd.argoproj.io as a finalizer on your Application resources. This ensures that when you delete an Application, Argo CD will cascade-delete the managed Kubernetes resources in the target cluster.

Sync Policies

Sync policies determine when and how Argo CD applies changes from Git to the cluster. Getting these right is critical for both agility and safety.

Manual vs Automated Sync

Policy Behavior Use Case
Manual Changes detected but not applied until user triggers sync Production environments, regulated workloads
Auto-Sync Automatically syncs when Git changes are detected Dev/staging environments, trusted repos
Self-Heal Reverts manual cluster changes back to Git state Drift prevention, compliance enforcement
Prune Deletes resources removed from Git Complete state ownership, cleanup

Sync Waves & Hooks

Sync waves allow you to control the order in which resources are applied. Resources with lower wave numbers are synced first. Sync hooks execute at specific phases of the sync process.

# Namespace created first (wave -1)
apiVersion: v1
kind: Namespace
metadata:
  name: myapp
  annotations:
    argocd.argoproj.io/sync-wave: "-1"
---
# ConfigMaps and Secrets next (wave 0, default)
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: myapp
  annotations:
    argocd.argoproj.io/sync-wave: "0"
data:
  DATABASE_URL: "postgres://db:5432/myapp"
---
# Database migration job (wave 1, PreSync hook)
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  namespace: myapp
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: myapp/migrations:latest
          command: ["./migrate", "up"]
      restartPolicy: Never
---
# Application deployment (wave 2)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: myapp
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp/api:v1.2.0
          ports:
            - containerPort: 8080
Hook Phases: PreSyncSyncPostSyncSyncFail (on error). Hook delete policies: HookSucceeded, HookFailed, BeforeHookCreation. Use PreSync for migrations, PostSync for notifications, SyncFail for rollback triggers.

Drift Detection & Remediation

Drift occurs when the live cluster state diverges from the desired state in Git. This can happen through manual kubectl edit commands, Helm operator conflicts, or external controllers modifying resources.

Drift Detection & Self-Heal Flow
flowchart TD
    A["Controller polls every 3min"] --> B{"Compare Git State
vs Live State"} B -->|"Match"| C["Status: Synced ✓"] B -->|"Drift Detected"| D["Status: OutOfSync"] D --> E{"Self-Heal
Enabled?"} E -->|"Yes"| F["Auto-Sync
Revert to Git State"] E -->|"No"| G["Alert / Manual
Intervention Required"] F --> H["Status: Synced ✓"] G --> I["Operator Reviews
Diff in UI"] I --> J{"Accept or
Reject?"} J -->|"Sync"| F J -->|"Update Git"| K["Commit Fix to Git"] K --> A

Argo CD uses a sophisticated diff engine that understands Kubernetes resource semantics. It normalizes fields like default values, managed fields, and server-side applied annotations before comparison.

Configuring Drift Handling

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: production-api
  namespace: argocd
spec:
  project: production
  source:
    repoURL: https://github.com/myorg/k8s-manifests.git
    targetRevision: main
    path: apps/production/api
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      selfHeal: true       # Revert drift automatically
      prune: true          # Remove resources deleted from Git
    syncOptions:
      - RespectIgnoreDifferences=true
  # Ignore specific fields that external controllers manage
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas   # HPA manages replicas
    - group: ""
      kind: Service
      jqPathExpressions:
        - .spec.clusterIP  # Cluster-assigned field

ApplicationSets

ApplicationSets solve the scalability challenge: how do you manage hundreds of Applications across multiple clusters, environments, or teams without duplicating YAML? The ApplicationSet controller uses generators to dynamically create Application resources from templates.

Git Directory Generator

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-addons
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/myorg/cluster-addons.git
        revision: HEAD
        directories:
          - path: addons/*
  template:
    metadata:
      name: '{{path.basename}}'
    spec:
      project: infrastructure
      source:
        repoURL: https://github.com/myorg/cluster-addons.git
        targetRevision: HEAD
        path: '{{path}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{path.basename}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

Matrix Generator (Multi-Cluster × Multi-Environment)

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: multi-env-apps
  namespace: argocd
spec:
  generators:
    - matrix:
        generators:
          # Generator 1: Clusters
          - clusters:
              selector:
                matchLabels:
                  env: production
          # Generator 2: Apps from Git
          - git:
              repoURL: https://github.com/myorg/apps.git
              revision: HEAD
              directories:
                - path: apps/*
  template:
    metadata:
      name: '{{name}}-{{path.basename}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/apps.git
        targetRevision: HEAD
        path: '{{path}}'
      destination:
        server: '{{server}}'
        namespace: '{{path.basename}}'
      syncPolicy:
        automated:
          selfHeal: true
Generator Types ApplicationSet Patterns
Available Generators
  • Git Generator — Creates apps from directories or files in a Git repo (most common)
  • Cluster Generator — Creates apps for each registered cluster matching a selector
  • Matrix Generator — Cartesian product of two generators (e.g., clusters × apps)
  • Merge Generator — Combines generators with override semantics
  • List Generator — Static list of parameter sets for known configurations
  • Pull Request Generator — Creates preview environments for open PRs
  • SCM Provider Generator — Discovers repos from GitHub/GitLab organizations
Git Generator Matrix Cluster Generator PR Previews

Multi-Cluster Deployment

Argo CD supports deploying to multiple Kubernetes clusters from a single management plane. External clusters are registered via the CLI or by creating cluster secrets.

# Register an external cluster via CLI
argocd cluster add production-us-east \
  --name production-us-east \
  --kubeconfig ~/.kube/production-us-east.yaml

# List registered clusters
argocd cluster list
# SERVER                                    NAME                  VERSION  STATUS
# https://kubernetes.default.svc            in-cluster            1.28     Successful
# https://api.prod-us-east.example.com      production-us-east    1.28     Successful
# https://api.prod-eu-west.example.com      production-eu-west    1.28     Successful

Cluster Secret (Declarative)

apiVersion: v1
kind: Secret
metadata:
  name: production-us-east-cluster
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: cluster
    env: production
    region: us-east
type: Opaque
stringData:
  name: production-us-east
  server: https://api.prod-us-east.example.com
  config: |
    {
      "bearerToken": "",
      "tlsClientConfig": {
        "insecure": false,
        "caData": ""
      }
    }
Security Best Practice: Use short-lived tokens or workload identity federation instead of long-lived bearer tokens. For production multi-cluster setups, integrate with your cloud provider's IAM (AWS IRSA, GCP Workload Identity, Azure Workload Identity) for automatic credential rotation.

RBAC & Security

Argo CD provides a powerful RBAC system through AppProjects and the built-in policy engine. Projects define boundaries — which repositories, clusters, and resource types an Application can access.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-backend
  namespace: argocd
spec:
  description: "Backend team project"
  # Allowed source repositories
  sourceRepos:
    - 'https://github.com/myorg/backend-*'
    - 'https://github.com/myorg/shared-libs.git'
  # Allowed destination clusters and namespaces
  destinations:
    - server: https://kubernetes.default.svc
      namespace: 'backend-*'
    - server: https://api.staging.example.com
      namespace: 'backend-*'
  # Deny-list of cluster resources (prevent privilege escalation)
  clusterResourceBlacklist:
    - group: ''
      kind: Namespace
    - group: 'rbac.authorization.k8s.io'
      kind: ClusterRole
    - group: 'rbac.authorization.k8s.io'
      kind: ClusterRoleBinding
  # Allowed namespace-scoped resources
  namespaceResourceWhitelist:
    - group: ''
      kind: '*'
    - group: 'apps'
      kind: '*'
    - group: 'networking.k8s.io'
      kind: '*'
  # Role definitions
  roles:
    - name: developer
      description: "Can sync and view apps"
      policies:
        - p, proj:team-backend:developer, applications, get, team-backend/*, allow
        - p, proj:team-backend:developer, applications, sync, team-backend/*, allow
      groups:
        - backend-developers  # Maps to SSO group
    - name: admin
      description: "Full access to project"
      policies:
        - p, proj:team-backend:admin, applications, *, team-backend/*, allow
      groups:
        - backend-leads

SSO Integration

# argocd-cm ConfigMap — Dex OIDC configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
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:
                - backend-developers
                - platform-engineers
                - backend-leads

Progressive Delivery with Argo Rollouts

While Argo CD handles deployment synchronization, Argo Rollouts extends Kubernetes with advanced deployment strategies — canary releases, blue-green deployments, and automated analysis-driven promotions.

Canary Deployment

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 10
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api
          image: myorg/api-server:v2.1.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
  strategy:
    canary:
      steps:
        - setWeight: 5
        - pause: { duration: 2m }
        - analysis:
            templates:
              - templateName: success-rate
            args:
              - name: service-name
                value: api-server
        - setWeight: 25
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 100
      canaryService: api-server-canary
      stableService: api-server-stable
      trafficRouting:
        nginx:
          stableIngress: api-server-ingress
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: production
spec:
  args:
    - name: service-name
  metrics:
    - name: success-rate
      interval: 30s
      count: 5
      successCondition: result[0] >= 0.95
      failureCondition: result[0] < 0.90
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service-name}}",
              status=~"2.."}[2m])) /
            sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))

Blue-Green Deployment

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: web-frontend
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: web-frontend
  template:
    metadata:
      labels:
        app: web-frontend
    spec:
      containers:
        - name: frontend
          image: myorg/web-frontend:v3.0.0
          ports:
            - containerPort: 3000
  strategy:
    blueGreen:
      activeService: web-frontend-active
      previewService: web-frontend-preview
      autoPromotionEnabled: false
      scaleDownDelaySeconds: 300
      prePromotionAnalysis:
        templates:
          - templateName: smoke-tests
        args:
          - name: preview-url
            value: http://web-frontend-preview.production.svc
Analysis Templates: Argo Rollouts supports multiple metric providers for automated analysis — Prometheus, Datadog, New Relic, CloudWatch, Wavefront, Kayenta, and custom web hooks. Define success/failure conditions to automatically promote or rollback canary deployments based on real production metrics.

Managing & Deleting Applications

App-of-Apps Pattern

The app-of-apps pattern uses a parent Application to manage child Applications, enabling bootstrapping of entire platforms from a single root Application.

# Root Application (manages all other apps)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: platform-root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/platform-gitops.git
    targetRevision: main
    path: apps  # Contains Application manifests for each component
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      selfHeal: true
      prune: true
# Directory structure for app-of-apps
# platform-gitops/
# ├── apps/
# │   ├── cert-manager.yaml      # Application CR for cert-manager
# │   ├── external-dns.yaml      # Application CR for external-dns
# │   ├── ingress-nginx.yaml     # Application CR for ingress-nginx
# │   ├── monitoring.yaml        # Application CR for prometheus stack
# │   └── api-server.yaml        # Application CR for our API
# └── components/
#     ├── cert-manager/
#     │   └── kustomization.yaml
#     ├── external-dns/
#     │   └── values.yaml
#     └── monitoring/
#         └── kustomization.yaml

Deleting Applications

# Delete app AND its managed resources (cascade delete via finalizer)
argocd app delete guestbook --cascade

# Delete app but KEEP resources in cluster (remove finalizer first)
kubectl -n argocd patch app guestbook \
  --type json \
  -p '[{"op": "remove", "path": "/metadata/finalizers"}]'
kubectl -n argocd delete app guestbook

# Force-delete stuck application
argocd app delete stuck-app --cascade --force

Conclusion & What's Next

Argo CD transforms Kubernetes deployment from an imperative, error-prone process into a declarative, self-healing system. By implementing GitOps with Argo CD, you gain:

  • Auditability — Every deployment is a Git commit with full history and blame
  • Reproducibility — Any environment can be recreated from Git at any point in time
  • Drift Prevention — Self-heal ensures live state matches declared state
  • Multi-Tenancy — Projects and RBAC enforce team boundaries securely
  • Scalability — ApplicationSets manage hundreds of apps with minimal YAML
  • Safe Releases — Argo Rollouts enables canary and blue-green with automated analysis

Next in the Series

In Part 7: Continuous Integration with GitHub Actions, we'll build production-grade CI pipelines that test, build container images, update Git manifests, and trigger Argo CD syncs — completing the GitOps feedback loop from code commit to production deployment.