
September 11, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
HCL: The HashiCorp Configuration Language is the file format you touch every time you write Terraform, Vault policy, or Nomad job specs. It looks like a trimmed-down programming language, but it is really a structured data format with expressions baked in. If you manage Linux servers, deploy Laravel apps, or wire CI/CD pipelines, you will eventually read or write HCL. This guide explains how HashiCorp Configuration Language syntax works, where it differs from JSON and YAML, and how to avoid the mistakes that break production runs.
What Is HCL: The HashiCorp Configuration Language?
HCL stands for HashiCorp Configuration Language. HashiCorp created it so operators could write infrastructure definitions that humans can scan quickly and machines can parse reliably. You do not compile HCL into a binary. A tool like Terraform reads .tf files, builds an internal object graph, and executes a plan against cloud APIs.
HCL sits in the same family as JSON and YAML, but it adds first-class blocks, comments, and expression evaluation. That combination matters when a single file describes dozens of related resources with cross-references. JSON forces you into nested brackets. YAML hides footguns around indentation. HCL gives you named blocks with clear headers and inline comments that survive code review.
Two versions exist in the wild. HCL1 powered early Terraform 0.11 and older tools. HCL2 replaced it and remains the only supported dialect today. If you open a modern Terraform project on Laravel 13 hosting or a Linux production server, every file uses HCL2 grammar. Legacy interpolation syntax like ${var.name} still parses, but unquoted references such as var.name are the current standard.
On client projects where I provision EC2 instances alongside PHP-FPM stacks, HCL files live beside application code in Git. That keeps DNS, firewall rules, and deploy targets versioned together. The same pattern appears on sister sites I maintain with Deployer 7 and GitLab CI on shared infrastructure.
How Does HashiCorp Configuration Language Syntax Work?
HCL2 files are built from three structural units: blocks, attributes, and expressions. Understanding that trio covers ninety percent of daily editing work.
Blocks define typed containers
A block starts with a type label, zero or more labels in quotes, and a body wrapped in braces. Terraform resource blocks follow this pattern exactly.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "production-web"
}
}
The block type is resource. The labels are aws_instance and web. Everything inside the braces is the body. Nested blocks like tags repeat the same structure. This nesting mirrors how cloud APIs group settings.
Attributes assign names to values
An attribute is a key, an equals sign, and a value. Values can be strings, numbers, booleans, lists, maps, or expressions. Lists use square brackets. Maps use curly braces with string keys.
variable "allowed_cidrs" {
type = list(string)
default = ["10.0.0.0/8", "192.168.0.0/16"]
}
locals {
app_ports = {
http = 80
https = 443
}
}
Type constraints on variables catch errors at terraform plan instead of mid-apply. That early feedback saves hours when a typo would otherwise create the wrong security group rule.
Expressions compute values at parse time
Expressions reference other attributes, call built-in functions, or use operators. Terraform evaluates them during planning, not at runtime on the server. A common pattern wires one resource into another.
resource "aws_security_group" "web_sg" {
name = "web-sg"
}
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.small"
vpc_security_group_ids = [aws_security_group.web_sg.id]
}
The reference aws_security_group.web_sg.id creates an implicit dependency. Terraform knows the security group must exist before the instance. You rarely need explicit depends_on when references already encode order.
How Do You Write Terraform Files With HCL?
Terraform is the most common HCL consumer. A minimal project splits concerns across a handful of files. You do not need one giant main.tf. Splitting improves review and reduces merge conflicts.
- Create a working directory and run
terraform initto download providers. - Define input variables in
variables.tfwith types and descriptions. - Declare providers and resources in
main.tfor domain-specific files. - Expose useful values through
outputs.tffor CI or human operators. - Run
terraform plan, review the diff, thenterraform apply.
Here is a compact example that provisions an Ubuntu 24.04 server—the same OS family I use for Apache and PHP-FPM production stacks.
# variables.tf
variable "region" {
type = string
description = "AWS region for deployment"
default = "ap-south-1"
}
variable "instance_type" {
type = string
default = "t3.micro"
}
# main.tf
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}
resource "aws_instance" "app" {
ami = "ami-ubuntu-24.04-example"
instance_type = var.instance_type
user_data = <<-EOF
#!/bin/bash
apt-get update && apt-get install -y nginx
EOF
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
# outputs.tf
output "public_ip" {
value = aws_instance.app.public_ip
description = "Public IP of the app server"
}
Store remote state in S3 or Terraform Cloud when more than one person applies changes. Local terraform.tfstate files break teamwork fast. Pair remote state with locking so two pipeline jobs cannot apply simultaneously. That pattern aligns with idempotent configuration management principles I follow on every deploy pipeline.
Use modules to reuse HCL across environments. A module is just a directory of .tf files with defined input variables and outputs. Call it from a root module and pass different variable values for staging versus production.
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.1.0.0/16"
az_count = 2
}
Modules are how teams share standards. One module might enforce encrypted volumes, mandatory tags, or approved instance sizes. Central control without copy-paste drift.
HCL vs JSON: Which Format Should You Choose?
Every HashiCorp tool accepts both native HCL and JSON representations of the same structure. Terraform auto-loads .tf, .tf.json, and auto-generated JSON state files. Pick the format based on who writes the file and how often humans edit it.
| Criterion | HCL (.tf, .hcl) | JSON (.tf.json) |
|---|---|---|
| Human readability | High — comments, unquoted keys, heredocs | Low — verbose, no comments, strict quoting |
| Machine generation | Good with jsonencode() output | Ideal for programs that emit config |
| Expression support | Full — functions, operators, references | Static values only in JSON files |
| Code review friction | Low — diffs are compact | High — large noisy diffs |
| Tooling ecosystem | terraform fmt, IDE plugins, tflint | Same parsers, fewer formatters |
| Best fit | Hand-written infrastructure, policy, jobs | CI-generated snippets, API bridges |
In practice, write HCL by hand and reserve JSON for automation. If a Laravel deployment script emits a one-off resource definition, JSON can work. For anything a team maintains weekly, HCL wins on readability alone. Validate generated JSON with the JSON formatter tool before feeding it to Terraform in CI.
The HCL and JSON forms are equivalent at the parser level. This Terraform resource in HCL:
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-prod"
}
…maps to this JSON file named main.tf.json:
{
"resource": {
"aws_s3_bucket": {
"logs": {
"bucket": "my-app-logs-prod"
}
}
}
}
Same result after parsing. The JSON version just hurts your eyes during review.
What HCL Features Do HashiCorp Tools Share Beyond Terraform?
Terraform gets most of the attention, but HCL appears wherever HashiCorp tools need structured config. Knowing the shared grammar means skills transfer across the stack.
Vault policies and agent config
Vault policy files use HCL blocks to grant or deny path access. The syntax differs from Terraform, but blocks and string attributes feel familiar.
# policy.hcl
path "secret/data/app/*" {
capabilities = ["create", "read", "update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
Vault Agent also ships an HCL config for auto-auth and template rendering. That matters when you rotate database credentials for a MySQL 9.7 backend without restarting PHP-FPM. See the dedicated guide on secrets management with HashiCorp Vault for production patterns.
Nomad job specifications
Nomad job files declare task groups, resources, and service registrations in HCL. A job might run a Redis 8.10 sidecar next to an application container.
job "web" {
datacenters = ["dc1"]
group "app" {
count = 2
task "server" {
driver = "docker"
config {
image = "myapp:latest"
ports = ["http"]
}
resources {
cpu = 500
memory = 256
}
}
}
}
Consul intentions and gateway config
Consul service mesh settings—intentions, ingress gateways, and resolver nodes—also use HCL. Teams running microservices alongside a monolithic Laravel 12 app sometimes split traffic this way.
Each tool adds its own block types and attributes. The parser and expression rules stay consistent. Learn HCL once, then read any HashiCorp product docs without relearning syntax basics.
What Are Common HCL Mistakes That Break Production Plans?
HCL errors usually show up at plan time, not after resources exist. That is good—failures are cheap. A few patterns cause repeat pain on real projects.
- Missing quotes on string values with special characters. Hyphens in unquoted values confuse the parser. Quote anything that is not a bare identifier.
- Count versus for_each confusion.
countuses integer indexes.for_eachuses map or set keys. Mixing them in outputs creates fragile references. - Hard-coded secrets in .tf files. Never commit API keys. Use environment variables, Vault, or Terraform Cloud variable sets.
- Ignoring
terraform fmt. Unformatted HCL passes parsing but fails team style checks in CI. - Overusing string interpolation. Prefer native expression syntax. Legacy
"${var.name}"still works but clutters diffs.
Dynamic blocks solve repetitive nested structures. Instead of copying five nearly identical ingress blocks, loop over a variable.
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
cidr_blocks = list(string)
}))
}
resource "aws_security_group" "app" {
name = "app-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Test expressions locally with terraform console. It opens a REPL where you can evaluate length(var.ingress_rules) or debug a ternary. Faster than running full plans for every tweak. For complex string patterns in user-data scripts, cross-check regex separately with a regex tester before embedding it in HCL heredocs.
Compare HCL-centric workflows with Ansible YAML in the Terraform vs Ansible guide. Terraform HCL declares desired state. Ansible playbooks describe procedural steps. Many teams use both: HCL provisions servers, Ansible configures PHP-FPM pools and Apache vhosts.
Pulumi offers an alternative if you prefer real programming languages over HCL. The Pulumi IaC guide covers that trade-off. HCL stays the default when operations teams—not application developers—own infrastructure repos.
Key Takeaways
- HCL2 is the current HashiCorp Configuration Language dialect—blocks, attributes, and expressions form every
.tfand.hclfile. - Write infrastructure by hand in HCL; reserve JSON for machine-generated snippets that share the same parser.
- Run
terraform fmt,validate, andplanin CI before any apply to catch syntax and type errors early. - Use modules, variables, and remote state so HCL configs scale across teams and environments without copy-paste drift.
- HCL skills transfer across Terraform, Vault, Nomad, and Consul—the grammar stays consistent even when block types differ.
- Never commit secrets in HCL files; integrate Vault or environment-backed variable sets instead.
People Also Ask
Is HCL a programming language?
No. HCL is a structured configuration language with expression evaluation. It has no loops, classes, or general-purpose control flow outside what Terraform adds through count, for_each, and dynamic blocks. Think of it as JSON with blocks, comments, and computed values—not a replacement for PHP or Python.
What is the difference between HCL1 and HCL2?
HCL1 powered Terraform 0.11 and older tooling with limited expression support. HCL2, released in 2019, added richer expressions, improved parsing, and native JSON compatibility. All current HashiCorp products require HCL2. Legacy ${…} interpolation still parses but unquoted references are preferred.
Can Terraform use YAML instead of HCL?
Terraform does not natively load YAML configuration files. Some wrappers convert YAML to HCL or JSON before invoking Terraform, but the core CLI expects .tf or .tf.json. For Kubernetes manifests alongside Terraform, keep YAML in separate files and reference them with the kubernetes_manifest resource or the file() function.
How do I format and lint HCL files automatically?
Run terraform fmt -recursive to standardise indentation and alignment. Add tflint or checkov in CI for policy and best-practice checks. Most IDE plugins for Terraform call the same formatter on save, which keeps pull request diffs clean.
Ship Infrastructure You Can Read and Review
HCL: The HashiCorp Configuration Language earns its place because infrastructure config must survive code review, diffs, and on-call debugging at 2 a.m. Blocks and expressions beat raw JSON for hand-maintained files. Pair clean HCL repos with remote state, CI validation, and secrets stored outside Git. That is the baseline I use when provisioning servers for production web platforms and Laravel applications.
If you are adopting Terraform, Vault, or Nomad and want HCL modules wired into your existing deploy pipeline, custom software and infrastructure integration is where this work usually lands. You can also review related posts on Vault PKI, Ubuntu server hardening, and PHP opcache tuning for the application layer that sits on top of your HCL-defined servers.
For ongoing server management after provisioning, support and maintenance services keep the stack healthy post-launch. Read more DevOps articles on the blog, explore the portfolio for shipped projects, or contact us to discuss your infrastructure setup.
Official references worth bookmarking: the HashiCorp HCL language specification, the Terraform configuration language documentation, and the HCL parser source on GitHub.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

