PlatformEngr® Terraform course

Module 01 · The loop · 35 min · You are: Platform team

Write, plan, apply: a namespace from code

In this module

  • You learn: How a resource block becomes a real object: the provider, the plan and its symbols, apply, and the lock file that pins what was downloaded. And how one resource refers to another, so Terraform knows the order.
  • You do: Configure the Kubernetes provider for your cluster. Write a namespace for team-checkout, plan it, apply it, and look at it with kubectl. Change it in place. Read a plan that would replace it, and say no. Add a quota that refers to the namespace.
  • You end with: Namespace checkout-dev (label team=checkout) with ResourceQuota team-quota and LimitRange team-limits, all created by Terraform, and a ConfigMap team-contact. envs/dev has versions.tf, providers.tf, main.tf and a committed .terraform.lock.hcl. The state is a file on your laptop that is not in Git.

kubectl apply sends a file and forgets. If you rename an object in the file, the old one stays; if you delete the file, nothing is deleted. Terraform works from a record: it knows what it made, so it can show you, before anything happens, exactly what a change will create, alter or destroy. Reading that plan is the skill this whole course builds on. You learn the symbols now, on objects that cost nothing to lose.

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

Loop practised: Write → Plan → Review → Apply → State

Concepts

Resource
resource "<type>" "<name>" { ... }. The type comes from a provider. The name is yours and exists only inside Terraform. Together they are the address: kubernetes_namespace_v1.checkout.
Plan
The difference between the configuration, the state and the real object, as a list of actions. A plan changes nothing.
Apply
Runs the actions and writes the result to state. It is not a transaction: if the third action fails, the first two stay done.
Reference
kubernetes_namespace_v1.checkout.metadata[0].name instead of "checkout-dev". It passes the value and tells Terraform the order: the namespace first.
Dependency lock file
.terraform.lock.hcl records the exact provider version and its checksums. It is committed, so every colleague and CI get the same provider.

Reading a plan

SymbolWords in the planWhat will happen
+will be createdA new object
~will be updated in-placeThe same object; some fields change
-/+must be replacedThe object is destroyed and a new one created. Look for # forces replacement
-will be destroyedThe object is removed
<=will be read during applyA data source; nothing changes

Read the last line first: Plan: 1 to add, 0 to change, 0 to destroy.

Files to know

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

Steps

1. Name the provider and its version

Terraform knows no API. Say which provider you need, from where, and the one version you accept. Then say which cluster it talks to.

Replace the whole file ~/platformengr/tf-platform/envs/dev/versions.tf

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

  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "3.4.0"
    }
  }
}

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

provider "kubernetes" {
  config_path    = "~/.kube/config"
  config_context = "kind-tf"
}

Run init again: there is now something to download. Look for - Installing hashicorp/kubernetes v3.4.0... and has created a lock file .terraform.lock.hcl.

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

Two new things. .terraform/ is the downloaded provider, which Git ignores. .terraform.lock.hcl pins the version and its checksums, and you commit it.

ls -a

2. Write the namespace

One resource: a namespace for team-checkout in dev, with the label that says whose it is.

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

resource "kubernetes_namespace_v1" "checkout" {
  metadata {
    name = "checkout-dev"
    labels = {
      team = "checkout"
    }
  }
}

Two cheap checks that need no cluster. fmt rewrites the files in the standard layout and prints the names of the files it changed (nothing, if you typed it neatly):

terraform fmt

validate checks names and types against the provider. Expect Success! The configuration is valid.

terraform validate

3. Plan

Ask what would happen. Nothing is changed by a plan.

Find three things: # kubernetes_namespace_v1.checkout will be created with a + in front, (known after apply) on values only the cluster can decide (such as uid), and the last line, Plan: 1 to add, 0 to change, 0 to destroy.

terraform plan

Nothing exists yet. Expect Error from server (NotFound):

kubectl get namespace checkout-dev

4. Apply

Now do it. apply makes a fresh plan, shows it, and waits for you.

Apply. It shows the same plan and asks Do you want to perform these actions? Type yes. Expect Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

terraform apply

Expect checkout-dev with team=checkout among its labels:

kubectl get namespace checkout-dev --show-labels

Plan again. Expect No changes.: code, state and cluster agree.

terraform plan

5. Change it in place

Add a label. A label can change on an existing namespace, so this is an update, not a replacement.

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

-       team = "checkout"
+       team        = "checkout"
+       environment = "dev"

Find ~ resource "kubernetes_namespace_v1" "checkout", the line + "environment" = "dev", and the last line Plan: 0 to add, 1 to change, 0 to destroy.

terraform plan

Apply it (type yes):

terraform apply

6. Read a replacement, and refuse it

Rename the namespace in the code and see what Terraform proposes. Do not apply.

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

- name = "checkout-dev"
+ name = "checkout"

Find -/+ and # forces replacement next to name. A namespace cannot be renamed: Terraform would delete it, with everything inside, and make a new one. The last line says so too: Plan: 1 to add, 0 to change, 1 to destroy.

terraform plan

Put the name back:

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

- name = "checkout"
+ name = "checkout-dev"

Expect No changes.:

terraform plan

7. Add the quota and the default limits, by reference

A ResourceQuota caps what the whole namespace may use. A LimitRange gives a container a size when its author gave none. Both name their namespace by reference, so Terraform creates the namespace first.

Add to ~/platformengr/tf-platform/envs/dev/main.tf at the end of the file


resource "kubernetes_resource_quota_v1" "checkout" {
  metadata {
    name      = "team-quota"
    namespace = kubernetes_namespace_v1.checkout.metadata[0].name
  }
  spec {
    hard = {
      pods              = "10"
      "requests.cpu"    = "2"
      "requests.memory" = "2Gi"
    }
  }
}

resource "kubernetes_limit_range_v1" "checkout" {
  metadata {
    name      = "team-limits"
    namespace = kubernetes_namespace_v1.checkout.metadata[0].name
  }
  spec {
    limit {
      type            = "Container"
      default         = { memory = "256Mi" }
      default_request = { cpu = "100m", memory = "128Mi" }
    }
  }
}

Apply. Expect Plan: 2 to add, 0 to change, 0 to destroy. and type yes.

terraform apply

The cluster has it. Find pods 0 10: used, then hard.

kubectl describe quota team-quota -n checkout-dev

8. See what Terraform remembers

The record of what Terraform made.

Expect three addresses, one per resource block:

terraform state list

The record is a file here, terraform.tfstate:

ls

Git does not offer it for commit. Expect .terraform.lock.hcl, main.tf, providers.tf and versions.tf under envs/dev/, and no state: .gitignore has *.tfstate. Module 03 is about why.

git status --short --untracked-files=all

9. Commit

Save the code and the lock file.

(cd ~/platformengr/tf-platform && git add -A && git commit -qm "checkout-dev from Terraform" && git push -q)

Check that the lock file went with it. Expect .terraform.lock.hcl, main.tf, providers.tf, versions.tf:

git ls-files

Simplified for the course

You ran apply from your laptop and typed yes. In a team, the plan is read in a pull request and one place applies. Module 06 builds the pull request half.

config_path points at your own kubeconfig. Against a managed cluster, the provider gets a host, a CA certificate and a short-lived token from the cloud's own data source or an exec plugin.

The provider's resources end in _v1 (the Kubernetes API version). Lab 2 in module 08 is about a configuration that still uses the old names without a suffix.

Break it on purpose: Half of the change is applied

team-checkout asks for a second label and a ConfigMap with their on-call channel. You are in a hurry and type the namespace name by hand, slightly wrong. Do this one, fix included: the next steps build on what the fix leaves in your repo.

You practise: reading an apply that fails after it started, and finding out what was done and what was not.

1. Break it

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

-       environment = "dev"
+       environment = "dev"
+       tier        = "web"

Add to ~/platformengr/tf-platform/envs/dev/main.tf at the end of the file


resource "kubernetes_config_map_v1" "checkout_contact" {
  metadata {
    name      = "team-contact"
    namespace = "checkout-develop"
  }
  data = {
    oncall = "#checkout-oncall"
  }
}
terraform apply -auto-approve

2. What you see

The plan was fine: Plan: 1 to add, 1 to change, 0 to destroy. Expected: Apply complete! Resources: 1 added, 1 changed, 0 destroyed. You get: Error: namespaces "checkout-develop" not found and no Apply complete! line.

Now find out which half happened: kubectl get namespace checkout-dev --show-labels and terraform state list.

3. Fix it yourself

Get the ConfigMap into checkout-dev so that this typo cannot happen again in this file. Done when terraform plan says No changes. and kubectl get configmap team-contact -n checkout-dev finds it.

Hint 1

The label is there and the ConfigMap is not. Apply is not all-or-nothing. What does terraform plan propose now?

Hint 2

The quota never had this problem. Compare how it names its namespace.

The fix

plan could not catch this: the provider does not ask the cluster whether a namespace exists while planning. A reference could not have been wrong.

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

- namespace = "checkout-develop"
+ namespace = kubernetes_namespace_v1.checkout.metadata[0].name
terraform apply

Expect No changes.:

terraform plan
(cd ~/platformengr/tf-platform && git add -A && git commit -qm "checkout: on-call ConfigMap" && git push -q)
After you fixed it: the five questions
What failed?
One of two actions. The label change succeeded and is recorded; the ConfigMap was refused by the API server.
How was it detected?
At apply time, by the cluster. The plan was valid.
Who responds?
The platform team, who ran the apply.
How is it corrected?
Correct the configuration and apply again. Terraform continues from what the state says is done.
How is it prevented?
References, never retyped names. Later: one module where the name is written once (04), and a test that the names line up (06).

Quiz

  1. A plan line starts with -/+ and a field shows # forces replacement. What will apply do?
    • Update the field in place and then restart whatever uses the object
    • Destroy the object and create a new one, because that field cannot change on an existing object
    • Create a second object next to the first one and keep both of them
  2. Why write kubernetes_namespace_v1.checkout.metadata[0].name instead of "checkout-dev" in the quota?
    • It makes the plan faster, because Terraform can skip reading the namespace
    • The provider rejects plain strings in the namespace field of a quota
    • It passes the value and tells Terraform to create the namespace first
  3. What belongs in Git from envs/dev after init?
    • .terraform.lock.hcl, but not the .terraform/ directory
    • Both, so that colleagues do not need to download the provider again
    • Neither of them: both are generated and can be made again
Answers
  1. Destroy the object and create a new one, because that field cannot change on an existing object -/+ is destroy then create; for a namespace that means everything inside it goes too.
  2. It passes the value and tells Terraform to create the namespace first A reference is a dependency edge; a string is only a string.
  3. .terraform.lock.hcl, but not the .terraform/ directory The lock file pins what everyone gets; .terraform/ is the download itself.

Field note

I read the last line of a plan first. If I expected one change and it says anything to destroy, I stop reading the details and start asking why.

Report a problem with this module