Skip to main content

HCL

HCL (HashiCorp Configuration Language) is the configuration language Terraform uses to define infrastructure. It is designed to be human-readable and machine-parseable — more readable than JSON and simpler than a full programming language.

What Is HCL?

HCL stands for HashiCorp Configuration Language. It is the language you write Terraform configurations in. Every .tf file in a Terraform project is written in HCL. The goal of HCL is to sit in the sweet spot between JSON (machine-friendly but hard to read) and a full programming language (powerful but complex for configuration).

HCL is also used by other HashiCorp tools — Packer, Vault, Consul, and Nomad all use HCL for their configuration files.

HCL Syntax Basics

HCL
# This is a comment
# A block has a type, optional labels, and a body
resource "aws_s3_bucket" "order_images" { # type=resource, labels="aws_s3_bucket" "order_images"
bucket = "swiggy-order-images-prod" # argument: name = value
tags = { # map value
Environment = "production"
Team = "platform"
}
}
# String interpolation
locals {
bucket_name = "swiggy-${var.environment}-images" # ${} for interpolation
}
# Multiline string (heredoc)
resource "aws_instance" "web" {
user_data = <<-EOF
#!/bin/bash
echo "Hello from ${var.environment}" > /tmp/init.txt
apt-get update -y
EOF
}

HCL vs JSON

Terraform accepts both HCL (.tf) and JSON (.tf.json). HCL is preferred for human-written files:

HCL
# HCL — readable, supports comments
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
tags = { Environment = "prod" }
}
TEXT
// JSON equivalent — verbose, no comments
{
"resource": {
"aws_s3_bucket": {
"example": {
"bucket": "my-bucket",
"tags": { "Environment": "prod" }
}
}
}
}

Types in HCL

HCL
variable "examples" {
# Primitive types
# string = "ap-south-1"
# number = 3
# bool = true
# Collection types
# list(string) = ["a", "b", "c"]
# map(string) = { key = "value" }
# set(string) = toset(["a", "b"])
# Structural types
# object({ name = string, count = number })
# tuple([string, number, bool])
}

Expressions and Functions

HCL
locals {
# Conditional expression (ternary)
instance_type = var.environment == "prod" ? "t3.large" : "t3.small"
# Built-in functions
upper_env = upper(var.environment) # "PROD"
joined = join("-", ["zerodha", "prod"]) # "zerodha-prod"
has_items = length(var.subnet_ids) > 0 # true/false
# For expression — transform a list
upper_tags = [for tag in var.tags : upper(tag)]
# For expression — transform a map
tag_map = { for k, v in var.raw_tags : k => lower(v) }
}

Dynamic Blocks

HCL
# Instead of repeating ingress blocks manually:
resource "aws_security_group" "app" {
name = "app-sg"
# Generate multiple ingress rules from a variable
dynamic "ingress" {
for_each = var.allowed_ports # e.g., [80, 443, 8080]
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}

HCL Formatting

Bash
# Format all .tf files to standard style
terraform fmt
# Check formatting without changing files (use in CI)
terraform fmt -check
# Format recursively through subdirectories
terraform fmt -recursive
Tip

Run terraform fmt before every commit. Add terraform fmt -check to your CI pipeline to reject unformatted code. This keeps the codebase consistent across engineers.

Remember

HCL is not a general-purpose programming language. It has no loops in the traditional sense (use for_each and for expressions), no if/else blocks (use ternary), and no functions you can define. For complex logic, you need to think in HCL's declarative model.

Frequently Asked Questions

Why did HashiCorp invent HCL instead of just using JSON for Terraform?

JSON has no comments, no native support for expressions, and gets unwieldy for humans to hand-write and diff in version control. HCL was designed to be both machine-parseable and comfortable to write by hand, supporting comments, string interpolation, conditionals, and loops (for_each, count) while still compiling down to the same underlying structure JSON can represent — Terraform actually accepts .tf.json as an alternative syntax for tooling that generates config programmatically.

What's a common mistake when formatting or structuring HCL files?

Not running `terraform fmt` before committing — HCL is whitespace-tolerant, so files drift into inconsistent indentation and alignment across a team, making diffs noisy and hard to review. Also common: cramming unrelated resources into one giant file instead of splitting by concern (network.tf, iam.tf, compute.tf), which makes merge conflicts far more likely when multiple engineers touch the same module.