Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

HCL: The HashiCorp Configuration Language

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.

HCL: The HashiCorp Configuration LanguageTerraform.tf filesVault.hcl policyConsulservice defsNomadjob specsHCL2 Parser (Go library)Blocks + attributes + expressionsPlan / Apply / Runtime API calls
HCL: The HashiCorp Configuration Language feeds Terraform, Vault, Consul, and Nomad through a shared HCL2 parser before runtime execution.

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.

HCL2 File StructureBlocktype + labels{ body }Attributekey = valuestrings, maps, listsExpressionvar.x, func()operatorsNested Blocks Exampleresource → tags → lifecycledynamic blocks for loops
HashiCorp Configuration Language files combine blocks, attributes, and expressions—nested blocks model complex API structures cleanly.

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.

  1. Create a working directory and run terraform init to download providers.
  2. Define input variables in variables.tf with types and descriptions.
  3. Declare providers and resources in main.tf or domain-specific files.
  4. Expose useful values through outputs.tf for CI or human operators.
  5. Run terraform plan, review the diff, then terraform 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.

CriterionHCL (.tf, .hcl)JSON (.tf.json)
Human readabilityHigh — comments, unquoted keys, heredocsLow — verbose, no comments, strict quoting
Machine generationGood with jsonencode() outputIdeal for programs that emit config
Expression supportFull — functions, operators, referencesStatic values only in JSON files
Code review frictionLow — diffs are compactHigh — large noisy diffs
Tooling ecosystemterraform fmt, IDE plugins, tflintSame parsers, fewer formatters
Best fitHand-written infrastructure, policy, jobsCI-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.

HCL vs JSON WorkflowHand-Written HCLComments + modulesterraform fmt in CIHuman code reviewGenerated JSONCI / API outputStatic values onlyProgrammatic bridgeShared HCL2 ParserIdentical internal representationTerraform Plan and Apply
HCL and JSON converge at the HCL2 parser—choose HCL for human maintenance and JSON for machine output.

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. count uses integer indexes. for_each uses 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.

HCL Error Catch PointsEdit .tf HCL Fileterraform fmtStyle checkterraform validateSchema checkterraform planDiff previewFix HCL Before ApplySyntax, types, referencesterraform apply (safe)
Run fmt, validate, and plan before every apply—HCL mistakes should fail early, not after partial infrastructure changes.

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 .tf and .hcl file.
  • Write infrastructure by hand in HCL; reserve JSON for machine-generated snippets that share the same parser.
  • Run terraform fmt, validate, and plan in 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

HashiCorp Configuration Language is a human-readable structured data format with blocks, attributes, and expressions, parsed by tools like Terraform at plan time—not compiled code.

No. HCL is structured configuration with expression evaluation—no loops, classes, or general control flow beyond Terraform's count, for_each, and dynamic blocks.

HCL1 powered Terraform 0.11 with limited expressions. HCL2 (2019) added richer syntax and JSON compatibility. All current HashiCorp tools require HCL2.

Every HCL2 file combines blocks, attributes, and expressions. Blocks start with a type label, optional quoted labels, and a braced body—Terraform resource blocks follow this exactly. Attributes are key-equals-value pairs holding strings, numbers, booleans, lists, maps, or computed expressions. Expressions reference other attributes, call built-in functions, or use operators. Terraform evaluates them during planning, not at runtime on the server. Mastering that trio covers most daily editing work on infrastructure repos I keep beside application code in Git.

Split a Terraform project across focused files rather than one giant main.tf. Run terraform init to download providers, define typed input variables in variables.tf, declare providers and resources in main.tf or domain-specific files, and expose useful values in outputs.tf. Review with terraform plan, then apply. A minimal stack might set region and instance type as variables, pin Terraform to version 1.9.0 or higher with the AWS provider around 5.x, and output the server's public IP. Store remote state in S3 or Terraform Cloud with locking when more than one person or pipeline applies changes.

Both formats parse through the same HCL2 engine and produce identical results. Choose based on who writes the file. HCL offers comments, unquoted keys, heredocs, full expression support, and compact diffs—ideal for hand-written infrastructure teams review weekly. JSON suits machine-generated snippets from CI or deployment scripts because programs emit it easily, but diffs are noisy and expressions are static. In practice I write HCL by hand and reserve JSON for automation output. Validate generated JSON with a formatter before feeding it to Terraform in CI.

Terraform's core CLI does not natively load YAML configuration files. It auto-loads .tf, .tf.json, and related JSON state files. Some wrappers convert YAML to HCL or JSON before invoking Terraform, but that is an extra layer—not built-in support. If you manage Kubernetes manifests alongside Terraform, keep YAML in separate files and reference them through appropriate Terraform resources rather than expecting Terraform to parse YAML directly. For infrastructure definitions your team maintains in version control, HCL or .tf.json remain the supported native formats.

HCL appears wherever HashiCorp products need structured configuration, and the grammar transfers across the stack. Vault policy files use HCL blocks to grant or deny path access with capabilities lists. Vault Agent ships HCL config for auto-auth and template rendering—useful when rotating database credentials without restarting application services. Nomad job specifications declare datacenters, task groups, Docker drivers, and resource limits in HCL blocks. Consul service mesh settings including intentions, ingress gateways, and resolver nodes also use HCL. Each tool defines its own block types, but blocks, attributes, and expression rules stay consistent.

Most HCL errors surface at plan time, which keeps failures cheap. Repeated problems include missing quotes on strings containing hyphens or special characters, confusing count with for_each—count uses integer indexes while for_each uses map or set keys, and mixing them creates fragile output references. Never commit API keys or secrets in .tf files; use environment variables, Vault, or Terraform Cloud variable sets instead. Skipping terraform fmt passes parsing but fails team style checks in CI. Overusing legacy ${var.name} interpolation clutters diffs when unquoted var.name references are cleaner. Run fmt, validate, and plan before every apply so mistakes fail early, not after partial infrastructure changes.

When one attribute references another resource's exported value, Terraform builds an implicit dependency automatically. For example, setting vpc_security_group_ids to a security group's id tells Terraform the group must exist before the instance. You rarely need explicit depends_on when references already encode execution order. Expressions like aws_security_group.web_sg.id are evaluated during planning, not on the running server. This pattern wires cross-resource relationships cleanly—DNS records pointing at load balancers, instances attached to security groups, or buckets referenced by logging policies—without manually listing every dependency in separate blocks.

A module is a directory of .tf files with defined input variables and outputs. Call it from a root module and pass different values for staging versus production. Modules let teams share standards—one module might enforce encrypted volumes, mandatory tags, or approved instance sizes—without copy-paste drift across environments. On projects where I provision servers alongside PHP application stacks, modules keep VPC layout, firewall rules, and instance sizing consistent. Splitting concerns into modules also reduces merge conflicts during code review because domain-specific changes stay in separate files rather than one monolithic configuration.

Local terraform.tfstate files break teamwork quickly when more than one person applies changes. Store remote state in S3 or Terraform Cloud so the entire team and CI pipelines share a single source of truth. Pair remote state with locking so two pipeline jobs cannot apply simultaneously—a pattern that aligns with idempotent configuration management on every deploy pipeline I maintain. Remote state holds the mapping between your HCL definitions and real cloud resources. Without it, teammates risk conflicting applies, lost resource tracking, and painful recovery after partial runs.

Dynamic blocks generate repeated nested block structures from a variable instead of copying nearly identical blocks by hand. Define a list or map variable describing the repeated settings—ingress rules with from_port, to_port, and cidr_blocks, for example—then use a dynamic block with for_each to expand one block per entry. This keeps security group rules, listener configs, and similar patterns DRY and reviewable. Test complex expressions locally with terraform console, which opens a REPL for evaluating length(var.ingress_rules) or debugging ternaries without running full plans for every tweak.

Never commit API keys, passwords, or tokens in .tf files checked into Git. HCL files belong in version control, so anything written literally in attributes becomes permanent history. Use environment variables, HashiCorp Vault, or Terraform Cloud variable sets for sensitive values instead. Vault policy files themselves use HCL to define path access, and Vault Agent HCL config can auto-auth and render templates—patterns worth adopting when you rotate database credentials without service restarts. Treat secret handling as part of infrastructure architecture from the start, not a cleanup task after someone pushes credentials in a pull request.

Terraform HCL declares desired infrastructure state—what should exist—while Ansible YAML playbooks describe procedural steps for configuring already-running servers. Many teams use both: HCL provisions EC2 instances and network rules, Ansible configures PHP-FPM pools and Apache vhosts afterward. Pulumi offers an alternative if you prefer real programming languages over HCL for infrastructure code. HCL stays the default when operations teams—not application developers—own infrastructure repositories. The choice is about who maintains the files and how often humans read them during review, not raw capability, since all three approaches can reach similar end states.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: