Here is a deployment pattern we see constantly in startup audits: a developer runs kubectl apply -f k8s/ from their laptop to deploy a fix, the change is not tracked anywhere, and two weeks later nobody knows what version is actually running in production.
GitOps fixes this at the source. The Git repository becomes the authoritative record of cluster state. The cluster reconciles itself to match. No manual kubectl commands. No configuration drift.
What GitOps Actually Is
GitOps is a pattern, not a tool. The principles:
- •Declarative - your desired cluster state is described in files, not imperative commands
- •Versioned - everything lives in Git with full history
- •Automated - a system continuously reconciles actual cluster state with the Git-declared state
- •Observable - when actual state drifts from desired state, the system alerts you
ArgoCD implements this pattern for Kubernetes. It watches Git repositories and automatically applies changes to clusters when the repository changes.
Why ArgoCD Over Push-Based CD
Most CI/CD pipelines are push-based: your pipeline runs kubectl apply or helm upgrade. The problems:
- •Your pipeline needs cluster credentials - now a compromised CI server can modify production
- •There is no continuous reconciliation - if someone manually modifies a resource, no one knows
- •No live view of what version is actually deployed vs what should be deployed
ArgoCD is pull-based. The cluster pulls from Git. CI only needs write access to Git, not to the cluster.
Installation
bashkubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml # Wait for ArgoCD to be ready kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=120s # Get initial admin password kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
For production, install via Helm with HA and ingress:
bashhelm repo add argo https://argoproj.github.io/argo-helm helm upgrade --install argocd argo/argo-cd \ --namespace argocd \ --create-namespace \ --set server.ingress.enabled=true \ --set server.ingress.hostname=argocd.yourdomain.com \ --set server.ingress.ingressClassName=nginx \ --set server.ingress.tls=true \ --set redis-ha.enabled=true \ --set controller.replicas=1 \ --set server.replicas=2 \ --set repoServer.replicas=2
Repository Structure
The most important decision in GitOps is how you structure your repository. Two common patterns:
App-of-Apps (Recommended)
One root ArgoCD Application that manages all other Applications:
├── argocd/
│ ├── root.yaml # The root Application
│ └── apps/
│ ├── api.yaml # Application for your API
│ ├── worker.yaml # Application for your worker
│ └── monitoring.yaml # Application for Prometheus/Grafana
├── apps/
│ ├── api/
│ │ ├── Chart.yaml
│ │ ├── values.yaml
│ │ └── values-prod.yaml
│ └── worker/
│ ├── Chart.yaml
│ └── values.yaml
└── infrastructure/
├── cert-manager/
├── external-dns/
└── ingress-nginx/
Root Application
yaml# argocd/root.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: root namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://github.com/your-org/k8s-config targetRevision: HEAD path: argocd/apps destination: server: https://kubernetes.default.svc namespace: argocd syncPolicy: automated: prune: true # delete resources removed from Git selfHeal: true # revert manual changes
Individual Application
yaml# argocd/apps/api.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: api namespace: argocd spec: project: default source: repoURL: https://github.com/your-org/k8s-config targetRevision: HEAD path: apps/api helm: valueFiles: - values.yaml - values-prod.yaml destination: server: https://kubernetes.default.svc namespace: production syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true retry: limit: 3 backoff: duration: 5s maxDuration: 3m factor: 2 ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/replicas # ignore HPA-managed replica counts
The CI/CD Flow
With GitOps, CI and CD are separated:
CI (stays the same):
- •Developer opens PR
- •Tests run, image builds
- •Image pushed to registry with
sha256tag:api:abc123def - •PR merged to main
CD (GitOps-specific):
- •CI pipeline updates the image tag in the Git repository:
bash# In your CI pipeline - update image tag in values file yq -i '.image.tag = "${{ github.sha }}"' apps/api/values-prod.yaml git config user.email "[email protected]" git config user.name "CI Bot" git add apps/api/values-prod.yaml git commit -m "chore: deploy api@${{ github.sha }}" git push
- •ArgoCD detects the change and applies it
- •Deployment rolls out with the new image
The cluster credentials never touch your CI system.
Handling Secrets in ArgoCD
ArgoCD reconciles from Git. Secrets should not be in Git. Use one of these patterns:
Option A: External Secrets Operator (simplest) ESO syncs secrets from AWS Secrets Manager into K8s Secrets before ArgoCD applies your app. ArgoCD sees a normal K8s Secret reference.
Option B: Sealed Secrets
Encrypt your secrets with a public key. Store the encrypted SealedSecret in Git. The in-cluster controller decrypts it.
bash# Install Sealed Secrets controller helm upgrade --install sealed-secrets sealed-secrets/sealed-secrets -n kube-system # Seal a secret kubectl create secret generic api-db \ --from-literal=password=supersecret \ --dry-run=client -o yaml | kubeseal -o yaml > k8s/sealed-secret.yaml # Commit the sealed secret - it's safe to store in Git git add k8s/sealed-secret.yaml git commit -m "Add sealed database secret"
Rollback
Rollback is a Git operation. Find the commit with the previous image tag and revert:
bash# Option 1: Revert the image tag commit in Git git revert <commit-sha> git push # ArgoCD detects the revert and rolls back # Option 2: Rollback in ArgoCD UI or CLI argocd app rollback api --revision 3
ArgoCD maintains a history of syncs with the Git revision used for each. Rollback to any previous sync point in seconds.
What to Monitor
After setting up ArgoCD, watch for:
- •OutOfSync applications - something in the cluster diverged from Git (manual change, failed sync)
- •Degraded applications - pod crashes, image pull failures
- •Failed syncs - Git webhook failures, resource validation errors
ArgoCD exposes Prometheus metrics. Add alerts:
yaml# Alert when any app is out of sync for more than 5 minutes - alert: ArgocdAppOutOfSync expr: argocd_app_info{sync_status!="Synced"} > 0 for: 5m labels: severity: warning annotations: summary: "ArgoCD app {{ $labels.name }} is out of sync"
The Result
After implementing GitOps with ArgoCD:
- •Every production change has a Git commit behind it
- •Configuration drift is detected and auto-healed within minutes
- •Rollback takes 30 seconds
- •Your CI pipeline does not hold production credentials
- •The ArgoCD UI gives you a real-time map of what is running vs what should be running
Want to migrate from push-based deployments to GitOps? Book a free audit - we will review your current pipeline and design the migration plan.