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 uksouth → uks, 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 $lockIdlockId=$(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 $jsonOutputtimestamp.py
import datetime
now = datetime.datetime.now()
timestamp = now.strftime("%d-%m-%Y:%H:%M")
print("{\"timestamp\": \"" + timestamp + "\"}")