G
GuideDevOps
Lesson 3 of 14

HCL Syntax & Configuration

Part of the Terraform tutorial series.

HCL Basics

HashiCorp Configuration Language (HCL) is designed to be both human-readable and expressive. All Terraform files end with .tf.

File Structure

Example Terraform configuration:

# Comment using hash symbol
 
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}
 
provider "aws" {
  region = "us-east-1"
}
 
resource "aws_instance" "web" {
  # Configuration here
}

Blocks

Blocks are containers for configuration. They define infrastructure objects that Terraform manages.

Syntax:

block_type "label1" "label2" {
  key       = value
  nested {
    key   = value
  }
}

Common block types:

BlockPurposeLabels
resourceDefine infrastructuretype, name
dataReference existing infrastructuretype, name
variableInput parametername
outputReturn valuename
providerCloud provider configtype
terraformTerraform settingsnone
localsLocal valuesnone
moduleReusable configurationname

Data Types

Primitive Types

String:

instance_type = "t2.micro"
region        = "us-east-1"
 
# String interpolation
name = "web-server-${var.environment}"

Number:

count       = 3
port        = 8080
cpu_credits = 100.5

Boolean:

enable_monitoring  = true
force_destroy      = false

Complex Types

List (ordered collection):

availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
port_list          = [80, 443, 3306]
 
# Access by index
availability_zones[0]  # "us-east-1a"
availability_zones[2]  # "us-east-1c"

Map (key-value pairs):

tags = {
  Name        = "web-server"
  Environment = "production"
  CostCenter  = "engineering"
}
 
# Access by key
tags["Name"]        # "web-server"
tags.Environment    # "production"
 
# List of maps
subnets = {
  private = "10.0.1.0/24"
  public  = "10.0.2.0/24"
}

Object (typed structure):

database = {
  name          = "myapp"
  engine        = "mysql"
  instance_type = "db.t3.micro"
  storage_gb    = 20
}
 
# Access nested properties
database.name      # "myapp"
database.storage_gb  # 20

Tuple (fixed-length collection with mixed types):

server_info = ["web-1", 80, true, "us-east-1a"]
 
# Access by index
server_info[0]  # "web-1"
server_info[2]  # true

Type Constraints

variable "instance_type" {
  type = string
}
 
variable "instance_count" {
  type = number
}
 
variable "tags" {
  type = map(string)  # Map where values are strings
}
 
variable "subnets" {
  type = list(object({
    name   = string
    cidr   = string
  }))
}

Operators

Arithmetic

a = 10 + 5       # 15
b = 10 - 3       # 7
c = 4 * 5        # 20
d = 20 / 4       # 5
e = 17 % 5       # 2 (modulo)
 
computed_count = var.min_count + var.extra_servers

Comparison

# == Equal
# != Not equal
# < Less than
# <= Less than or equal
# > Greater than
# >= Greater than or equal
 
should_create = var.environment == "prod"
has_capacity  = var.available_cores >= 4

Logical

# && AND
# || OR
# ! NOT
 
is_production = var.environment == "prod" && var.enable_ha == true
should_alert  = error_rate > 0.05 || cpu_usage > 90
is_not_dev    = !contains(["dev", "test"], var.environment)

Collection

result = [1, 2, 3][0]           # Indexing: 1
result = {a = "x", b = "y"}["a"] # Map lookup: "x"

Interpolation & String

String Interpolation

# Using ${} syntax
resource "aws_instance" "web" {
  tags = {
    Name = "web-server-${var.environment}"
    # becomes: "web-server-production"
  }
}
 
# Multi-line template
user_data = <<EOF
#!/bin/bash
echo "Environment: ${var.environment}"
echo "Region: ${var.region}"
EOF

String Functions

# Concatenation
"hello" + " " + "world"  # "hello world"
 
# String functions
resource "aws_s3_bucket" "logs" {
  bucket = lower("MyBucket-${var.env}")  # lowercase
  
  tags = {
    Name = upper(var.project_name)       # UPPERCASE
    Size = tonumber("1024")              # string to number
  }
}

References

Variable References

variable "instance_type" {
  type    = string
  default = "t2.micro"
}
 
resource "aws_instance" "web" {
  instance_type = var.instance_type  # Reference variable
}

Resource References

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}
 
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id          # Resource attribute
  cidr_block        = "10.0.1.0/24"
  availability_zone = var.availability_zone
}

Output References

output "vpc_id" {
  value = aws_vpc.main.id
}
 
resource "aws_security_group" "main" {
  vpc_id = aws_vpc.main.id  # Reference another resource
}

For Each References

resource "aws_instance" "servers" {
  for_each      = var.server_configs
  instance_type = each.value["type"]
  tags = {
    Name = each.key  # Map key
  }
}
 
# or access in other resources:
security_group_ids = [for server in aws_instance.servers : server.security_groups[0]]

Functions

HCL includes many built-in functions:

String Functions

length("hello")     # 5
upper("hello")      # "HELLO"
lower("HELLO")      # "hello"
join(",", ["a", "b", "c"])  # "a,b,c"
split(",", "a,b,c") # ["a", "b", "c"]
contains("hello", "ell")  # true

Collection Functions

length([1, 2, 3])   # 3
element([1, 2, 3], 1)  # 2 (zero-indexed)
concat([1, 2], [3]) # [1, 2, 3]
merge({a=1}, {b=2}) # {a=1, b=2}
distinct([1, 1, 2, 2])  # [1, 2]

Numeric Functions

floor(4.9)      # 4
ceil(4.1)       # 5
round(4.6)      # 5
min(1, 3, 2)    # 1
max(1, 3, 2)    # 3

Type Conversion

tostring(42)             # "42"
tonumber("123")          # 123
tobool("true")           # true
tolist(["a"])            # ["a"]
tomap({a = "x"})         # {a = "x"}
toset(["a", "b"])        # {"a", "b"}

Conditional Functions

# ternary operator
can_ssh = var.environment == "prod" ? false : true
 
# try function (try, fallback)
az = try(var.availability_zone, "us-east-1a")
 
# lookup with fallback
env_tag = lookup(var.tags, "Environment", "dev")

Comments

# Single line comment
 
/*
  Multi-line comment
  Can span multiple lines
*/
 
# Good comments explain WHY, not WHAT
# ✓ GOOD: "Force destroy because this is ephemeral test infrastructure"
# ✗ BAD: "Setting force_destroy to true"

Meta-Arguments

Meta-arguments control resource behavior:

Meta-ArgumentPurposeExample
countCreate multiple resourcescount = 3
for_eachLoop with map/setfor_each = var.servers
depends_onExplicit dependencydepends_on = [aws_security_group.web]
providerSpecific provider instanceprovider = aws.us-west-2
lifecycleControl creation/destructionlifecycle

Splat Syntax

Extract values from multiple resources:

# Create 3 instances
resource "aws_instance" "web" {
  count         = 3
  instance_type = "t2.micro"
}
 
# Get all instance IDs
instance_ids = aws_instance.web[*].id
# Result: ["i-001", "i-002", "i-003"]
 
# In output
output "all_private_ips" {
  value = aws_instance.web[*].private_ip
}

For Expressions

Create derived data structures:

# List comprehension
instance_names = [for inst in aws_instance.web : inst.tags["Name"]]
 
# Map from list
instance_map = { for inst in aws_instance.web : inst.id => inst.private_ip }
# Result: { "i-001" => "10.0.1.5", "i-002" => "10.0.1.6" }
 
# Filter with condition
public_instances = [for inst in aws_instance.web : inst if inst.associate_public_ip_address]

Type System

Terraform is dynamically typed but supports type constraints:

# Type checking in variables
variable "config" {
  type = object({
    name    = string
    replicas = number
    tags    = map(string)
    ports   = list(number)
  })
}
 
# Dynamic typing (avoid when possible)
variable "flexible" {
  type = any
}

HCL Best Practices

PracticeExampleBenefit
Use consistent formattingUse terraform fmtReadability
Name resources meaningfullyaws_security_group_webClarity
Add descriptionsdescription = "..."Documentation
Use locals for computed valueslocals { name_prefix = "..." }DRY principle
Prefer named resources in loopsfor_each over countBetter state mapping
Add comments for complex logic/* multi-line comment */Future understanding
Validate variable typestype = list(string)Error prevention
Use functions for transformationsjoin(), split(), merge()Cleaner code