Terraform HCL cheatsheet

Table of Contents

Loops

for_each

Creating resources from a list

variable "iterator" {
  default = ["2", "5"]
}

resource "random_string" "for_each_example" {
  for_each = toset(var.iterator)

  length = each.value
}

output "strings_created" {
  # Outputs a map of the string resources, keyed by var.iterator values
  value = random_string.for_each_example
}

output "long_string_length" {
  value = random_string.for_each_example["5"].length # Should be 5
}

Creating resources from a map

variable "pet_prefixes_and_lengths" {
  default = {
    "five-long" : 5,
    "seven-long" : 7
  }
}

resource "random_pet" "for_each_example" {
  for_each = var.pet_prefixes_and_lengths

  prefix = each.key
  length = each.value
}

output "prefixed_pets_created" {
  # Outputs a map of the resources, keyed by the var.pet_prefixes_and_lengths keys
  value = random_pet.for_each_example
}

output "short_pet_length" {
  value = random_pet.for_each_example["five-long"].length # Should be 5
}

Creating sections within a resource

variable "zip_sources" {
  default = {
    first_source = {
      source_content = "Some text in my first source"
      filename       = "first_source.txt"
    }

    second_source = {
      source_content = "Some text in my second source."
      filename       = "second_source.txt"
    }
  }
}

data "archive_file" "inline_block_example" {
  type        = "zip"
  output_path = "example.zip"

  # Generates two inline blocks of the form:
  # source {
  #   content = <content>
  #   filename = <filename>
  # }
  dynamic "source" {
    for_each = var.zip_sources

    content {
      content  = source.value["source_content"]
      filename = source.value["filename"]
    }
  }
}

output "example_output" {
  value = data.archive_file.inline_block_example
}

count

Multiple resources from list

# Better to use for_each if you can, as count makes the resources
# addresses less readable (e.g. random_string.count_basic[0])
# will recreate all the resources if you remove the first one in
# the array (as their index will have changed).
# However, if you're hitting errors like:
# "value depends on resource attributes that cannot be determined until apply"
# then using count can be a good workaround

variable "string_lengths" {
  default = [2, 5, 1]
}

resource "random_string" "count_basic" {
  count = length(var.string_lengths)

  length = var.string_lengths[count.index]
}

Flagging resources on and off

variable "env" {
  default = "test"
}

resource "random_string" "test_env_only" {
  count  = var.env == "test" ? 1 : 0
  length = 5
}

resource "random_string" "prod_env_only" {
  count  = var.env == "prod" ? 1 : 0
  length = 5
}

# We need a ternary here or the terraform will fail to plan when
# var.env != "test" because random_string.test_env_only[0] will not exist
output "test_env_only" {
  value = var.env == "test" ? random_string.test_env_only[0] : null
}

output "prod_env_only" {
  value = var.env == "prod" ? random_string.prod_env_only[0] : null
}

for

Iterate over lists

variable "favourite_vegetables" {
  default = ["Artichoke", "Broccoli", "Potato"]
}

output "vegetable_statements" {
  value = [for veg in var.favourite_vegetables : "${veg} is great."]
}

Iterate over maps

variable "vegetable_opinions" {
  default = {
    artichoke   = "great"
    cauliflower = "terrible"
  }
}

output "uppercase_opinions" {
  value = { for veg, opinion in var.vegetable_opinions : upper(veg) => upper(opinion) }
}

For loop in a string (string directive)

variable "fruits" {
  default = ["apple", "tangerine", "mango"]
}

output "for_within_string" {
  # ~ character strips empty newlines and whitespace
  value = <<EOF
%{~for fruit in var.fruits}
  ${fruit}
%{~endfor}
EOF
}

Get property of iterated resource

resource "random_pet" "my_animals" {
  count = 5
}

output "farm_register" {
  value = [for pet in random_pet.my_animals[*] : pet.id]
}

Wildcard to reference all resources created (splat)

Created using count

variable "string_lengths" {
  default = [2, 5, 1]
}

resource "random_string" "splat_count" {
  count = length(var.string_lengths)

  length = var.string_lengths[count.index]
}

output "all_random_strings_created" {
  # Outputs a list of all the random_strings created
  value = random_string.splat_count[*].result
}

Created using for_each

variable "iterator" {
  default = ["2", "5"]
}

resource "random_string" "for_each_splat" {
  for_each = toset(var.iterator)

  length = each.value
}

output "map_of_resources_created" {
  value = random_string.for_each_splat[*]
}

Ternary

# Very useful for varying resources by environment

variable "env" {
  default = "prod"
}

resource "random_string" "longer_in_prod" {
  length = var.env == "prod" ? 5 : 3
}

output "string_produced" {
  # Will be 5 characters long
  value = random_string.longer_in_prod.result
}

Path to terraform directory

# This gives the path of the terraform running relative to the directory
# in which the entry terraform was run.
# This example is being run directly, rather than called in a module, so it returns '.'
# path.module is useful when creating and referencing files in a module

output "entry_terraform_path" {
  value = path.module
}

Variable templating (string interpolation)

# If the value you're interpolating is part of a resource
# then Terraform will infer the dependency between the two -
# it won't try to build the resource containing the interpolation
# until it has built the referenced resource.

resource "random_string" "insertion" {
  length = 7
}

locals {
  my_string = "Random string value is ${random_string.insertion.result}"
}

output "my_string" {
  value = local.my_string
}

JSON encoding

# Particularly useful when working with the
# AWS provider, as it makes IAM policy writing neater

output "example_json_policy" {
  value = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ec2:Describe*",
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

Variable validation

Basic validation

variable "sauce_for_roast_lamb" {
  description = "Which sauce to serve with roast lamb. Options are mint or redcurrant jelly."
  type = string
  # Variable validation gives you a faster feedback loop as invalid variables will
  # fail at plan rather than apply (and will not create incorrect resources). It also reduces
  # errors if your modules is being used by third parties.
  validation {
      # Only the variable being validated can be referenced in the condition,
      # no other variables, resources or locals can be referenced
      condition = (var.sauce_for_roast_lamb == "mint" || 
                   var.sauce_for_roast_lamb == "redcurrant jelly")
      # Terraform is very opinionated about this error message and will fail if
      # it doesn't begin with a capital letter and end with a full stop.
      error_message = "The sauce must be either mint or redcurrant jelly."
  }
}

Check strings in list match regex

variable "formatted_first_names" {
  description = "List of first names, formatted with an initial capital letter."
  default = ["A", "Bar"]
  type = list(string)
  validation {
      # Checks that all strings in var.formatted_first_names match the regex
      condition = alltrue([for name in var.formatted_first_names: length(regexall("[A-Z][a-z]*", name)) > 0])
      error_message = "Names in the list must begin with a capital letter and otherwise be lowercase alphabetic characters."
  }
}

Modules

Creating a module

# You may notice this module looks like a normal piece of terraform configuration.
# That's because it is: modules are just terraform configurations in a
# different directory

terraform {
  required_providers {
    random = {
      source = "hashicorp/random"
    }
  }
  required_version = "> 0.15"
}

# Variables aren't required for a module but allow users of the module to
# parameterise what the module creates
variable "example_string_length" {
  description = "The length of string to create in this module."
  type        = number
  default     = 5
}

resource "random_string" "example_string" {
  length = var.example_string_length
}

# Outputs aren't required for a module but allows the module
# to pass values back to the calling terraform
output "example_string_value" {
  value = random_string.example_string.result
}

Referencing a module

module "example_string" {
  # Path to module relative to running terraform. './' is required
  # if first element of path is in the same directory.
  # This module is in a directory called example_module
  # located in the same directory as this terraform
  source = "./example_module"

  example_string_length = 10
}

Module in Github

module "example_string" {
  source = "github.com/enicholson125/terraform-cheatsheet.git//example_module"

  example_string_length = 10
}

Module in Github - specific commit

module "example_string" {
  # Use specifically the version of the module present in the
  # 085ac8443aae62fbe85a21d2abff88464d750e39 commit version in the repo
  source = "github.com/enicholson125/terraform-cheatsheet.git//example_module?ref=72d9d56"

  example_string_length = 10
}

Getting module outputs

module "example_string" {
  source = "./example_module"

  example_string_length = 10
}

output "example_module_output" {
  # This prints the example_string_value output defined in the
  # example_string module. You can only reference outputs defined
  # by a module, you cannot directly reference resources in them
  value = module.example_string.example_string_value
}

Version Constraints

Version exactly equal to

# This specifies that the only allowed Terraform version for this
# config is 0.15.5

terraform {
  required_version = "0.15.5"
}

Version approximately equal to

# This allows the right-most number specified to increment
# so would allow 0.15.1 and 0.15.5 but not 0.15.0

terraform {
  required_version = "~> 0.15.1"
}

Version greater than

# This allows versions equal to or greater than 0.15.1

terraform {
  required_version = ">= 0.15.1"
}

Combining version constraints

# You can combine version constraints. For example, this
# configuration requires a terraform version greater than
# 0.15.0 but less than or equal to 0.15.5, excluding 0.15.3

terraform {
  required_version = "> 0.15.0, <= 0.15.5, != 0.15.3"
}