Skip to main content
HCL Language: Complete Guide to HashiCorp Configuration Language

HCL Language: Complete Guide to HashiCorp Configuration Language

Key Takeaway

Learn HCL (HashiCorp Configuration Language) for Terraform. Covers syntax, data types, variables, blocks, expressions, functions

Table of Contents

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

TypeExampleDescription
string"hello"Text
number42, 3.14Integer or float
booltrue, falseBoolean

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.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.