DEV Community

jesus manrique
jesus manrique

Posted on • Originally published at guayoyo.tech

Zero to Kubernetes Part 3: ArgoCD, GitOps and App-of-Apps

K8s Terraform ArgoCD — Header

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
└──────────────┘
Enter fullscreen mode Exit fullscreen mode

The three properties that differentiate GitOps from a deploy script:

  1. Reconciliation loop: Every 3 minutes ArgoCD compares the cluster against git. If there's drift, it corrects it.
  2. Self-healing: Someone deleted a Deployment by mistake → ArgoCD recreates it.
  3. 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]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The three flags that matter:

  • prune: true → If you delete apps/backend/deployment.yaml from the repo, ArgoCD deletes the Deployment from the cluster. Without this, the resource stays orphaned forever.
  • selfHeal: true → If someone runs kubectl edit deployment backend and 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 -
Enter fullscreen mode Exit fullscreen mode

Label the secret so ArgoCD uses it:

kubectl label secret github-repo-creds -n argocd \
  argocd.argoproj.io/secret-type=repository
Enter fullscreen mode Exit fullscreen mode

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"]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)