TerraformPilot

HCL

HCL Language: Complete Guide to HashiCorp Configuration Language

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

LLuca Berton2 min read

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.

#Terraform#HashiCorp#DevOps#Infrastructure as Code#HCL

Share this article