Module 02 · The loop · 35 min · You are: Platform team
Inputs, locals, outputs and validation
In this module
- You learn: How to take the hard-coded values out of a configuration: input variables with types and validation, locals for values you compute once, outputs for what others need to know, and the order in which Terraform picks up a variable's value.
- You do: Turn the environment name, the kube context and the quota sizes into variables. Compute the namespace name and the labels in a
localsblock. Add a validation and watch it refuse a value. Add an output. - You end with: The same objects in
checkout-dev, now with labelsteam,environment,tierandapp.kubernetes.io/managed-by=terraform.envs/devhasvariables.tf,terraform.tfvarsandoutputs.tf.terraform plansaysNo changes.
"checkout-dev" is written in your file, and the environment is hidden inside it. The moment a second environment or a second team appears, somebody copies the file and edits strings, and a typo becomes a namespace. Variables make the differences explicit and typed. Validation moves a mistake from "the plan looks odd" to "Terraform refuses to plan". The proof that a refactoring is safe is a plan that says No changes.
Open this module in the interactive course to track progress and switch tracks.
Loop practised: Write → Plan → Review → Apply → State
Concepts
- Input variable
variable "environment" { type = string }. A named input, with a type, an optional default and a description.- Validation
- A
validationblock inside a variable: aconditionthat must be true and anerror_messageshown when it is not. It runs before the plan. - Local
- A name for an expression, computed once:
local.namespace. Not an input. Use it so a value is written in one place. - Output
- A value the root module prints and other tools can read:
terraform output -raw checkout_namespace. - Where a value comes from
- Later wins: the default, then
TF_VAR_namein the environment, thenterraform.tfvars, then*.auto.tfvarsin alphabetical order, then-varand-var-fileon the command line.
Variable, local or output?
| You have | Use | Example here |
|---|---|---|
| A value that differs per environment or team | Variable | environment, checkout_quota |
| A value built from others | Local | local.namespace, local.labels |
| A value someone else needs after apply | Output | checkout_namespace |
| A value that never changes | Write it in place | "team-quota" |
Files to know
Open them in your editor or on GitHub; the bullets say what to look at.
envs/dev/variables.tf(reference, your repo): Where you are heading: the inputs of the dev directory. Today you write the first two and one for checkout's quota.envs/dev/terraform.tfvars(reference, your repo): The only place that says this directory is dev.- Read automatically by every plan in this directory L1
envs/dev/outputs.tf(reference, your repo): What this directory tells others after apply.- An output with a description. Yours points at the resource; from module 04 at the module L1–3
Steps
1. Declare the inputs
Three variables: which environment, which kube context, and how much checkout may use. Then the file that sets the environment for this directory.
Create ~/platformengr/tf-platform/envs/dev/variables.tf
variable "environment" {
description = "Which environment this directory manages."
type = string
validation {
condition = contains(["dev", "prod"], var.environment)
error_message = "environment must be dev or prod."
}
}
variable "kube_context" {
description = "The kubeconfig context Terraform uses. Never the current one by accident."
type = string
default = "kind-tf"
}
variable "checkout_quota" {
description = "What team-checkout may use in this environment."
type = object({
pods = optional(number, 10)
cpu = optional(string, "2")
memory = optional(string, "2Gi")
})
default = {}
validation {
condition = var.checkout_quota.pods >= 1 && var.checkout_quota.pods <= 50
error_message = "pods must be between 1 and 50."
}
}
Create ~/platformengr/tf-platform/envs/dev/terraform.tfvars
environment = "dev"
2. Use them
The provider reads the context from a variable. The namespace name and the labels are computed once, in locals, at the top of main.tf. The quota reads its numbers from the variable.
Change in ~/platformengr/tf-platform/envs/dev/providers.tf
- config_context = "kind-tf"
+ config_context = var.kube_context
Add to ~/platformengr/tf-platform/envs/dev/main.tf at the top of the file
locals {
team = "checkout"
namespace = "${local.team}-${var.environment}"
labels = {
team = local.team
environment = var.environment
}
}
Change in ~/platformengr/tf-platform/envs/dev/main.tf
- name = "checkout-dev"
- labels = {
- team = "checkout"
- environment = "dev"
- tier = "web"
- }
+ name = local.namespace
+ labels = merge(local.labels, { tier = "web" })
Change in ~/platformengr/tf-platform/envs/dev/main.tf
- pods = "10"
- "requests.cpu" = "2"
- "requests.memory" = "2Gi"
+ pods = tostring(var.checkout_quota.pods)
+ "requests.cpu" = var.checkout_quota.cpu
+ "requests.memory" = var.checkout_quota.memory
3. Prove nothing changed
A refactoring is safe when the plan is empty.
Expect No changes. Your infrastructure matches the configuration. If it is not empty, read the ~ line: it names the value you changed by accident.
terraform plan
4. Ask Terraform what an expression is
console is where you try an expression before you put it in a file. Start it, type each line below and press Enter, then type exit.
terraform console
Expect "checkout-dev":
local.namespace
Expect the object with all three defaults filled in: cpu = "2", memory = "2Gi", pods = 10.
var.checkout_quota
exit
5. Make validation refuse a value
Give the variables values they do not allow, from the command line.
Expect Error: Invalid value for variable and your own sentence, environment must be dev or prod. No plan was made.
terraform plan -var environment=staging
Expect pods must be between 1 and 50.:
terraform plan -var 'checkout_quota={pods=500}'
6. Add a label through the local
One line in local.labels reaches every object that uses it. Today that is only the namespace.
Change in ~/platformengr/tf-platform/envs/dev/main.tf
- team = local.team
- environment = var.environment
- }
+ team = local.team
+ environment = var.environment
+ "app.kubernetes.io/managed-by" = "terraform"
+ }
Expect Plan: 0 to add, 1 to change, 0 to destroy.: only the namespace.
terraform plan
Apply it (type yes):
terraform apply
kubectl get namespace checkout-dev --show-labels
7. Publish an output
Other tools (a pipeline, another configuration) need to know which namespace team-checkout deploys into. An output says it.
Create ~/platformengr/tf-platform/envs/dev/outputs.tf
output "checkout_namespace" {
description = "The namespace team-checkout deploys into."
value = kubernetes_namespace_v1.checkout.metadata[0].name
}
Apply. Expect Changes to Outputs: with + checkout_namespace = "checkout-dev" and no resource changes. Type yes.
terraform apply
Read it the way a script would. Expect checkout-dev:
terraform output -raw checkout_namespace
8. Commit
Save it.
(cd ~/platformengr/tf-platform && git add -A && git commit -qm "Variables, locals and an output for dev" && git push -q)
Simplified for the course
terraform.tfvarsis committed here because it holds no secret, only the environment name. A tfvars file with a secret in it does not belong in Git; the secret does not belong in a variable at all (module 03).
teamis a local for now. It becomes a variable of a module in module 04, when there is a second team.
Break it on purpose: The plan wants to replace everything, and git status is clean
A colleague tested something against prod values last week, on this checkout, with a small override file. The repo's .gitignore hides it, as most Terraform .gitignore templates do.
You practise: finding out where a variable's value comes from when the files you can see say something else.
1. Break it
Create ~/platformengr/tf-platform/envs/dev/zz-local.auto.tfvars
environment = "prod"
Git sees nothing:
git status --short
terraform plan
2. What you see
Expected: No changes. You get: Plan: 4 to add, 0 to change, 4 to destroy. with # kubernetes_namespace_v1.checkout must be replaced and ~ name = "checkout-dev" -> "checkout-prod" # forces replacement. terraform.tfvars still says dev, and Git says nothing changed. Do not apply.
3. Fix it yourself
Find where prod comes from and remove the cause. Done when terraform plan says No changes. Then decide what in the repo would have made this impossible.
Hint 1
Ask Terraform, not Git: terraform console, then var.environment.
Hint 2
Terraform loads every *.auto.tfvars file in the directory, after terraform.tfvars. ls -a shows files that git status hides.
The fix
The validation passed: prod is a legal value. A value can be valid and still wrong for this directory.
rm zz-local.auto.tfvars
Expect No changes.:
terraform plan
After you fixed it: the five questions
- What failed?
- Nothing yet. The plan would have destroyed dev's namespace, with everything in it, and created
checkout-prodin its place. - How was it detected?
- By reading the last line of the plan before applying.
- Who responds?
- Whoever runs the plan. The cause was a colleague's leftover file.
- How is it corrected?
- Delete the override file.
- How is it prevented?
- Do not make "which environment is this" something a stray file can override: module 05 gives each environment its own directory and state. Plans for review come from CI's clean checkout, not from a laptop with leftovers (module 06).
prevent_destroyon the namespace (module 04) turns this plan into an error.
Quiz
terraform.tfvarssetsenvironment = "dev"and the shell hasTF_VAR_environment=prod. Which wins?prod, because environment variables always override filesdev, because a tfvars file is read after environment variables- Neither: Terraform stops and asks you which one you mean
- What is the difference between a variable and a local?
- A variable is an input set from outside; a local names an expression inside
- A local can be overridden with
-varon the command line; a variable cannot - A variable is stored in the state file; a local is never written there
- After moving values into variables, how do you know you changed nothing real?
terraform validatesays that the configuration is validterraform fmthas no files left to rewrite in the directoryterraform plansaysNo changes.for the directory
Answers
dev, because a tfvars file is read after environment variables Later sources win, andTF_VAR_comes beforeterraform.tfvarsin the order.- A variable is an input set from outside; a local names an expression inside Only variables are part of a module's interface.
terraform plansaysNo changes.for the directory Only a plan compares the result of your expressions with what exists.
Field note
A validation message is documentation that runs. I write it for the colleague who will see it at the worst moment: say what is allowed, not only that the value was wrong.