Skip to main content
Terraform · lesson 4 of 17
Terraform beginner Lesson 4 of 17

Terraform 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

terraform variables outputs tfvars

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.

Frequently Asked Questions

What's the difference between a Terraform variable and an output?
A variable is an input set from outside the configuration, through -var, a .tfvars file or a TF_VAR_ environment variable. An output is a value the configuration exposes after apply, for a human, a script calling terraform output, or a parent module reading module.name.output.
Which wins if a variable is set in several places?
Later sources override earlier ones in this order: the default in the variable block, TF_VAR_ environment variables, terraform.tfvars, *.auto.tfvars files, then -var and -var-file flags in the order they appear on the command line. So a -var flag beats a value from a .tfvars file, and both beat an environment variable.
Does Terraform remember variable values between runs?
No. Variables are read fresh on every plan and apply. If you pass -var replicas=3 once and omit it next time, the default comes back and the plan shows the change. Put values you always want in terraform.tfvars, which Terraform loads automatically, or in a named .tfvars file you pass every time.
What happens when a variable has no default and no value?
In a terminal, Terraform prompts for it interactively. With -input=false, as CI should use, it fails with 'No value for required variable' before planning anything. A variable without a default is the way to force every caller to make a choice.
What does a validation block do?
It checks a variable's value against a condition before any planning happens and fails with your error_message if the condition is false. It turns a typo like environment = "qa" into an immediate, readable error instead of a resource with a wrong name.