GitOps with ArgoCD — Kubernetes CI/CD Done Right
You have a CI pipeline that builds your container image and runs tests. The last step runs kubectl apply -f manifests/ against the production cluster. It works, until someone SSH-es into the server and runs kubectl edit deployment to "hotfix" something. Now your Git repository says one thing and the cluster says another. Nobody knows what is actually running in production. This is the exact problem GitOps solves.
GitOps Principles
GitOps is an operational model where Git is the single source of truth for your infrastructure and application state. The four principles:
- Declarative — The entire system is described declaratively (Kubernetes YAML, Helm charts, Kustomize)
- Versioned and immutable — The desired state is stored in Git, giving you an audit trail and easy rollbacks
- Pulled automatically — An agent in the cluster pulls the desired state from Git (not pushed by CI)
- Continuously reconciled — The agent continuously compares desired state (Git) vs actual state (cluster) and corrects drift
The critical shift is from push-based (kubectl apply from CI) to pull-based (an agent inside the cluster watches Git and syncs).
ArgoCD Architecture
ArgoCD runs inside your Kubernetes cluster and watches one or more Git repositories. When the repo changes, ArgoCD detects the difference and syncs the cluster.
┌──────────────┐ watches ┌──────────────┐
│ Git Repo │ ◄────────────── │ ArgoCD │
│ (manifests) │ │ (in-cluster) │
└──────────────┘ └──────┬───────┘
│ applies
▼
┌──────────────┐
│ Kubernetes │
│ Cluster │
└──────────────┘
Key components:
- API Server — Exposes the ArgoCD API and Web UI
- Repo Server — Clones Git repos, renders manifests (Helm, Kustomize, plain YAML)
- Application Controller — Watches Applications, compares desired vs live state, syncs
- Redis — Caching layer for performance
Installing ArgoCD
# Create namespace and install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for all pods to be ready
kubectl wait --for=condition=ready pod -l app.kubernetes.io/part-of=argocd -n argocd --timeout=120s
# Get the initial admin password
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d
# Access the UI
kubectl port-forward svc/argocd-server -n argocd 8443:443
# Open https://localhost:8443 — login with admin/<password>
Install the ArgoCD CLI for command-line management:
# Install CLI (macOS)
brew install argocd
# Login to ArgoCD
argocd login localhost:8443 --username admin --password <password> --insecure
# Change the default password
argocd account update-password
The Application CRD
An ArgoCD Application is a Kubernetes custom resource that defines which Git repository to watch, which path contains the manifests, and which cluster/namespace to deploy to.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: apps/payment-service/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Revert manual changes in cluster
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Sync Policies Explained
| Policy | Behavior | When to Use |
|---|---|---|
| Manual sync | ArgoCD detects drift but waits for you to click Sync | Production with change approval |
| Automated sync | ArgoCD syncs automatically when Git changes | Dev/staging environments |
| Self-heal | Reverts manual kubectl edit changes back to Git state | Preventing drift in production |
| Prune | Deletes resources that were removed from Git | Keeping cluster clean |
For production, a common pattern is automated sync with self-heal but without prune — this prevents accidental resource deletion if someone removes a file from Git by mistake.
App of Apps Pattern
Managing dozens of Applications individually does not scale. The App of Apps pattern uses one "root" Application that points to a directory containing other Application manifests.
k8s-manifests/
├── apps/
│ ├── payment-service/
│ ├── frontend/
│ └── user-service/
└── argocd/
├── root-app.yaml ← Root Application
└── apps/
├── payment-service.yaml ← Application CR for payment
├── frontend.yaml ← Application CR for frontend
└── user-service.yaml ← Application CR for users
# argocd/root-app.yaml — The root Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: argocd/apps # Directory containing Application CRs
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
selfHeal: true
prune: true
Add a new service by creating a new Application YAML in argocd/apps/. The root app syncs it automatically.
ApplicationSet for Multi-Cluster
When you manage multiple clusters or environments, ApplicationSet generates Application CRs from templates:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payment-service-all-envs
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: dev
url: https://dev-cluster.example.com
revision: develop
- cluster: staging
url: https://staging-cluster.example.com
revision: main
- cluster: production
url: https://prod-cluster.example.com
revision: main
template:
metadata:
name: 'payment-service-{{cluster}}'
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: '{{revision}}'
path: 'apps/payment-service/overlays/{{cluster}}'
destination:
server: '{{url}}'
namespace: production
syncPolicy:
automated:
selfHeal: true
This creates three Application CRs — one for each environment — from a single template.
Kustomize and Helm Integration
ArgoCD natively supports both Kustomize and Helm. It detects the tool based on files in the path.
Kustomize (detects kustomization.yaml):
source:
repoURL: https://github.com/myorg/k8s-manifests.git
path: apps/payment-service/overlays/production
# ArgoCD auto-detects Kustomize and runs `kustomize build`
Helm (detects Chart.yaml):
source:
repoURL: https://github.com/myorg/helm-charts.git
path: charts/payment-service
helm:
valueFiles:
- values-production.yaml
parameters:
- name: image.tag
value: "v2.5.1"
- name: replicas
value: "3"
Secrets Handling — Sealed Secrets
You cannot store raw Kubernetes Secrets in Git. Sealed Secrets encrypts them so only the cluster can decrypt:
# Install Sealed Secrets controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# Install kubeseal CLI
brew install kubeseal
# Create a regular secret, then seal it
kubectl create secret generic db-creds \
--from-literal=username=admin \
--from-literal=password=s3cret \
--dry-run=client -o yaml > secret.yaml
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# sealed-secret.yaml is safe to commit to Git
# Only the controller in the cluster can decrypt it
ArgoCD vs Flux Comparison
| Feature | ArgoCD | Flux v2 |
|---|---|---|
| UI | Built-in web UI | No built-in UI (use Weave GitOps) |
| Multi-cluster | Native support | Via Kustomization controllers |
| Helm support | Renders in repo-server | HelmRelease CRD |
| Image automation | Argo CD Image Updater | Built-in Image Reflector |
| RBAC | Built-in with SSO | Uses K8s RBAC |
| Learning curve | Moderate | Steeper (more CRDs) |
| App of Apps | Native pattern | Kustomization dependencies |
| Community | Larger (CNCF Graduated) | Active (CNCF Graduated) |
Both are excellent. ArgoCD has a better UI and is easier to start with. Flux is more composable and integrates deeper with Kubernetes primitives. Most teams pick ArgoCD for its visual feedback loop.
Wrapping Up
GitOps with ArgoCD transforms your deployment workflow from imperative kubectl apply commands to a declarative, auditable, Git-driven process. The cluster continuously reconciles itself to match Git, manual drift is automatically corrected, and every change is a Git commit that can be reviewed, approved, and rolled back.
But ArgoCD deploys what you give it — standard Kubernetes resources. For truly complex stateful applications like databases and message queues, you need something more intelligent. In the next post, we will explore Kubernetes Operators — encoding operational knowledge into code that manages complex applications automatically.
