Terraform Cheat Sheet

Apply number padding

Convert 1 to 01, 2 to 02 etc via format("%02d", )

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",
    ]

Perform longhand conversion for inconsistent naming

Convert longhand name uksouth to shorthand uks or perform any other match on a key value

variable "loc" {
  description = "The shorthand name of the Azure location, for example, for UK South, use uks.  For UK West, use ukw. 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"

Perform conditional based on a match being found in a regex, if the condition is true, do something, if not, do nothing

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" {
  for_each = {
  for key, value in local.names : key => value
    if length(regexall("${var.environment}-", value)) > 0 // Checks the values of the map called local.names, if the any value of that map contains the name "prd-" followed by anything else, then make a resource group for it, with that value of the map as the name of the resource group.  If no match is found, do nothing.
  }
  location = local.location
  name     = each.value // makes 2 rgs, prd-vm and prd-biscuit
}

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"

Various Type Conversions

Example - Full Code

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" {
  for_each = {
  for key, value in local.names : key => value
    if length(regexall("${var.environment}-", value)) > 0 // Checks the values of the map called local.names, if the any value of that map contains the name "prd-" followed by anything else, then make a resource group for it, with that value of the map as the name of the resource group.  If no match is found, do nothing.
  }
  location = local.location
  name     = each.value // makes 2 rgs, prd-vm and prd-biscuit
}

output "rg_name" {
  value = element(azurerm_resource_group.test_rg[*], 0)
}

Example Output - Full Rg_name - list(map(object({})))

# Outputs in 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 specific key value from map(object({}))

output "rg_name" {
  value = {
    for key, value in element(azurerm_resource_group.test_rg[*], 0) : key => value.name
  }
}

Example Output in map(object({}))

rg_name  = {
      + key1 = "prd-vm"
      + key2 = "prd-biscuit
}

Fetch the location key from the 2nd object in map(object({})), then get the value only to be used as an input

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" {
  for_each = {
  for key, value in local.names : key => value
    if length(regexall("${var.environment}-", value)) > 0 // Checks the values of the map called local.names, if the any value of that map contains the name "prd-" followed by anything else, then make a resource group for it, with that value of the map as the name of the resource group.  If no match is found, do nothing.
  }
  location = local.location
  name     = each.value // makes 2 rgs, prd-vm and prd-biscuit
}

// Use local or output from within a module to keep tidy, you could do this in-line but its a bad idea
locals {
  resource_group_locations = {
  for key, value in element(azurerm_resource_group.test_rg[*], 0) : key => value.location
  }
  /*
  Outputs:
  location  = {
      + key1 = "uksouth"
      + key2 = "uksouth
}
  */

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

resource "azurerm_application_security_group" "example" {
  name                = "libre-devops-asg"
  location            = element(values(local.resource_group_locations) , 0) // filters first element and gets value = uksouth
  resource_group_name = element(values(local.resource_group_name) , 0) // filters second element and gets value = prd-biscuit

  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
}

Example Output in map(object({}))

  + 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"
      ... // Not complete code
    },
    
    fnc_app2 = {
      name = "fnc_app2"
      ... // Not complete code
    }
  }
}

resource "azurerm_function_app" "fnc" {

  for_each = local.fnc_apps

  identity {
    type = each.value.identity
  }
  ... //Not complete code
}

output "managed_identity_prinicpal_id" {
  value = {
    for key, value in element(azurerm_function_app.fnc[*], 0) : key => element(value.identity, 0).principal_id
  }
}

Example Output in map(object({}))

  managed_identity_prinicpal_id  = {
      + fnc_app1 = "3ca56017-d384-4899-bbad-1066800809c0"
      + fnc_app2 = "0cca0226-011d-444d-8763-e210878ef4dc
}

Fetch your Outbound IP from terraform

// If running locally, running this block will fetch your outbound public IP of your home/office/ISP/VPN and add it.  It will add the hosted agent etc if running from Microsoft/GitLab
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
}

// You will want to chomp to get rid of the heredoc response
output "chomp_my_ip" {
  value = chomp(data.http.user_ip.body)
}

Example Output

  + my_ip           = <<-EOT
        20.108.154.139
    EOT
  + my_ip_chomp     = "20.108.154.139"

Viurtual 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"
        }

Multiple options for nested blocks with Dynamic

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 : []
    }
  }

## Remove - and spaces from a string and title it

  replace(replace(title("rg-craig-test"), "-", ""), " ", "")
  RgCraigTest

##Make override.tf for local dev

terraform {
  #Use the latest by default, uncomment below to pin or use hcl.lck
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      #      configuration_aliases = [azurerm.default-provider]
      #      version = "~> 2.68.0"
    }
  }
  backend "azurerm" {
    subscription_id      = "blah"
    storage_account_name = "blah"
    container_name       = "blah"
    key                  = "blah.terraform.tfstate"
  }
}

Create complex data structures from nested list of objects and dynamic blocks

Determine your OS with terraform

First, add this to your repo and call it printf.cmd:

:: This is a hack for terraform to consider whether an OS is Linux or Windows.
@echo off
echo {"os": "Windows"}

Then in terraform:

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

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


    # Environment Variables
    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 and Install terraform
    setup_tfenv() {
        if [ -z "${terraform_version}" ]; then
            echo "terraform_version is empty or not set., setting to latest" && export terraform_version="latest"

        else
            echo "terraform_version is set, installing terraform version ${terraform_version}"
        fi

        tfenv install ${terraform_version} && tfenv use ${terraform_version}
    }

    # Terraform Init, Validate & Plan
    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_check() {
        terraform-compliance -p "$(pwd)/tfplan.json" -f ${terraform_compliance_policy_path}
    }
0
    # TFSec Check
    tfsec_check() {
        tfsec . --force-all-dirs
    }

    # CheckOv Check
    checkov_check() {
        checkov -f tfplan.json --skip-check "${checkov_skipped_test}"
    }

    # Cleanup tfplan
    cleanup_tfplan() {
        rm -rf "$(pwd)/tfplan" && rm -rf "$(pwd)/tfplan.json"
    }

    # Call the functions in sequence
    setup_tfenv && \
    terraform_plan && \
    terraform_compliance_check && \
    tfsec_check && \
    checkov_check
    cleanup_tfplan
}

Terraform state force-unlock one-liner

$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 terraform function (not known until apply issue)

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 = "The tags variable"
  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

# Generate the current timestamp in the required format
$date = Get-Date -Format "dd-MM-yyyy:HH:mm"

# Convert it to a JSON output for Terraform's external data source
$jsonOutput = @{
    timestamp = $date
} | ConvertTo-Json

# Print the JSON output
Write-Output $jsonOutput

timestamp.py

import datetime

# Get current time
now = datetime.datetime.now()

# Format the time
timestamp = now.strftime("%d-%m-%Y:%H:%M")

# Print as JSON
print("{\"timestamp\": \"" + timestamp + "\"}")

Source: docs/cheatsheets/terraform-cheat-sheet.md