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 hyphenslower(...)
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 charactersreplace(..., " ", "")
Removes spaceslower(...)
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-80Port-443Port-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:
lowerreplacesubstr
- Merge maps using:
merge
- Enforce provider naming rules using nested functions
- Convert strings to lists using:
split
- Generate multiple blocks using:
forexpressions- 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:
dev→Standard_D2s_v3prod→Standard_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 lengthlower(...)converts to lowercasestrcontains(...)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 liststoset(...)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 positiveforexpression applies it to every elementmax(... )finds the largest number...expands the list into arguments
Result:
positive_costs→[50, 75, 200, 100]max_cost→200
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 timeformatdate()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 stringsensitive(...)hides it from outputjsondecode(...)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:
containslengthstrcontainsendswith
- Protect secrets with
sensitive - Combine and deduplicate lists with:
concattoset
- Process numbers using:
absmax
- Work with time using:
timestampformatdate
- Safely read and decode files using:
filejsondecode
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
datakeyword - 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 sourcename = "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 VNetresource_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 subnetresource_group_name
Taken from the Resource Group data sourcevirtual_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
- Virtual Network:
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.

Leave a Reply