Skip to content

Argo CD

Argo CD is the only thing that writes to the cluster. Every namespace, every Helm release, every Deployment, every Secret is the output of an Application sync — auto-sync, auto-prune, and self-heal are on by default, so anything not in this repo gets reverted within minutes.

One GKE cluster (gke-tomoda), three logical tiers wired up by Argo CD: platform (cluster-wide system services), dev (the dev tenant data plane), prod (the prod tenant data plane). See Environments for the full tenancy model and tier sizing.

For the underlying GCP service account, IAM bindings, and how the Argo CD Helm release itself gets installed, see Infrastructure → GCP → Argo CD. That layer is Terraform-managed (infrastructure/gcp/argocd.tf); this page covers the GitOps layer on top.

Bootstrap

One file, one command, applied once after terraform apply brings up the cluster + Argo CD:

gcloud container clusters get-credentials gke-tomoda \
  --zone us-central1-a --project development-485000

kubectl apply -f k8s/envs/bootstrap.yaml

k8s/envs/bootstrap.yaml creates three top-level Applications: platform, dev, prod. Each recurses through its respective directory under k8s/envs/ and picks up the sub-Applications it finds.

Reconciliation takes 5–10 minutes the first time. Watch progress:

kubectl get applications -n argocd -w

To skip a tier (rare — e.g. scratch cluster with no prod tenant), apply the bootstrap and then delete the unwanted top-level app:

kubectl delete application dev -n argocd     # or: prod

platform is required — uninstalling it takes down ingress, TLS, monitoring, and secret syncing for every tenant.

See Operations → Bootstrap for the full cluster-rebuild walkthrough.

The three apps

kubectl apply -f k8s/envs/bootstrap.yaml
↓ creates three top-level Application CRs
Application: platform
Application: dev
Application: prod
directory.recurse: true — each app discovers sub-Applications
platform/* sub-Apps traefik, cert-manager, monitoring, …
dev/* sub-Apps postgres, redis, tomoda overlay
prod/* sub-Apps postgres, redis, tomoda overlay
App-of-apps: one bootstrap file creates three parents; each parent recurses into its directory and picks up the children it finds. After this, nothing is kubectl apply'd by hand — Argo CD owns reconciliation.
App Source path Scope Skippable?
platform k8s/envs/platform/ Cluster-wide system services — required by both tenants No
dev k8s/envs/dev/ Dev tenant data plane (1 Gi PVCs, no backups) Yes — kubectl delete application dev -n argocd
prod k8s/envs/prod/ Prod tenant data plane (10 Gi PVCs, daily backups) Yes — rarely useful

What each contains

platform brings up cert-manager, External Secrets Operator, Traefik, External-DNS, oauth2-proxy, the Prometheus / Grafana / Alertmanager stack, Loki, Tempo, Pyrra (SLO controller), blackbox-exporter, cloudflared, Photon + Photon-indexer, the ARC controller + runner pool, and Trivy Operator (runtime image CVE scanning). Each subdirectory under k8s/envs/platform/ carries its own Application CR. See System services for per-service docs.

dev brings up postgres-dev (CNPG), redis-dev (Bitnami), pgAdmin, RedisInsight, and the tomoda dev overlay. Sized to validate changes cheaply — see Environments → Tier sizing.

prod brings up postgres-prod, redis-prod, pgAdmin, RedisInsight, and the tomoda prod overlay. Real PVCs, daily Postgres backups, HPA-driven app scale-out.

Recurse + exclude

Each top-level app uses directory.recurse: true plus an exclude pattern so sub-Applications don't double-apply manifests their child apps already own:

App exclude pattern Why
platform manifests/** The manifests/ subtree (alerting rules, ESO configs, dashboards, image-updater config, SLOs) is owned by the sys-resources Application — applied separately so Helm-installed CRDs land first
dev **/manifests.yaml Sub-apps (postgres, redis, etc.) own their own manifests.yaml via their own Application
prod **/manifests.yaml Same as dev

Image Updater

argocd-image-updater runs in the argocd namespace (manifest at k8s/envs/platform/manifests/image-updater.yaml) and:

  • Authenticates to Artifact Registry via Workload Identity (KSA argocd-image-updater → GSA argocd-image-updater-sa@development-485000.iam.gserviceaccount.com).
  • Watches the dev tomoda images in tomoda-dev-repo.
  • When a new tag matches the configured constraint, writes a Kustomize image-tag update back into this repo. Argo CD then reconciles the dev tomoda Application normally.

Prod's Application/tomoda-prod is intentionally not annotated with argocd-image-updater.argoproj.io/*. Prod tag bumps require a manual scripts/release.sh invocation — the explicit gate between dev and prod. See Operations → Deploy for the promotion workflow.

OAuth gating on the UI

The Argo CD UI is exposed at argo-app.tomoda.life, fronted by Traefik with two layers of auth:

  1. oauth2-proxy (Traefik middlewares on the Ingress at k8s/envs/platform/argocd-ingress/ingress.yaml) — gates the host at the edge. Only @tomoda.life Google accounts get past it.
  2. Argo CD Dex — once past oauth2-proxy, Dex maps the Google identity into an Argo CD RBAC role.

Sign in once with Google; oauth2-proxy plants a .tomoda.life cookie and Dex picks up the identity. See oauth2-proxy for proxy-side details.

RBAC and access hardening

Two Helm values in infrastructure/gcp/argocd.tf (configs.cm / configs.rbac) lock down what an authenticated user can do:

  • exec.enabled = "false" — the browser Web Terminal (exec into pods from the Argo UI) is disabled. No shell into workloads through Argo CD.
  • policy.default = "role:readonly" — the default SSO policy is read-only. Every mapped Google identity can view but not mutate. Write and admin actions require an explicit grant in policy.csv.

Sync workflow

  1. Edit and push to main.
  2. Argo CD polls (default ~3 min) and detects the diff. Click Refresh in the UI to force an immediate poll.
  3. Sync runs automatically — pruned resources deleted, new resources created, drifted resources patched.
  4. Self-heal pins the cluster to whatever is in Git.

Manual sync is still available — argocd app sync <name> or the UI's Sync button — useful when you don't want to wait for the next poll. No kubectl apply is needed after the initial bootstrap.