Day 09: HCL (HashiCorp Configuration Language) — The Terraform Syntax Guide I Wish I Had.


Terraform uses HCL—a clean, declarative language—to describe infrastructure. Once you understand HCL’s building blocks, every Terraform repo starts to make sense.
HCL at a glance
Blocks:
TYPE "LABEL" "LABEL" { ... }
Arguments:
key = value
Expressions: references (
var.region
,local.tags
), functions (merge()
,format()
), conditionals, loops.Types:
string
,number
,bool
,list(...)
,set(...)
,map(...)
,object({...})
,tuple([...])
Comments:
#
,//
, or/* ... */
1) terraform
block
Declares provider dependencies, Terraform version, and (optionally) backend.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "6.9.0" }
random = { source = "hashicorp/random", version = "3.7.2" }
}
# backend "s3" { ... } # optional: store tfstate remotely
}
Why: pins versions for reproducibility.
2) provider
block
Configures how Terraform talks to a cloud.
provider "aws" {
region = var.region
# alias = "west" # use aliases to configure multiple regions/accounts
}
Pro tip: use env vars (AWS_PROFILE
, AWS_ACCESS_KEY_ID
…) for credentials.
3) variable
blocks
Inputs you pass to your configuration.
variable "region" {
description = "AWS region"
type = string
default = "ap-south-1"
}
variable "bucket_name_prefix" {
description = "Prefix for S3 bucket name"
type = string
validation {
condition = length(var.bucket_name_prefix) >= 3
error_message = "Prefix must be at least 3 chars."
}
}
Tips:
sensitive = true
hides values in CLI output.TF_VAR_region=ap-south-1 terraform apply
can set vars via env.
4) locals
block
Reusable computed values.
locals {
name_prefix = "hcl101"
common_tags = {
Project = "Terraform-HCL"
Owner = "Abdul Raheem"
Env = "dev"
}
}
Why: keeps code DRY.
5) data
blocks (read existing things)
Query cloud for read-only info (no creation).
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter { name = "name", values = ["amzn2-ami-hvm-*-x86_64-gp2"] }
filter { name = "virtualization-type", values = ["hvm"] }
}
Why: no hardcoded IDs; always fetch latest AMI, VPC, SG, etc.
6) resource
blocks (create/update things)
Declaratively manage infrastructure.
resource "random_id" "suffix" {
byte_length = 2
}
resource "aws_s3_bucket" "demo" {
bucket = "${var.bucket_name_prefix}-${random_id.suffix.hex}"
tags = merge(local.common_tags, { Name = "${local.name_prefix}-bucket" })
lifecycle {
prevent_destroy = false
ignore_changes = [] # specify attributes to ignore if needed
}
}
Meta-arguments you should know:
count
/for_each
→ multiple resourcesdepends_on
→ explicit dependencylifecycle
→create_before_destroy
,prevent_destroy
,ignore_changes
(Provisioners exist, but prefer cloud-native bootstrapping via user data)
7) output
blocks
Expose values after apply
.
output "bucket_name" {
value = aws_s3_bucket.demo.bucket
description = "Created S3 bucket name"
}
output "ec2_public_ip" {
value = aws_instance.demo.public_ip
sensitive = false
}
8) module
blocks
Package and reuse infra (from local path, Git, or Registry).
# Example (disabled by default). Set count=1 to use.
module "vpc" {
count = 0
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${local.name_prefix}-vpc"
cidr = "10.1.0.0/16"
azs = ["ap-south-1a","ap-south-1b"]
public_subnets = ["10.1.1.0/24","10.1.2.0/24"]
enable_dns_hostnames = true
tags = local.common_tags
}
Module anatomy:
Inputs → variables the module expects
Resources → what it creates
Outputs → what it returns
Versioning → always pin
version
for reproducibility
9) Expressions you’ll use daily
Conditionals:
var.env == "prod" ? "m5.large" : "t3.micro"
For-expressions:
{ for k, v in var.tags : upper(k) => v }
Functions:
merge()
,coalesce()
,join()
,format()
,file()
,length()
Dynamic blocks (generate nested blocks in loops)
✨ Mini Demo (complete main.tf
below)
Variables →
region
,bucket_name_prefix
Locals → tags & name prefix
Data → latest Amazon Linux AMI
Resources → random suffix, S3 bucket, EC2 with Nginx via user data
Outputs → bucket name & public IP
(See GitHub section for the ready-to-run file.)
Key takeaways
HCL is block + arguments + expressions.
Use variables, locals, data to keep configs clean and dynamic.
Modules make infra reusable and production-grade.
Prefer user data/cloud-init over provisioners.
Pin versions and use backends for teamwork.
Follow my journey
GitHub (learning repo):
https://github.com/<your-username>/terraform-learning-journey
X / Twitter: https://x.com/Abdulraheem183
Terraform series: https://abdulraheem.hashnode.dev/series/terraform-with-aws
Subscribe to my newsletter
Read articles from Abdul Raheem directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abdul Raheem
Abdul Raheem
Cloud DevOps | AWS | Terraform | CI/CD | Obsessed with clean infrastructure. Cloud DevOps Engineer 🚀 | Automating Infrastructure & Securing Pipelines | Bridging Gaps Between Code and Cloud ☁️ I’m on a mission to master DevOps from the ground up—building scalable systems, automating workflows, and integrating security into every phase of the SDLC. Currently working with AWS, Terraform, Docker, CI/CD, and learning the art of cloud-native development.