Skip to content

ArgoCD

ArgoCD is the cluster’s GitOps controller. Every addon under kubernetes/apps/ exists in the cluster because ArgoCD reconciles it from this repo. It is the last thing terraform/bootstrap/ installs and the first thing the operator interacts with after the bootstrap finishes.

ArgoCD cannot reconcile itself before it exists, and an empty cluster has no controller capable of installing it. Terraform owns the “hand the cluster off to ArgoCD” seam — see Platform / Why Terraform and ADR-0001 for the full rationale. After the first tofu apply, Terraform’s role in ArgoCD’s lifecycle is over: chart upgrades happen through a normal PR (see Self-management below).

Two directories, two responsibilities:

  • terraform/bootstrap/argocd.tf — the initial Helm release, the namespace, the GSM-backed repository credential, and the two Application manifests (self-managed + root).
  • kubernetes/bootstrap/argocd/ — the Helm values.yaml and the self-managed Application template. Both files are also referenced post-bootstrap: the self-managed Application reads values.yaml via $values, so a PR editing values.yaml reaches the cluster on the next ArgoCD sync without a Terraform run.
KnobValue
Namespaceargocd
Helm chartargo-proj/argo-cd (chart version 9.5.13)
UI hostnameargocd.lab.jackhall.dev
TLSTerminated at the lab Gateway, server.insecure: true inside the pod
Repo URLgit@github.com:RaptGroup/homelab.git (SSH; deploy key synced from GSM by ESO)
Tracked revisionmain

The HTTPRoute and Gateway attachment for the UI live under kubernetes/apps/argocd/, not in terraform/bootstrap/. Once ArgoCD is up, the routing layer in front of it is just another addon directory like any other.

ArgoCD watches an Application named argocd that points back at the same Helm chart Terraform bootstrapped it from. The chart version comes from terraform/bootstrap/variables.tf; the values come from kubernetes/bootstrap/argocd/values.yaml via the multi-source $values ref:

sources:
- repoURL: https://argoproj.github.io/argo-helm
chart: argo-cd
targetRevision: ${chart_version}
helm:
valueFiles:
- $values/kubernetes/bootstrap/argocd/values.yaml
- repoURL: ${repo_url}
targetRevision: ${target_revision}
ref: values

This is what makes the Terraform-vs-ArgoCD boundary work for the one service that lives on both sides: values changes are PRs, not Terraform applies. Bumping the ArgoCD chart version is the only edit that still requires a Terraform run, because the version is templated into the Application manifest at apply time.

A second Application — the root — watches the kubernetes/apps/ directory and turns every subdirectory into its own Application. This is the seam that makes “drop a directory and push” the entire workflow for adding an addon: no Terraform, no ApplicationSet generators, no UI clicks. The mechanism is plain app-of-apps, deliberately not ApplicationSet, because the directory-per-addon convention maps to one manifest per child app and is easier to read than the equivalent generator config.

  • UIhttps://argocd.lab.jackhall.dev, reachable from any device using AdGuard Home for DNS (see Split-horizon DNS). The UI is the canonical view of which Applications are healthy, what’s out of sync, and what each child sync touched.
  • Initial admin password — auto-generated into the argocd-initial-admin-secret Secret in the argocd namespace on first install. Read it once with kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d, log in, then rotate.
  • Homepage card — the dashboard shows ArgoCD app counts via the accounts.homepage API-key account configured in values.yaml. The account has role:readonly cluster-wide and is restricted to apiKey (no UI login), so a leaked token is bounded to read-only API access.

For every addon under kubernetes/apps/:

  1. The addon directory holds an application.yaml (and usually a helm-values.yaml).
  2. The root app-of-apps notices the new directory on the next sync and creates the child Application.
  3. The child Application syncs the addon’s manifests with ServerSideApply=true and CreateNamespace=true.

Two child Applications deserve special mention:

  • The repo credentialterraform/bootstrap/argocd.tf creates an ExternalSecret named argocd-repo-homelab that pulls the SSH deploy key from GSM into a labelled repository Secret. ArgoCD matches Applications to credentials by repo URL automatically; without this Secret the root app-of-apps can’t clone the repo.
  • controller.diff.server.side: true — set in values.yaml so diffs go through the kube-apiserver instead of ArgoCD’s bundled schema. Required because newer API-server fields (e.g. Deployment. status.terminatingReplicas, beta since k8s 1.32) aren’t in Argo’s shipped schema and would put any addon whose live Deployment populates them into ComparisonError. See issue #40 for the trigger.

terraform/bootstrap/README.md covers the run book — what to do on a partial apply, how to bump the chart version, and the tofu output argocd_initial_admin_secret shortcut for the first login.