Module 01 · The loop · 35 min · You are: Platform team
Write, plan, apply: a namespace from code
In this module
- You learn: How a
resourceblock 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(labelteam=checkout) with ResourceQuotateam-quotaand LimitRangeteam-limits, all created by Terraform, and a ConfigMapteam-contact.envs/devhasversions.tf,providers.tf,main.tfand 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].nameinstead of"checkout-dev". It passes the value and tells Terraform the order: the namespace first.- Dependency lock file
.terraform.lock.hclrecords the exact provider version and its checksums. It is committed, so every colleague and CI get the same provider.
Reading a plan
| Symbol | Words in the plan | What will happen |
|---|---|---|
+ | will be created | A new object |
~ | will be updated in-place | The same object; some fields change |
-/+ | must be replaced | The object is destroyed and a new one created. Look for # forces replacement |
- | will be destroyed | The object is removed |
<= | will be read during apply | A 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.
envs/dev/versions.tf(reference, your repo): Where you are heading: the CLI range and the exact provider versions. Today you write the first provider.- Where the provider comes from, and the one version you accept L4–7
envs/dev/providers.tf(reference, your repo): How Terraform reaches the cluster.- Terraform uses this context, whatever kubectl is pointed at today. A variable from module 02 L3
envs/dev/.terraform.lock.hcl(reference, your repo): Written byinit, never by hand.- The version picked, the constraints it satisfies, and below them its checksums L4–7
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
applyfrom your laptop and typedyes. In a team, the plan is read in a pull request and one place applies. Module 06 builds the pull request half.
config_pathpoints 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 anexecplugin.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
- 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
- Why write
kubernetes_namespace_v1.checkout.metadata[0].nameinstead 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
namespacefield of a quota - It passes the value and tells Terraform to create the namespace first
- What belongs in Git from
envs/devafterinit?.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
- 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. - It passes the value and tells Terraform to create the namespace first A reference is a dependency edge; a string is only a string.
.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.