PlatformEngr® Terraform course

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 locals block. Add a validation and watch it refuse a value. Add an output.
  • You end with: The same objects in checkout-dev, now with labels team, environment, tier and app.kubernetes.io/managed-by=terraform. envs/dev has variables.tf, terraform.tfvars and outputs.tf. terraform plan says No 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 validation block inside a variable: a condition that must be true and an error_message shown 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_name in the environment, then terraform.tfvars, then *.auto.tfvars in alphabetical order, then -var and -var-file on the command line.

Variable, local or output?

You haveUseExample here
A value that differs per environment or teamVariableenvironment, checkout_quota
A value built from othersLocallocal.namespace, local.labels
A value someone else needs after applyOutputcheckout_namespace
A value that never changesWrite it in place"team-quota"

Files to know

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

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.tfvars is 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).

team is 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-prod in 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_destroy on the namespace (module 04) turns this plan into an error.

Quiz

  1. terraform.tfvars sets environment = "dev" and the shell has TF_VAR_environment=prod. Which wins?
    • prod, because environment variables always override files
    • dev, because a tfvars file is read after environment variables
    • Neither: Terraform stops and asks you which one you mean
  2. 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 -var on the command line; a variable cannot
    • A variable is stored in the state file; a local is never written there
  3. After moving values into variables, how do you know you changed nothing real?
    • terraform validate says that the configuration is valid
    • terraform fmt has no files left to rewrite in the directory
    • terraform plan says No changes. for the directory
Answers
  1. dev, because a tfvars file is read after environment variables Later sources win, and TF_VAR_ comes before terraform.tfvars in the order.
  2. A variable is an input set from outside; a local names an expression inside Only variables are part of a module's interface.
  3. terraform plan says No 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.

Report a problem with this module