Skip to main content

GitOps with ArgoCD: Deploy to Kubernetes the Right Way

March 24, 2026

If you are still SSH-ing into servers to run kubectl apply or relying on a CI pipeline that pushes deployments imperatively, you are doing Kubernetes the hard way. GitOps flips the model: your Git repository becomes the single source of truth, and a controller running inside the cluster continuously reconciles the actual state with the desired state.

ArgoCD is the most popular GitOps tool in the CNCF ecosystem, and for good reason. It gives you a declarative deployment engine, a slick UI, RBAC, multi-cluster support, and an ecosystem of extensions for progressive delivery and secrets management.

This guide walks you through everything — from core principles to a production-ready ArgoCD setup.

What Is GitOps, Really?

GitOps is an operational framework that applies DevOps best practices (version control, code review, CI/CD) to infrastructure automation. The term was coined by Weaveworks in 2017, but the ideas have matured significantly since then.

The Four Principles

PrincipleWhat It Means
DeclarativeThe entire system is described declaratively (YAML, Helm, Kustomize)
Versioned and immutableGit is the single source of truth; every change is a commit
Pulled automaticallyAgents inside the cluster pull desired state — no external push access needed
Continuously reconciledControllers detect drift and self-heal automatically

The key insight is the pull model. Traditional CI/CD pipelines push changes into the cluster, which means your CI system needs cluster credentials. With GitOps, the agent lives inside the cluster and pulls changes from Git. This dramatically reduces the attack surface.

GitOps vs Traditional CI/CD

Traditional CI/CD:
  Developer  Git Push  CI Build  CI Deploy (push to cluster)
                                      
                              CI needs cluster creds

GitOps:
  Developer  Git Push  CI Build  Update Git Manifests
                                          
                              ArgoCD (in-cluster) pulls & applies

The CI pipeline's job ends at building the image and updating the manifest repository. It never touches the cluster directly.

Setting Up ArgoCD on Kubernetes

Prerequisites

You need a running Kubernetes cluster (1.26+) and kubectl configured. For local development, kind or minikube work great.

# Create a kind cluster for testing
kind create cluster --name argocd-lab

# Verify connectivity
kubectl cluster-info

Install ArgoCD

ArgoCD provides a straightforward installation via manifests.

# Create namespace
kubectl create namespace argocd

# Install ArgoCD (stable release)
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=ready pod -l app.kubernetes.io/part-of=argocd -n argocd --timeout=300s

This installs several components:

  • argocd-server — the API server and web UI
  • argocd-repo-server — clones Git repos and generates manifests
  • argocd-application-controller — watches applications and syncs state
  • argocd-redis — caching layer
  • argocd-dex-server — SSO integration

Access the UI

# Port-forward the server
kubectl port-forward svc/argocd-server -n argocd 8080:443

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

Open https://localhost:8080 and log in with username admin and the password from above.

Install the CLI

# macOS
brew install argocd

# Linux
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd
sudo mv argocd /usr/local/bin/

# Authenticate
argocd login localhost:8080 --username admin --password <your-password> --insecure

Change the default password immediately:

argocd account update-password

Your First ArgoCD Application

Repository Structure

A well-organized GitOps repo separates application code from deployment manifests. Here is a typical structure:

gitops-repo/
├── apps/                    # ArgoCD Application manifests
   ├── api.yaml
   ├── web.yaml
   └── worker.yaml
├── base/                    # Base Kustomize manifests
   ├── api/
      ├── deployment.yaml
      ├── service.yaml
      └── kustomization.yaml
   └── web/
       ├── deployment.yaml
       ├── service.yaml
       └── kustomization.yaml
├── overlays/                # Environment-specific patches
   ├── dev/
      └── kustomization.yaml
   ├── staging/
      └── kustomization.yaml
   └── production/
       └── kustomization.yaml
└── README.md

Creating an Application Manifest

ArgoCD applications are defined as Kubernetes custom resources:

# apps/api.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: main
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: api
  syncPolicy:
    automated:
      prune: true        # Delete resources removed from Git
      selfHeal: true     # Revert manual changes in cluster
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Apply it:

kubectl apply -f apps/api.yaml

Sync Policies Explained

The syncPolicy section controls how ArgoCD behaves:

PolicyEffect
automatedSyncs automatically when Git changes are detected
prune: trueDeletes resources that no longer exist in Git
selfHeal: trueReverts manual kubectl changes back to Git state
retryRetries failed syncs with exponential backoff

For production, I recommend starting with automated disabled and using manual syncs until you build confidence. Then enable selfHeal first (to prevent drift), and prune last (to prevent accidental deletions).

The App of Apps Pattern

Managing dozens of Application manifests individually gets tedious. The "App of Apps" pattern uses a parent Application that manages child Applications:

# apps/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: main
    path: apps          # Points to the directory containing all app manifests
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      selfHeal: true
      prune: true

Now adding a new application is just adding a YAML file to the apps/ directory and pushing to Git.

Managing Secrets in GitOps

The biggest challenge with GitOps is secrets. You cannot commit plaintext secrets to Git. There are two mainstream solutions.

Option 1: Sealed Secrets

Bitnami Sealed Secrets encrypts secrets using a cluster-side controller. Only the controller can decrypt them, so the encrypted version is safe to commit.

# Install the controller
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.3/controller.yaml

# Install kubeseal CLI
brew install kubeseal

Encrypt a secret:

# Create a regular secret (don't apply it)
kubectl create secret generic db-creds \
  --from-literal=username=admin \
  --from-literal=password=supersecret \
  --dry-run=client -o yaml > secret.yaml

# Seal it
kubeseal --format yaml < secret.yaml > sealed-secret.yaml

# View the sealed secret (safe to commit)
cat sealed-secret.yaml

The sealed secret looks like this:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-creds
  namespace: default
spec:
  encryptedData:
    username: AgBy3i... # encrypted
    password: AgCtr8... # encrypted

Option 2: SOPS + Age

Mozilla SOPS encrypts specific values in YAML files using age keys. This integrates with ArgoCD via a plugin.

# Install age and sops
brew install age sops

# Generate an age key
age-keygen -o age-key.txt
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

# Create a SOPS config
cat > .sops.yaml << 'EOF'
creation_rules:
  - path_regex: .*secrets.*\.yaml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
EOF

# Encrypt a secrets file
sops --encrypt secrets.yaml > secrets.enc.yaml

Configure ArgoCD to decrypt SOPS files using a custom plugin:

# argocd-cm ConfigMap patch
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  configManagementPlugins: |
    - name: sops
      generate:
        command: ["sh", "-c"]
        args:
          - |
            for f in *.yaml; do
              if sops --decrypt "$f" > /dev/null 2>&1; then
                sops --decrypt "$f"
              else
                cat "$f"
              fi
            done

Which One Should You Pick?

CriteriaSealed SecretsSOPS + Age
Ease of setupSimplerMore configuration
Key managementCluster-managedYou manage keys
Multi-clusterTricky (keys per cluster)Easy (share age keys)
RotationRe-seal all secretsRe-encrypt with SOPS
Local developmentNeed cluster accessWorks offline

For most teams, Sealed Secrets is the faster path. For multi-cluster setups or teams that want more control, SOPS + Age scales better.

Progressive Delivery with Argo Rollouts

Deploying everything at once is risky. Argo Rollouts extends Kubernetes with canary deployments, blue-green deployments, and automated analysis.

Install Argo Rollouts

kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

# Install the kubectl plugin
brew install argoproj/tap/kubectl-argo-rollouts

Canary Deployment

Replace your Deployment with a Rollout:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api
spec:
  replicas: 5
  strategy:
    canary:
      steps:
        - setWeight: 10        # Send 10% traffic to canary
        - pause: { duration: 5m }
        - setWeight: 30
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 60
        - pause: { duration: 5m }
        - setWeight: 100
      canaryService: api-canary
      stableService: api-stable
      trafficRouting:
        istio:
          virtualService:
            name: api-vsvc
            routes:
              - primary
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: your-registry/api:v2.1.0
          ports:
            - containerPort: 8080

Automated Analysis

The real power of Argo Rollouts is automated rollback based on metrics:

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  metrics:
    - name: success-rate
      interval: 60s
      count: 5
      successCondition: result[0] >= 0.95
      failureLimit: 3
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{status=~"2.*", app="api", canary="true"}[5m]))
            /
            sum(rate(http_requests_total{app="api", canary="true"}[5m]))

This queries Prometheus every 60 seconds, checks that the success rate is at least 95%, and automatically rolls back if it fails 3 times.

The Rollout Flow

New image pushed to Git
        
ArgoCD detects change, syncs
        
Argo Rollouts creates canary (10% traffic)
        
Wait 5 minutes  promote to 30%
        
Run AnalysisTemplate (check error rate, latency)
        
  ┌─── PASS ───┐     ┌─── FAIL ───┐
                                 
Promote to 60%    Auto-rollback to stable
  
Wait 5 minutes
  
Promote to 100% (stable)

Multi-Cluster Management

ArgoCD supports deploying to multiple clusters from a single control plane.

# Add a remote cluster
argocd cluster add my-staging-cluster --name staging

# List registered clusters
argocd cluster list

Then reference the cluster in your Application:

spec:
  destination:
    server: https://staging-api.example.com  # Remote cluster API
    namespace: api

ApplicationSets for Multi-Cluster

ApplicationSet is a controller that generates Application resources from templates. This is incredibly useful for deploying the same app across environments:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: api
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - cluster: dev
            url: https://dev-api.example.com
            revision: develop
          - cluster: staging
            url: https://staging-api.example.com
            revision: main
          - cluster: production
            url: https://prod-api.example.com
            revision: v2.1.0   # Pin production to a tag
  template:
    metadata:
      name: 'api-{{cluster}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/your-org/gitops-repo.git
        targetRevision: '{{revision}}'
        path: 'overlays/{{cluster}}'
      destination:
        server: '{{url}}'
        namespace: api
      syncPolicy:
        automated:
          selfHeal: true

This generates three Application resources — one per cluster — each pointing at the correct overlay and Git revision.

ArgoCD vs FluxCD

Both are CNCF-graduated GitOps tools. Here is an honest comparison.

FeatureArgoCDFluxCD
UIRich web UI with dependency graphNo built-in UI (Weave GitOps adds one)
ArchitectureCentralized controllerDistributed toolkit (source, kustomize, helm, notification controllers)
Multi-tenancyProjects with RBACNamespace-scoped reconcilers
Helm supportRenders Helm in-clusterNative Helm controller
Multi-clusterBuilt-in, single pane of glassCluster API or manual setup
Progressive deliveryArgo Rollouts (native)Flagger (separate project)
Learning curveModerate (UI helps)Steeper (CLI-first)
Resource usageHigher (UI, Redis, Dex)Lower footprint
CommunityLarger (17k+ GitHub stars)Strong but smaller

When to Choose ArgoCD

  • You want a UI for visibility and onboarding
  • Multi-cluster management is a priority
  • Your team includes people who prefer clicking over kubectl
  • You want progressive delivery tightly integrated

When to Choose FluxCD

  • You prefer a lightweight, composable toolkit
  • You are already heavy on Helm and want native Helm OCI support
  • Resource constraints matter (edge deployments)
  • You want tighter integration with Terraform via the Flux Terraform controller

For most teams starting with GitOps, ArgoCD wins on developer experience. The UI alone saves hours of debugging sync issues. FluxCD is excellent for platform teams who want fine-grained control and minimal resource overhead.

Production Checklist

Before going live, make sure you have covered these items:

[ ] ArgoCD installed via Helm chart (not raw manifests) for easier upgrades
[ ] SSO configured (OIDC with your identity provider)
[ ] RBAC policies restricting who can sync to production
[ ] Resource tracking set to annotation-based (avoids label conflicts)
[ ] Notifications configured (Slack, Teams, or PagerDuty)
[ ] Secrets management solution deployed (Sealed Secrets or SOPS)
[ ] Repository credentials stored as Kubernetes secrets (not in ArgoCD UI)
[ ] Health checks defined for custom resources
[ ] Sync windows configured (prevent production deploys on Fridays)
[ ] Disaster recovery: ArgoCD backup with argocd-util export
[ ] Monitoring: ArgoCD metrics exported to Prometheus/Grafana

Sync Windows

Prevent deployments during risky periods:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
  namespace: argocd
spec:
  syncWindows:
    - kind: deny
      schedule: '0 0 * * 5'    # No deploys on Fridays
      duration: 24h
      clusters: ['*']
    - kind: deny
      schedule: '0 18 * * *'   # No deploys after 6 PM
      duration: 14h
      clusters: ['*']

Common Pitfalls

1. Putting app code and manifests in the same repo. Separate them. A commit to application code should not trigger a manifest sync, and vice versa.

2. Not using Kustomize or Helm. Raw YAML duplicated across environments leads to drift. Use overlays.

3. Forgetting about CRDs. If your app depends on CRDs, sync them first using sync waves:

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "-1"  # CRDs sync before the app

4. Ignoring resource limits on ArgoCD itself. The repo-server can consume significant memory when rendering large Helm charts. Set appropriate resource requests and limits.

5. Not monitoring ArgoCD. ArgoCD exposes Prometheus metrics on :8082/metrics. Set up alerts for sync failures and degraded health.

Wrapping Up

GitOps with ArgoCD gives you auditable, reproducible, self-healing deployments. Every change goes through a pull request, every deployment is a Git commit, and every rollback is a git revert.

The learning curve is real, but the payoff is enormous: fewer deployment failures, faster recovery, and a clear audit trail of every change that ever hit your cluster.

Start with a single application in a dev cluster. Get comfortable with sync policies and the UI. Then expand to multi-cluster with ApplicationSets. Add progressive delivery with Argo Rollouts when you are ready.

The days of kubectl apply -f yolo.yaml in production should be behind us.

Recommended Posts