HCL#
HCL, the HashiCorp Configuration Language, is the configuration / declarative DSL behind Terraform, OpenTofu, Packer, Vault, Consul, Nomad, and many adjacent tools. Designed to be more readable than JSON and more structured than YAML, with first-class expressions and references.
In 2026 HCL2 is the version everyone uses; the original (HCL1) is historical.
What HCL Looks Like#
# Provider configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Variables
variable "env" {
type = string
default = "dev"
}
# Locals (computed values)
locals {
bucket_name = "myorg-${var.env}-logs"
tags = {
Environment = var.env
Owner = "data-team"
}
}
# Resources
resource "aws_s3_bucket" "logs" {
bucket = local.bucket_name
tags = local.tags
}
# Outputs
output "bucket" {
value = aws_s3_bucket.logs.id
}
Building Blocks#
The structural primitives of HCL. Blocks introduce sections; arguments fill them; expressions reference other blocks. The exact valid block names depend on the consuming tool – Terraform’s vocabulary differs from Vault’s, but the structure of every block is the same.
Block,
type "label" { ... }. The major structuring unit.Argument,
key = valueinside a block.Identifier, the name on the left.
Expression, the value on the right; can reference other blocks.
Object / map / list / tuple, nested values.
The exact valid block types depend on the consuming tool. Terraform
defines terraform, provider, resource, data,
variable, output, module, locals. Packer / Vault /
Nomad each define their own.
Expressions#
HCL is a real expression language.
# Strings with interpolation
bucket = "myorg-${var.env}-logs"
# Heredocs
policy = <<-EOT
{
"Version": "2012-10-17",
"Statement": [...]
}
EOT
# Conditional
storage_class = var.env == "prod" ? "STANDARD" : "STANDARD_IA"
# For expressions (list comprehension)
names = [for u in var.users : u.name]
# For expressions (object)
user_emails = { for u in var.users : u.id => u.email }
# Splat expressions
ids = aws_instance.app[*].id
Built-In Functions#
The standard library exposed by the consuming tool. Common needs (list and map manipulation, JSON and YAML encode/decode, file reading, template rendering, CIDR arithmetic, base64) are all built in. The full set is documented per tool; the table below covers the everyday hits.
Function |
Use |
|---|---|
|
list / map / string length |
|
membership |
|
join lists |
|
merge maps (right wins) |
|
list → string |
|
string → list |
|
sprintf-style formatting |
|
|
|
JSON conversion |
|
|
|
read a file |
|
render a template |
|
|
|
current time |
|
|
|
mark a value as sensitive (don’t print) |
The full set is documented in each tool’s reference.
References and Dependencies#
Inside a Terraform configuration, HCL expressions can reference other blocks. Terraform builds a dependency graph from these references.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = aws_vpc.main.id # dependency edge
cidr_block = "10.0.1.0/24"
}
You don’t CREATE TABLE order things by hand; the tool computes the
order from the references.
Modules#
A Terraform “module” is just a directory of .tf files. Other
modules consume it by module blocks:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}
module "app" {
source = "./modules/app"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
}
Modules are the unit of reuse. Inputs are variable; outputs are
output.
Tools That Use HCL#
The HCL footprint runs across nearly every HashiCorp product. Terraform and OpenTofu lead; Packer builds images; Vault holds policies; Consul, Nomad, Boundary, and Waypoint each bring their own block schema. The DSL is constant; the valid block types differ.
Terraform / OpenTofu, infrastructure as code.
Packer, machine image builds (
source { ... } build { ... }).Vault, HCL policies (
path "secret/*" { capabilities = [...] }).Consul, service definitions, configuration.
Nomad, job specs.
Boundary, session management.
Waypoint, application deployment.
The DSL is the same; the block schema differs.
JSON-Compatible Form#
Every HCL document has a JSON equivalent (.tf.json). Useful for
generating configuration programmatically.
{
"resource": {
"aws_s3_bucket": {
"logs": { "bucket": "myorg-prod-logs" }
}
}
}
Modules vs. Programs#
HCL is declarative. There are no loops in the imperative sense, no
if statements that block execution. Repetition is via:
count,count = length(var.names); each instance getscount.index.for_each,for_each = toset(var.names); each gets a key / value.For-expressions,
[for x in xs : f(x)].Modules, the unit of programmatic composition.
If your needs exceed these (real if-then-else, real loops, dynamic plan-time logic), HashiCorp’s CDKTF or Pulumi (write infra in TypeScript / Python / Go) is a better fit. CUE is an alternative declarative language with a stronger type system.
Linting and Validation#
The CI guardrails that turn HCL from “looks right” into
“verified before merge”. terraform fmt enforces standard
style; terraform validate checks the schema; tflint
catches anti-patterns; tfsec, Checkov, and KICS scan for
security misconfigurations.
Pitfalls#
The traps that catch teams new to HCL. The non-Turing-complete boundary frustrates imperative-trained authors; heredoc indentation has two flavors; type coercion between strings, numbers, and booleans is not silent; sensitive values leak into plan output if not marked.
HCL is not Turing-complete, when a need feels imperative, it often is. Reach for CDKTF / Pulumi rather than fighting HCL.
Heredoc indentation,
<<-EOTstrips the smallest common indentation;<<EOTkeeps it.String / number / bool coercion, be explicit;
var.x == "true"is not the same asvar.x == true.Sensitive values, mark them; they appear in plan output otherwise.
JSON output of HCL, some types (functions) don’t round-trip; generated configs need care.
Provider version drift, pin
required_providersversions; use the lockfile (.terraform.lock.hcl).
When (Not) to Use HCL#
Use for any HashiCorp tool; HCL is the native config there.
Don’t use outside its tools; it’s not a general-purpose config language. (You can, via the parser libraries, but YAML / TOML / JSON serve general configuration better.)
Don’t use when you need real programming, pick CDKTF (HCL generated from TS / Python / Go), Pulumi, or generate
.tf.jsonfrom a real language.
See Also#
Infrastructure as Code, Terraform-level workflow patterns.