Skip to content
6 min read·Lesson 3 of 10

HCL Basics

Learn HashiCorp Configuration Language (HCL) — the syntax, block types, expressions, and built-in functions used to write Terraform configurations.

HCL (HashiCorp Configuration Language) is the language used to write Terraform configurations. It's designed to be readable by humans and machines, with a syntax that's more concise than JSON/YAML while still being structured and parseable.

Basic Syntax

HCL files have a .tf extension. A Terraform project can have multiple .tf files — Terraform merges them all before processing.

# This is a comment

# A block has a type, optional labels, and a body
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-unique-bucket-name"
  tags = {
    Environment = "production"
    Team        = "platform"
  }
}

The structure is: block_type "label1" "label2" { ... }

Block Types

BlockPurpose
terraformConfigure Terraform itself (required version, backend, providers)
providerConfigure a provider (credentials, region)
resourceDeclare a cloud resource to manage
dataRead existing resources without managing them
variableDeclare an input parameter
outputExport a value after apply
localsCompute intermediate values
moduleCall a reusable module

Data Types

variable "examples" {
  # Primitive types
  string_val  = "hello"
  number_val  = 42
  bool_val    = true

  # Collection types
  list_val    = ["a", "b", "c"]
  set_val     = toset(["x", "y", "z"])
  map_val     = { key1 = "value1", key2 = "value2" }

  # Complex types
  object_val = {
    name = "web"
    port = 80
  }
}

Expressions and References

# Reference another resource's attribute
resource "aws_subnet" "example" {
  vpc_id = aws_vpc.main.id      # references aws_vpc.main resource
  cidr_block = "10.0.1.0/24"
}

# String interpolation
resource "aws_instance" "web" {
  tags = {
    Name = "${var.env}-web-server"   # interpolation
  }
}

# Conditional expression
instance_type = var.env == "production" ? "t3.large" : "t3.micro"

# For expression (creates a list/map from another collection)
locals {
  instance_names = [for i in var.instances : "${var.prefix}-${i}"]
}

Built-in Functions

Terraform provides many built-in functions. You cannot define your own, but the built-ins cover most needs:

locals {
  # String functions
  upper_env   = upper(var.env)             # "PRODUCTION"
  trimmed     = trimspace("  hello  ")     # "hello"
  formatted   = format("app-%s-%d", "web", 1) # "app-web-1"

  # Collection functions
  combined    = concat(var.list_a, var.list_b)
  joined      = join(",", var.tags)        # "a,b,c"
  unique_list = distinct(var.ids)

  # Map functions
  value       = lookup(var.region_map, "us-east-1", "default")
  keys_list   = keys(var.tags_map)

  # Type conversion
  as_set      = toset(var.my_list)
  as_list     = tolist(var.my_set)

  # Numeric
  max_val     = max(5, 10, 3)             # 10
  ceil_val    = ceil(1.2)                 # 2
}

Locals

locals blocks define intermediate computed values — like variables but derived from other values rather than input:

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_instance" "web" {
  tags = local.common_tags   # reference local values
}

Count and for_each

Create multiple resources without copy-pasting:

# count: create N copies
resource "aws_iam_user" "devs" {
  count = length(var.dev_names)
  name  = var.dev_names[count.index]
}

# for_each: create one per map key (preferred — avoids ordering issues)
resource "aws_s3_bucket" "logs" {
  for_each = toset(["access-logs", "app-logs", "audit-logs"])
  bucket   = "${var.prefix}-${each.value}"
}

Prefer for_each over count for resources that may be added or removed mid-list, as count uses index-based addressing and removing an element can cause unintended resource replacements.

Next: Providers and Resources — the core building blocks of any Terraform configuration.

Key Takeaways

  • HCL uses blocks (resource, variable, output, locals, provider, terraform) as its top-level constructs.
  • Arguments assign values within blocks; expressions can reference other resources and call functions.
  • The ${...} interpolation syntax embeds expressions in strings.
  • Built-in functions (format, join, lookup, toset, etc.) handle common transformations without code.
  • terraform fmt auto-formats your code; terraform validate checks for syntax errors before plan.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →