Terraform is an infrastructure-as-code tool for building, changing, and versioning infrastructure safely and efficiently.

Getting started

Introduction

Terraform is an infrastructure-as-code tool for building, changing, and versioning infrastructure safely and efficiently. This reference covers the CLI, configuration, state, and modules.

Install

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

terraform -version
terraform -help
terraform plan -help

See: Install Terraform

Core workflow

terraform init                 # Initialize providers and backend
terraform fmt -recursive       # Format configuration
terraform validate             # Check syntax and consistency
terraform plan                 # Preview changes
terraform apply                # Preview, approve, and apply
terraform destroy              # Destroy managed infrastructure

Run commands from the root module directory.

See: Core workflow

Saved plans

terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplan

terraform show -json tfplan > tfplan.json

Saved plans can contain sensitive configuration and values. JSON output exposes sensitive values in plain text, so protect generated JSON files.

See: Create a plan

Useful options

terraform -chdir=environments/prod plan
terraform plan -var-file=prod.tfvars
terraform plan -out=tfplan -detailed-exitcode
terraform apply -auto-approve
terraform plan -refresh-only
terraform apply -refresh-only

-detailed-exitcode returns 0 for no changes, 1 for errors, and 2 for changes.

See: Terraform CLI

Configuration

Terraform and providers

terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

Commit .terraform.lock.hcl; do not commit .terraform/.

See: Provider requirements

Resources

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = "t3.micro"

  tags = {
    Name = "web-${terraform.workspace}"
  }
}

# Reference: aws_instance.web.id

References create implicit dependencies.

See: Resource blocks

Data sources

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
  }
}

# Reference: data.aws_ami.ubuntu.id

Data sources read information without managing its lifecycle.

See: Data sources

Input variables

variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"

  validation {
    condition = contains(
      ["dev", "staging", "prod"],
      var.environment
    )
    error_message = "Use dev, staging, or prod."
  }
}

variable "api_token" {
  type      = string
  sensitive = true
  nullable  = false
}

See: Input variables

Set input values

terraform apply -var='environment=prod'
terraform apply -var-file='prod.tfvars'
export TF_VAR_environment=prod
# terraform.tfvars
environment = "prod"

Precedence increases from environment variables, terraform.tfvars, *.auto.tfvars, to command-line options (-var, -var-file).

See: Assign variable values

Local values and outputs

locals {
  name = "${var.project}-${var.environment}"
  tags = {
    Project     = var.project
    Environment = var.environment
  }
}

output "instance_ip" {
  description = "Public IP address"
  value       = aws_instance.web.public_ip
}

output "token" {
  value     = var.api_token
  sensitive = true
}
terraform output
terraform output -raw instance_ip
terraform output -json

The -raw and -json options expose sensitive outputs in plain text.

See: Local values, Output values

Expressions

Value types

"hello"                    # string
true                       # bool
42                         # number
["a", "b"]                 # tuple/list
{ name = "web", port = 80 } # object/map
null                       # absence of a value

See: Types and values

String templates

name = "web-${var.environment}"

script = <<-EOT
  #!/bin/bash
  echo "${local.name}"
EOT

message = "Enabled: %{if var.enabled}yes%{else}no%{endif}"

See: Strings and templates

Conditionals

instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

Both result values must have compatible types.

See: Conditional expressions

For expressions

[for name in var.names : upper(name)]

{
  for user in var.users :
  user.name => user.id
  if user.enabled
}

See: For expressions

Splat expressions

aws_instance.web[*].id
aws_instance.web[*].private_ip

Splat syntax works with lists, sets, and tuples.

See: Splat expressions

Common functions

length(var.names)
lookup(var.tags, "Name", "default")
merge(local.common_tags, var.extra_tags)
toset(var.names)
try(local.value.deep, "fallback")
can(regex("^[a-z]+$", var.name))
jsonencode(local.policy)
file("${path.module}/script.sh")

See: Expressions, Functions

Repetition and lifecycle

count

resource "aws_instance" "web" {
  count = 3

  ami           = var.ami_id
  instance_type = "t3.micro"
  tags          = { Name = "web-${count.index}" }
}

# aws_instance.web[0].id

Use count for nearly identical instances indexed by number.

See: count meta-argument

for_each

resource "aws_s3_bucket" "logs" {
  for_each = toset(["app", "audit"])

  bucket = "${var.prefix}-${each.key}"
}

# aws_s3_bucket.logs["app"].id

Use for_each when instances need stable keys.

See: for_each meta-argument

Explicit dependencies

resource "aws_instance" "app" {
  # ...
  depends_on = [aws_iam_role_policy.app]
}

Use depends_on only for dependencies Terraform cannot infer.

See: depends_on meta-argument

Lifecycle rules

resource "aws_instance" "web" {
  # ...

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = true
    ignore_changes        = [tags["LastModified"]]
  }
}

prevent_destroy does not protect an object after its configuration is removed.

See: Meta-arguments, Lifecycle

Modules

Call a module

module "network" {
  source = "./modules/network"

  cidr_block  = "10.0.0.0/16"
  environment = var.environment
}

# Reference: module.network.vpc_id

See: Module blocks

Registry module

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = local.name
  cidr = "10.0.0.0/16"
}

Pin registry module versions and run terraform init after changing sources.

See: Using modules

Module structure

.
├── main.tf
├── outputs.tf
├── variables.tf
├── versions.tf
└── modules/
    └── network/
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

All .tf files in a directory form one module.

See: Modules

State and existing resources

Inspect state

terraform state list
terraform state show aws_instance.web
terraform show
terraform output

State can contain secrets; store remote state securely and restrict access.

See: state list

Move or remove addresses

terraform state mv aws_instance.old aws_instance.web
terraform state rm aws_instance.web

Prefer configuration-driven moved and removed blocks for reviewable changes.

moved {
  from = aws_instance.old
  to   = aws_instance.web
}

removed {
  from = aws_instance.legacy
  lifecycle { destroy = false }
}

See: Refactoring, removed block

Import existing resources

import {
  to = aws_instance.web
  id = "i-0123456789abcdef0"
}
terraform plan
terraform apply

# Imperative alternative
terraform import aws_instance.web i-0123456789abcdef0

Import associates an existing object with one Terraform resource address.

See: Import

Workspaces

terraform workspace list
terraform workspace new staging
terraform workspace select staging
terraform workspace show
terraform workspace select default
terraform workspace delete staging

CLI workspaces share configuration but use separate state instances.

See: State, Import, Workspaces

Debugging and automation

Evaluate expressions

terraform console

> cidrsubnet("10.0.0.0/16", 8, 2)
"10.0.2.0/24"

See: terraform console

Logging

TF_LOG=DEBUG terraform plan
TF_LOG=TRACE TF_LOG_PATH=terraform.log terraform apply

Logs may contain sensitive values.

See: Debugging

CI checks

# Validate without initializing the backend:
terraform fmt -check -recursive
terraform init -backend=false -input=false
terraform validate

# Initialize the backend before planning:
terraform init -input=false
terraform plan -input=false -no-color -detailed-exitcode

See: Automation

Targeting

terraform plan -target=aws_instance.web
terraform apply -replace=aws_instance.web

Use -target only for exceptional recovery, not routine workflows.

See: Debugging, Automation

Also see

0 Comments for this cheatsheet. Write yours!