Introduction
If you've spent the last few days exploring Terraform's architecture and understanding why Infrastructure as Code matters, you've probably noticed that everything in Terraform is written in HCL—HashiCorp Configuration Language. HCL syntax basics: a Terraform & IaC guide for developers means learning a relatively small, deliberate set of constructs that let you describe cloud infrastructure with precision. Once you understand blocks, arguments, expressions, and data types, you'll write and read Terraform configurations as naturally as any other code in your stack.
HCL is designed to be human-readable without being verbose. It borrows familiar ideas from JSON, Python, and other languages, but strips away unnecessary complexity. Whether you're provisioning a single S3 bucket or orchestrating a multi-region deployment, the syntax remains consistent. Let's break down the fundamentals so you can move confidently into real-world infrastructure definitions.
Blocks, arguments and expressions
Everything in Terraform revolves around three core concepts: blocks, arguments, and expressions. A block is a container for content, defined by a type name followed by one or more labels, then curly braces. An argument is a key-value pair inside a block that assigns a value to a setting. An expression is any value or combination of values that Terraform evaluates.
The most common block you'll write is a resource block. Here's a simple example:
resource "aws_s3_bucket" "logs" {
bucket = "my-company-logs-${var.environment}"
tags = {
Name = "Log Bucket"
Environment = var.environment
}
}In this block, resource is the block type, "aws_s3_bucket" is the resource type, and "logs" is the resource name (your local identifier). Inside the block, bucket and tags are arguments. The value for bucket contains an expression: a string interpolation using ${var.environment}.
Blocks can be nested. A resource block contains nested blocks for configuration. For example, a security group resource often includes multiple ingress blocks:
resource "aws_security_group" "web" {
name_prefix = "web-"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}Each ingress and egress block is a nested block with its own arguments. Terraform parses this hierarchical structure and generates the appropriate API calls to your cloud provider.
Strings, numbers, bools, lists and maps
HCL supports five primitive data types that you'll use constantly in infrastructure definitions.
Strings are sequences of characters enclosed in double quotes. They support escape sequences and interpolation:
environment_name = "production"
description = "Database for ${var.app_name}"
file_path = "C:\\Users\\Admin\\config.txt" # Windows path with escapingNumbers can be integers or floating-point values:
instance_count = 5
cpu_limit = 2.5
port = 3306Booleans are true or false:
enable_monitoring = true
publicly_accessible = falseLists are ordered collections of values, enclosed in square brackets. All elements should be the same type:
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
enabled_log_types = ["error", "general", "slowquery"]
allowed_ports = [80, 443, 3306]Maps are collections of key-value pairs, enclosed in curly braces. Keys are strings:
tags = {
Name = "My Database"
Environment = "staging"
Owner = "platform-team"
CostCenter = "engineering"
}
environment_variables = {
LOG_LEVEL = "DEBUG"
API_KEY = var.api_key
}You can mix types in complex structures, but consistency makes your configurations easier to read and maintain. When you're building variable definitions or locals, use explicit type constraints to help both Terraform and your teammates understand what values belong where.
References like aws_s3_bucket.logs.arn
One of HCL's most powerful features is the ability to reference other resources and their attributes. Instead of hardcoding values, you use a reference expression to point Terraform to the attribute you need. This creates implicit dependencies that Terraform tracks automatically.
The general syntax for a reference is: resource_type.resource_name.attribute. Let's see this in practice:
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-${var.environment}"
}
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_logging" "logs" {
bucket = aws_s3_bucket.logs.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "logs/"
}In this snippet, we reference aws_s3_bucket.logs.id multiple times. Each reference tells Terraform: "Make sure the S3 bucket exists first, then grab its ID." Terraform builds a dependency graph and applies resources in the correct order.
You can also reference variables, locals, data sources, and outputs:
variable "instance_type" {
type = string
default = "t3.micro"
}
locals {
common_tags = {
Project = var.project_name
Managed = "terraform"
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = local.common_tags
}References make your infrastructure code DRY and maintainable. When you need to change a value, you update it in one place, and all dependents automatically pick up the change.
Comments with # or //
Good infrastructure code needs comments just like application code. HCL supports two comment styles: single-line comments with # and single-line comments with //. There are no block comments in HCL.
Use comments to explain why you made a configuration choice, especially when that choice might not be obvious to the next person reading the code:
# Enable versioning so we can recover from accidental deletions.
# S3 bucket versioning is required for cross-region replication.
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id
versioning_configuration {
status = "Enabled"
}
}
// Multi-AZ deployment increases operational cost by ~50% but ensures
// the database remains available during maintenance windows.
resource "aws_db_instance" "primary" {
allocated_storage = 100
engine = "postgres"
engine_version = "14.7"
instance_class = "db.t3.medium"
multi_az = true // High availability requirement
publicly_accessible = false // Security best practice
skip_final_snapshot = false // Protect against accidental data loss
}Comments are stripped out during parsing, so they have zero runtime cost. But they have enormous value for your team's productivity. When you find yourself adding a comment about a security decision, a cost trade-off, or a workaround for a cloud provider limitation, you're helping your future self and your colleagues understand the infrastructure's reasoning.
Common mistakes
Learning HCL syntax is straightforward, but a few patterns trip up developers coming from other languages:
- Forgetting curly braces in nested blocks: HCL requires braces around every block body, even single-argument blocks.
ingress { from_port = 80 }is valid;ingress from_port = 80is not. - Confusing variable syntax in strings: String interpolation requires
${...}syntax.bucket = "my-${var.name}-bucket"works;bucket = "my-var.name-bucket"just stores the literal string. - Missing type declarations in variables: While optional, omitting the
typeargument in variable blocks makes your configuration harder to validate. Always declare types for variables, locals, and data source attributes where possible. - Using reserved keywords as identifiers: Words like
count,depends_on, andfor_eachhave special meaning in Terraform. You can't name a local or variable any of these without causing parse errors. - Incorrect reference paths: References must match the exact resource type and name from the resource declaration.
aws_s3_bucket.my_bucketis correct;aws_s3.my_bucketors3_bucket.my_bucketwill fail.
Key takeaways
- Blocks, arguments, and expressions are the three building blocks of HCL. Every Terraform configuration is a hierarchy of nested blocks with arguments that accept expressions as values.
- Strings, numbers, booleans, lists, and maps give you the primitives to represent any infrastructure configuration. Use appropriate types and keep your data structures consistent.
- References let you connect resources without hardcoding values. Terraform tracks these dependencies automatically and applies resources in the correct order.
- Comments with
#or//cost nothing at runtime but add enormous value by documenting the reasoning behind your infrastructure decisions. - HCL is intentionally simple and opinionated. Once you internalize these fundamentals, you'll read and write Terraform configurations as naturally as any code in your codebase.






Comments
Loading comments…