terraform · level 3

Variables & Outputs

Inputs, outputs, locals, and how modules speak.

100 XP

Variables & Outputs

Variables let your config accept inputs from the outside. Outputs let it expose values to the outside (other configs, CI, you on the CLI).

Input variables

variable "region" {
  type        = string
  default     = "us-east-1"
  description = "AWS region to deploy to."
}

variable "instance_count" {
  type    = number
  default = 1
}

variable "tags" {
  type = map(string)
  default = {
    Environment = "dev"
    Owner       = "platform"
  }
}

Then reference with var.<name>:

provider "aws" {
  region = var.region
}

resource "aws_instance" "web" {
  count = var.instance_count
  tags  = var.tags
}

Setting variable values

Five ways, in increasing override precedence:

  1. The variable's own default value.
  2. A terraform.tfvars (or *.auto.tfvars) file.
  3. A specific -var-file=... on the command line.
  4. -var foo=bar on the command line.
  5. Environment variable: TF_VAR_foo=bar.

The most common pattern: default for the local-dev value, terraform.tfvars (gitignored) for per-developer overrides, environment variables in CI.

Variable types

Five primitive + three structural types:

Type Example value
string "us-east-1"
number 42
bool true
list(T) ["a", "b"]
set(T) toset(["a", "b"])
map(T) { "a" = 1, "b" = 2 }
object({ ... }) { name = "x", age = 30 }
tuple([...]) ["x", 30, true]

object is for fixed-shape structures (preferred over map when the keys are known up front). tuple is for fixed-length, mixed-type lists.

Validation

A validation block guards against bad inputs at plan time:

variable "instance_type" {
  type = string

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "instance_type must be one of: t3.micro, t3.small, t3.medium."
  }
}

Catch the misconfiguration before any resource gets touched.

Sensitive variables

Mark variables that hold secrets:

variable "db_password" {
  type      = string
  sensitive = true
}

sensitive = true keeps the value out of terraform plan / terraform apply output and out of terraform output. The value still ends up in state — protect that with a backend that has restricted access.

Outputs

Outputs expose values from a config:

output "bucket_arn" {
  value       = aws_s3_bucket.logs.arn
  description = "ARN of the logs bucket."
}

output "instance_ips" {
  value = aws_instance.web[*].public_ip
}

output "db_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

After apply:

terraform output                    # all outputs
terraform output bucket_arn         # one specific output
terraform output -json              # machine-readable

Outputs are how modules talk

When you write a Terraform module (next lessons), outputs are its return values. The parent that calls the module reads module.<name>.<output_name>:

module "vpc" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
}

resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnet_ids[0]   # reads the module's output
}

Locals

When you compute the same expression in two places, factor it into a locals block:

locals {
  common_tags = {
    Environment = var.environment
    Project     = "myapp"
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket" "logs" {
  tags = merge(local.common_tags, { Purpose = "logs" })
}

resource "aws_s3_bucket" "uploads" {
  tags = merge(local.common_tags, { Purpose = "uploads" })
}

local.<name> reads them. Locals are evaluated once per plan and never overridable from the outside — they're for internal computation, unlike variables.