HashiCorp Configuration Language (HCL) is often the unsung hero behind the immense popularity and power of tools like Terraform, Consul, Vault, and Nomad. While Terraform gets the spotlight for provisioning and managing infrastructure as code, it's HCL that provides the human-readable, machine-parsable syntax making it all possible. This journey from HCL1 to HCL2 marks a significant hcl language evolution, transforming how developers and operators interact with their infrastructure definitions.
This post will explore the development and refinements of HashiCorp Configuration Language (HCL), delving into the foundational principles of HCL1, the catalysts for change, and the groundbreaking features introduced in HCL2. We’ll see how these advancements have directly impacted the ability to write more concise, powerful, and maintainable terraform configuration files, ultimately enhancing the infrastructure as code language history.
In the early days of HashiCorp tools, a specific need arose: a configuration language that was both easy for humans to read and write, and straightforward for machines to parse. Existing formats like JSON or YAML, while machine-friendly, could become verbose and less intuitive for complex infrastructure definitions. This led to the birth of HCL1.
HCL1 was designed around a simple, declarative block-based syntax. Its core tenets were:
A typical HCL1 snippet, especially in Terraform HCL, would look like this:
resource "aws_instance" "example" {
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
tags {
Name = "MyServer"
Env = "Dev"
}
}
This syntax was concise and effective for its time, embodying the declarative nature of infrastructure as code. You declared what you wanted, and HCL translated that into a structured format for the underlying HashiCorp tool to execute.
However, as infrastructure as code practices matured and infrastructure itself grew more complex, the limitations of HCL1 began to emerge. Its primary expressive mechanism was string interpolation, and it lacked robust support for complex data structures, conditional logic, or iterative operations directly within the language. For advanced scenarios, users often had to resort to external templating engines or rely on extensive scripting around Terraform. This hinted at the need for a more sophisticated iac language history entry.
The journey from HCL1 to HCL2 wasn't merely about incremental improvements; it was a response to the evolving demands of modern infrastructure management. As organizations embraced cloud-native architectures, microservices, and dynamic environments, the complexity of managing their infrastructure definitions skyrocketed.
Several key pain points and emerging requirements drove the HCL evolution:
Limited Expressiveness for Complex Logic:
"${var.my_variable}"
) which, while simple, made complex operations cumbersome. Combining strings, numbers, and booleans often required multiple element()
or join()
functions.count
with count = var.condition ? 1 : 0
, which was clunky and didn't apply universally to all resources or arguments.count
and lookup
combinations that were hard to read and maintain.Weak Type System: HCL1 was largely "stringly-typed." Variables were often treated as strings, and type conversions were implicit, leading to unexpected behavior or runtime errors that were difficult to debug. There was no native concept of a complex object or map with defined schema.
Absence of First-Class Collections and Functions: While HCL1 had some basic functions, it lacked the ability to truly manipulate collections (lists, maps, sets) or to define reusable logical blocks within the configuration. This meant more repetitive code or complex external scripts.
Desire for Greater Modularity and Reusability: As Terraform modules became more prevalent, there was a clear need for a more powerful language to define module inputs, outputs, and internal logic without resorting to external processing.
Alignment Across HashiCorp Products: With the increasing adoption of various HashiCorp tools, there was a strategic push to unify the configuration language experience, ensuring consistency and leveraging shared improvements across the entire ecosystem.
These limitations highlighted that while HCL1 was an excellent starting point, Terraform HCL needed a significant upgrade to meet the demands of sophisticated infrastructure automation. The goal for HCL2 was not to replace HCL1 entirely but to enhance it, making it a powerful superset capable of handling the intricacies of modern cloud infrastructure with elegance and efficiency.
The release of HCL2 marked a pivotal moment in the hcl language evolution. It was designed as a superset of HCL1, meaning most valid HCL1 configurations are also valid HCL2. However, HCL2 introduced a wealth of new features that fundamentally changed how Terraform configuration could be written. It brought concepts commonly found in general-purpose programming languages into the declarative world of infrastructure.
Here are the cornerstone features that define HCL2:
HCL2 replaced the clunky string interpolation with a powerful expression language. This change alone revolutionized how data is manipulated:
variable "name" { type = list(string) }
), leading to much better validation and earlier error detection during terraform plan
.+
, -
, *
, /
, ==
, !=
, <
, >
, &&
, ||
, !
) are now fully supported for type-aware operations.variable "instance_count" {
type = number
default = 3
}
locals {
is_prod = var.environment == "production"
server_names = [for i in range(var.instance_count) : "web-server-${i}"]
}
output "instance_details" {
value = {
count_is_valid = var.instance_count > 0 && var.instance_count <= 10
total_servers = length(local.server_names)
}
}
for
Expressions and for_each
Meta-ArgumentThese are perhaps the most impactful additions for reducing boilerplate and increasing reusability in Terraform HCL:
for
Expressions: Allow you to transform and filter lists or maps into new lists or maps, similar to list comprehensions in Python or map
/filter
in JavaScript. This is incredibly powerful for preparing data.
locals {
raw_tags = {
Project = "Aurora"
Env = "Dev"
Owner = "TeamAlpha"
}
# Transform map into a list of objects with a specific structure
formatted_tags = [for k, v in local.raw_tags : { key = k, value = v }]
}
/*
formatted_tags will be:
[
{key = "Project", value = "Aurora"},
{key = "Env", value = "Dev"},
{key = "Owner", value = "TeamAlpha"}
]
*/
for_each
: Used in resource
and module
blocks, for_each
allows you to create multiple instances of a resource or module based on the elements of a given map or set of strings. This replaced the cumbersome count
for many use cases, especially when managing distinct, non-numeric instances.
variable "environments" {
type = map(object({
ami_id = string
instance_type = string
}))
default = {
dev = { ami_id = "ami-0abcdef1234567890", instance_type = "t3.micro" }
prod = { ami_id = "ami-0fedcba9876543210", instance_type = "m5.large" }
}
}
resource "aws_instance" "env_servers" {
for_each = var.environments
ami = each.value.ami_id
instance_type = each.value.instance_type
tags = {
Name = "server-${each.key}"
Env = each.key
}
}
This single aws_instance
block now provisions a separate instance for dev
and prod
, each with its specific AMI and instance type.
dynamic
BlocksThe dynamic
block provides a way to generate nested blocks within a resource or data source based on a complex expression. This is invaluable when certain nested blocks (like ingress
rules in a security group or setting
blocks in an App Service) need to be conditionally or iteratively defined.
resource "aws_security_group" "web_access" {
name = "web-access"
description = "Allow inbound HTTP/S"
vpc_id = "vpc-1234567890abcdef0"
dynamic "ingress" {
for_each = var.ingress_ports
content {
description = "Allow ${ingress.value.name} from ${var.source_cidr_blocks}"
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = var.source_cidr_blocks
}
}
}
variable "ingress_ports" {
type = set(object({ name = string, port = number }))
default = [
{ name = "HTTP", port = 80 },
{ name = "HTTPS", port = 443 }
]
}
variable "source_cidr_blocks" {
type = list(string)
default = ["0.0.0.0/0"]
}
This example dynamically creates ingress
blocks for HTTP and HTTPS ports, demonstrating how HCL2 makes complex, repeatable configurations more manageable.
try
FunctionWhile count
provided a crude form of conditional resource creation, HCL2 brought true conditional expressions (condition ? true_val : false_val
) and the try
function for error handling.
Conditional Expressions:
resource "aws_s3_bucket" "my_bucket" {
bucket = var.env == "prod" ? "production-bucket-123" : "dev-bucket-456"
}
try
Function: Allows attempts to evaluate an expression and, if it fails (e.g., due to a missing attribute), returns a fallback value instead of causing a panic. This is crucial for handling optional attributes or outputs.
output "bucket_region" {
value = try(aws_s3_bucket.my_bucket.region, "unknown")
}
null
ValuesHCL2 introduced the concept of null
values, which can be used to explicitly mean "no value" or "unset." This is particularly useful for making arguments optional or for conditionally omitting them entirely, as many Terraform providers treat null
as if the argument was never provided.
resource "aws_instance" "example" {
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
monitoring = var.enable_monitoring ? true : null # Omit if false
}
These features collectively empower Terraform HCL to be far more expressive, enabling developers to write highly flexible, reusable, and less verbose configurations. This marks a significant milestone in the hashicorp language journey, pushing infrastructure as code capabilities further.
The advancements in HCL2 reverberated deeply through the Terraform configuration landscape, transforming how infrastructure is defined, managed, and scaled.
HCL2 fundamentally improved the creation and consumption of Terraform modules. With for_each
and richer data structures, modules can now be designed to be truly generic and adaptable. A single module can provision multiple, distinct resources based on complex input maps, reducing the need for numerous copies of similar resource blocks. This directly contributes to a cleaner codebase and more maintainable infrastructure automation.
Before HCL2, achieving dynamic or repetitive configurations often involved a lot of repetitive code or convoluted interpolation. HCL2's for
expressions, dynamic
blocks, and for_each
significantly cut down on this boilerplate. Configurations become more declarative and less like procedural scripts, making them easier to read, understand, and debug. The improved type system also helps in writing more robust and less error-prone code.
The stronger type system and type constraints introduced in HCL2 mean that many configuration errors are caught earlier during the terraform validate
or terraform plan
phase, rather than failing during apply or leading to unexpected runtime behavior. This dramatically improves the developer experience and reduces the time spent debugging Terraform HCL issues. The explicit null
values also make it clearer when an attribute is intentionally absent.
The ability to perform complex transformations on lists and maps directly within the HashiCorp language means less reliance on external scripts (like Python or Bash) to preprocess data before feeding it into Terraform. This keeps the infrastructure as code definition self-contained and closer to its declarative intent.
As infrastructure patterns become increasingly complex (e.g., multi-region deployments, intricate networking, service meshes), HCL2 provides the expressive power required to model these complexities gracefully. Its flexibility ensures that Terraform configuration can evolve with infrastructure needs without hitting hard language limitations.
The transition to HCL2 was largely seamless for existing Terraform users due to its backward compatibility. HashiCorp provided clear guidelines and tools, such as terraform 0.12upgrade
, to help users migrate their existing configurations. Today, all modern Terraform HCL is written using HCL2, and leveraging its advanced features is considered best practice.
Looking ahead, the hcl language evolution is an ongoing process. While HCL2 offers a robust foundation, the infrastructure as code language history is still being written. We might see further refinements focusing on:
HCL stands as a testament to the power of a purpose-built configuration language. It strikes a remarkable balance between human readability and machine efficiency. Its evolution from HCL1 to HCL2 mirrors the growth and increasing sophistication of infrastructure as code itself, solidifying its place as a cornerstone of modern cloud operations.
The journey of HCL from its humble beginnings to its current powerful iteration in HCL2 showcases HashiCorp's commitment to building robust, developer-friendly tools. It's a prime example of how thoughtful language design can empower users to manage increasingly complex systems with clarity and confidence. The next time you write a concise for_each
loop or leverage a dynamic
block in your Terraform configuration, take a moment to appreciate the significant hcl language evolution that made it possible.
We encourage you to delve deeper into the official HashiCorp documentation to discover more advanced HCL2 patterns. Share this article with your colleagues to help them understand the power behind Terraform's language, and explore the myriad ways HCL2 can streamline your infrastructure automation efforts.