What Is HCL?
HCL (HashiCorp Configuration Language) is the declarative language used to write Terraform configurations. It’s designed to be human-readable while remaining machine-parseable — you describe what you want, and Terraform figures out how to create it.
HCL files use the .tf extension. If you’ve seen Terraform code, you’ve seen HCL:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "my-server"
}
}
HCL vs JSON
Terraform also accepts JSON (.tf.json files), but HCL is the standard:
HCL (readable):
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
}
JSON (verbose):
{
"resource": {
"aws_instance": {
"web": {
"ami": "ami-12345",
"instance_type": "t3.micro"
}
}
}
}
Use HCL for human-written configs. Use JSON only for machine-generated configs.
Basic Syntax
Comments
# Single-line comment
// Also a single-line comment
/*
Multi-line
comment
*/
Arguments (Key-Value Pairs)
ami = "ami-12345" # String
count = 3 # Number
enabled = true # Boolean
Blocks
Blocks are containers for related configuration. They have a type, optional labels, and a body:
# Block with two labels
resource "aws_instance" "web" {
ami = "ami-12345"
}
# Block with one label
variable "instance_type" {
default = "t3.micro"
}
# Block with no labels
terraform {
required_version = ">= 1.5"
}
Nested Blocks
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
root_block_device {
volume_size = 20
volume_type = "gp3"
}
tags = {
Name = "web-server"
}
}
Data Types
Primitive Types
| Type | Example | Description |
|---|---|---|
string | "hello" | Text |
number | 42, 3.14 | Integer or float |
bool | true, false | Boolean |
Complex Types
List (ordered sequence):
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Access by index
availability_zones[0] # "us-east-1a"
Map (key-value pairs):
variable "instance_tags" {
type = map(string)
default = {
Name = "web-server"
Environment = "production"
}
}
# Access by key
instance_tags["Name"] # "web-server"
Set (unique unordered values):
variable "allowed_ports" {
type = set(number)
default = [80, 443, 8080]
}
Object (structured type with named attributes):
variable "server_config" {
type = object({
name = string
instance_type = string
disk_size = number
public = bool
})
default = {
name = "web"
instance_type = "t3.micro"
disk_size = 20
public = true
}
}
Tuple (fixed-length sequence with different types):
variable "mixed" {
type = tuple([string, number, bool])
default = ["hello", 42, true]
}
Terraform Block Types
resource — Create Infrastructure
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
tags = {
Environment = "production"
}
}
data — Read Existing Infrastructure
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# Reference: data.aws_ami.ubuntu.id
variable — Accept Input
variable "environment" {
type = string
description = "Deployment environment"
default = "dev"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
output — Export Values
output "instance_ip" {
value = aws_instance.web.public_ip
description = "The public IP of the web server"
}
locals — Define Reusable Values
locals {
common_tags = {
Project = "my-app"
Environment = var.environment
ManagedBy = "terraform"
}
name_prefix = "${var.project}-${var.environment}"
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = local.common_tags
}
module — Reuse Configuration
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}
provider — Configure Cloud APIs
provider "aws" {
region = "us-east-1"
}
terraform — Configure Terraform Itself
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "global/s3/terraform.tfstate"
region = "us-east-1"
}
}
Expressions
String Interpolation
name = "web-${var.environment}"
# Result: "web-production"
Conditional Expression
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
For Expressions
# Transform a list
upper_names = [for name in var.names : upper(name)]
# Filter a list
long_names = [for name in var.names : name if length(name) > 5]
# Transform a map
tagged = {for k, v in var.servers : k => "${v}-tagged"}
Splat Expressions
# Get all IDs from a list of resources
instance_ids = aws_instance.web[*].id
Built-In Functions
HCL includes many built-in functions:
String Functions
upper("hello") # "HELLO"
lower("HELLO") # "hello"
format("Hello, %s!", "world") # "Hello, world!"
join(", ", ["a", "b", "c"]) # "a, b, c"
split(",", "a,b,c") # ["a", "b", "c"]
replace("hello", "l", "L") # "heLLo"
trimspace(" hello ") # "hello"
Numeric Functions
max(5, 12, 9) # 12
min(5, 12, 9) # 5
ceil(4.3) # 5
floor(4.7) # 4
abs(-5) # 5
Collection Functions
length(["a", "b", "c"]) # 3
contains(["a", "b"], "a") # true
merge({a=1}, {b=2}) # {a=1, b=2}
lookup({a=1, b=2}, "a", 0) # 1
flatten([[1,2],[3,4]]) # [1,2,3,4]
keys({a=1, b=2}) # ["a", "b"]
values({a=1, b=2}) # [1, 2]
Type Conversion Functions
tostring(42) # "42"
tonumber("42") # 42
tobool("true") # true
tolist(toset([1,2])) # [1, 2]
tomap({a = "b"}) # {a = "b"}
Encoding Functions
jsonencode({name = "hello"}) # '{"name":"hello"}'
jsondecode("{\"name\":\"hello\"}") # {name = "hello"}
base64encode("hello") # "aGVsbG8="
base64decode("aGVsbG8=") # "hello"
Test functions in the Terraform console:
$ terraform console
> upper("hello")
"HELLO"
> length([1, 2, 3])
3
Loops: count and for_each
count
resource "aws_instance" "web" {
count = 3
ami = "ami-12345"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
for_each
resource "aws_instance" "web" {
for_each = toset(["app", "api", "worker"])
ami = "ami-12345"
instance_type = "t3.micro"
tags = {
Name = each.value
}
}
See our detailed guide: Terraform count and for_each Explained
Hands-On Courses
Learn by doing with interactive courses on CopyPasteLearn:
Conclusion
HCL is the foundation of every Terraform project. It’s designed to be readable and declarative — you describe your desired infrastructure state, and Terraform handles the rest. Master the block types (resource, data, variable, output, locals), understand the type system, and use expressions and functions to keep your configurations DRY and maintainable.




