HCL Basics
HashiCorp Configuration Language (HCL) is designed to be both human-readable and expressive. All Terraform files end with .tf.
File Structure
Example Terraform configuration:
# Comment using hash symbol
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
# Configuration here
}Blocks
Blocks are containers for configuration. They define infrastructure objects that Terraform manages.
Syntax:
block_type "label1" "label2" {
key = value
nested {
key = value
}
}Common block types:
| Block | Purpose | Labels |
|---|---|---|
| resource | Define infrastructure | type, name |
| data | Reference existing infrastructure | type, name |
| variable | Input parameter | name |
| output | Return value | name |
| provider | Cloud provider config | type |
| terraform | Terraform settings | none |
| locals | Local values | none |
| module | Reusable configuration | name |
Data Types
Primitive Types
String:
instance_type = "t2.micro"
region = "us-east-1"
# String interpolation
name = "web-server-${var.environment}"Number:
count = 3
port = 8080
cpu_credits = 100.5Boolean:
enable_monitoring = true
force_destroy = falseComplex Types
List (ordered collection):
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
port_list = [80, 443, 3306]
# Access by index
availability_zones[0] # "us-east-1a"
availability_zones[2] # "us-east-1c"Map (key-value pairs):
tags = {
Name = "web-server"
Environment = "production"
CostCenter = "engineering"
}
# Access by key
tags["Name"] # "web-server"
tags.Environment # "production"
# List of maps
subnets = {
private = "10.0.1.0/24"
public = "10.0.2.0/24"
}Object (typed structure):
database = {
name = "myapp"
engine = "mysql"
instance_type = "db.t3.micro"
storage_gb = 20
}
# Access nested properties
database.name # "myapp"
database.storage_gb # 20Tuple (fixed-length collection with mixed types):
server_info = ["web-1", 80, true, "us-east-1a"]
# Access by index
server_info[0] # "web-1"
server_info[2] # trueType Constraints
variable "instance_type" {
type = string
}
variable "instance_count" {
type = number
}
variable "tags" {
type = map(string) # Map where values are strings
}
variable "subnets" {
type = list(object({
name = string
cidr = string
}))
}Operators
Arithmetic
a = 10 + 5 # 15
b = 10 - 3 # 7
c = 4 * 5 # 20
d = 20 / 4 # 5
e = 17 % 5 # 2 (modulo)
computed_count = var.min_count + var.extra_serversComparison
# == Equal
# != Not equal
# < Less than
# <= Less than or equal
# > Greater than
# >= Greater than or equal
should_create = var.environment == "prod"
has_capacity = var.available_cores >= 4Logical
# && AND
# || OR
# ! NOT
is_production = var.environment == "prod" && var.enable_ha == true
should_alert = error_rate > 0.05 || cpu_usage > 90
is_not_dev = !contains(["dev", "test"], var.environment)Collection
result = [1, 2, 3][0] # Indexing: 1
result = {a = "x", b = "y"}["a"] # Map lookup: "x"Interpolation & String
String Interpolation
# Using ${} syntax
resource "aws_instance" "web" {
tags = {
Name = "web-server-${var.environment}"
# becomes: "web-server-production"
}
}
# Multi-line template
user_data = <<EOF
#!/bin/bash
echo "Environment: ${var.environment}"
echo "Region: ${var.region}"
EOFString Functions
# Concatenation
"hello" + " " + "world" # "hello world"
# String functions
resource "aws_s3_bucket" "logs" {
bucket = lower("MyBucket-${var.env}") # lowercase
tags = {
Name = upper(var.project_name) # UPPERCASE
Size = tonumber("1024") # string to number
}
}References
Variable References
variable "instance_type" {
type = string
default = "t2.micro"
}
resource "aws_instance" "web" {
instance_type = var.instance_type # Reference variable
}Resource References
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id # Resource attribute
cidr_block = "10.0.1.0/24"
availability_zone = var.availability_zone
}Output References
output "vpc_id" {
value = aws_vpc.main.id
}
resource "aws_security_group" "main" {
vpc_id = aws_vpc.main.id # Reference another resource
}For Each References
resource "aws_instance" "servers" {
for_each = var.server_configs
instance_type = each.value["type"]
tags = {
Name = each.key # Map key
}
}
# or access in other resources:
security_group_ids = [for server in aws_instance.servers : server.security_groups[0]]Functions
HCL includes many built-in functions:
String Functions
length("hello") # 5
upper("hello") # "HELLO"
lower("HELLO") # "hello"
join(",", ["a", "b", "c"]) # "a,b,c"
split(",", "a,b,c") # ["a", "b", "c"]
contains("hello", "ell") # trueCollection Functions
length([1, 2, 3]) # 3
element([1, 2, 3], 1) # 2 (zero-indexed)
concat([1, 2], [3]) # [1, 2, 3]
merge({a=1}, {b=2}) # {a=1, b=2}
distinct([1, 1, 2, 2]) # [1, 2]Numeric Functions
floor(4.9) # 4
ceil(4.1) # 5
round(4.6) # 5
min(1, 3, 2) # 1
max(1, 3, 2) # 3Type Conversion
tostring(42) # "42"
tonumber("123") # 123
tobool("true") # true
tolist(["a"]) # ["a"]
tomap({a = "x"}) # {a = "x"}
toset(["a", "b"]) # {"a", "b"}Conditional Functions
# ternary operator
can_ssh = var.environment == "prod" ? false : true
# try function (try, fallback)
az = try(var.availability_zone, "us-east-1a")
# lookup with fallback
env_tag = lookup(var.tags, "Environment", "dev")Comments
# Single line comment
/*
Multi-line comment
Can span multiple lines
*/
# Good comments explain WHY, not WHAT
# ✓ GOOD: "Force destroy because this is ephemeral test infrastructure"
# ✗ BAD: "Setting force_destroy to true"Meta-Arguments
Meta-arguments control resource behavior:
| Meta-Argument | Purpose | Example |
|---|---|---|
| count | Create multiple resources | count = 3 |
| for_each | Loop with map/set | for_each = var.servers |
| depends_on | Explicit dependency | depends_on = [aws_security_group.web] |
| provider | Specific provider instance | provider = aws.us-west-2 |
| lifecycle | Control creation/destruction | lifecycle |
Splat Syntax
Extract values from multiple resources:
# Create 3 instances
resource "aws_instance" "web" {
count = 3
instance_type = "t2.micro"
}
# Get all instance IDs
instance_ids = aws_instance.web[*].id
# Result: ["i-001", "i-002", "i-003"]
# In output
output "all_private_ips" {
value = aws_instance.web[*].private_ip
}For Expressions
Create derived data structures:
# List comprehension
instance_names = [for inst in aws_instance.web : inst.tags["Name"]]
# Map from list
instance_map = { for inst in aws_instance.web : inst.id => inst.private_ip }
# Result: { "i-001" => "10.0.1.5", "i-002" => "10.0.1.6" }
# Filter with condition
public_instances = [for inst in aws_instance.web : inst if inst.associate_public_ip_address]Type System
Terraform is dynamically typed but supports type constraints:
# Type checking in variables
variable "config" {
type = object({
name = string
replicas = number
tags = map(string)
ports = list(number)
})
}
# Dynamic typing (avoid when possible)
variable "flexible" {
type = any
}HCL Best Practices
| Practice | Example | Benefit |
|---|---|---|
| Use consistent formatting | Use terraform fmt | Readability |
| Name resources meaningfully | aws_security_group_web | Clarity |
| Add descriptions | description = "..." | Documentation |
| Use locals for computed values | locals { name_prefix = "..." } | DRY principle |
| Prefer named resources in loops | for_each over count | Better state mapping |
| Add comments for complex logic | /* multi-line comment */ | Future understanding |
| Validate variable types | type = list(string) | Error prevention |
| Use functions for transformations | join(), split(), merge() | Cleaner code |