PlatformEngr® Fleet GitOps course

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/kind reads every clusters/*.yaml with provider: kind. Add a file and terraform apply creates the cluster.
Declarative registration
A cluster is a Secret in the argocd namespace with the label argocd.argoproj.io/secret-type: cluster. Terraform writes it, with fleet.gitops/environment, region, upgrade-wave labels from the inventory.
Stable addresses
kind nodes share a Docker network, so the hub reaches dev-control-plane:6443 by 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.

Steps

1. Request it

Open a Platform change issue: "Provision the local fleet with Terraform".

Open the issue form ↗

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

Open the pull request ↗

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), plan runs on the pull request and apply runs from CI after approval, usually from a separate infrastructure-live repository.

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 plan shows 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

  1. 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
  2. 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
  3. Why https://dev-control-plane:6443 instead 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
  1. Terraform derives them from clusters/*.yaml One source of truth: the inventory. Module 06 uses the environment label to place products.
  2. 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.
  3. 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.

Report a problem with this module