Terraform Functions: Where Your Infrastructure Gets Superpowers
Source: Dev.to
Introduction
During the AWS 30‑Day Challenge I spent two full days exploring Terraform’s built‑in functions. They turned many long, error‑prone configurations into concise, production‑ready code. Below are the most useful patterns I discovered, each illustrated with a practical example.
Using the Terraform Console
Before editing main.tf, spin up the interactive console to test expressions:
terraform console
> lower("HELLO WORLD")
"hello world"
> max(5, 12, 9)
12
> split(",", "80,443,8080")
[
"80",
"443",
"8080",
]
> reverse(["a", "b", "c"])
[
"c",
"b",
"a",
]
The console lets you verify logic without any deployment.
1. Normalizing Resource Names
Problem – AWS resources often require lowercase, hyphenated names.
Solution
locals {
clean_name = lower(replace(var.project_name, " ", "-"))
}
resource "aws_resourcegroups_group" "project" {
name = local.clean_name
}
lower() and replace() are essential for consistent naming.
2. Sanitizing S3 Bucket Names
Problem – S3 bucket names must be lowercase, contain no underscores or special characters, and be ≤ 63 characters.
Solution
locals {
sanitized_bucket_name = substr(
lower(
replace(
replace(var.bucket_name_input, "_", "-"),
"/[^a-z0-9-]/", ""
)
),
0,
63
)
}
Nested functions let you replace underscores, strip invalid characters, lowercase, and truncate—all in one expression.
3. Generating Security‑Group Rules from a CSV
Problem – Turning a comma‑separated list of ports into individual ingress blocks.
Solution
variable "allowed_ports" {
default = "80,443,8080"
}
locals {
ports_list = split(",", var.allowed_ports)
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = local.ports_list
content {
from_port = tonumber(ingress.value)
to_port = tonumber(ingress.value)
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
split() converts the string to a list, which drives a dynamic block.
4. Selecting Instance Types per Environment
Problem – Different environments need different instance sizes.
Solution
variable "environment" {
default = "dev"
}
locals {
instance_types = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
selected_type = lookup(local.instance_types, var.environment, "t3.micro")
}
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = local.selected_type
}
lookup() provides map‑based conditional logic with a safe fallback.
5. Validating Instance‑Type Patterns
Problem – Enforce that instance types follow a company naming convention (e.g., t2.* or t3.*).
Solution
variable "instance_type" {
type = string
validation {
condition = can(regex("^(t2|t3)\\.", var.instance_type))
error_message = "Instance type must start with 't2.' or 't3.'"
}
}
regex() checks the pattern, while can() safely returns false instead of erroring.
6. Naming and Secret Handling
Problem – Ensure backup names follow a convention and keep passwords secret.
Solution
variable "backup_name" {
type = string
validation {
condition = endswith(var.backup_name, "-backup")
error_message = "Backup name must end with '-backup'"
}
}
variable "db_password" {
type = string
sensitive = true
}
output "backup_info" {
value = {
name = var.backup_name
password = sensitive(var.db_password)
}
sensitive = true
}
endswith()/startswith() validate strings, and sensitive() prevents secret leakage.
7. Cost Calculations
Problem – Compute total monthly cost, apply credits, and find the highest single expense.
Solution
variable "monthly_costs" {
default = [120.50, 85.00, 200.75, 50.00]
}
variable "credit" {
default = -50.00 # Negative credit
}
locals {
total_cost = sum(var.monthly_costs)
actual_credit = abs(var.credit)
final_cost = max(local.total_cost - local.actual_credit, 0)
highest_cost = max(var.monthly_costs...)
}
output "cost_summary" {
value = {
total_before_credit = local.total_cost
credit_applied = local.actual_credit
final_bill = local.final_cost
highest_single_cost = local.highest_cost
}
}
sum(), abs(), max(), and the splat operator (...) make arithmetic straightforward.
8. Timestamp Formatting
Problem – Generate consistent timestamps for tags and resource names.
Solution
locals {
current_time = timestamp()
formatted_tags = {
CreatedAt = formatdate("YYYY-MM-DD hh:mm:ss ZZZ", local.current_time)
BackupDate = formatdate("YYYY-MM-DD", local.current_time)
Year = formatdate("YYYY", local.current_time)
}
}
resource "aws_s3_bucket" "backups" {
bucket = "backups-${formatdate("YYYY-MM-DD", timestamp())}"
tags = local.formatted_tags
}
timestamp() returns UTC time, and formatdate() lets you shape it as needed.
9. Reading JSON Configs and Storing Secrets
Problem – Load a JSON file with database credentials and store them in AWS Secrets Manager.
Solution
locals {
# Verify the file exists
config_exists = fileexists("${path.module}/config/database.json")
# Read and decode JSON (only if the file is present)
db_config = jsondecode(
file("${path.module}/config/database.json")
)
}
resource "aws_secretsmanager_secret" "db" {
name = "db-credentials"
}
resource "aws_secretsmanager_secret_version" "db_version" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode(local.db_config)
}