PlatformEngr® Terraform course

Module 00 · Orientation · 20 min · You are: Platform and product

Orientation: what a platform team keeps in Terraform

In this module

  • You learn: What Terraform does (you describe what should exist, it works out the calls that get you there and remembers what it made), what a platform team keeps in it, and how this course is laid out.
  • You do: Tell the course your GitHub name and which command you use, check your tools, create your repo tf-platform, create a one-node cluster, and run terraform init on a configuration that has no resources yet.
  • You end with: A cluster kind-tf with one Ready node. Your repo on GitHub holds cluster/kind.yaml, .gitignore and envs/dev/versions.tf. terraform init succeeds in envs/dev. Nothing is managed yet.

A platform team gives every product team the same starting kit: a namespace, a budget, permissions. Done by hand, the fourth team's kit differs from the first, nobody remembers why, and nobody dares to change any of them. Terraform turns the kit into code with a record of what it created, so a change is a diff a colleague can read before anything happens. You start with the smallest possible configuration, so the first thing you see is what init does on its own. You play the platform team, and now and then team-checkout, a product team.

Open this module in the interactive course to track progress and switch tracks.

Concepts

Desired state in files
.tf files say what should exist, not how to get there. Terraform compares them with what it already manages and proposes the steps.
Provider
A plugin that knows one API. The kubernetes provider turns a resource block into calls to the cluster's API server. Terraform itself knows no API at all.
State
Terraform's record of what it created, and which block each object belongs to. Without it, Terraform cannot tell "mine" from "someone else's".
Root module
One directory of .tf files that you run Terraform in. It has one state. Your first one is envs/dev.
Terraform and OpenTofu
Two programs that read the same language. This course works with both. You pick one command and keep it for the whole repo.

Who owns what in this repo

PathWhat it isOwnerModule
modules/team-namespace/The kit every team getsPlatform team04
envs/dev/Which teams exist in dev, and their sizesPlatform team; product teams propose changes by pull request01
envs/prod/The same for prodPlatform team only05
state/The store that holds the statePlatform team03
.github/What runs on a pull request, and who must reviewPlatform team05, 06

Terraform and OpenTofu, where they differ in this course

TerraformOpenTofu
Commandterraformtofu
Provider address in the lock fileregistry.terraform.io/hashicorp/kubernetesregistry.opentofu.org/hashicorp/kubernetes
CIhashicorp/setup-terraformopentofu/setup-opentofu

Pick one per repo: switching rewrites the lock file.

Files to know

Where you are heading, in two minutes. These files from the reference repo describe the finished setup. Click Quick look on each: it opens the real file right here, with numbered circles on the lines that matter.

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Tell the course who you are

Your GitHub user (or organisation) and a folder for your repo and the reference clone. Every command and link uses them. They stay in your browser.

Set your GitHub user and workspace folder in the interactive course or the form at the top of the notes index; these notes then show your values.

2. Pick your command

Terraform or OpenTofu. Every step is the same; only the word you type changes. If you have neither, install Terraform.

These notes show Terraform commands, with the OpenTofu variants folded underneath.

3. Check your tools

Paste this into a terminal. Each tool reports ok (with its version), MISSING (with where to get it) or PROBLEM (installed but not usable yet). Install what is missing your usual way, then run it again until it says All set. Versions: Terraform or OpenTofu 1.11 or newer (the course is written on Terraform 1.17.2 and OpenTofu 1.13.9), kind 0.33 or newer, Docker with 4 GB of memory.

check() {  # tool, works-check, version, problem, where-to-get-it
  if ! command -v "${1%% *}" >/dev/null 2>&1; then printf '  MISSING   %-22s install it: %s\n' "$1" "$5"; bad=$((bad+1))
  elif ! eval "$2" >/dev/null 2>&1; then printf '  PROBLEM   %-22s %s\n' "$1" "$4"; bad=$((bad+1))
  else printf '  ok        %-22s %s\n' "$1" "$(eval "$3" 2>&1 | head -n 1)"; fi
}
bad=0
check 'git' 'git --version' 'git --version' 'installed, but it does not run' 'https://git-scm.com/downloads'
check 'docker' 'docker info' "docker version --format '{{.Server.Version}}'" 'installed, but the Docker daemon is not running: start Docker Desktop (or the docker service)' 'https://docs.docker.com/get-docker/'
check 'docker compose' 'docker compose version' 'docker compose version' 'the Compose plugin is missing: install Docker Compose v2' 'https://docs.docker.com/compose/install/'
check 'kind' 'kind version' 'kind version' 'installed, but it does not run' 'https://kind.sigs.k8s.io/docs/user/quick-start/#installation'
check 'kubectl' 'kubectl version --client' 'kubectl version --client' 'installed, but it does not run' 'https://kubernetes.io/docs/tasks/tools/'
check 'gh' 'gh auth status' 'gh --version' 'installed, but not logged in: run gh auth login' 'https://cli.github.com/'
check 'terraform' "terraform version | head -n 1 | grep -Eq 'v1\\.(1[1-9]|[2-9][0-9])\\.'" 'terraform version' 'installed, but older than 1.11: install a newer Terraform' 'https://developer.hashicorp.com/terraform/install'
if [ "$bad" -eq 0 ]; then echo "All set: every tool is ready."; else echo "$bad tool(s) need attention. Install or fix them, then run this check again."; fi
On openTofu
check() {  # tool, works-check, version, problem, where-to-get-it
  if ! command -v "${1%% *}" >/dev/null 2>&1; then printf '  MISSING   %-22s install it: %s\n' "$1" "$5"; bad=$((bad+1))
  elif ! eval "$2" >/dev/null 2>&1; then printf '  PROBLEM   %-22s %s\n' "$1" "$4"; bad=$((bad+1))
  else printf '  ok        %-22s %s\n' "$1" "$(eval "$3" 2>&1 | head -n 1)"; fi
}
bad=0
check 'git' 'git --version' 'git --version' 'installed, but it does not run' 'https://git-scm.com/downloads'
check 'docker' 'docker info' "docker version --format '{{.Server.Version}}'" 'installed, but the Docker daemon is not running: start Docker Desktop (or the docker service)' 'https://docs.docker.com/get-docker/'
check 'docker compose' 'docker compose version' 'docker compose version' 'the Compose plugin is missing: install Docker Compose v2' 'https://docs.docker.com/compose/install/'
check 'kind' 'kind version' 'kind version' 'installed, but it does not run' 'https://kind.sigs.k8s.io/docs/user/quick-start/#installation'
check 'kubectl' 'kubectl version --client' 'kubectl version --client' 'installed, but it does not run' 'https://kubernetes.io/docs/tasks/tools/'
check 'gh' 'gh auth status' 'gh --version' 'installed, but not logged in: run gh auth login' 'https://cli.github.com/'
check 'tofu' "tofu version | head -n 1 | grep -Eq 'v1\\.(1[1-9]|[2-9][0-9])\\.'" 'tofu version' 'installed, but older than 1.11: install a newer OpenTofu' 'https://opentofu.org/docs/intro/install/'
if [ "$bad" -eq 0 ]; then echo "All set: every tool is ready."; else echo "$bad tool(s) need attention. Install or fix them, then run this check again."; fi

4. Clone the reference repo

The finished implementation. You copy a few files from it and compare with it; you never push to it.

mkdir -p ~/platformengr && git clone -q https://github.com/platformengr/terraform-platform.git ~/platformengr/terraform-platform

Check that it arrived. You should see cluster, docs, envs, labs, modules, policy and state:

ls ~/platformengr/terraform-platform

5. Create your repo

Your repo: the platform team's Terraform. Public, so the lab checks on this site can read it.

cd ~/platformengr && gh repo create YOUR_GITHUB_USER/tf-platform --public --clone

Go into it, make the first folders, and copy two files in: the cluster definition and the list of files Git must ignore.

cd ~/platformengr/tf-platform && git switch -C main && mkdir -p cluster envs/dev && cp ~/platformengr/terraform-platform/cluster/kind.yaml cluster/ && cp ~/platformengr/terraform-platform/.gitignore .

6. Create the cluster

One node is enough: Terraform creates namespaces, quotas and permissions, and nothing has to be scheduled. It is a separate cluster from the other courses on purpose: Terraform must be the only owner of the namespaces it manages.

Create the cluster. It takes about a minute, and pulls the node image the first time.

kind create cluster --config ~/platformengr/tf-platform/cluster/kind.yaml

kind pointed kubectl at the new cluster. Expect kind-tf:

kubectl config current-context

Expect one row, tf-control-plane, STATUS Ready:

kubectl get nodes

Expect five namespaces: the four Kubernetes made for itself, default, kube-node-lease, kube-public and kube-system, and local-path-storage, which kind adds. None of them is yours. Terraform will never touch them, because they are not in its state.

kubectl get namespaces

7. Write the smallest configuration

A configuration with no resources: only the versions of Terraform it accepts. A range, so Terraform and OpenTofu both pass.

Create ~/platformengr/tf-platform/envs/dev/versions.tf

terraform {
  required_version = ">= 1.11.0, < 2.0.0"
}

init prepares a directory: it reads the configuration and downloads what it needs. There is nothing to download yet. Look for has been successfully initialized!

cd ~/platformengr/tf-platform/envs/dev
terraform init

Ask for a plan. Expect No changes. Your infrastructure matches the configuration. An empty configuration and an empty state are equal. Every plan you read from now on is a difference between those two, plus the real world.

terraform plan

8. Commit

Push it, so it is on GitHub. The course page offers Connect your repo after this step: it adds a small workflow so this site can read your progress.

cd ~/platformengr/tf-platform && git add -A && git commit -qm "Cluster and an empty dev configuration" && git push -qu origin main

Simplified for the course

The "infrastructure" in this course is Kubernetes objects on a laptop cluster, because that needs no account and costs nothing. The language, the state, the modules and the workflow are the same when the provider is a cloud. docs/cloud.md in the reference repo shows the same setup on real S3.

You are the platform team and every product team at once. In a company these are different people, and the pull request is where they meet.

The CLI version is a range in the code and an exact version in CI. Teams that need everyone on the same build add a version manager (tenv, mise) with a version file in the repo.

Break it on purpose: The configuration says it does not want your Terraform

You copy versions.tf from an older internal project to save typing. Its author pinned the CLI the way it was right at the time.

You practise: reading a version constraint, and deciding whether to change the tool or the constraint.

1. Break it

Change in ~/platformengr/tf-platform/envs/dev/versions.tf

- required_version = ">= 1.11.0, < 2.0.0"
+ required_version = "~> 1.5.0"
cd ~/platformengr/tf-platform/envs/dev
terraform init

2. What you see

Expected: has been successfully initialized! You get: Error: Unsupported Terraform Core version (OpenTofu says Unsupported OpenTofu Core version) and, below it, This configuration does not support your version. Nothing was downloaded and nothing was changed. Read the line the error points at.

3. Fix it yourself

Make terraform init succeed with the version you have installed, without allowing a future 2.0. Done when init succeeds and the constraint in versions.tf is one you can explain.

Hint 1

~> lets only the last number you wrote grow. What is the highest version ~> 1.5.0 accepts?

Hint 2

~> 1.5.0 means 1.5.0 or newer, but below 1.6.0. You want 1.11 or newer, below 2.0.

The fix

Change in ~/platformengr/tf-platform/envs/dev/versions.tf

- required_version = "~> 1.5.0"
+ required_version = ">= 1.11.0, < 2.0.0"
terraform init
After you fixed it: the five questions
What failed?
Nothing in the infrastructure. The configuration refused the tool before reading anything else.
How was it detected?
By Terraform itself, on the first command, with the file and line.
Who responds?
Whoever owns the root module: here the platform team.
How is it corrected?
Widen the constraint to what the configuration really needs. Do not install an old CLI to satisfy an old constraint unless the code needs it.
How is it prevented?
One constraint for the whole repo, and CI that runs the pinned version on every pull request (module 06), so the constraint is tested and not guessed.

Quiz

  1. What does terraform init do?
    • It prepares the directory: reads the configuration, downloads the providers and modules it names, and sets up where state is kept
    • It creates the resources in the configuration for the first time, then records them in a new state file
    • It checks the cluster for objects that are not in the configuration yet and lists them for import
  2. Which versions does required_version = "~> 1.5.0" accept?
    • 1.5.0 and everything newer, including 2.0 and later major versions
    • 1.5.0 and later patch releases such as 1.5.7, but not 1.6.0
    • Only exactly 1.5.0, so every colleague runs the same build
  3. Why does Terraform need a state at all?
    • To cache the provider downloads between runs, so init is faster the second time
    • To keep a backup copy of your .tf files in case the Git history is lost
    • To know which real objects belong to which block, so it can change or remove exactly those
Answers
  1. It prepares the directory: reads the configuration, downloads the providers and modules it names, and sets up where state is kept init never touches infrastructure; it only prepares the working directory.
  2. 1.5.0 and later patch releases such as 1.5.7, but not 1.6.0 ~> lets only the rightmost number you wrote increase.
  3. To know which real objects belong to which block, so it can change or remove exactly those Without state, Terraform could not tell its own namespace from one that Kubernetes or a colleague created.

Field note

When I join a team that already uses Terraform, I read versions.tf and the backend block before any resource. They tell me how old the setup is, who else can write to it, and how careful I have to be with the first plan.

Report a problem with this module