3 – Terraform Advanced

Terraform Built-in Functions (Part 1): Learning Functions Through Hands-on Assignments

In this section, we begin exploring Terraform built-in functions through practical, hands-on assignments.
Instead of only reading documentation, the focus here is on:

  • Practicing functions directly in terraform console
  • Applying them in real Terraform files
  • Solving common problems such as:
    • Formatting names
    • Enforcing naming rules
    • Merging maps
    • Validating resource constraints
    • Generating dynamic values

This approach helps beginners understand why functions exist and how to use them correctly.


Practicing Functions Using terraform console

Before writing full Terraform files, we can experiment with functions interactively.

terraform console

Inside the console, you can directly test functions.

Example:

max(2, 4, 1)

Result:

4

This shows:

  • You do not need to write a full Terraform file
  • You can quickly test function behavior
  • This is the fastest way to learn functions safely

Terraform only supports built-in functions.
You cannot create custom functions in Terraform.


Assignment 1: Formatting Resource Names with lower and replace

Requirement:

  • Resource names must:
    • Be lowercase
    • Replace spaces with hyphens

Input example:

Project Alpha Resource

Expected output:

project-alpha-resource

Step 1: Define the Variable

variable "project_name" {
  type        = string
  description = "Name of the project"
  default     = "Project Alpha Resource"
}

Step 2: Format the Name Using Functions

locals {
  formatted_name = lower(replace(var.project_name, " ", "-"))
}

Explanation:

  • replace(var.project_name, " ", "-")
    Replaces all spaces with hyphens
  • lower(...)
    Converts the entire string to lowercase

Step 3: Use It in a Resource

resource "azurerm_resource_group" "rg" {
  name     = "${local.formatted_name}-rg"
  location = "West US 2"
}

Now:

  • "Project Alpha Resource"
    Becomes:
    project-alpha-resource-rg

This ensures consistent, policy-compliant naming.


Assignment 2: Merging Tags Using merge

Scenario:

You have:

  • Default tags
  • Environment-specific tags

You want to combine both maps into one.

Step 1: Define the Tag Maps

variable "default_tags" {
  type = map(string)
  default = {
    owner   = "team-a"
    project = "demo"
  }
}

variable "environment_tags" {
  type = map(string)
  default = {
    environment = "dev"
    costcenter  = "1001"
  }
}

Step 2: Merge Them Using merge

locals {
  merged_tags = merge(var.default_tags, var.environment_tags)
}

Explanation:

  • merge(map1, map2)
    Combines both maps
  • If the same key exists in both, the last one wins

Step 3: Apply to a Resource

resource "azurerm_resource_group" "rg" {
  name     = "${local.formatted_name}-rg"
  location = "West US 2"
  tags     = local.merged_tags
}

This avoids repeating the same merge logic in multiple places.


Assignment 3: Formatting Storage Account Names with Multiple Functions

Azure Storage Account Rules:

  • Only lowercase letters and numbers
  • Length between 3 and 24 characters
  • No spaces
  • No special characters

Step 1: Define an Invalid Input

variable "storage_account_name" {
  type    = string
  default = "Tech Tutorials @ Demo 2024!!!"
}

This input:

  • Has spaces
  • Has uppercase
  • Has special characters
  • Is longer than allowed

Step 2: Format the Name Using Nested Functions

locals {
  formatted_storage_name = lower(
    replace(
      substr(var.storage_account_name, 0, 23),
      " ",
      ""
    )
  )
}

Explanation:

  • substr(var.storage_account_name, 0, 23)
    Limits length to 23 characters
  • replace(..., " ", "")
    Removes spaces
  • lower(...)
    Converts to lowercase

This produces a valid Azure storage account name.


Step 3: Use It in the Resource

resource "azurerm_storage_account" "example" {
  name                     = local.formatted_storage_name
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

This shows how multiple functions can be nested to enforce strict provider rules.


Assignment 4: Generating NSG Rule Names Using split, for, and String Interpolation

Scenario:

You start with a comma-separated list of ports:

"80,443,3306"

You want to generate rule names like:

  • Port-80
  • Port-443
  • Port-3306

Step 1: Define the Variable

variable "allowed_ports" {
  type    = string
  default = "80,443,3306"
}

Step 2: Split the String into a List

locals {
  formatted_ports = split(",", var.allowed_ports)
}

Explanation:

  • split(",", var.allowed_ports)
    Converts "80,443,3306" into:
["80", "443", "3306"]

Step 3: Build a Map of NSG Rules Using a for Expression

locals {
  nsg_rules = {
    for port in local.formatted_ports :
    "Port-${port}" => {
      name        = "Port-${port}"
      port        = port
      description = "Allow traffic on port ${port}"
    }
  }
}

Explanation:

  • for port in local.formatted_ports
    Loops through each port
  • "Port-${port}"
    Dynamically builds the rule name
  • Each iteration creates a map entry for one rule

Step 4: Use the Map in a Dynamic Block

resource "azurerm_network_security_group" "example" {
  name                = "${local.formatted_name}-nsg"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  dynamic "security_rule" {
    for_each = local.nsg_rules

    content {
      name                       = security_rule.value.name
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = security_rule.value.port
      source_address_prefix      = "*"
      destination_address_prefix = "*"
      description                = security_rule.value.description
    }
  }
}

Now Terraform automatically creates:

  • One rule per port
  • With correct names and descriptions
  • Without manually writing each rule

Summary

In this first part of Terraform functions, you learned how to:

  • Practice functions using terraform console
  • Format names using:
    • lower
    • replace
    • substr
  • Merge maps using:
    • merge
  • Enforce provider naming rules using nested functions
  • Convert strings to lists using:
    • split
  • Generate multiple blocks using:
    • for expressions
    • Dynamic maps

These assignments show how Terraform functions help you write:

  • Cleaner code
  • Fewer hardcoded values
  • More reusable configurations
  • Provider-compliant resource definitions

This forms the foundation for writing dynamic, production-ready Terraform code.

Terraform Built-in Functions (Part 2): Practical Demos with Lookup, Validation, Sets, Math, Time, and Files

In this section, we continue learning Terraform built-in functions through a set of hands-on assignments.
The focus here is on how functions are used in real Terraform code to solve practical problems such as:

  • Selecting values dynamically
  • Validating user input
  • Enforcing naming rules
  • Removing duplicates
  • Performing math on lists
  • Working with timestamps
  • Handling sensitive data and files

All examples below are written in a beginner-friendly, step-by-step way.


Using lookup to Select Values from an Environment Map

Instead of writing long conditional expressions, we use a map + lookup function to select the correct VM size based on the environment.

Defining the Environment Variable with Validation

variable "environment" {
  type        = string
  description = "Environment name"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Enter a valid value for environment: dev, staging, or prod"
  }
}

Explanation:

  • contains(["dev", "staging", "prod"], var.environment)
    Ensures the value is only one of the allowed environments
  • If the value is invalid, Terraform stops with the custom error message

This prevents accidental typos like prods or testing.


Mapping Environments to VM Sizes

variable "vm_sizes" {
  type = map(string)
  default = {
    dev     = "Standard_D2s_v3"
    staging = "Standard_D4s_v3"
    prod    = "Standard_D8s_v3"
  }
}

This map defines which VM size should be used in each environment.


Using lookup with a Fallback Value

locals {
  selected_vm_size = lookup(var.vm_sizes, var.environment, "Standard_D2s_v3")
}

Explanation:

  • First argument → the input map
  • Second argument → the key to search (var.environment)
  • Third argument → fallback value if the key does not exist

This means:

  • devStandard_D2s_v3
  • prodStandard_D8s_v3
  • Missing key → default VM size

Printing the Result with an Output

output "vm_size" {
  value = local.selected_vm_size
}

Running:

terraform plan

Shows the VM size selected based on the environment.


Validating VM Size Using length and strcontains

Now we add validation rules to a VM size string.

Rules:

  • Length must be between 2 and 20 characters
  • It must contain the word “standard”
variable "vm_size" {
  type    = string
  default = "Standard_D2s_v3"

  validation {
    condition = length(var.vm_size) >= 2 && length(var.vm_size) <= 20
    error_message = "VM size should be between 2 and 20 characters"
  }

  validation {
    condition = strcontains(lower(var.vm_size), "standard")
    error_message = "VM size should contain the word 'standard'"
  }
}

Explanation:

  • length(var.vm_size) checks the string length
  • lower(...) converts to lowercase
  • strcontains(...) checks if "standard" exists in the string

Terraform throws a validation error if either rule fails.


Marking Sensitive Variables with sensitive

To protect secrets:

variable "credential" {
  type      = string
  default   = "XYZ123"
  sensitive = true
}

And in the output:

output "credential" {
  value     = var.credential
  sensitive = true
}

Terraform will display:

credential = <sensitive>

This prevents secrets from being printed in logs.


Enforcing Naming Rules with endswith

We ensure backup names end with _backup.

variable "backup_name" {
  type    = string
  default = "test_backup"

  validation {
    condition     = endswith(var.backup_name, "_backup")
    error_message = "Backup name must end with _backup"
  }
}

If the name does not end with _backup, Terraform stops with an error.


Combining Lists and Removing Duplicates with concat and toset

locals {
  user_locations    = ["East US", "West US", "East US"]
  default_locations = ["Central US"]

  unique_locations = toset(concat(local.user_locations, local.default_locations))
}

Explanation:

  • concat(...) joins both lists
  • toset(...) removes duplicate values

Result:

["East US", "West US", "Central US"]

Working with Numbers Using abs and max

locals {
  monthly_costs = [-50, 75, -200, 100]

  positive_costs = [for c in local.monthly_costs : abs(c)]
  max_cost       = max(local.positive_costs...)
}

Explanation:

  • abs(c) converts negative numbers to positive
  • for expression applies it to every element
  • max(... ) finds the largest number
  • ... expands the list into arguments

Result:

  • positive_costs[50, 75, 200, 100]
  • max_cost200

Working with Time Using timestamp and formatdate

locals {
  current_time  = timestamp()
  resource_name = formatdate("YYYYMMDD", local.current_time)
  tag_date      = formatdate("DD-MM-YYYY", local.current_time)
}

Explanation:

  • timestamp() returns the current UTC time
  • formatdate() converts it into readable formats

These values are commonly used in:

  • Resource names
  • Tags
  • Audit metadata

Handling File Content with file, jsondecode, and sensitive

locals {
  config_content = sensitive(file("config.json"))
  decoded_config = jsondecode(file("config.json"))
}

Explanation:

  • file("config.json") reads file content as a string
  • sensitive(...) hides it from output
  • jsondecode(...) converts JSON into a Terraform object

This allows you to safely load structured configuration from files.


Summary

In this section, you learned how to use Terraform built-in functions to:

  • Select values dynamically with lookup
  • Validate inputs using:
    • contains
    • length
    • strcontains
    • endswith
  • Protect secrets with sensitive
  • Combine and deduplicate lists with:
    • concat
    • toset
  • Process numbers using:
    • abs
    • max
  • Work with time using:
    • timestamp
    • formatdate
  • Safely read and decode files using:
    • file
    • jsondecode

These examples show how Terraform functions transform static configuration into intelligent, validated, and production-ready Infrastructure as Code.

Terraform Data Sources: Using Existing Infrastructure in Your Terraform Code

In this section, we learn about Terraform Data Sources — what they are, why we need them, and how to use them in a real Azure example.

This is a very important concept for real-world projects, because in most organizations:

  • You do not create everything yourself
  • Many core resources (networks, subnets, security) are already managed by other teams
  • Your Terraform code must reuse existing infrastructure, not recreate it

Let’s understand this step by step.


Why Do We Need Terraform Data Sources?

Imagine this common enterprise setup:

  • A central network team manages:
    • A shared Virtual Network (VNet)
    • Multiple subnets for different teams and environments
  • Each team is not allowed to create their own VNet or subnet
  • You only get permission to:
    • Create your own Resource Group
    • Create your own Virtual Machine
    • But you must place it inside an existing subnet

Without data sources:

  • Terraform would try to create a new VNet and subnet
  • This would:
    • Break governance rules
    • Duplicate infrastructure
    • Cause conflicts

With data sources:

  • Terraform can read existing resources
  • And attach new resources to them

This is exactly what data sources are for:

Data sources allow Terraform to read information about resources that already exist, without creating or modifying them.


What Is a Terraform Data Source?

A data source:

  • Starts with the data keyword
  • Reads an existing resource from the provider
  • Makes its attributes available in your configuration

It does not create anything.
It only fetches information.

Basic pattern:

data "provider_resource_type" "local_name" {
  name                = "existing-resource-name"
  resource_group_name = "existing-rg-name"
}

You then use it like:

data.provider_resource_type.local_name.attribute

Scenario Used in This Demo

Already existing in Azure:

  • Resource Group: shared-network-rg
  • Virtual Network: shared-network-vnet
  • Subnet: shared-primary-sn

Our goal:

  • Create a new Resource Group
  • Create a new Virtual Machine
  • Attach it to:
    • The existing VNet
    • The existing Subnet

Without creating any new network resources.


Step 1: Create a Data Source for the Existing Resource Group

data "azurerm_resource_group" "rg_shared" {
  name = "shared-network-rg"
}

Line-by-line explanation:

  • data "azurerm_resource_group"
    Tells Terraform this is a data source, not a resource
  • "rg_shared"
    Local name to reference this data source
  • name = "shared-network-rg"
    The exact name of the existing Resource Group in Azure

This lets us read:

  • Location
  • ID
  • Name
    From the existing resource group.

Step 2: Create a Data Source for the Existing Virtual Network

data "azurerm_virtual_network" "vnet_shared" {
  name                = "shared-network-vnet"
  resource_group_name = data.azurerm_resource_group.rg_shared.name
}

Explanation:

  • name
    Name of the existing VNet
  • resource_group_name
    We do not hardcode it
    We reuse it from the previous data source:
data.azurerm_resource_group.rg_shared.name

This creates a dependency chain:

  • First read Resource Group
  • Then read VNet from that Resource Group

Step 3: Create a Data Source for the Existing Subnet

data "azurerm_subnet" "subnet_shared" {
  name                 = "shared-primary-sn"
  resource_group_name  = data.azurerm_resource_group.rg_shared.name
  virtual_network_name = data.azurerm_virtual_network.vnet_shared.name
}

Explanation:

  • name
    Name of the existing subnet
  • resource_group_name
    Taken from the Resource Group data source
  • virtual_network_name
    Taken from the VNet data source

Now Terraform knows exactly:

  • Which subnet
  • In which VNet
  • In which Resource Group

Step 4: Use Data Sources in Your Own Resources

Now we create our own Resource Group, but we align its location with the shared network.

resource "azurerm_resource_group" "example" {
  name     = "day13-rg"
  location = data.azurerm_resource_group.rg_shared.location
}

Why this matters:

  • We are not hardcoding "East US" or "Canada Central"
  • We are reusing the same location as the shared network
  • This avoids region mismatch errors

Step 5: Attach the VM to the Existing Subnet

Inside the network interface configuration:

subnet_id = data.azurerm_subnet.subnet_shared.id

Explanation:

  • data.azurerm_subnet.subnet_shared.id
    Fetches the ID of the existing subnet

This ensures:

  • Terraform does not create a new subnet
  • The VM is placed inside the shared subnet

What Happens When We Run terraform plan?

Terraform shows:

  • It will create:
    • Resource Group
    • Network Interface
    • Virtual Machine
  • It will not create:
    • Virtual Network
    • Subnet

This confirms:

  • Data sources are being used correctly
  • Existing infrastructure is reused

Verifying in Azure Portal

After terraform apply:

  • The new VM appears in your new Resource Group
  • In Networking settings, you can see:
    • Virtual Network: shared-network-vnet
    • Subnet: shared-primary-sn

This proves:

The VM was created in your Resource Group,
but connected to shared infrastructure managed by another team.


Key Takeaways

  • Use data sources when:
    • A resource already exists
    • You are not allowed to recreate it
    • You need to reference it safely
  • Data sources:
    • Read existing resources
    • Do not create or modify them
    • Help enforce enterprise governance
  • Common use cases:
    • Shared VNets and subnets
    • Existing Resource Groups
    • Existing images
    • Existing Key Vaults
    • Existing Load Balancers

This pattern is essential for working in real enterprise Terraform environments.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

TechMilestoneHub

Build Skills, Unlock Milestones

This is a test – edited from front page