Terraform HCL Syntax
Comprehensive guide to Terraform's HashiCorp Configuration Language (HCL) syntax, covering structural elements, expressions, templates, operators, and style conventions.
When to Use This Skill
- User asks about "Terraform syntax", "HCL syntax", "how to write Terraform"
- Writing new Terraform configuration files
- Reviewing or understanding existing Terraform code
- Questions about expressions, operators, or templates
- Formatting and style questions
- Syntax errors or validation issues
- Learning Terraform language fundamentals
Language Components
Terraform's configuration language consists of three integrated sub-languages:
- Structural Language - Defines hierarchical configuration (bodies, blocks, attributes)
- Expression Language - Specifies values (literals, operations, functions)
- Template Language - Composes strings (interpolation, directives)
Structural Language
Configuration Files
A configuration file is a UTF-8 encoded text file whose top-level is interpreted as a Body.
File Extensions:
.tf- Native Terraform syntax.tf.json- JSON syntax (for programmatic generation)
Character Encoding:
- Must be UTF-8
- Unix-style line endings (LF) preferred
- Windows-style (CRLF) accepted but may be auto-converted
Bodies
A body is a collection of associated attributes and blocks.
# Body contains attributes and blocks
resource "aws_instance" "example" {
# This is a body - contains attributes and nested blocks
ami = "abc123"
instance_type = "t2.micro"
tags = {
Name = "example"
}
}
Attributes (Arguments)
An attribute assigns a value to a name. Each distinct attribute name may be defined no more than once within a single body.
Syntax:
Attribute = Identifier "=" Expression Newline
Examples:
# Simple attribute
ami = "abc123"
# Attribute with expression
instance_type = var.instance_type
# Attribute with complex expression
count = var.enabled ? 1 : 0
# Object attribute
tags = {
Name = "example"
Environment = var.environment
}
Formatting:
# ✅ Good - Align equals signs
ami = "abc123"
instance_type = "t2.micro"
subnet_id = var.subnet_id
# ❌ Bad - Unaligned
ami = "abc123"
instance_type = "t2.micro"
subnet_id = var.subnet_id
Blocks
A block creates a child body annotated with a type and optional labels.
Syntax:
Block = Identifier (StringLit|Identifier)* "{" Newline Body "}" Newline
Structure:
block_type "label1" "label2" {
# Block body - can contain attributes and nested blocks
argument = value
nested_block {
# Nested block body
}
}
Examples:
# Resource block (2 labels: type, name)
resource "aws_instance" "web" {
ami = "abc123"
}
# Variable block (1 label: name)
variable "instance_count" {
type = number
default = 1
}
# Nested blocks
resource "aws_instance" "web" {
ami = "abc123"
# Nested block
root_block_device {
volume_size = 20
}
# Another nested block
network_interface {
device_index = 0
}
}
One-Line Blocks:
# Short blocks can be on one line
lifecycle { create_before_destroy = true }
Top-Level Blocks
Terraform uses a limited number of top-level block types:
terraform {
required_version = ">= 1.3"
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "abc123"
}
data "aws_ami" "ubuntu" {
most_recent = true
}
variable "region" {
type = string
}
output "instance_id" {
value = aws_instance.web.id
}
module "vpc" {
source = "./modules/vpc"
}
locals {
common_tags = {
Project = "example"
}
}
Identifiers
Identifiers name entities (blocks, attributes, variables).
Rules:
- Can contain: letters, digits, underscores (
_), hyphens (-) - Must start with: letter or underscore (not a digit)
- Follows Unicode identifier syntax (UAX #31)
- Dash
-is allowed (Terraform extension)
Examples:
# ✅ Valid identifiers
my_variable
aws_instance
web-server
resource_1
_private
# ❌ Invalid identifiers
1st_resource # Starts with digit
my variable # Contains space
my@resource # Contains @
Conventions:
# Use snake_case for multi-word identifiers
instance_type
security_group
vpc_cidr_block
# Use snake_case for labels (when appropriate)
resource "aws_instance" "web_server" {
# ...
}
Comments
Three comment syntaxes:
# Single-line comment (preferred)
// Single-line comment (alternative)
/*
Multi-line comment
spans multiple lines
*/
Best Practice:
# ✅ Preferred - Use # for single and multi-line comments
# This is a comment
# explaining the resource
# ❌ Avoid - // style
// This comment style is less idiomatic
Expression Language
Expressions specify values for attributes.
Literal Values
Numbers:
42 # Integer
3.14159 # Decimal
1.5e10 # Exponent notation
Booleans:
true
false
Null:
null
Strings:
# Strings use template syntax
"hello"
"hello ${var.name}"
# Heredoc strings
<<EOT
Multi-line
string content
EOT
# Indented heredoc (strips leading whitespace)
<<-EOT
Indented content
will have leading spaces stripped
EOT
Collection Values
Tuples (ordered lists):
["a", "b", "c"]
[
"item1",
"item2",
"item3",
]
# Trailing comma allowed
[1, 2, 3,]
Objects (maps with named attributes):
{
name = "example"
age = 42
}
# Keys can be expressions (use parentheses)
{
(var.key_name) = "value"
}
# Colon syntax also allowed
{
name: "example",
age: 42
}
# Mixed newlines and commas
{
name = "example",
age = 42
city = "Boston"
}
Ambiguity Resolution:
# ❌ Syntax error - for expression vs collection
[for, foo, baz]
# ✅ Use parentheses
[(for), foo, baz]
# ❌ Syntax error - for as key
{for = 1, baz = 2}
# ✅ Quote the key
{"for" = 1, baz = 2}
# ✅ Or reorder
{baz = 2, for = 1}
# ✅ Or use parentheses
{(for) = 1, baz = 2}
Variables
Access variables using identifiers:
var.instance_type
var.region
local.common_tags
Functions
Call functions with parentheses:
# Basic function call
length(var.list)
# Multiple arguments
merge(local.tags, var.additional_tags)
# Nested function calls
upper(substr(var.name, 0, 3))
# Spread operator (...)
concat(var.list1, var.list2...)
Operators
Arithmetic:
a + b # Addition
a - b # Subtraction
a * b # Multiplication
a / b # Division
a % b # Modulo
-a # Negation
Comparison:
a == b # Equal
a != b # Not equal
a < b # Less than
a <= b # Less than or equal
a > b # Greater than
a >= b # Greater than or equal
Logical:
a && b # AND
a || b # OR
!a # NOT
Operator Precedence (highest to lowest):
1. -, ! (unary)
2. *, /, %
3. +, -
4. >, >=, <, <=
5. ==, !=
6. &&
7. ||
Examples:
# Arithmetic
var.count * 2
(var.price * var.quantity) + var.tax
# Comparisons
var.age >= 18
var.status == "active"
# Logical
var.enabled && var.count > 0
var.env == "prod" || var.env == "staging"
# Combined
(var.count > 0) && (var.enabled || var.force)
Conditional Expressions
Ternary operator:
condition ? true_val : false_val
Examples:
# Simple conditional
var.enabled ? 1 : 0
# With complex expressions
var.env == "prod" ? "m5.large" : "t2.micro"
# Nested conditionals
var.env == "prod" ? "m5.large" : var.env == "staging" ? "t3.medium" : "t2.micro"
# Safe indexing
length(var.list) > 0 ? var.list[0] : "default"
For Expressions
Transform collections:
Tuple For:
# Basic syntax
[for item in collection : transform_expression]
# With index
[for i, v in collection : expression]
# With condition
[for v in collection : v if condition]
# Examples
[for s in var.list : upper(s)]
[for i, v in var.list : "${i}: ${v}"]
[for s in var.list : s if length(s) > 3]
Object For:
# Basic syntax
{for item in collection : key_expr => value_expr}
# With grouping (...)
{for item in collection : key_expr => value_expr...}
# Examples
{for k, v in var.map : k => upper(v)}
{for s in var.list : s => length(s)}
# Grouping mode
{for i, v in ["a", "a", "b"] : v => i...}
# Result: {a = [0, 1], b = [2]}
Splat Expressions
Convenient attribute/element access:
Attribute-Only Splat:
# Syntax
collection.*.attribute
# Example
var.instances.*.id
# Equivalent to: [for i in var.instances : i.id]
# Chained attributes
var.instances.*.tags.*.Name
Full Splat:
# Syntax
collection[*].attribute_or_index
# Examples
var.instances[*].id
var.instances[*].tags["Name"]
var.instances[*].network_interfaces[0]
Auto-Wrapping:
# If value is not a list, it's wrapped
single_object.*.id
# Equivalent to: [single_object.id]
# Null becomes empty list
null_value.*.id
# Result: []
Index Operator
Access collection elements:
# Tuple/list indexing (zero-based)
var.list[0]
var.list[var.index]
# Object/map indexing (by key)
var.map["key"]
var.map[var.dynamic_key]
# Legacy syntax (compatibility only)
var.list.0 # Don't use in new code
Attribute Access
Access object attributes:
# Syntax
object.attribute
# Examples
var.config.name
aws_instance.web.public_ip
module.vpc.vpc_id
data.aws_ami.ubuntu.id
# Chained access
var.config.network.vpc.id
Template Language
Templates compose values into strings.
Template Expressions
Quoted Template:
"Hello, ${var.name}!"
"The result is ${var.a + var.b}"
Heredoc Template:
<<EOT
Hello, ${var.name}!
Your instance is ${aws_instance.web.id}
EOT
# Indented heredoc (strips leading spaces)
<<-EOT
Line 1
Line 2
EOT
Escape Sequences
In quoted templates:
"Line 1\nLine 2" # Newline
"Column 1\tColumn 2" # Tab
"She said \"Hello\"" # Quote
"Path: C:\\Users\\name" # Backslash
"Unicode: \u0041" # Unicode (A)
"Emoji: \U0001F600" # Unicode emoji
Escaping Template Sequences:
# Escape interpolation
"Dollar sign: $${var.name}" # Outputs: Dollar sign: ${var.name}
# Escape directive
"Percent: %%{if true}text%{endif}" # Outputs: Percent: %{if true}text%{endif}
Interpolation
Embed expressions in strings:
# Basic interpolation
"Hello, ${var.name}!"
# Expression interpolation
"Total: ${var.price * var.quantity}"
# Function calls
"Uppercase: ${upper(var.name)}"
# Conditional
"Status: ${var.enabled ? "active" : "inactive"}"
Strip Markers
Remove whitespace with ~:
# Strip after opening
"hello ${~ "world" }"
# Result: "helloworld"
# Strip before closing
"hello${ "world" ~} test"
# Result: "hellotest"
# Strip both
"%{ if true ~} hello %{~ endif }"
# Result: "hello"
Template Directives
If Directive:
%{ if condition }
true_content
%{ else }
false_content
%{ endif }
# Example
<<EOT
%{ if var.enabled }
Service is enabled
%{ else }
Service is disabled
%{ endif }
EOT
For Directive:
%{ for item in collection }
${item}
%{ endfor }
# Example with index
<<EOT
%{ for i, name in var.names }
${i + 1}. ${name}
%{ endfor }
EOT
# Result:
# 1. Alice
# 2. Bob
# 3. Carol
Template Unwrapping
A template with only a single interpolation is "unwrapped":
# Unwrapped - returns boolean
"${true}"
# Result: true (boolean, not string)
# Not unwrapped - returns string
"hello ${true}"
# Result: "hello true"
# Double unwrapping
"${"${true}"}"
# Result: true (boolean)
Common Patterns
Meta-Arguments
Meta-arguments modify resource behavior:
resource "aws_instance" "web" {
# Meta-arguments first
count = var.instance_count
for_each = toset(var.availability_zones)
depends_on = [aws_security_group.web]
provider = aws.west
# Regular arguments
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
# Lifecycle block last
lifecycle {
create_before_destroy = true
prevent_destroy = true
}
}
Dynamic Blocks
Generate nested blocks:
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
}
}
}
For comprehensive examples and best practices, see the terraform-resources skill.
Type Constraints
Specify variable types:
variable "instance_type" {
type = string
}
variable "instance_count" {
type = number
}
variable "enabled" {
type = bool
}
variable "tags" {
type = map(string)
}
variable "subnets" {
type = list(string)
}
variable "config" {
type = object({
name = string
size = number
enabled = bool
})
}
variable "instances" {
type = list(object({
name = string
type = string
}))
}
# Optional attributes
variable "server" {
type = object({
name = string
port = optional(number, 80)
enabled = optional(bool, true)
})
}
Style Guidelines
Formatting
Indentation:
# ✅ Use 2 spaces per nesting level
resource "aws_instance" "web" {
ami = "abc123"
network_interface {
device_index = 0
}
}
Alignment:
# ✅ Align equals signs
ami = "abc123"
instance_type = "t2.micro"
subnet_id = var.subnet_id
Blank Lines:
# ✅ Separate top-level blocks with blank line
resource "aws_instance" "web" {
ami = "abc123"
}
resource "aws_security_group" "web" {
name = "web"
}
Block Ordering:
resource "aws_instance" "example" {
# 1. Meta-arguments first
count = 2
# 2. Required arguments
ami = "abc123"
instance_type = "t2.micro"
# 3. Optional arguments
monitoring = true
# 4. Nested blocks
root_block_device {
volume_size = 20
}
# 5. Lifecycle block last
lifecycle {
create_before_destroy = true
}
}
Naming Conventions
Resources:
# ✅ Use descriptive nouns, snake_case
resource "aws_instance" "web_server" {}
resource "aws_security_group" "database" {}
# ❌ Avoid redundant naming
resource "aws_instance" "aws_instance_web" {}
Variables and Outputs:
# ✅ Descriptive, snake_case
variable "instance_count" {}
output "vpc_id" {}
# Order: type, description, default, sensitive, validation
variable "db_password" {
type = string
description = "Database password"
sensitive = true
validation {
condition = length(var.db_password) >= 8
error_message = "Password must be at least 8 characters"
}
}
File Organization
Standard Files:
main.tf # Primary resources
variables.tf # Input variables (alphabetical)
outputs.tf # Output values (alphabetical)
versions.tf # Version constraints
providers.tf # Provider configurations
locals.tf # Local values
backend.tf # Backend configuration
Resource-Specific Files:
network.tf # Networking resources
security.tf # Security groups, NACLs
iam.tf # IAM roles and policies
compute.tf # Compute resources
Validation and Formatting
terraform fmt
Format code to standard style:
# Format current directory
terraform fmt
# Format recursively
terraform fmt -recursive
# Check formatting without changes
terraform fmt -check
terraform validate
Validate syntax and internal consistency:
terraform validate
What it checks:
- Syntax correctness
- Type consistency
- Argument requirements
- Variable references
- Does NOT check: Provider-specific validation
Common Syntax Errors
Missing Equals Sign
# ❌ Error
ami "abc123"
# ✅ Correct
ami = "abc123"
Unclosed Blocks
# ❌ Error - missing closing brace
resource "aws_instance" "web" {
ami = "abc123"
# ✅ Correct
resource "aws_instance" "web" {
ami = "abc123"
}
Invalid Identifiers
# ❌ Error - starts with digit
variable "1st_instance" {}
# ✅ Correct
variable "first_instance" {}
Type Mismatches
# ❌ Error - string where number expected
variable "count" {
type = number
default = "five"
}
# ✅ Correct
variable "count" {
type = number
default = 5
}
Duplicate Attributes
# ❌ Error - duplicate attribute
resource "aws_instance" "web" {
ami = "abc123"
ami = "def456"
}
# ✅ Correct - use one value
resource "aws_instance" "web" {
ami = "abc123"
}
Best Practices Summary
Do's
✅ Use terraform fmt before committing ✅ Use 2-space indentation ✅ Align equals signs for readability ✅ Use # for comments (not //) ✅ Use snake_case for identifiers ✅ Separate top-level blocks with blank lines ✅ Put meta-arguments first ✅ Put lifecycle blocks last ✅ Use descriptive, clear names ✅ Add descriptions to variables and outputs ✅ Use type constraints on variables
Don'ts
❌ Don't use // for comments ❌ Don't start identifiers with digits ❌ Don't duplicate attribute names ❌ Don't use CRLF line endings ❌ Don't include resource type in resource name ❌ Don't skip terraform fmt ❌ Don't use legacy syntax (.0 indexing) ❌ Don't hardcode values that should be variables
Quick Reference
Attribute:
name = value
Block:
type "label" {
attribute = value
}
Expression:
value
var.name
function(arg)
condition ? true_val : false_val
[for x in list : transform]
{for k, v in map : k => v}
Template:
"String with ${interpolation}"
<<EOT
Heredoc with ${interpolation}
EOT
Operators:
+ - * / % # Arithmetic
== != < > <= >= # Comparison
&& || ! # Logical
Comments:
# Preferred style
// Alternative
/* Multi-line */
References
- HCL Spec: https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md
- Terraform Configuration: https://developer.hashicorp.com/terraform/language/syntax/configuration
- Terraform Style Guide: https://developer.hashicorp.com/terraform/language/style
- Expressions: https://developer.hashicorp.com/terraform/language/expressions
- Functions: https://developer.hashicorp.com/terraform/language/functions
- terraform-refactoring skill for safe code restructuring
- terraform.instructions.md for complete coding guidelines
Converted and distributed by TomeVault — claim your Tome and manage your conversions.