Terraform Cheatsheet
Variables
Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Declaring Variables
# variables.tf variable "instance_type" { description = "EC2 instance type for the web tier" type = string default = "t3.micro" } variable "environment" { description = "Deployment environment" type = string # no default, so a value is required } variable "replica_count" { type = number default = 1 }
| Argument | Type | Purpose | Since |
|---|---|---|---|
type | type constraint | reject wrong shapes early | 0.12 |
default | expression | makes the variable optional | 0.x |
description | string | documents the input | 0.x |
validation | block | custom rule, may repeat | 0.13 |
sensitive | bool | redact from CLI output | 0.14 |
nullable | bool | allow an explicit null | 1.1 |
ephemeral | bool | never write to state or plan | 1.10 |
const | bool | usable at load time, e.g. module source | 1.15 |
deprecated | string | warn when a caller sets it | 1.15 |
Names must be unique per module and cannot be source, version, providers, count, for_each, lifecycle, depends_on or locals.
The Type System
| Type | Example | Notes |
|---|---|---|
string | "us-east-1" | |
number | 3, 3.14 | |
bool | true | "true" converts |
list(string) | ["a", "b"] | ordered, duplicates allowed, one element type |
set(string) | toset(["a", "b"]) | unordered, unique, one element type |
map(string) | { k = "v" } | string keys, one value type |
object({ ... }) | see below | named attributes, mixed types, extra keys rejected |
tuple([...]) | ["x", 1, true] | fixed length, positional, mixed types |
any | anything | no checking, avoid it |
variable "tags" { type = map(string) default = { Project = "myapp", Env = "prod" } } variable "db_config" { type = object({ engine = string version = string size = number multi_az = optional(bool, false) # optional + default, 1.3+ backup_window = optional(string) # optional, defaults to null }) default = { engine = "postgres" version = "17" size = 20 } } variable "subnets" { type = list(object({ name = string cidr = string tags = optional(map(string), {}) })) default = [] }
optional(type, default) only works inside object({...}), and it composes: an optional attribute nested inside an optional object gets its default filled in when the parent is supplied. Prefer it over deep try() chains.
# Referencing resource "aws_instance" "web" { instance_type = var.instance_type tags = var.tags } engine_version = var.db_config.version # object attribute first_cidr = var.subnets[0].cidr # list index maybe = lookup(var.tags, "Owner", "-") # map with default
Precedence Order
Highest wins. Later sources in each row override earlier ones.
| # | Source | Notes |
|---|---|---|
| 1 | -var and -var-file on the command line | applied in the order written, later wins, so -var does not automatically beat -var-file |
| 2 | *.auto.tfvars and *.auto.tfvars.json | auto-loaded, lexical filename order, later wins |
| 3 | terraform.tfvars.json | auto-loaded |
| 4 | terraform.tfvars | auto-loaded |
| 5 | TF_VAR_<name> environment variable | |
| 6 | default in the variable block | |
| 7 | interactive prompt | only when nothing above supplies a value and stdin is a TTY |
terraform apply -var="environment=prod" -var="replica_count=3" terraform apply -var-file="envs/prod.tfvars" # Order matters: prod.tfvars wins here terraform apply -var="environment=dev" -var-file="envs/prod.tfvars" export TF_VAR_environment=prod export TF_VAR_tags='{ Project = "myapp" }' # HCL syntax for complex types
-var-fileis never auto-loaded from a subdirectory. Onlyterraform.tfvarsand*.auto.tfvarsin the working directory load themselves.TF_VAR_values are parsed as HCL for non-string types, and a malformed one fails at parse time with a confusing message.- Pass
-input=falsein CI so a missing required variable errors instead of hanging on a prompt.
tfvars Files
# terraform.tfvars (auto-loaded) environment = "prod" instance_type = "t3.small" replica_count = 2 tags = { Project = "myapp" Owner = "platform-team" } allowed_cidrs = [ "10.0.0.0/8", "192.168.0.0/16", ]
A .tfvars file is HCL but holds only name = value assignments. No blocks, no variable declarations, no interpolation of other variables.
Never commit a
.tfvarsfile containing secrets. Add*.tfvarsto.gitignoreand commit aterraform.tfvars.exampleinstead. Real secrets belong in a secrets manager, read at apply time through a data source or anephemeralresource.
Validation
variable "environment" { type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment must be dev, staging, or prod." } } variable "cidr_block" { type = string validation { condition = can(cidrhost(var.cidr_block, 0)) error_message = "cidr_block must be valid CIDR notation." } }
Multiple validation blocks are allowed and all are evaluated, so one plan reports every problem.
Cross-variable validation (1.9+). Conditions are now ordinary expressions and may reference other variables, locals and data sources, not just the variable being validated.
variable "min_size" { type = number } variable "max_size" { type = number validation { condition = var.max_size >= var.min_size error_message = "max_size must be at least min_size." } }
sensitive, nullable, ephemeral, const, deprecated
variable "db_password" { type = string sensitive = true # redacted in CLI output, still plaintext in state } variable "image_tag" { type = string default = "latest" nullable = true # a caller may pass null and get the default } variable "vault_token" { type = string ephemeral = true # never written to state or plan files sensitive = true } variable "module_version" { type = string const = true # usable in module source/version, 1.15+ } variable "old_name" { type = string default = null deprecated = "Use new_name instead. old_name is removed in v3." }
export TF_VAR_db_password="$(vault kv get -field=password secret/db)"
sensitivehides values from output, it does not encrypt state. Assume state is a secret.nullable = false(the default istrue) rejectsnulloutright, which is often what you want for a required setting.ephemeralvariables may only be consumed by provider config, provisioners, write-only arguments and other ephemeral values. Using one in a normal resource argument is an error.deprecatedemits a warning when a caller sets the variable, which is how you retire a module input without breaking anyone.