HCL: The HashiCorp Configuration Language

Khimananda Oli 9 min read Virtualization
HCL: The HashiCorp Configuration Language

By Khimananda Oli | Last reviewed: August 2026

HCL: The HashiCorp Configuration Language is the declarative syntax engine powering Terraform, Vault, Consul, Packer, and Nomad. Unlike general-purpose programming languages or rigid data formats like JSON, HCL is designed specifically to define infrastructure, policies, and service configurations in a way that is both human-readable and machine-parseable. If you are managing cloud resources or secrets in 2026, understanding this language is not optional—it is the foundation of reliable infrastructure as code with Terraform and secure automation.

HCL Source Filesmain.tf / config.hclVariables & LocalsModules & ProvidersHCL Parser / ASTLexing & TokenizationBlock & Attribute MappingExpression EvaluationTool ExecutionTerraform Plan/ApplyVault Policy EnforcementConsul Service Config
HCL: The HashiCorp Configuration Language processing flow from source files through parsing to tool execution

What is HCL: The HashiCorp Configuration Language and why does it exist?

HCL was created to solve a specific gap between pure data serialization formats and full programming languages. JSON and YAML are excellent for transporting data but lack native support for comments, complex expressions, or structural reuse without external templating. General-purpose languages like Python or Go offer power but introduce runtime dependencies, side effects, and steep learning curves for non-developers. HCL occupies the middle ground: it is statically analyzable, supports rich type systems and functions, yet remains declarative and focused solely on configuration intent.

In practice, this means you can write infrastructure definitions that are version-controlled, reviewed in pull requests, and validated before execution—all without embedding arbitrary code execution risks. For teams in Nepal and globally adopting cloud-native practices, this balance is critical. You get the safety of declarative configs with enough expressiveness to handle dynamic environments, conditional resource creation, and computed values. The language has evolved significantly since its v1 days; modern HCL (v2+) uses a refined syntax that is stricter about types but far more predictable for tooling authors and operators alike.

Core design principles

  • Human-first readability: Syntax prioritizes clarity over brevity, using named blocks instead of nested indentation hell.
  • JSON compatibility: Every valid HCL file has an equivalent JSON representation, enabling programmatic generation and API integration.
  • Static analysis friendly: Tools can validate structure, detect unused variables, and enforce policies without executing the configuration.
  • Ecosystem consistency: Same language skills transfer across Terraform, Vault, Consul, Packer, Nomad, and Boundary.

How do you write basic blocks and attributes in HCL syntax?

The fundamental unit in HCL: The HashiCorp Configuration Language is the block. Blocks define objects or group related settings, while attributes assign values within those blocks. Understanding this distinction prevents most beginner confusion. A block has a type identifier, optional labels, and a body enclosed in braces. Attributes use simple key = value assignment.

# Resource block with two labels: type and name
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  # Nested block for tagging
  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }

  # Conditional attribute using expression
  associate_public_ip_address = var.enable_public_access ? true : false
}

Notice several key syntactic elements here. Comments use # or // for single lines and /* */ for multi-line—something impossible in pure JSON. String interpolation uses ${...} syntax inside double quotes, allowing you to embed variable references and function calls directly. Block labels (like "aws_instance" and "web_server") are positional and their meaning depends entirely on the parent block type; there is no universal schema enforced by HCL itself.

Common attribute types and gotchas

  1. Strings: Always double-quoted. Single quotes are not valid string delimiters in HCL.
  2. Numbers: Unquoted integers or floats. No commas in large numbers; use underscores for readability (1_000_000).
  3. Booleans: Literal true or false, never quoted.
  4. Collections: Lists use square brackets [...], maps/objects use curly braces {...}. Trailing commas are allowed and encouraged for cleaner diffs.
  5. Null handling: Explicit null removes an attribute from the final object, unlike omitting it which may trigger defaults.

A frequent mistake I see in audits is confusing map syntax with block syntax. When you write tags = { ... }, that is an attribute assigned a map value. When you write tags { ... } without equals, that is a nested block. Some providers accept both forms historically, but only one is correct per schema. Always check provider documentation rather than assuming interchangeability.

HCL Native Syntaxresource "aws_s3_bucket" "logs" {bucket = "app-logs-${var.env}"acl = "private"versioning {enabled = true}tags = {Team = "platform"}}JSON Equivalent{"resource": [{"aws_s3_bucket": [{"logs": {"bucket": "app-logs-${var.env}","acl": "private","versioning": [{"enabled": true}],"tags": {"Team": "platform"}}}]}]}Equivalent
HCL native syntax compared to verbose JSON equivalent demonstrating readability advantages

How do variables, locals, and functions work in HCL?

Static configuration alone cannot handle real-world infrastructure. HCL provides three mechanisms for dynamism: input variables, local values, and built-in functions. Input variables (variable blocks) define parameters that callers must supply, typically via .tfvars files, environment variables, or CLI flags. Local values (locals blocks) compute intermediate results reusable within the same module, reducing repetition and centralizing complex logic.

variable "environment" {
  description = "Deployment environment name"
  type        = string
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

locals {
  common_tags = {
    Project     = "payment-gateway"
    Environment = var.environment
    Owner       = "platform-team"
  }

  # Function usage: format, lookup, conditional
  instance_size = lookup(
    { dev = "t3.small", staging = "t3.medium", prod = "t3.large" },
    var.environment,
    "t3.micro"
  )

  bucket_name = format("payments-%s-%s", var.environment, random_id.suffix.hex)
}

Functions in HCL are pure and deterministic—they cannot make network calls, read files at runtime (except during plan), or produce side effects. This restriction is intentional: it ensures configurations remain reproducible and auditable. Common functions include merge() for combining maps, flatten() for nested lists, try() for safe access with fallbacks, and can() for testing expression validity. Avoid overusing complex function chains; if your locals block reads like a puzzle, extract it into a custom module or reconsider whether a general-purpose language would be more appropriate.

Variable precedence and override behavior

Understanding precedence prevents subtle bugs when multiple sources define the same variable. From highest to lowest priority:

  1. CLI -var or -var-file flags
  2. *.auto.tfvars / *.auto.tfvars.json files (loaded automatically)
  3. Terraform Cloud / Enterprise workspace variables
  4. Environment variables (TF_VAR_name)
  5. terraform.tfvars / terraform.tfvars.json
  6. Default value in variable block

This ordering means auto-loaded files silently override explicit tfvars—a common source of confusion during incident response. Always document your variable sourcing strategy in team runbooks.

How does HCL compare to YAML, JSON, and CDK for infrastructure?

Choosing a configuration language involves trade-offs between expressiveness, safety, tooling maturity, and team familiarity. Here is how HCL: The HashiCorp Configuration Language stacks up against alternatives commonly evaluated in 2026:

CriteriaHCLYAMLJSONCDK (TypeScript/Python)
Comments & DocumentationNative supportSupported (#)Not supportedFull language comments
Type Safety & ValidationStrong (schema-enforced)Weak (stringly typed)None nativelyCompile-time checking
Expressions & LogicLimited, declarativeRequires templatingNoneFull programming language
Tooling EcosystemTerraform/Vault/Consul nativeKubernetes/Ansible focusedUniversal interchangeAWS/Azure/GCP specific
Learning CurveModerate (new syntax)Low (familiar)Low (universal)High (requires dev skills)
Audit & Compliance FriendlinessExcellent (static analysis)Poor (templating obscures intent)Good (structured)Moderate (code review required)

For teams already invested in the HashiCorp ecosystem, HCL is the obvious choice. Its tight integration with state management, plan/apply workflows, and policy engines like Sentinel or OPA makes compliance automation straightforward—I have used it extensively for secrets management with HashiCorp Vault where policy-as-code is mandatory. YAML dominates Kubernetes manifests but becomes unwieldy for complex infrastructure due to indentation sensitivity and lack of native composition. CDK offers maximum flexibility for developers who want to use familiar languages, but sacrifices the declarative safety guarantees that make infrastructure reviewable by non-programmers.

Start: Choose Config LanguageUsing HashiCorp tools?YESNOUse HCLBest integration & safetyNeed full programming?NOYESUse YAML / JSONK8s manifests / simple dataUse CDKDev-heavy teams
Decision flowchart for selecting HCL versus alternative configuration languages based on project requirements

What are common mistakes and best practices when writing HCL?

After reviewing hundreds of Terraform codebases across startups and enterprises, certain anti-patterns recur consistently. Avoiding these will save you debugging time and audit headaches:

  • Overusing string interpolation: Writing "${var.name}" when var.name suffices adds noise and breaks type inference. Only interpolate when actually concatenating or transforming.
  • Ignoring validation blocks: Variables without constraints fail late during apply. Add validation blocks to catch bad inputs during plan.
  • Nesting too deeply: HCL supports nested blocks, but excessive nesting harms readability. Extract repeated structures into separate modules or locals.
  • Hardcoding provider versions: Pin provider versions in required_providers but avoid pinning exact patch versions unless necessary for bug fixes. Use optimistic constraints (~> 5.0) for maintainability.
  • Mixing concerns in root modules: Your root module should orchestrate, not implement. Delegate resource creation to child modules; keep root focused on wiring and environment-specific overrides.

On the positive side, adopt consistent formatting with terraform fmt (or hclfmt for non-Terraform HCL). Enable linting with tflint or checkov in CI. Document non-obvious decisions inline—future maintainers (including yourself during incidents) need context that code alone cannot provide. For teams managing sensitive configurations, integrate policy checks early; catching violations pre-apply is exponentially cheaper than remediating live infrastructure. See my guide on reusable Terraform modules for patterns that scale across environments.

Getting started with HCL in production workflows

HCL: The HashiCorp Configuration Language is more than syntax—it is the interface between human intent and automated infrastructure. Mastery comes not from memorizing every function but from understanding its constraints and leveraging them to build safer, more maintainable systems. Start small: convert existing JSON configs to HCL, add validation to legacy variables, refactor monolithic files into focused modules. Measure progress by reduced plan times, fewer failed applies, and faster onboarding of new team members.

If your team is adopting HCL for compliance-critical workloads or needs help structuring modules for multi-environment deployments, reach out to discuss your infrastructure strategy. Whether you are building greenfield platforms in Kathmandu or migrating legacy stacks globally, getting the configuration language right pays dividends for years.

Frequently Asked Questions

HCL is the declarative configuration language used across Terraform, Packer, Vault, and Nomad for defining infrastructure as code.

Yes, HCL is fully JSON-compatible, allowing tools to accept either syntax interchangeably for automation pipelines and API integrations.

HCL supports native expressions, functions, and type checking that YAML lacks, making it safer for complex infrastructure definitions.

Terraform 0.12 released HCL2 in 2019, adding first-class expressions, dynamic blocks, and improved type safety over HCL1.

No, HashiCorp tools require HCL or JSON natively; external languages only work via provisioners, modules, or CDKTF abstractions.

Run terraform validate or hclfmt to check syntax errors, type mismatches, and structural issues before applying changes.

Dynamic blocks generate repeated nested blocks programmatically using for_each, reducing duplication when configuring security groups or IAM policies.

Never hardcode secrets; use environment variables, Vault provider, or SSM parameter store references within HCL data sources.

Built-in functions exist, but custom logic requires provider extensions or external data sources since HCL lacks user-defined functions.

Use terraform fmt or hclfmt in CI pipelines to enforce canonical formatting, indentation, and alignment across team repositories.

Yes, terraform import maps existing resources into HCL state, then you write matching configuration manually or use terrafy.

Type mismatches occur when variable types conflict with resource arguments; fix by declaring explicit types in variable blocks.

Split configurations into modules, use workspaces for environments, and organize files by resource type for maintainability.

Initially yes due to expression syntax, but HCL’s readability and validation reduce long-term maintenance burden significantly.

Use the official HCL playground at play.hashicorp.com to experiment with syntax without installing local tooling.