Module 02 · Platform foundation · 45 min · You are: Platform team
Clusters as code with Terraform
In this module
- You learn: How clusters are created and registered by code: the inventory (
clusters/*.yaml) drives Terraform, which creates the clusters, installs Argo CD and registers dev and prod with labels. - You do: Request the change with an issue, propose the Terraform code by pull request, apply it from
main, check what it created, and open the Argo CD UI. - You end with: Three kind clusters: a hub running Argo CD, plus dev and prod. Argo CD knows dev and prod, with labels copied from your inventory. Argo CD deploys nothing yet.
The old way is a runbook: create a cluster, install Argo CD, copy a token, paste an IP. Each step is a chance to drift. Here the inventory decides which clusters exist and Terraform makes it so, including the labels Argo CD will later use to place products.
Open this module in the interactive course to track progress and switch tracks.
Change journey practised: Requested → Proposed → Validated → Previewed → Approved → Merged → Reconciled → Healthy
Concepts
- The bootstrap chain
- Inventory, then clusters, then Argo CD, then cluster registration, then (module 03) the root app. Terraform owns the chain up to the point where Git can take over.
- Inventory drives Terraform
infrastructure/kindreads everyclusters/*.yamlwithprovider: kind. Add a file andterraform applycreates the cluster.- Declarative registration
- A cluster is a Secret in the
argocdnamespace with the labelargocd.argoproj.io/secret-type: cluster. Terraform writes it, withfleet.gitops/environment,region,upgrade-wavelabels from the inventory. - Stable addresses
- kind nodes share a Docker network, so the hub reaches
dev-control-plane:6443by name. Rebuilding a cluster keeps the address; the inventory never changes.
Files to know
Open them in your editor or on GitHub; the bullets say what to look at.
infrastructure/kind/main.tf(reference, your repo): The local fleet in Terraform. Four blocks matter.infrastructure/modules/fleet-hub/main.tf(reference, your repo): What the hub needs before Git takes over. Works on any cluster.infrastructure/argocd/values-local.yaml(reference, your repo): Argo CD settings for a laptop.infrastructure/README.md(reference, your repo): How to use the Terraform, and what production does differently.
Steps
1. Request it
Open a Platform change issue: "Provision the local fleet with Terraform".
2. Propose the infrastructure code
validate.sh now also runs terraform fmt -check and terraform validate (it skips Terraform if you have not installed it, CI never skips).
cd ~/work/fleet-gitops && git switch main && git pull -q
git switch -c infrastructure
Copy the Terraform code from the reference:
git checkout reference/main -- infrastructure
Run all the checks, including Terraform formatting and validation. Expect them to pass:
scripts/validate.sh
git add -A && git commit -m "Infrastructure: kind fleet and Argo CD with Terraform"
git push -u origin HEAD
3. Merge, then apply from main
Apply what was reviewed: from main, never from a branch. Read the plan first. It should create 3 clusters, Argo CD and 2 cluster Secrets, and no root app yet.
Cloud track: the scripts are imperative so you can read every command; module 11 shows the Terraform route for clouds. Clusters cost money while they run.
cd ~/work/fleet-gitops && git switch main && git pull -q
cd infrastructure/kind
Download the Terraform providers it needs (once):
terraform init
Preview. plan changes nothing: it lists what Terraform would create. Read it. Expect 3 clusters, Argo CD, 2 cluster Secrets, and no root app.
terraform plan
Create it. This builds the clusters and installs Argo CD (a few minutes). Type yes when asked.
terraform apply
On AKS, EKS or GKE
cd ~/work/fleet-gitops && git switch main && git pull -q
cp infrastructure/cloud/aks/env.example infrastructure/cloud/aks/env # git-ignored
Open infrastructure/cloud/aks/env in your editor and fill in PREFIX, REGION and the other values; the file explains each one. Then read every command the script would run:
infrastructure/cloud/aks/up.sh --init-fleet-dir --skip-root-app --dry-run
Create the clusters (this costs money while they run):
infrastructure/cloud/aks/up.sh --init-fleet-dir --skip-root-app
up.sh wrote the real API servers into clusters/: propose them like any change.
git switch -c cloud-inventory && git add clusters
git commit -m "Inventory: aks endpoints" && git push -u origin HEAD
4. See what Terraform registered
Three short checks: the clusters exist, Argo CD knows about them, and your inventory files are where their labels came from.
1. Three clusters. One kube context per cluster: the hub that runs Argo CD, plus dev and prod.
kubectl config get-contexts | grep -E 'kind-platform|kind-dev|kind-prod'
2. Argo CD knows about dev and prod. Argo CD stores each cluster it can deploy to as a Secret labelled secret-type=cluster. Expect two, with labels such as fleet.gitops/environment=dev, region and upgrade-wave copied from your inventory files: that is how an ApplicationSet picks clusters.
kubectl --context kind-platform -n argocd get secrets -l argocd.argoproj.io/secret-type=cluster --show-labels
3. Terraform's view (kind track): the same clusters, as Terraform recorded them.
cd ~/work/fleet-gitops/infrastructure/kind && terraform output registered_clusters
5. Open the Argo CD UI
Same as in the Argo CD course: a port-forward to the hub's argocd-server. The port-forward is required: the UI and the argocd CLI both go through it, and later modules assume the clusters are up and it is running.
1. Print the admin password and copy it.
kubectl --context kind-platform -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo
2. In a new terminal, start the port-forward and leave it running for the whole course. If you close it, the UI and the CLI both stop working; start it again after a fresh session or a cluster restart.
kubectl --context kind-platform -n argocd port-forward svc/argocd-server 8080:443
3. Open https://localhost:8080. The browser warns Your connection is not private: Argo CD made its own certificate, which your computer does not trust. It is safe here, because the connection only reaches your own cluster through kubectl: Advanced, then Proceed to localhost (unsafe) (Firefox: Accept the Risk and Continue). Sign in as admin.
4. Open Settings, then Clusters: dev and prod are listed, with the same names as the Secrets above. The Applications page is empty: nothing tells Argo CD what to deploy yet.
Simplified for the workshop
Terraform state stays on your laptop and you apply by hand. In production, state is remote (S3, GCS, Azure Storage, Terraform Cloud),
planruns on the pull request andapplyruns from CI after approval, usually from a separateinfrastructure-liverepository.Argo CD reaches the kind clusters with the kind admin certificate. In production each spoke trusts a scoped identity: a ServiceAccount token at least, workload identity on managed Kubernetes (module 11).
Break it on purpose: Lose a cluster
The dev cluster disappears behind Terraform's back, as if a laptop restart or a cloud incident took it.
You practise: rebuilding a cluster from code alone, and using terraform plan to see what is wrong before you change anything.
1. Break it
Delete dev:
kind delete cluster --name dev
On AKS, EKS or GKE
# Cloud track: read along. Deleting a managed cluster takes 10+ minutes, and the
# scripts are not declarative: re-running up.sh recreates what is missing.
2. What you see
kubectl --context kind-dev get nodes fails: the cluster and its kube context are gone. In the Argo CD UI, Settings, then Clusters, dev can no longer be reached. Expected: two working clusters.
3. Fix it yourself
Bring dev back exactly as it was, from code only: no kind create, no hand edits. Done when kubectl --context kind-dev get nodes works and Argo CD reaches dev again.
Hint 1
Before you change anything, ask the tool that built dev what it thinks is different.
Hint 2
terraform plan in infrastructure/kind compares the code with reality: dev must be created again, and its cluster Secret updated with the new cluster's credentials.
The fix
Ask Terraform what is different. Expect dev to be created, and its cluster Secret to be updated:
cd ~/work/fleet-gitops/infrastructure/kind
terraform plan
Let the code put it back (type yes):
terraform apply
The cluster is back. Expect one node, Ready:
kubectl --context kind-dev get nodes
And Argo CD's registration of it is updated. Expect the Secret with its labels:
kubectl --context kind-platform -n argocd get secret cluster-dev --show-labels
On AKS, EKS or GKE
# Cloud track: re-run your cloud's up.sh; it recreates what is missing.
After you fixed it: the five questions
- What failed?
- A workload cluster disappeared.
- How was it detected?
terraform planshows the drift between code and reality. Argo CD shows the cluster as unreachable, and its Applications turn Unknown.- Who responds?
- The platform team.
- How is it corrected?
terraform apply: same name, same server address, fresh credentials in the cluster Secret.- How is it prevented?
- Nothing about a cluster lives only in the cluster: the inventory and Terraform can always rebuild it.
Quiz
- Where do the labels on an Argo CD cluster Secret come from here?
- Terraform derives them from clusters/*.yaml
- You add them yourself in the Argo CD UI, one cluster at a time, after each Terraform run
- Argo CD detects them from the cluster, reading the node labels and the cloud region
- Why does the plan in this module not create the root app?
- Terraform cannot create Applications, so the root app always has to be applied by hand
- root-app.yaml is not in the repo yet; the module skips the root app until it is
- kind creates the root app itself when the cluster starts, so Terraform leaves it alone
- Why
https://dev-control-plane:6443instead of the address in your kubeconfig?- It is shorter and easier to type, and the addresses in a kubeconfig change too often to be useful in a cluster Secret, so a name is the better habit
- Argo CD rejects IP addresses in a cluster Secret and accepts only names it can resolve in public DNS, which is why the container name is used here
- The hub runs in its own container; 127.0.0.1 would be itself. The container name is reachable on the kind network and stable across rebuilds
Answers
- Terraform derives them from clusters/*.yaml One source of truth: the inventory. Module 06 uses the environment label to place products.
- root-app.yaml is not in the repo yet; the module skips the root app until it is The fleet-hub module only applies root-app.yaml when it exists. You add it in module 03.
- The hub runs in its own container; 127.0.0.1 would be itself. The container name is reachable on the kind network and stable across rebuilds And the API server certificate is valid for that name, so TLS verification stays on.
Field note
Before this was Terraform, the most common workshop question was "my IPs changed after a reboot". Stable names and a declarative chain make a rebuild boring, which is exactly what you want.