Getting Started
Basic Syntax
# Single-line comment
// Alternative single-line comment
/* Multi-line
comment */
# Argument assignment
image_id = "abc123"
Simple key-value pairs form the foundation of HCL configuration.
Block Structure
resource "aws_instance" "example" {
ami = "ami-abc123"
# Nested block
network_interface {
# ...
}
}
Blocks have a type, optional labels, and contain arguments or nested blocks.
Data Types
# String
name = "hello"
# Number
count = 42
# Bool
enabled = true
# List
zones = ["us-east-1a", "us-east-1b"]
# Map
tags = {
Name = "web-server"
}
# Null
optional = null
HCL supports primitive types (string, number, bool) and complex types (list, map, set).
Multiline Strings
multiline = <<-EOT
Hello
World
EOT
Heredoc syntax for multiline strings. Use <<- to strip leading whitespace.
String Interpolation
Basic Interpolation
name = "World"
greeting = "Hello, ${var.name}!"
Embed expressions in strings using ${} syntax.
Conditional Directive
message = "Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"
Use %{ if } directive for conditional content in strings.
For Directive
servers = <<EOT
%{ for ip in aws_instance.example[*].private_ip ~}
server ${ip}
%{ endfor ~}
EOT
Generate repeated content. The ~ strips whitespace/newlines.
Escaping
literal_interpolation = "$${not_a_variable}"
literal_directive = "%%{not_a_directive}"
Use $$ and %% to escape interpolation and directives.
Core Blocks
Resource
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "web-server"
}
}
Declares infrastructure resource to create.
Variable
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
sensitive = false
validation {
condition = contains(["t2.micro", "t2.small"], var.instance_type)
error_message = "Invalid type."
}
}
Input variables with validation. Reference with var.instance_type.
Output
output "instance_ip" {
description = "Public IP of instance"
value = aws_instance.web.public_ip
sensitive = false
}
Output values for modules or CLI display.
Local Values
locals {
common_tags = {
Project = "MyApp"
Env = var.environment
}
name = "${var.environment}-web"
}
Computed values reused throughout config. Reference with local.common_tags.
Data Source
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/*22.04*"]
}
}
Query existing infrastructure. Reference with data.aws_ami.ubuntu.id.
Module
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}
Reusable configuration packages. Reference outputs with module.vpc.vpc_id.
Provider
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
ManagedBy = "Terraform"
}
}
}
Configure provider plugin.
Provider Aliases
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_instance" "west" {
provider = aws.west
ami = "ami-456"
}
Multiple configurations for same provider.
Terraform Settings
terraform {
required_version = ">= 1.3"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
Configure Terraform behavior, providers, and state backend.
Expressions
Conditional (Ternary)
instance_type = var.environment == "prod" ? "t3.large" : "t2.micro"
Classic ternary operator: condition ? true_value : false_value.
For Expression (List)
# Transform
upper_names = [for name in var.names : upper(name)]
# Filter
prod = [for inst in var.instances : inst if inst.env == "prod"]
# With index
indexed = [for i, v in var.list : "${i}: ${v}"]
List comprehensions with optional filtering and indexing.
For Expression (Map)
# Transform map
upper_tags = {for k, v in var.tags : k => upper(v)}
# Filter map
filtered = {for k, v in var.tags : k => v if v != ""}
# Group by (with ...)
by_role = {for name, user in var.users : user.role => name...}
Map comprehensions. Use ... for grouping mode.
Splat Expression
# Get all IDs
instance_ids = aws_instance.example[*].id
# Nested access
private_ips = aws_instance.example[*].network_interface[0].private_ip
Shorthand for extracting attributes. Equivalent to [for inst in ... : inst.id].
Resource References
# Single resource
aws_instance.web.id
# With count
aws_instance.server[0].id
aws_instance.server[*].id
# With for_each
aws_instance.server["web"].id
Reference resource attributes in expressions.
Meta-Arguments
count
resource "aws_instance" "server" {
count = 4
ami = "ami-a1b2c3d4"
tags = {
Name = "Server ${count.index}"
}
}
Create multiple instances. Access index with count.index. Reference: aws_instance.server[0].
for_each
# With set
resource "aws_instance" "server" {
for_each = toset(["web", "api", "worker"])
ami = "ami-a1b2c3d4"
tags = {
Name = each.key
}
}
Create instances from set/map. Access with each.key and each.value. Reference: aws_instance.server["web"].
for_each (Map)
resource "aws_iam_user" "users" {
for_each = {
alice = "admin"
bob = "developer"
}
name = each.key
tags = {
Role = each.value
}
}
Use maps for key-value iteration.
depends_on
resource "aws_iam_role_policy" "example" {
role = aws_iam_role.example.id
policy = "..."
depends_on = [aws_iam_role.example]
}
Explicit dependency when implicit dependencies aren't sufficient.
provider
resource "aws_instance" "west" {
provider = aws.west
ami = "ami-456"
}
Use specific provider configuration.
Lifecycle Meta-Arguments
create_before_destroy
lifecycle {
create_before_destroy = true
}
Create replacement before destroying original. Useful for zero-downtime changes.
prevent_destroy
lifecycle {
prevent_destroy = true
}
Terraform will error if resource deletion attempted. Doesn't prevent manual deletion.
ignore_changes
lifecycle {
ignore_changes = [
tags,
ami,
]
}
Ignore changes to specific attributes.
ignore_changes (All)
lifecycle {
ignore_changes = all
}
Ignore all attribute changes. Prevents any updates to resource.
replace_triggered_by
lifecycle {
replace_triggered_by = [
aws_ecs_service.svc.id
]
}
Force replacement when referenced resource changes.
Preconditions
lifecycle {
precondition {
condition = data.aws_ami.example.architecture == "x86_64"
error_message = "AMI must be x86_64."
}
}
Validate conditions before apply.
Postconditions
lifecycle {
postcondition {
condition = self.public_dns != ""
error_message = "Instance must have public DNS."
}
}
Validate conditions after apply. Use self to reference resource.
Dynamic Blocks
Basic Dynamic Block
resource "aws_security_group" "example" {
name = "example"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Generate repeated nested blocks. The block label becomes the default iterator name.
Custom Iterator
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.from_port
to_port = rule.value.to_port
}
}
Use iterator to customize the iterator name instead of block label.
String Functions
format
format("instance-%03d", 42)
# "instance-042"
Printf-style string formatting.
join / split
join(", ", ["web", "api", "db"])
# "web, api, db"
split("-", "web-server-01")
# ["web", "server", "01"]
Join list to string or split string to list.
replace
replace("hello world", "world", "tf")
# "hello tf"
Replace substring in string.
trim functions
trimspace(" hello ")
# "hello"
Remove leading/trailing whitespace.
upper / lower
upper("hello") # "HELLO"
lower("HELLO") # "hello"
Change string case.
substr
substr("hello world", 0, 5)
# "hello"
Extract substring: substr(string, offset, length).
regexall
regexall("[0-9]+", "abc123def456")
# ["123", "456"]
Extract all regex matches.
Collection Functions
concat
concat(["a", "b"], ["c", "d"])
# ["a", "b", "c", "d"]
Combine multiple lists.
flatten
flatten([["a", "b"], ["c", "d"]])
# ["a", "b", "c", "d"]
Flatten nested lists.
merge
merge({a = 1}, {b = 2})
# {a = 1, b = 2}
Combine multiple maps.
lookup
lookup({a = 1}, "b", 0)
# 0
Get map value with default: lookup(map, key, default).
element
element(["a", "b", "c"], 4)
# "b"
Get list element by index (wraps around).
length
length(["a", "b", "c"]) # 3
Get collection length.
keys / values
keys({a = 1, b = 2}) # ["a", "b"]
values({a = 1, b = 2}) # [1, 2]
Extract map keys or values.
contains
contains(["a", "b"], "a")
# true
Check if list contains value.
distinct
distinct(["a", "b", "a"])
# ["a", "b"]
Remove duplicates from list.
slice
slice(["a", "b", "c", "d"], 1, 3)
# ["b", "c"]
Extract sublist: slice(list, start, end).
sort
sort(["c", "a", "b"])
# ["a", "b", "c"]
Sort list alphabetically.
range
range(5)
# [0, 1, 2, 3, 4]
Generate sequence of numbers.
Numeric Functions
max / min
max(5, 12, 9) # 12
min(5, 12, 9) # 5
Find maximum or minimum value.
abs
abs(-5) # 5
Absolute value.
ceil / floor
ceil(5.2) # 6
floor(5.8) # 5
Round up or down.
pow
pow(2, 3) # 8
Exponentiation: pow(base, exponent).
Encoding Functions
JSON
jsonencode({name = "example", count = 3})
jsondecode('{"name":"example"}')
Encode/decode JSON.
YAML
yamlencode({name = "example"})
yamldecode("name: example")
Encode/decode YAML.
Base64
base64encode("hello")
base64decode("aGVsbG8=")
Encode/decode Base64.
URL Encoding
urlencode("hello world")
# "hello+world"
URL-encode string.
Filesystem Functions
file
file("${path.module}/config.txt")
Read file contents as string.
filebase64
filebase64("image.png")
Read file contents as base64.
templatefile
templatefile("${path.module}/template.tpl", {
name = "example"
port = 8080
})
Render template file with variables.
basename / dirname
basename("/path/to/file.txt") # "file.txt"
dirname("/path/to/file.txt") # "/path/to"
Extract filename or directory path.
abspath
abspath("./relative/path")
Convert relative to absolute path.
Type Conversion
tostring / tonumber
tostring(42) # "42"
tonumber("42") # 42
Convert to string or number.
tobool
tobool("true") # true
Convert string to boolean.
tolist / toset
tolist(["a", "b"])
toset(["a", "b", "a"]) # ["a", "b"]
Convert to list or set (removes duplicates).
tomap
tomap({a = 1})
Convert to map type.
Date/Time Functions
timestamp
timestamp()
# "2024-01-15T10:30:00Z"
Current timestamp in UTC.
formatdate
formatdate("YYYY-MM-DD", timestamp())
Format timestamp string.
timeadd
timeadd(timestamp(), "24h")
Add duration to timestamp.
Terragrunt
Basic Structure
# Include parent config
include "root" {
path = find_in_parent_folders()
}
# Local values
locals {
environment = "prod"
region = "us-east-1"
}
# Terraform source
terraform {
source = "git::git@github.com:example/modules.git//vpc?ref=v1.0.0"
}
Terragrunt extends Terraform with DRY configuration and dependency management.
Remote State
remote_state {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
}
}
Configure remote state backend.
Dependencies
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
Declare dependencies on other Terragrunt modules.
Generate Blocks
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "${local.region}"
}
EOF
}
Generate files before running Terraform.
Terragrunt Functions
find_in_parent_folders("root.hcl")
path_relative_to_include()
get_terragrunt_dir()
get_parent_terragrunt_dir()
get_aws_account_id()
run_cmd("echo", "hello")
Terragrunt-specific helper functions.
Terraform CLI
Core Workflow
terraform init
terraform validate
terraform fmt
terraform plan
terraform apply
terraform apply -auto-approve
terraform destroy
Standard workflow commands.
Plan Output
terraform plan -out=plan.tfplan
terraform apply plan.tfplan
Save plan to file for later apply.
State Management
terraform state list
terraform state show aws_instance.example
terraform state mv aws_instance.old aws_instance.new
terraform state rm aws_instance.example
Inspect and manipulate state.
State Backup
terraform state pull > terraform.tfstate
terraform state push terraform.tfstate
Pull/push state manually.
Import Resources
terraform import aws_instance.example i-1234567890abcdef0
terraform import module.vpc.aws_vpc.main vpc-12345
Import existing infrastructure into state.
Workspaces
terraform workspace list
terraform workspace new dev
terraform workspace select prod
terraform workspace show
terraform workspace delete dev
Manage multiple named states.
Utilities
terraform console
terraform graph | dot -Tpng > graph.png
terraform providers
terraform version
terraform test
Additional utility commands.
Gotchas
Meta-Arguments
- Cannot use both
countandfor_eachin same block lifecycleconfigurations affect dependency graphignore_changes = allprevents all updatesprevent_destroydoesn't work if resource removed from config
Expressions
- For expressions with sets have no guaranteed order
- Splat
[*]only works with lists, use for expression for maps
Strings
- Heredoc strings don't support backslash escapes
- Use
$${and%%{to escape interpolation in heredocs
State & Security
- Sensitive variables still stored in plaintext state
- State files may contain secrets - encrypt and restrict access
Dynamic Blocks
- Dynamic blocks can make code hard to read - use sparingly
- Cannot use dynamic blocks for lifecycle or provisioner blocks
Also See
- Terraform Documentation - Official Terraform docs
- HCL Native Syntax - HCL language specification
- Terraform Registry - Provider and module registry
- Terragrunt Documentation - Official Terragrunt docs
- Terraform Best Practices - Community best practices