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
.tffiles in the working directory - File names are arbitrary but conventions exist:
main.tf- Primary resource definitionsvariables.tf- Input variable declarationsoutputs.tf- Output value declarationsproviders.tf- Provider configurationterraform.tf- Terraform settings blocklocals.tf- Local value definitionsdata.tf- Data source definitions.tf.jsonfiles 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 configrequired_providers- Specifies provider source and version constraintsbackend- 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)¶
- Default values in
variableblock terraform.tfvarsfile (auto-loaded)*.auto.tfvarsfiles (auto-loaded, alphabetical order)-var-filecommand line flag-varcommand line flagTF_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 outputcommand - Used to pass values between modules
sensitive = trueredacts 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: singularlocal, notlocals)
π 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
iteratorargument
π 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
}
}
countandfor_eachcannot be used together on the same resourcecountreferences:aws_instance.web[0]for_eachreferences: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
countandfor_each - When to use
localsvsvariables - Output values and their
sensitiveattribute - Dynamic blocks for repeated nested configuration
- Data sources vs resources
- String interpolation syntax
${...} - The
~>pessimistic version constraint
Things That Catch People Off Guard¶
localsblock uses plural, but references use singularlocal.namesensitive = trueon outputs only hides from CLI, value is still in state- Variables without defaults are required and must be provided
terraform.tfvarsis auto-loaded, other.tfvarsfiles are not (unless*.auto.tfvars)