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.
- Terraform documentation (developer.hashicorp.com)
- Terraform Registry (registry.terraform.io)
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/.
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).
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}"
Conditionals
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
Both result values must have compatible types.
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.
Explicit dependencies
resource "aws_instance" "app" {
# ...
depends_on = [aws_iam_role_policy.app]
}
Use depends_on only for dependencies Terraform cannot infer.
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
0 Comments for this cheatsheet. Write yours!