Skip to Content
CheatsheetsTerraform

Terraform Cheat Sheet

Apply number padding

Convert 1 to 01, 2 to 02 etc via format("%02d", count.index + 1).

resource "azurerm_application_security_group" "with_pad" { count = 4 name = "asg-${var.short}-${var.loc}-${terraform.workspace}-web-${format("%02d", count.index + 1)}" location = local.location resource_group_name = azurerm_resource_group.example_rg.name tags = local.tags } resource "azurerm_application_security_group" "without_pad" { count = 4 name = "asg-${var.short}-${var.loc}-${terraform.workspace}-web-${count.index + 1}" location = local.location resource_group_name = azurerm_resource_group.example_rg.name tags = local.tags }

Example output

Changes to Outputs: + asg_with_pad_output = [ + "asg-lbdo-euw-tst-web-01", + "asg-lbdo-euw-tst-web-02", + "asg-lbdo-euw-tst-web-03", + "asg-lbdo-euw-tst-web-04", ] + asg_without_pad_output = [ + "asg-lbdo-euw-tst-web-1", + "asg-lbdo-euw-tst-web-2", + "asg-lbdo-euw-tst-web-3", + "asg-lbdo-euw-tst-web-4", ]

Longhand to shorthand region name lookup

Convert uksouthuks, or perform any other key-value map lookup via lookup().

variable "loc" { description = "Shorthand Azure location. Normally passed as TF_VAR in pipeline." type = string default = "ukw" } variable "regions" { type = map(string) default = { uks = "UK South" ukw = "UK West" eus = "East US" } description = "Converts shorthand name to longhand name via lookup on map list." } locals { location = lookup(var.regions, var.loc, "UK South") }

Example output

Changes to Outputs: + location_output = "UK West"

Conditional for_each using regex

Only create resources where the map value matches a regex pattern; skip everything else.

variable "environment" { default = "prd" type = string description = "Used as an alternative to terraform.workspace" } locals { names = { key0 = var.environment # "prd" key1 = "${var.environment}-vm" # "prd-vm" key2 = "prd-biscuit" key3 = "tst_pizza" } } resource "azurerm_resource_group" "test_rg" { # Creates RGs only for values that contain "prd-" (key1 and key2) for_each = { for key, value in local.names : key => value if length(regexall("${var.environment}-", value)) > 0 } location = local.location name = each.value }

Example output

# azurerm_resource_group.test_rg["key1"] will be created + resource "azurerm_resource_group" "test_rg" { + id = (known after apply) + location = "uksouth" + name = "prd-vm" } # azurerm_resource_group.test_rg["key2"] will be created + resource "azurerm_resource_group" "test_rg" { + id = (known after apply) + location = "uksouth" + name = "prd-biscuit" }

Type conversions

Full example — for_each with output as list(map(object({})))

variable "environment" { default = "prd" type = string description = "Used as an alternative to terraform.workspace" } locals { names = { key0 = var.environment key1 = "${var.environment}-vm" key2 = "prd-biscuit" key3 = "tst_pizza" } } resource "azurerm_resource_group" "test_rg" { for_each = { for key, value in local.names : key => value if length(regexall("${var.environment}-", value)) > 0 } location = local.location name = each.value } output "rg_name" { value = element(azurerm_resource_group.test_rg[*], 0) }

Example output — list(map(object({})))

rg_name = [ + { + key1 = { + id = (known after apply) + location = "uksouth" + name = "prd-vm" + tags = null + timeouts = null } + key2 = { + id = (known after apply) + location = "uksouth" + name = "prd-biscuit" + tags = null + timeouts = null } }, ]

Get a specific key value from map(object({}))

output "rg_name" { value = { for key, value in element(azurerm_resource_group.test_rg[*], 0) : key => value.name } }
rg_name = { + key1 = "prd-vm" + key2 = "prd-biscuit" }

Fetch a specific attribute from an element of map(object({}))

Extract location from the first map entry and pass it as input to another resource.

locals { resource_group_locations = { for key, value in element(azurerm_resource_group.test_rg[*], 0) : key => value.location } resource_group_name = { for key, value in element(azurerm_resource_group.test_rg[*], 0) : key => value.name } } resource "azurerm_application_security_group" "example" { name = "libre-devops-asg" location = element(values(local.resource_group_locations), 0) resource_group_name = element(values(local.resource_group_name), 0) tags = { Hello = "World" } } output "asg_location" { value = azurerm_application_security_group.example.location } output "asg_rg_name" { value = azurerm_application_security_group.example.resource_group_name }
+ asg_location = "uksouth" + asg_rg_name = "prd-vm"

Access an inner object within a map with multiple elements

locals { fnc_apps = { fnc_app1 = { name = "fnc_app1" # ... } fnc_app2 = { name = "fnc_app2" # ... } } } resource "azurerm_function_app" "fnc" { for_each = local.fnc_apps identity { type = each.value.identity } # ... } output "managed_identity_principal_id" { value = { for key, value in element(azurerm_function_app.fnc[*], 0) : key => element(value.identity, 0).principal_id } }
managed_identity_principal_id = { + fnc_app1 = "3ca56017-d384-4899-bbad-1066800809c0" + fnc_app2 = "0cca0226-011d-444d-8763-e210878ef4dc" }

Fetch your outbound IP from Terraform

Useful for dynamically allowing a runner or local machine IP in firewall rules.

data "http" "user_ip" { url = "https://ipv4.icanhazip.com" } data "http" "user_ip_from_aws" { url = "https://checkip.amazonaws.com" } output "my_ip" { value = data.http.user_ip.body } # chomp() strips the trailing newline from the HTTP response body output "my_ip_chomp" { value = chomp(data.http.user_ip.body) }
+ my_ip = <<-EOT 20.108.154.139 EOT + my_ip_chomp = "20.108.154.139"

Virtual Machine Scale Set — agent extension block

extension { auto_upgrade_minor_version = false automatic_upgrade_enabled = false name = "Microsoft.Azure.DevOps.Pipelines.Agent" provision_after_extensions = [] publisher = "Microsoft.VisualStudio.Services" settings = jsonencode({ agentDownloadUrl = "https://vstsagentpackage.azureedge.net/agent/2.209.0/vsts-agent-linux-x64-2.209.0.tar.gz" agentFolder = "/agent" enableScriptDownloadUrl = "https://vstsagenttools.blob.core.windows.net/tools/ElasticPools/Linux/13/enableagent.sh" isPipelinesAgent = true }) type = "TeamServicesAgentLinux" type_handler_version = "1.22" }

Dynamic blocks — multiple identity type conditions

dynamic "identity" { for_each = length(var.identity_ids) == 0 && var.identity_type == "SystemAssigned" ? [var.identity_type] : [] content { type = var.identity_type } } dynamic "identity" { for_each = length(var.identity_ids) > 0 || var.identity_type == "UserAssigned" ? [var.identity_type] : [] content { type = var.identity_type identity_ids = length(var.identity_ids) > 0 ? var.identity_ids : [] } } dynamic "identity" { for_each = length(var.identity_ids) > 0 || var.identity_type == "SystemAssigned, UserAssigned" ? [var.identity_type] : [] content { type = var.identity_type identity_ids = length(var.identity_ids) > 0 ? var.identity_ids : [] } }

String manipulation

Remove dashes and spaces, then title-case

replace(replace(title("rg-craig-test"), "-", ""), " ", "") # => "RgCraigTest"

override.tf for local development

Swap the remote backend for a local run without touching the shared config.

terraform { required_providers { azurerm = { source = "hashicorp/azurerm" # version = "~> 2.68.0" } } backend "azurerm" { subscription_id = "blah" storage_account_name = "blah" container_name = "blah" key = "blah.terraform.tfstate" } }

Detect the runner OS

Add printf.cmd to the repo root to handle Windows runners:

@echo off echo {"os": "Windows"}

Then in Terraform, call the script via data "external":

data "external" "os" { working_dir = path.module program = ["printf", "{\"os\": \"Linux\"}"] } locals { os = data.external.os.result.os check = local.os == "Windows" ? "We are on Windows" : "We are on Linux" } output "os" { value = local.os }

Local workflow script

Full end-to-end pipeline: init → validate → plan → compliance → tfsec → checkov.

terraform_run() { rm -rf .terraform tfplan* terraform.lock.hcl if command -v tfenv &>/dev/null && \ command -v terraform &>/dev/null && \ command -v terraform-compliance &>/dev/null && \ command -v tfsec &>/dev/null && \ command -v checkov &>/dev/null; then echo "All packages are installed" else echo "Packages needed to run are not installed, exiting" && return 1 fi terraform_workspace="prd" checkov_skipped_tests="" terraform_compliance_policy_path="git:https://github.com/libre-devops/azure-naming-convention.git//?ref=main" terraform_version="1.5.5" setup_tfenv() { if [ -z "${terraform_version}" ]; then echo "terraform_version not set, defaulting to latest" export terraform_version="latest" fi tfenv install "${terraform_version}" && tfenv use "${terraform_version}" } terraform_plan() { terraform init && \ terraform workspace new "${terraform_workspace}" || \ terraform workspace select "${terraform_workspace}" terraform validate && \ terraform fmt -recursive && \ terraform plan -out "$(pwd)/tfplan.plan" terraform show -json tfplan.plan | tee tfplan.json >/dev/null } terraform_compliance_check() { terraform-compliance -p "$(pwd)/tfplan.json" -f "${terraform_compliance_policy_path}" } tfsec_check() { tfsec . --force-all-dirs } checkov_check() { checkov -f tfplan.json --skip-check "${checkov_skipped_tests}" } cleanup_tfplan() { rm -rf "$(pwd)/tfplan" "$(pwd)/tfplan.json" } setup_tfenv && \ terraform_plan && \ terraform_compliance_check && \ tfsec_check && \ checkov_check cleanup_tfplan }

State force-unlock one-liner

Grab the lock ID from a failed plan and immediately force-unlock.

$lockId = (terraform plan 2>&1 | Select-String -Pattern 'ID:\s+([\w-]+)' | ForEach-Object { $_.Matches.Groups[1].Value }) terraform force-unlock -force $lockId
lockId=$(terraform plan 2>&1 | grep -oP 'ID:\s+\K[\w-]+') terraform force-unlock -force "$lockId"

Generate timestamp tags without timestamp()

timestamp() resolves at plan time and is “not known until apply” — use an external data source instead.

data "external" "detect_os" { working_dir = path.module program = ["printf", "{\"os\": \"Linux\"}"] } data "external" "generate_timestamp" { program = data.external.detect_os.result.os == "Linux" ? [ "${path.module}/timestamp.sh" ] : ["powershell", "${path.module}/timestamp.ps1"] } locals { dynamic_tags = { "LastUpdated" = data.external.generate_timestamp.result["timestamp"] "Environment" = terraform.workspace } tags = merge(var.static_tags, local.dynamic_tags) } variable "static_tags" { type = map(string) description = "Static tags merged with dynamic ones." default = { "CostCentre" = "671888" "ManagedBy" = "Terraform" "Contact" = "help@libredevops.org" } }

timestamp.sh

#!/usr/bin/env bash DATE=$(date '+%d-%m-%Y:%H:%M') echo "{\"timestamp\": \"$DATE\"}"

timestamp.ps1

#!/usr/bin/env pwsh $date = Get-Date -Format "dd-MM-yyyy:HH:mm" $jsonOutput = @{ timestamp = $date } | ConvertTo-Json Write-Output $jsonOutput

timestamp.py

import datetime now = datetime.datetime.now() timestamp = now.strftime("%d-%m-%Y:%H:%M") print("{\"timestamp\": \"" + timestamp + "\"}")
Last updated on
Libre DevOpsSuggest a change