Terraform basics · Lesson 5 of 9 · about 25 minutes

Variables and outputs

How to turn values written into your code into input variables, set them from a file, the command line or the environment, check them with validation, hide secrets with sensitive, and report results with outputs.

By the end you'll know

  • How to declare input variables, with types, descriptions and defaults
  • Four ways to set a variable, and which one wins
  • How to reject bad values with a validation rule
  • What sensitive hides, and what it doesn't
  • How to report useful values with outputs

Where you are

At the end of Lesson 4, your web server works, but its settings are written straight into web.tf: port 8080, image version 1.28, and the page's text. To run it on another port, or with a newer nginx, you'd have to edit the code. And if a colleague wanted the same setup with their own settings, they'd need their own copy of the code.

This lesson pulls those settings out into input variables, so the same code works with different values, and adds outputs, so Terraform tells you what you need to know when it's done. Lesson 4 ended with terraform destroy, so you'll build the web server again along the way.

Input variables

A form has the same questions for everyone, with blanks for the answers, and some blanks come filled in with the usual answer. Input variables are the blanks in your Terraform code.

Step 1. Create a file called variables.tf in your terraform-practice folder:

HCL
variable "external_port" {
  description = "Port on your computer for the web server"
  type        = number
  default     = 8080
}

variable "nginx_version" {
  description = "Which nginx image version to run"
  type        = string
  default     = "1.28"
}

variable "page_text" {
  description = "Text to show on the web page"
  type        = string
}

Each variable block declares one blank:

  • description says what it's for. Terraform shows it when it asks you for a value, and it's the first thing the next person reads.
  • type says what kind of value it accepts: string for text, number, or bool for true or false. There are also list and map types for several values, which Lesson 6 uses.
  • default is the pre-filled answer, used when nobody gives one. page_text has no default, so it's required: Terraform won't run without it.

Step 2. Now use them. In web.tf, change three lines:

HCL
  name = "nginx:${var.nginx_version}"

in docker_image "nginx",

HCL
  content  = "<h1>${var.page_text}</h1>"

in local_file "page", and

HCL
    external = var.external_port

in the container's ports block. var.name is how you use a variable. Inside text, wrap it in ${ } to fill it in, the same idea as a Python f-string: "nginx:${var.nginx_version}" becomes "nginx:1.28".

Setting values

Run terraform plan. page_text has no default, so Terraform asks:

OUTPUT
var.page_text
  Text to show on the web page

  Enter a value:

That's the description you wrote. Type Hello from a variable and press Enter, and the plan carries on. Being asked every time gets old quickly, though, so there are three other ways to give a value.

A tfvars file. Saved answers for the form. Create a file called terraform.tfvars:

HCL
page_text     = "Hello from a tfvars file"
external_port = 8081

Terraform reads a file with exactly that name automatically. Run terraform apply and type yes: no question this time, and the web server comes back, now on port 8081. Open http://localhost:8081 to see your text.

On the command line, for a one-off:

BASH
terraform plan -var external_port=9000

The plan shows the port changing from 8081 to 9000. You're only planning, so nothing changes.

From the environment, like the settings in Python Lesson 9. Any environment variable named TF_VAR_ followed by a variable's name sets that variable:

BASH
export TF_VAR_page_text="Hello from the environment"

When the same variable is set in more than one way, the most specific wins: -var on the command line beats terraform.tfvars, which beats TF_VAR_ environment variables, which beat the default. So right now, with terraform.tfvars setting page_text, your export has no effect. Run unset TF_VAR_page_text to remove it, so it doesn't surprise you later.

Rejecting bad values

A form that asks for a date of birth can refuse "next Tuesday". Terraform can refuse bad values too, before anything is built. Ports below 1024 need special permissions on most computers, so add a validation rule inside the external_port variable, below default:

HCL
  validation {
    condition     = var.external_port >= 1024 && var.external_port <= 65535
    error_message = "Choose a port from 1024 to 65535."
  }

condition must be true for the value to be accepted. && means "and", so the port must be at least 1024 and at most 65535, the highest port there is. If it isn't, Terraform stops and shows your error_message. The break-it section tries it.

Outputs

A receipt tells you what you got after you've paid. Outputs are Terraform's receipt: values it reports after an apply, like the address of the thing it just built.

Step 3. Create a file called outputs.tf:

HCL
output "url" {
  description = "Where to see the web page"
  value       = "http://localhost:${var.external_port}"
}

Run terraform apply and type yes. Nothing in your infrastructure changes, but the end of the output now says:

OUTPUT
Outputs:

url = "http://localhost:8081"

You can ask for outputs any time, without applying:

BASH
terraform output
terraform output -raw url

The first lists every output. The second prints just the value, with no quotes, which is handy in scripts: open $(terraform output -raw url) opens the page in your browser, because $( ) runs a command and uses what it prints.

Outputs also matter beyond your own screen. In bigger projects, one part's outputs become another part's inputs, which Lesson 9 shows with modules.

Sensitive values

Some values are secrets: a password, a token, a private key. Marking a variable sensitive stops Terraform printing it. Try it with a pretend secret. Add this to variables.tf:

HCL
variable "api_token" {
  description = "A pretend secret, for practice"
  type        = string
  default     = "pretend-token-123"
  sensitive   = true
}

and this to main.tf, to use it:

HCL
resource "terraform_data" "token_demo" {
  input = var.api_token
}

Run terraform plan. Where the value would be, the plan says:

OUTPUT
      + input  = (sensitive value)

Your screen, your logs and anyone looking over your shoulder are protected. But now run terraform apply, type yes, and search the state file:

BASH
grep pretend-token-123 terraform.tfstate

grep searches a file for some text, and prints each line that contains it. It finds the token, in plain text. That's Lesson 3's warning made real: sensitive hides values from output, not from state. Treat the state as the secret it is. Then delete the api_token variable and the token_demo resource, and terraform apply once more to remove it.

Checkpoint: your configuration so far

Your folder now has main.tf, versions.tf and web.tf from before, plus three new files. variables.tf:

HCL
variable "external_port" {
  description = "Port on your computer for the web server"
  type        = number
  default     = 8080

  validation {
    condition     = var.external_port >= 1024 && var.external_port <= 65535
    error_message = "Choose a port from 1024 to 65535."
  }
}

variable "nginx_version" {
  description = "Which nginx image version to run"
  type        = string
  default     = "1.28"
}

variable "page_text" {
  description = "Text to show on the web page"
  type        = string
}

outputs.tf, with the url output from step 3, and terraform.tfvars:

HCL
page_text     = "Hello from a tfvars file"
external_port = 8081

Run terraform plan: No changes.

One habit worth starting now: a real terraform.tfvars often holds secrets, so it usually stays out of Git, with *.tfvars in .gitignore. Projects commit a terraform.tfvars.example instead, with made-up values, to show what needs setting.

Values in, values outFour sources can set a variable. From weakest to strongest: the default in variables.tf, a TF_VAR_ environment variable, terraform.tfvars, and -var on the command line. The strongest one set wins. Validation checks the value. The resources in web.tf use the variables, and outputs report values like the URL after apply.where values come from-var on the command lineterraform.tfvarsTF_VAR_ environmentdefaultstrongest at the topvar.external_portchecked by validationweb.tfresources use itoutputsurl = ...8081
Whichever source sets a variable most specifically wins. Validation checks the value before anything is built, the resources use it, and outputs report what you need afterwards.

In my real setup

CivicGrid's Terraform has about a dozen variables. The everyday ones have sensible defaults, like the region, nyc3, and the size and number of cluster nodes, so changing the cluster size is a one-line change. The secrets, like the DigitalOcean and Cloudflare tokens and the database connection string, have no defaults and are marked sensitive. Their values aren't in any file at all: they're set in HCP Terraform, where my runs happen, which Lesson 8 covers. The repository only has a terraform.tfvars.example, with placeholders, to show what needs setting.

Break it on purpose

Try each one. None of them changes anything, because they all fail before the plan is made.

A value the rule refuses.

BASH
terraform plan -var external_port=80
OUTPUT
Error: Invalid value for variable

  on variables.tf line 1:
   1: variable "external_port" {
    ├────────────────
    │ var.external_port is 80

Choose a port from 1024 to 65535.

Your rule caught it, and your message explains it. Without the rule, Terraform would have gone ahead, and the failure would have come later, from Docker, with a far less helpful message.

A value of the wrong type.

BASH
terraform plan -var external_port=abc
OUTPUT
Error: Invalid value for input variable

followed by an explanation that ends with a number is required. You declared type = number, so Terraform refuses text before even checking your validation rule. Types are the first check; validation is the second.

A required value with nobody to ask. Rename terraform.tfvars to terraform.tfvars.off, so Terraform doesn't read it, and plan with -input=false, which tells Terraform not to ask questions, the way automated runs work:

BASH
mv terraform.tfvars terraform.tfvars.off
terraform plan -input=false
OUTPUT
Error: No value for required variable

followed by the file and line of page_text, and a reminder to use -var or a variable file. With nobody there to type an answer, a variable with no default and no value is a dead end. Put the file back with mv terraform.tfvars.off terraform.tfvars.

Where you are now

Your configuration takes its settings from outside the code, checks them, protects secrets from your screen, and reports what you need afterwards. But it still makes one of everything: one container, one page. Lesson 6 makes several from one block, with count and for_each, and creates things only when you want them, with conditionals.

Check yourself

  1. What does a variable's default do?

  2. external_port is set in terraform.tfvars and with -var. Which value is used?

  3. With the validation rule in place, what happens with external_port = 80?

  4. A variable is marked sensitive. Where can its value still be seen?

  5. What is an output for?

  6. terraform plan -input=false fails with No value for required variable. Why?

Key terms

  • Input variable: a value your configuration takes from outside, declared with a variable block and used as var.name.
  • Default: a variable's value when nobody sets it. A variable with no default is required.
  • terraform.tfvars: a file of variable values that Terraform reads automatically.
  • TF_VAR_: the start of an environment variable's name that sets a Terraform variable.
  • Validation: a rule that rejects bad values before anything is built.
  • sensitive: hides a value from plans and output. It's still in state.
  • Output: a value Terraform reports after an apply, read any time with terraform output.
  • Interpolation: filling a value into text with ${ }, like "nginx:${var.nginx_version}".

Go deeper