Series: Zero to Kubernetes — Part 1 · Part 2 · Part 3 · Part 4 · Part 5
So far we've built the platform with Terraform. But Terraform isn't a CD pipeline — running terraform apply every time a container image changes is torture. This is where ArgoCD comes in.
ArgoCD implements GitOps: your cluster's desired state lives in a git repository. ArgoCD monitors that repo and ensures the cluster reflects exactly what's committed. If someone manually runs kubectl edit, ArgoCD reverts it. If a deploy fails, ArgoCD tells you exactly which resource is broken and why.
GitOps in 30 Seconds
┌──────────┐ git push ┌──────────┐
│ GitHub │◄──────────────│ You │
│ (repo) │ └──────────┘
└────┬─────┘
│ ArgoCD reads repo every 3 min
▼
┌──────────┐ sync ┌──────────────────┐
│ ArgoCD │───────────►│ k8s cluster │
│ (inside │◄───────────│ (your apps) │
│ cluster) │ status └──────────────────┘
│
▼ (optional UI at argocd.yourdomain.com)
┌──────────────┐
│ 🌳 App tree │ → see diff, health, logs
│ │ → manual or auto sync
│ │ → rollback by reverting commit
└──────────────┘
The three properties that differentiate GitOps from a deploy script:
- Reconciliation loop: Every 3 minutes ArgoCD compares the cluster against git. If there's drift, it corrects it.
- Self-healing: Someone deleted a Deployment by mistake → ArgoCD recreates it.
- Auditability: Every change to the cluster has an associated commit. You know who, what, when, and why.
Step 1: Install ArgoCD with Terraform
resource "helm_release" "argocd" {
name = "argocd"
namespace = kubernetes_namespace.argocd.metadata[0].name
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
version = "7.6.7"
set {
name = "server.service.type"
value = "ClusterIP"
}
set {
name = "server.ingress.enabled"
value = "true"
}
set {
name = "server.ingress.hosts[0]"
value = "argocd.yourdomain.com"
}
set {
name = "server.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/ssl-redirect"
value = "true"
}
set {
name = "server.ingress.annotations.cert-manager\\.io/cluster-issuer"
value = "letsencrypt-production"
}
set {
name = "server.ingress.tls[0].hosts[0]"
value = "argocd.yourdomain.com"
}
set {
name = "server.ingress.tls[0].secretName"
value = "argocd-tls"
}
set {
name = "server.extraArgs[0]"
value = "--insecure"
}
depends_on = [helm_release.cert_manager]
}
Access the UI at https://argocd.yourdomain.com. Get the initial admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
Step 2: GitOps Repository Structure
Create a separate repo (or a directory in the monorepo) with this structure:
gitops/
├── apps/
│ ├── backend/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── ingress.yaml
│ │ ├── configmap.yaml
│ │ └── kustomization.yaml
│ ├── frontend-app1/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── ingress.yaml
│ │ ├── configmap.yaml
│ │ └── kustomization.yaml
│ ├── frontend-app2/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── ingress.yaml
│ │ ├── configmap.yaml
│ │ └── kustomization.yaml
│ └── database/
│ ├── postgresql-cluster.yaml
│ └── kustomization.yaml
└── clusters/
└── production/
└── app-of-apps.yaml ← THE MASTER KEY
Step 3: App-of-Apps — the Pattern That Scales Everything
Instead of creating 15 Applications manually in ArgoCD, you create one root Application that points to apps/. Each subdirectory automatically becomes an Application. Adding a new app is creating a folder. Without touching ArgoCD.
clusters/production/app-of-apps.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-of-apps
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-org/gitops-repo
targetRevision: main
path: apps
directory:
recurse: true
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true # deletes resources you removed from the repo
selfHeal: true # reverts manual changes on the cluster
syncOptions:
- CreateNamespace=true
The three flags that matter:
-
prune: true→ If you deleteapps/backend/deployment.yamlfrom the repo, ArgoCD deletes the Deployment from the cluster. Without this, the resource stays orphaned forever. -
selfHeal: true→ If someone runskubectl edit deployment backendand changes replicas from 2 to 5, ArgoCD reverts it on the next reconciliation (≤3 min). -
CreateNamespace=true→ If the namespace doesn't exist, it creates it. Prevents the "namespace not found" error.
Step 4: Register the Repo in ArgoCD
ArgoCD needs read access to the repo. Use a GitHub token with repo scope:
kubectl create secret generic github-repo-creds \
-n argocd \
--from-literal=url=https://github.com/your-org/gitops-repo \
--from-literal=username=gitops-bot \
--from-literal=password=ghp_yourGitHubToken \
--from-literal=type=git \
--dry-run=client -o yaml | kubectl apply -f -
Label the secret so ArgoCD uses it:
kubectl label secret github-repo-creds -n argocd \
argocd.argoproj.io/secret-type=repository
Step 5: Create the Root Application
The app-of-apps is created just once. You can do it with Terraform or directly:
resource "kubernetes_manifest" "app_of_apps" {
manifest = {
apiVersion = "argoproj.io/v1alpha1"
kind = "Application"
metadata = {
name = "app-of-apps"
namespace = "argocd"
}
spec = {
project = "default"
source = {
repoURL = "https://github.com/your-org/gitops-repo"
targetRevision = "main"
path = "apps"
directory = { recurse = true }
}
destination = {
server = "https://kubernetes.default.svc"
namespace = "argocd"
}
syncPolicy = {
automated = { prune = true, selfHeal = true }
syncOptions = ["CreateNamespace=true"]
}
}
}
}
Step 6: See the App Tree
Within seconds of applying, the ArgoCD UI shows:
🌳 app-of-apps
├── 🟢 backend (Healthy · Synced)
├── 🟢 frontend-app1 (Healthy · Synced)
├── 🟢 frontend-app2 (Healthy · Synced)
└── 🟢 database (Healthy · Synced)
Green circles mean the resource is Healthy (pods running, probes passing) and Synced (cluster matches git).
If something fails:
- Yellow (Progressing): Pods starting up, health checks not yet passing.
- Red (Degraded): CrashLoopBackOff, ImagePullBackOff, probes failing.
- OutOfSync: Someone changed something manually → ArgoCD marks it for correction or auto-corrects it (selfHeal).
Step 7: ArgoCD CLI — for Terminal Fans
brew install argocd
argocd login argocd.yourdomain.com --username admin --password ...
argocd app list
argocd app diff backend
argocd app sync backend # manual sync if not using auto-sync
argocd app rollback backend # revert via git commit is better
argocd app logs backend --tail=50
There's also a VS Code extension that shows ArgoCD status in the editor. Very useful when coding and wanting to see if the deploy passed without switching windows.
The Daily Workflow with GitOps
1. Change something in app code (main.py, App.tsx, etc.)
2. Docker build → new image (ghcr.io/your-org/backend:v1.2.3)
3. Update gitops/apps/backend/deployment.yaml:
image: ghcr.io/your-org/backend:v1.2.3
4. git add && git commit && git push
5. ArgoCD detects the change → auto sync
6. If something fails → rollback: git revert && git push
The entire deploy history is the git history. All rollbacks are git revert. There's no "rollback procedure" — it's the same procedure as any code change.
What You Learned in This Part
- GitOps as a philosophy: git is the single source of truth for the cluster
- ArgoCD installed with Terraform, exposed via ingress with TLS
- The app-of-apps pattern: one root Application managing N applications
- syncPolicy: prune + selfHeal + auto-sync for automated deployments
- How ArgoCD detects drift and corrects it without human intervention
- ArgoCD CLI and UI for debugging
- The complete flow: git push → ArgoCD sync → deploy in seconds
In Part 4, we'll deploy the PostgreSQL database and the backend API. With real code, health checks, automatic migrations, and properly managed secrets.
At Guayoyo Tech, we implement GitOps for teams still doing manual deploys. ArgoCD, Flux, CI/CD pipelines — we help you go from "it works on my machine" to "it's in production and fixes itself." Talk to us free for 15 minutes and we'll show you how.

Top comments (0)