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
# This is a comment # A block has a type, optional labels, and a bodyresource "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 interpolationlocals { 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 — readable, supports commentsresource "aws_s3_bucket" "example" { bucket = "my-bucket" tags = { Environment = "prod" }}// JSON equivalent — verbose, no comments{ "resource": { "aws_s3_bucket": { "example": { "bucket": "my-bucket", "tags": { "Environment": "prod" } } } }}Types in 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
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
# 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
# Format all .tf files to standard styleterraform fmt # Check formatting without changing files (use in CI)terraform fmt -check # Format recursively through subdirectoriesterraform fmt -recursiveTipRun
terraform fmtbefore every commit. Addterraform fmt -checkto your CI pipeline to reject unformatted code. This keeps the codebase consistent across engineers.
RememberHCL is not a general-purpose programming language. It has no loops in the traditional sense (use
for_eachandforexpressions), 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.