Skip to content

HCL Syntax and Configuration

Overview

This document covers the HashiCorp Configuration Language (HCL) syntax, configuration structure, variables, expressions, and functions. Understanding HCL is fundamental to Domain 3 (Understand Terraform basics - 20%), the largest single domain on the exam.

πŸ“– Configuration Language - Complete HCL language reference

Configuration File Structure

File Organization

  • Terraform reads all .tf files in the working directory
  • File names are arbitrary but conventions exist:
  • main.tf - Primary resource definitions
  • variables.tf - Input variable declarations
  • outputs.tf - Output value declarations
  • providers.tf - Provider configuration
  • terraform.tf - Terraform settings block
  • locals.tf - Local value definitions
  • data.tf - Data source definitions
  • .tf.json files are also valid (JSON format)
  • Files are processed in alphabetical order but order generally does not matter

πŸ“– Files and Directories - File organization conventions

Terraform Settings Block

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket = "my-state-bucket"
    key    = "terraform.tfstate"
    region = "us-east-1"
  }
}
  • required_version - Constrains which Terraform CLI versions can use this config
  • required_providers - Specifies provider source and version constraints
  • backend - Configures state storage location

πŸ“– Terraform Settings - Settings block reference

Variables

Input Variables

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
  sensitive   = false

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
  }
}

Variable Types

  • Primitive: string, number, bool
  • Collection: list(type), set(type), map(type)
  • Structural: object({...}), tuple([...])
  • Special: any (accepts any type)

πŸ“– Input Variables - Variable configuration reference

Variable Precedence (lowest to highest)

  1. Default values in variable block
  2. terraform.tfvars file (auto-loaded)
  3. *.auto.tfvars files (auto-loaded, alphabetical order)
  4. -var-file command line flag
  5. -var command line flag
  6. TF_VAR_<name> environment variables

This order is heavily tested on the exam. Environment variables and CLI flags override file-based values.

Output Values

output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.app.id
  sensitive   = false
}

output "db_password" {
  description = "Database password"
  value       = aws_db_instance.main.password
  sensitive   = true
}
  • Outputs are displayed after terraform apply
  • Accessible via terraform output command
  • Used to pass values between modules
  • sensitive = true redacts value from CLI output (still in state)

πŸ“– Output Values - Output configuration reference

Local Values

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

  name_prefix = "${var.project_name}-${var.environment}"
}
  • Simplify configurations by naming expressions
  • Computed once and referenced multiple times
  • Cannot be overridden from outside the module
  • Use local.<name> to reference (note: singular local, not locals)

πŸ“– Local Values - Local values reference

Expressions

String Interpolation

name = "web-${var.environment}-${count.index}"

Conditional Expressions

instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

For Expressions

# List comprehension
upper_names = [for name in var.names : upper(name)]

# Map comprehension
instance_ids = { for instance in aws_instance.app : instance.tags.Name => instance.id }

# Filtering
prod_instances = [for i in var.instances : i if i.environment == "prod"]

πŸ“– Expressions - Expression types and syntax

Dynamic Blocks

resource "aws_security_group" "example" {
  name = "example"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}
  • Generate repeated nested blocks dynamically
  • Iterator defaults to the label of the dynamic block
  • Can use custom iterator name with iterator argument

πŸ“– Dynamic Blocks - Dynamic block syntax

Built-in Functions

String Functions

Function Example Result
upper("hello") upper("hello") "HELLO"
lower("HELLO") lower("HELLO") "hello"
format("Hello, %s!", "world") "Hello, world!"
join("-", ["a", "b", "c"]) "a-b-c"
split(",", "a,b,c") ["a", "b", "c"]
replace("hello", "l", "L") "heLLo"
trimspace(" hello ") "hello"
substr("hello", 0, 3) "hel"

Collection Functions

Function Purpose
length(list) Number of elements
merge(map1, map2) Combine maps
lookup(map, key, default) Map key lookup with default
flatten(list) Flatten nested lists
keys(map) Get map keys
values(map) Get map values
contains(list, value) Check list membership
distinct(list) Remove duplicates
element(list, index) Get element by index
concat(list1, list2) Combine lists

Filesystem Functions

Function Purpose
file(path) Read file contents
fileexists(path) Check if file exists
templatefile(path, vars) Render template with variables
pathexpand(path) Expand ~ in path
abspath(path) Convert to absolute path

Type Conversion Functions

Function Purpose
tostring(value) Convert to string
tonumber(value) Convert to number
tobool(value) Convert to boolean
tolist(value) Convert to list
tomap(value) Convert to map
toset(value) Convert to set

πŸ“– Built-in Functions - Complete function reference

Resource Configuration

Resource Syntax

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
  subnet_id     = aws_subnet.main.id

  tags = local.common_tags
}
  • First label is resource type (provider_resource)
  • Second label is local name (for referencing)
  • Arguments configure the resource
  • Attributes are computed values available after creation

Meta-Arguments

# count - create multiple identical resources
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-12345678"
  instance_type = "t3.micro"
  tags = {
    Name = "web-${count.index}"
  }
}

# for_each - create resources from a map or set
resource "aws_instance" "web" {
  for_each      = toset(["web1", "web2", "web3"])
  ami           = "ami-12345678"
  instance_type = "t3.micro"
  tags = {
    Name = each.key
  }
}
  • count and for_each cannot be used together on the same resource
  • count references: aws_instance.web[0]
  • for_each references: aws_instance.web["web1"]

πŸ“– Resources - Resource block reference

Lifecycle Meta-Argument

resource "aws_instance" "web" {
  # ...

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = true
    ignore_changes        = [tags]
    replace_triggered_by  = [null_resource.trigger.id]
  }
}

πŸ“– Lifecycle Meta-Argument - Lifecycle customization

Data Sources

Data Source Syntax

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
}
  • Read-only queries to existing infrastructure
  • Populated during the plan phase
  • Referenced with data.<type>.<name>.<attribute>

πŸ“– Data Sources - Data source configuration

Key Exam Points

Common Exam Topics

  • Variable precedence order (memorize this)
  • Difference between count and for_each
  • When to use locals vs variables
  • Output values and their sensitive attribute
  • Dynamic blocks for repeated nested configuration
  • Data sources vs resources
  • String interpolation syntax ${...}
  • The ~> pessimistic version constraint

Things That Catch People Off Guard

  • locals block uses plural, but references use singular local.name
  • sensitive = true on outputs only hides from CLI, value is still in state
  • Variables without defaults are required and must be provided
  • terraform.tfvars is auto-loaded, other .tfvars files are not (unless *.auto.tfvars)