NexusCS

HCL

DevOps
HCL (HashiCorp Configuration Language) is the declarative language used by Terraform for infrastructure as code. This cheatsheet covers syntax, expressions, functions, meta-arguments, and Terragrunt extensions.

Getting Started

Basic Syntax

# Single-line comment
// Alternative single-line comment
/* Multi-line
   comment */

# Argument assignment
image_id = "abc123"

Simple key-value pairs form the foundation of HCL configuration.

Block Structure

resource "aws_instance" "example" {
  ami = "ami-abc123"

  # Nested block
  network_interface {
    # ...
  }
}

Blocks have a type, optional labels, and contain arguments or nested blocks.

Data Types

# String
name = "hello"

# Number
count = 42

# Bool
enabled = true

# List
zones = ["us-east-1a", "us-east-1b"]

# Map
tags = {
  Name = "web-server"
}

# Null
optional = null

HCL supports primitive types (string, number, bool) and complex types (list, map, set).

Multiline Strings

multiline = <<-EOT
  Hello
  World
EOT

Heredoc syntax for multiline strings. Use <<- to strip leading whitespace.

String Interpolation

Basic Interpolation

name = "World"
greeting = "Hello, ${var.name}!"

Embed expressions in strings using ${} syntax.

Conditional Directive

message = "Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"

Use %{ if } directive for conditional content in strings.

For Directive

servers = <<EOT
%{ for ip in aws_instance.example[*].private_ip ~}
server ${ip}
%{ endfor ~}
EOT

Generate repeated content. The ~ strips whitespace/newlines.

Escaping

literal_interpolation = "$${not_a_variable}"
literal_directive = "%%{not_a_directive}"

Use $$ and %% to escape interpolation and directives.

Core Blocks

Resource

resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  tags = {
    Name = "web-server"
  }
}

Declares infrastructure resource to create.

Variable

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

  validation {
    condition     = contains(["t2.micro", "t2.small"], var.instance_type)
    error_message = "Invalid type."
  }
}

Input variables with validation. Reference with var.instance_type.

Output

output "instance_ip" {
  description = "Public IP of instance"
  value       = aws_instance.web.public_ip
  sensitive   = false
}

Output values for modules or CLI display.

Local Values

locals {
  common_tags = {
    Project = "MyApp"
    Env     = var.environment
  }

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

Computed values reused throughout config. Reference with local.common_tags.

Data Source

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

  filter {
    name   = "name"
    values = ["ubuntu/images/*22.04*"]
  }
}

Query existing infrastructure. Reference with data.aws_ami.ubuntu.id.

Module

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.14.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

Reusable configuration packages. Reference outputs with module.vpc.vpc_id.

Provider

provider "aws" {
  region = "us-east-1"

  default_tags {
    tags = {
      ManagedBy = "Terraform"
    }
  }
}

Configure provider plugin.

Provider Aliases

provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

resource "aws_instance" "west" {
  provider = aws.west
  ami      = "ami-456"
}

Multiple configurations for same provider.

Terraform Settings

terraform {
  required_version = ">= 1.3"

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

  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

Configure Terraform behavior, providers, and state backend.

Expressions

Conditional (Ternary)

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

Classic ternary operator: condition ? true_value : false_value.

For Expression (List)

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

# Filter
prod = [for inst in var.instances : inst if inst.env == "prod"]

# With index
indexed = [for i, v in var.list : "${i}: ${v}"]

List comprehensions with optional filtering and indexing.

For Expression (Map)

# Transform map
upper_tags = {for k, v in var.tags : k => upper(v)}

# Filter map
filtered = {for k, v in var.tags : k => v if v != ""}

# Group by (with ...)
by_role = {for name, user in var.users : user.role => name...}

Map comprehensions. Use ... for grouping mode.

Splat Expression

# Get all IDs
instance_ids = aws_instance.example[*].id

# Nested access
private_ips = aws_instance.example[*].network_interface[0].private_ip

Shorthand for extracting attributes. Equivalent to [for inst in ... : inst.id].

Resource References

# Single resource
aws_instance.web.id

# With count
aws_instance.server[0].id
aws_instance.server[*].id

# With for_each
aws_instance.server["web"].id

Reference resource attributes in expressions.

Meta-Arguments

count

resource "aws_instance" "server" {
  count = 4

  ami = "ami-a1b2c3d4"

  tags = {
    Name = "Server ${count.index}"
  }
}

Create multiple instances. Access index with count.index. Reference: aws_instance.server[0].

for_each

# With set
resource "aws_instance" "server" {
  for_each = toset(["web", "api", "worker"])

  ami = "ami-a1b2c3d4"
  tags = {
    Name = each.key
  }
}

Create instances from set/map. Access with each.key and each.value. Reference: aws_instance.server["web"].

for_each (Map)

resource "aws_iam_user" "users" {
  for_each = {
    alice = "admin"
    bob   = "developer"
  }

  name = each.key
  tags = {
    Role = each.value
  }
}

Use maps for key-value iteration.

depends_on

resource "aws_iam_role_policy" "example" {
  role   = aws_iam_role.example.id
  policy = "..."

  depends_on = [aws_iam_role.example]
}

Explicit dependency when implicit dependencies aren't sufficient.

provider

resource "aws_instance" "west" {
  provider = aws.west
  ami      = "ami-456"
}

Use specific provider configuration.

Lifecycle Meta-Arguments

create_before_destroy

lifecycle {
  create_before_destroy = true
}

Create replacement before destroying original. Useful for zero-downtime changes.

prevent_destroy

lifecycle {
  prevent_destroy = true
}

Terraform will error if resource deletion attempted. Doesn't prevent manual deletion.

ignore_changes

lifecycle {
  ignore_changes = [
    tags,
    ami,
  ]
}

Ignore changes to specific attributes.

ignore_changes (All)

lifecycle {
  ignore_changes = all
}

Ignore all attribute changes. Prevents any updates to resource.

replace_triggered_by

lifecycle {
  replace_triggered_by = [
    aws_ecs_service.svc.id
  ]
}

Force replacement when referenced resource changes.

Preconditions

lifecycle {
  precondition {
    condition     = data.aws_ami.example.architecture == "x86_64"
    error_message = "AMI must be x86_64."
  }
}

Validate conditions before apply.

Postconditions

lifecycle {
  postcondition {
    condition     = self.public_dns != ""
    error_message = "Instance must have public DNS."
  }
}

Validate conditions after apply. Use self to reference resource.

Dynamic Blocks

Basic Dynamic Block

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. The block label becomes the default iterator name.

Custom Iterator

dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = rule
  content {
    from_port = rule.value.from_port
    to_port   = rule.value.to_port
  }
}

Use iterator to customize the iterator name instead of block label.

String Functions

format

format("instance-%03d", 42)
# "instance-042"

Printf-style string formatting.

join / split

join(", ", ["web", "api", "db"])
# "web, api, db"

split("-", "web-server-01")
# ["web", "server", "01"]

Join list to string or split string to list.

replace

replace("hello world", "world", "tf")
# "hello tf"

Replace substring in string.

trim functions

trimspace("  hello  ")
# "hello"

Remove leading/trailing whitespace.

upper / lower

upper("hello")  # "HELLO"
lower("HELLO")  # "hello"

Change string case.

substr

substr("hello world", 0, 5)
# "hello"

Extract substring: substr(string, offset, length).

regexall

regexall("[0-9]+", "abc123def456")
# ["123", "456"]

Extract all regex matches.

Collection Functions

concat

concat(["a", "b"], ["c", "d"])
# ["a", "b", "c", "d"]

Combine multiple lists.

flatten

flatten([["a", "b"], ["c", "d"]])
# ["a", "b", "c", "d"]

Flatten nested lists.

merge

merge({a = 1}, {b = 2})
# {a = 1, b = 2}

Combine multiple maps.

lookup

lookup({a = 1}, "b", 0)
# 0

Get map value with default: lookup(map, key, default).

element

element(["a", "b", "c"], 4)
# "b"

Get list element by index (wraps around).

length

length(["a", "b", "c"])  # 3

Get collection length.

keys / values

keys({a = 1, b = 2})    # ["a", "b"]
values({a = 1, b = 2})  # [1, 2]

Extract map keys or values.

contains

contains(["a", "b"], "a")
# true

Check if list contains value.

distinct

distinct(["a", "b", "a"])
# ["a", "b"]

Remove duplicates from list.

slice

slice(["a", "b", "c", "d"], 1, 3)
# ["b", "c"]

Extract sublist: slice(list, start, end).

sort

sort(["c", "a", "b"])
# ["a", "b", "c"]

Sort list alphabetically.

range

range(5)
# [0, 1, 2, 3, 4]

Generate sequence of numbers.

Numeric Functions

max / min

max(5, 12, 9)  # 12
min(5, 12, 9)  # 5

Find maximum or minimum value.

abs

abs(-5)  # 5

Absolute value.

ceil / floor

ceil(5.2)   # 6
floor(5.8)  # 5

Round up or down.

pow

pow(2, 3)  # 8

Exponentiation: pow(base, exponent).

Encoding Functions

JSON

jsonencode({name = "example", count = 3})
jsondecode('{"name":"example"}')

Encode/decode JSON.

YAML

yamlencode({name = "example"})
yamldecode("name: example")

Encode/decode YAML.

Base64

base64encode("hello")
base64decode("aGVsbG8=")

Encode/decode Base64.

URL Encoding

urlencode("hello world")
# "hello+world"

URL-encode string.

Filesystem Functions

file

file("${path.module}/config.txt")

Read file contents as string.

filebase64

filebase64("image.png")

Read file contents as base64.

templatefile

templatefile("${path.module}/template.tpl", {
  name = "example"
  port = 8080
})

Render template file with variables.

basename / dirname

basename("/path/to/file.txt")  # "file.txt"
dirname("/path/to/file.txt")   # "/path/to"

Extract filename or directory path.

abspath

abspath("./relative/path")

Convert relative to absolute path.

Type Conversion

tostring / tonumber

tostring(42)   # "42"
tonumber("42") # 42

Convert to string or number.

tobool

tobool("true")  # true

Convert string to boolean.

tolist / toset

tolist(["a", "b"])
toset(["a", "b", "a"])  # ["a", "b"]

Convert to list or set (removes duplicates).

tomap

tomap({a = 1})

Convert to map type.

Date/Time Functions

timestamp

timestamp()
# "2024-01-15T10:30:00Z"

Current timestamp in UTC.

formatdate

formatdate("YYYY-MM-DD", timestamp())

Format timestamp string.

timeadd

timeadd(timestamp(), "24h")

Add duration to timestamp.

Terragrunt

Basic Structure

# Include parent config
include "root" {
  path = find_in_parent_folders()
}

# Local values
locals {
  environment = "prod"
  region      = "us-east-1"
}

# Terraform source
terraform {
  source = "git::git@github.com:example/modules.git//vpc?ref=v1.0.0"
}

Terragrunt extends Terraform with DRY configuration and dependency management.

Remote State

remote_state {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key    = "${path_relative_to_include()}/terraform.tfstate"
    region = "us-east-1"
  }
}

Configure remote state backend.

Dependencies

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
}

Declare dependencies on other Terragrunt modules.

Generate Blocks

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "${local.region}"
}
EOF
}

Generate files before running Terraform.

Terragrunt Functions

find_in_parent_folders("root.hcl")
path_relative_to_include()
get_terragrunt_dir()
get_parent_terragrunt_dir()
get_aws_account_id()
run_cmd("echo", "hello")

Terragrunt-specific helper functions.

Terraform CLI

Core Workflow

terraform init
terraform validate
terraform fmt
terraform plan
terraform apply
terraform apply -auto-approve
terraform destroy

Standard workflow commands.

Plan Output

terraform plan -out=plan.tfplan
terraform apply plan.tfplan

Save plan to file for later apply.

State Management

terraform state list
terraform state show aws_instance.example
terraform state mv aws_instance.old aws_instance.new
terraform state rm aws_instance.example

Inspect and manipulate state.

State Backup

terraform state pull > terraform.tfstate
terraform state push terraform.tfstate

Pull/push state manually.

Import Resources

terraform import aws_instance.example i-1234567890abcdef0
terraform import module.vpc.aws_vpc.main vpc-12345

Import existing infrastructure into state.

Workspaces

terraform workspace list
terraform workspace new dev
terraform workspace select prod
terraform workspace show
terraform workspace delete dev

Manage multiple named states.

Utilities

terraform console
terraform graph | dot -Tpng > graph.png
terraform providers
terraform version
terraform test

Additional utility commands.

Gotchas

Meta-Arguments

  • Cannot use both count and for_each in same block
  • lifecycle configurations affect dependency graph
  • ignore_changes = all prevents all updates
  • prevent_destroy doesn't work if resource removed from config

Expressions

  • For expressions with sets have no guaranteed order
  • Splat [*] only works with lists, use for expression for maps

Strings

  • Heredoc strings don't support backslash escapes
  • Use $${ and %%{ to escape interpolation in heredocs

State & Security

  • Sensitive variables still stored in plaintext state
  • State files may contain secrets - encrypt and restrict access

Dynamic Blocks

  • Dynamic blocks can make code hard to read - use sparingly
  • Cannot use dynamic blocks for lifecycle or provisioner blocks

Also See