Terraform · lesson 4 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4Terraform Variables and Outputs
Input variables with types, defaults and validation, the order -var, tfvars and TF_VAR_ values win in, and outputs that scripts read, with real output.
By SkillByExample · Updated
Variables are a configuration’s inputs and outputs are its return values. Together they let one
set of .tf files serve every environment. The examples use Terraform 1.16.5 and
hashicorp/random 3.9.1, starting from this main.tf:
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
variable "project" {
type = string
description = "Short project name used in every resource name"
}
variable "environment" {
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging, or prod."
}
}
resource "random_pet" "name" {
prefix = "${var.project}-${var.environment}"
}
output "name" {
value = random_pet.name.id
}
A required variable, and a value that fails validation
$ terraform plan -input=false -no-color
Error: No value for required variable
on main.tf line 10:
10: variable "project" {
The root module input variable "project" is not set, and has no default
value. Use a -var or -var-file command line argument to provide a value for
this variable.
$ terraform plan -var project=shop -var environment=qa -no-color
Planning failed. Terraform encountered an error while generating this plan.
Error: Invalid value for variable
on main.tf line 15:
15: variable "environment" {
├────────────────
│ var.environment is "qa"
environment must be dev, staging, or prod.
This was checked by the validation rule at main.tf:19,3-13.
project has no default, so every caller has to supply it. At a terminal Terraform would prompt
for it. -input=false, which is what CI uses, turns the prompt into an error. The validation
rule rejected qa before Terraform planned anything, and the error repeats the bad value and
your own message.
Supplying values
$ terraform apply -var project=shop -auto-approve -no-color
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# random_pet.name will be created
+ resource "random_pet" "name" {
+ id = (known after apply)
+ length = 2
+ prefix = "shop-dev"
+ separator = "-"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ name = (known after apply)
random_pet.name: Creating...
random_pet.name: Creation complete after 0s [id=shop-dev-renewed-scorpion]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
name = "shop-dev-renewed-scorpion"
environment fell back to its default, dev. The plan shows the variables already
substituted (prefix = "shop-dev"), so a reviewer sees the values that will be used, not the
expressions that produce them.
Which value wins
Create a prod.tfvars file and set the same variable from three places at once:
project = "shop"
environment = "prod"
$ export TF_VAR_environment=staging
$ terraform plan -var project=shop -no-color | grep prefix
~ prefix = "shop-dev" -> "shop-staging" # forces replacement
$ terraform plan -var-file=prod.tfvars -no-color | grep prefix
~ prefix = "shop-dev" -> "shop-prod" # forces replacement
$ terraform plan -var-file=prod.tfvars -var environment=dev -no-color | grep -E "prefix|No changes"
No changes. Your infrastructure matches the configuration.
The environment variable TF_VAR_environment beat the default, the file beat the environment
variable, and the -var flag, which came last on the command line, beat the file. The rule is
“the last source read wins”. That makes a stray TF_VAR_ in someone’s shell profile risky,
because it silently changes plans for anyone who doesn’t override it.
Structured types, and outputs scripts can read
Add a types.tf next to main.tf:
variable "sizing" {
type = object({
replicas = number
tags = optional(map(string), {})
})
default = {
replicas = 1
}
}
output "sizing" {
value = var.sizing
}
output "summary" {
description = "One-line description of what was deployed"
value = "${random_pet.name.id} x${var.sizing.replicas}"
}
$ terraform plan -var project=shop -var 'sizing={replicas="two"}' -no-color
random_pet.name: Refreshing state... [id=shop-dev-renewed-scorpion]
Planning failed. Terraform encountered an error while generating this plan.
Error: Invalid value for input variable
on types.tf line 1:
1: variable "sizing" {
Unsuitable value for var.sizing set using -var="sizing=...": a number is
required.
$ terraform apply -var project=shop -var 'sizing={replicas="3"}' -auto-approve -no-color | tail -12
state, without changing any real infrastructure.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
name = "shop-dev-renewed-scorpion"
sizing = {
"replicas" = 3
"tags" = tomap({})
}
summary = "shop-dev-renewed-scorpion x3"
$ terraform output -json
{
"name": {
"sensitive": false,
"type": "string",
"value": "shop-dev-renewed-scorpion"
},
"sizing": {
"sensitive": false,
"type": [
"object",
{
"replicas": "number",
"tags": [
"map",
"string"
]
}
],
"value": {
"replicas": 3,
"tags": {}
}
},
"summary": {
"sensitive": false,
"type": "string",
"value": "shop-dev-renewed-scorpion x3"
}
}
"two" was rejected, but "3", also a string, was accepted. Terraform converts a string to a
number when the conversion is unambiguous, and only fails when it can’t be done. optional(..., {}) filled in the tags you left out, so var.sizing.tags is always a map and never null.
terraform output -json includes each output’s type and sensitivity alongside its value, which
is the format to use when another tool reads the outputs.
Practice
1. Add a validation block to sizing that only allows 1 to 10 replicas, then pass 20.
Solution
validation {
condition = var.sizing.replicas >= 1 && var.sizing.replicas <= 10
error_message = "replicas must be between 1 and 10."
}
$ terraform plan -var project=shop -var 'sizing={replicas=20}' -no-color
random_pet.name: Refreshing state... [id=shop-dev-renewed-scorpion]
Planning failed. Terraform encountered an error while generating this plan.
Error: Invalid value for variable
on types.tf line 1:
1: variable "sizing" {
├────────────────
│ var.sizing.replicas is 20
replicas must be between 1 and 10.
This was checked by the validation rule at types.tf:10,3-13.
A validation condition can look at a single attribute of an object variable. The error shows exactly which value failed.
2. Stop typing -var project=shop every time by putting it in terraform.tfvars, then plan
with no flags at all. Why isn’t the plan empty?
Solution
$ cat terraform.tfvars
project = "shop"
$ terraform plan -no-color
random_pet.name: Refreshing state... [id=shop-dev-renewed-scorpion]
Changes to Outputs:
~ sizing = {
~ replicas = 3 -> 1
tags = {}
}
~ summary = "shop-dev-renewed-scorpion x3" -> "shop-dev-renewed-scorpion x1"
You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
Terraform loads terraform.tfvars automatically, so project is set. But replicas=3 was only
passed on the last apply, and Terraform never remembers variable values. Without the flag the
default of 1 comes back. Any value you need every time belongs in the file.
3. Use the name output inside a shell command.
Solution
$ echo "deployed $(terraform output -raw name)"
deployed shop-dev-renewed-scorpion
-raw prints a string output with no quotes and no trailing newline, which is the form shell
substitution needs. It only works for strings, numbers and booleans. Use -json for the sizing
object.
Next: providers and resources, covering how a resource block becomes a real object and how references between resources order the apply.