Skip to main content

Command Palette

Search for a command to run...

Day 15: Built-in Functions in Terraform Part-II

Updated
12 min read
Day 15: Built-in Functions in Terraform Part-II

Today, we’ll explore Terraform functions — powerful helpers that make your infrastructure more flexible and smart.

Terraform functions allow you to manipulate data, perform calculations, handle text, work with networks, and much more — all inside your configuration files.

🎯 Today’s Goals

By the end of this lesson, you’ll be able to:

  • Master essential Terraform functions

  • Use encoding and cryptographic functions

  • Work with date/time and filesystem functions

  • Apply functions to real-world scenarios


🔐 Encoding Functions

Terraform’s encoding functions convert data between different formats so you can use it across APIs, files, and configurations.
These are extremely useful when you:

  • pass data to AWS user_data or templates

  • store structured info in strings

  • interact with APIs that expect Base64, JSON, or YAML


🧩 1. base64encode() and base64decode()

📘 Description

These convert text into Base64 format and back again.

  • base64encode(string) → encodes plain text

  • base64decode(base64_string) → decodes back to original text

Base64 encoding is commonly used in:

  • AWS EC2 user_data scripts

  • Secrets stored in S3 or external systems

  • API payloads

🧠 Example

locals {
  original = "Hello, World"
  encoded  = base64encode(local.original)
  decoded  = base64decode(local.encoded)
}

🧾 Output

encoded = "SGVsbG8sIFdvcmxkCg=="
decoded = "Hello, World"

💡 Real-world use case

Encoding a startup script for an EC2 instance:

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  user_data     = base64encode(file("${path.module}/scripts/startup.sh"))
}

→ AWS expects Base64 input, so this function is perfect.


💾 2. jsonencode() and jsondecode()

📘 Description

Terraform natively uses HCL (HashiCorp Configuration Language),
but many cloud APIs and tools (like AWS Lambda, Datadog, Kubernetes) require JSON input.

  • jsonencode(map or list) → converts Terraform data into JSON string

  • jsondecode(json_string) → parses JSON back into Terraform data types

🧠 Example

locals {
  data = {
    name    = "Terraform"
    version = 1.6
  }

  json_string = jsonencode(local.data)
  parsed      = jsondecode(local.json_string)
}

🧾 Output

json_string = "{\"name\":\"Terraform\",\"version\":1.6}"
parsed      = { name = "Terraform", version = 1.6 }

💡 Real-world use case

You can store configuration data in S3 or pass it to an API in JSON format:

resource "aws_s3_object" "config" {
  bucket = "my-config-bucket"
  key    = "app_config.json"
  content = jsonencode({
    env     = "production"
    version = "1.0.0"
    enabled = true
  })
}

This automatically converts the Terraform map into a JSON file inside S3.


🧱 3. yamlencode() and yamldecode()

📘 Description

YAML is widely used for configuration files (like Kubernetes manifests or Ansible playbooks).
Terraform can convert its internal data structures to YAML and back.

  • yamlencode() → Converts Terraform data → YAML string

  • yamldecode() → Parses YAML string → Terraform data

🧠 Example

locals {
  data = {
    app = "web"
    port = 8080
  }

  yaml_string = yamlencode(local.data)
  yaml_parsed = yamldecode(local.yaml_string)
}

🧾 Output

yaml_string:
app: web
port: 8080
yaml_parsed: { app = "web", port = 8080 }

💡 Real-world use case

Generate a Kubernetes deployment file dynamically:

resource "local_file" "k8s_manifest" {
  content = yamlencode({
    apiVersion = "v1"
    kind       = "Pod"
    metadata = { name = "nginx" }
    spec = {
      containers = [{
        name  = "nginx"
        image = "nginx:latest"
      }]
    }
  })
  filename = "${path.module}/nginx.yaml"
}

📋 4. csvdecode()

📘 Description

Parses a CSV-formatted string into a list of maps.
This is helpful when reading structured data from CSV files like user lists, IPs, or configuration tables.

🧠 Example

locals {
  csv_data = csvdecode("name,age\nAlice,30\nBob,25")
}

🧾 Output

[
  { name = "Alice", age = "30" },
  { name = "Bob",   age = "25" }
]

💡 Real-world use case

Imagine you have a CSV file of instance names and sizes:

name,type
web,t2.micro
db,t3.micro

You can load and use it:

locals {
  instances = csvdecode(file("${path.module}/instances.csv"))
}

resource "aws_instance" "csv_based" {
  for_each = { for row in local.instances : row.name => row }

  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = each.value.type
  tags = {
    Name = each.value.name
  }
}

✅ Automatically creates EC2 instances from your CSV!


🔑 Cryptographic Functions

These functions deal with data integrity, security, and uniqueness.
They are especially important when:

  • creating hashed passwords

  • verifying file content integrity

  • generating stable resource names


🔹 1. md5()

📘 Description

Generates a 128-bit MD5 hash of a string (commonly used for file integrity).

md5("Terraform") # => "e5bdf6e80e63f03d7a26dd9a88a234f0"

💡 Example use:

locals {
  checksum = md5(file("${path.module}/app.zip"))
}

→ Useful for detecting when a file changes.


🔹 2. sha1(), sha256(), sha512()

📘 Description

These are stronger cryptographic hash functions —
they generate longer, more secure digests than MD5.

FunctionHash LengthTypical Use
sha1()160-bitDeprecated but still used for simple checks
sha256()256-bitMost common for data integrity
sha512()512-bitFor maximum security

🧠 Example

locals {
  data = "Hello, Terraform!"
  sha1_value   = sha1(local.data)
  sha256_value = sha256(local.data)
  sha512_value = sha512(local.data)
}

🔹 3. bcrypt()

📘 Description

Generates a secure hash for passwords using the bcrypt algorithm.
Unlike SHA, bcrypt hashes are salted — even if the same password is hashed twice, the result is different.

🧠 Example

locals {
  password = "supersecret"
  bcrypt_hash = bcrypt(local.password)
}

Output → $2a$10$Jd2P8y... (random each time)

💡 Common in user provisioning:

resource "aws_iam_user_login_profile" "user" {
  user    = "admin"
  pgp_key = "keybase:exampleuser"
  password_reset_required = false
  password = bcrypt("MySecurePassword123")
}

🧠 Summary

FunctionPurposeExample Use
base64encode / decodeConvert text to/from Base64EC2 user_data
jsonencode / decodeConvert between Terraform maps and JSONStore configs in S3
urlencodePrepare strings for API URLsAPI queries
yamlencode / decodeWork with YAML config filesKubernetes manifests
csvdecodeParse CSV into list of mapsBulk instance creation
md5 / sha1 / sha256 / sha512Create hashes for integrity/securityVerify file changes
bcryptSecure password hashingIAM user creation

📅 Date and Time Functions

These are used to manage timestamps — such as for naming resources with deployment time, scheduling, auditing, or versioning.


🕒 1. timestamp()

Purpose:
Returns the current UTC timestamp in ISO 8601 format (e.g., "2025-10-22T08:00:00Z").

locals {
  now = timestamp()
}

Example Output:

"2025-10-22T08:00:00Z"

Use Cases:

  • Tagging resources with deployment time.

  • Creating unique names for artifacts.

  • Logging when a deployment occurred.

tags = {
  CreatedAt = timestamp()
}

🗓️ 2. formatdate(format, timestamp)

Purpose:
Formats a timestamp into a human-readable string using custom format tokens.

locals {
  formatted1 = formatdate("DD MMM YYYY hh:mm:ss", timestamp())
  formatted2 = formatdate("YYYY-MM-DD", timestamp())
}

Output:

"22 Oct 2025 14:30:00"
"2025-10-22"

Use Cases:

  • Version tagging (e.g., v2025-10-22)

  • Logging or backup filenames

  • Human-readable labels in outputs

output "build_version" {
  value = "v${formatdate("YYYYMMDD-hhmm", timestamp())}"
}

⏩ 3. timeadd(timestamp, duration)

Purpose:
Adds a duration to a timestamp (supports seconds, minutes, hours, days, weeks).

locals {
  now        = timestamp()
  tomorrow   = timeadd(local.now, "24h")
  next_week  = timeadd(local.now, "168h") # 7 days
}

Use Cases:

  • Setting expiry times (e.g., certificate expiry, temporary access)

  • Scheduling delayed actions


📁 Filesystem Functions

These allow Terraform to interact with files on your local filesystem — reading, checking, finding, or rendering templates.


📄 1. file(path)

Purpose:
Reads the contents of a file as a string.

locals {
  script_content = file("${path.module}/scripts/init.sh")
}

Use Cases:

  • Load shell scripts, config files, or policies to inject into resources.

  • Pass user_data to EC2 or container startup scripts.


✅ 2. fileexists(path)

Purpose:
Checks whether a file exists and returns true or false.

locals {
  has_config = fileexists("${path.module}/config.yaml")
}

Use Cases:

  • Conditional logic to include optional files.

  • Avoid Terraform errors when optional inputs are missing.


📂 3. fileset(directory, pattern)

Purpose:
Returns a list of filenames in a directory matching a pattern (e.g., all .sh files).

locals {
  all_scripts = fileset(path.module, "scripts/*.sh")
}

Use Cases:

  • Automatically include all scripts or templates in a directory.

  • Create multiple resources (e.g., upload all scripts to S3).


🧩 4. templatefile(path, vars)

Purpose:
Reads a file and replaces placeholders with variable values.
Placeholders use the ${var_name} syntax.

locals {
  user_data = templatefile("${path.module}/templates/user-data.sh", {
    hostname = "web-server"
    port     = 8080
  })
}

If user-data.sh contains:

#!/bin/bash
echo "Starting ${hostname} on port ${port}"

Output:

#!/bin/bash
echo "Starting web-server on port 8080"

Use Cases:

  • Dynamically generate scripts, configuration files, or manifest templates.

🧭 5. Path Utility Functions

FunctionDescriptionExampleOutput
dirname(path)Returns the directory portiondirname("/opt/data/file.txt")/opt/data
basename(path)Returns filenamebasename("/opt/data/file.txt")file.txt
abspath(path)Converts relative path to absoluteabspath("./relative")/home/user/project/relative
pathexpand(path)Expands ~ to home directorypathexpand("~/data")/home/username/data

Use Cases:

  • Managing file paths in reusable modules.

  • Normalizing paths across systems.


🌐 IP Network Functions

These functions help manipulate and calculate IP network addresses — crucial for VPC, subnets, and CIDR management in AWS, Azure, or GCP.


🌍 1. cidrhost(cidr, hostnum)

Purpose:
Returns a specific host IP address from a given subnet.

locals {
  vpc_cidr = "10.0.0.0/16"
  first_ip = cidrhost(local.vpc_cidr, 0)
  second_ip = cidrhost(local.vpc_cidr, 1)
}

Output:

"10.0.0.0"
"10.0.0.1"

Use Cases:

  • Assign static IPs in subnets.

  • Generate predictable host IPs.


🧱 2. cidrnetmask(cidr)

Purpose:
Returns the netmask of a CIDR block.

cidrnetmask("10.0.0.0/16") # => "255.255.0.0"

Use Case: Documentation or tagging (helps identify subnet size easily).


🧩 3. cidrsubnet(cidr, newbits, netnum)

Purpose:
Subdivides a larger CIDR into smaller subnet blocks.

locals {
  subnet1 = cidrsubnet("10.0.0.0/16", 8, 0) # 10.0.0.0/24
  subnet2 = cidrsubnet("10.0.0.0/16", 8, 1) # 10.0.1.0/24
}

Use Cases:

  • Create multiple subnets dynamically for different environments or AZs.

🧮 4. cidrsubnets(cidr, newbits...)

Purpose:
Generates multiple subnets at once from one parent network.

locals {
  subnets = cidrsubnets("10.0.0.0/16", 8, 8, 8)
}

Output:

["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]

Use Cases:

  • Automatically allocate subnets for each application layer.

🧪 Hands-On Lab

📁project structure

terraform-day15/
├── main.tf
├── variables.tf
├── locals.tf
├── outputs.tf
├── scripts/
│   └── init.sh
├── templates/
│   └── user-data.sh
└── config.yaml

This lab uses Date/Time, Filesystem, IP Network, and Type Conversion functions all together.


🧠 Scenario

We’ll simulate deploying a web server into a generated subnet inside a VPC.
Terraform will:

  • Calculate subnets using IP functions. 🧮

  • Generate timestamps and version tags using Date/Time functions ⏱️

  • Read files and templates using Filesystem functions 📁

  • Use Type Conversion functions for safe, flexible data handling 🔄


1️⃣ variables.tf

variable "project_name" {
  description = "Project name for tagging and resource naming"
  type        = string
  default     = "terraform-lab"
}

variable "region" {
  description = "AWS region to deploy to"
  type        = string
  default     = "us-east-1"
}

variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

variable "enable_public_access" {
  description = "Allow public access to the instance?"
  type        = string
  default     = "true" # as string for demonstration of tobool()
}

2️⃣ locals.tf

locals {
  # -------------------------
  # 📅 Date & Time Functions
  # -------------------------
  current_time   = timestamp()
  formatted_time = formatdate("YYYY-MM-DD hh:mm:ss", local.current_time)
  build_version  = formatdate("YYYYMMDD-hhmm", local.current_time)
  expire_time    = timeadd(local.current_time, "168h") # +7 days

  # -------------------------
  # 🌐 IP Network Functions
  # -------------------------
  vpc_cidr  = var.vpc_cidr
  subnet1   = cidrsubnet(local.vpc_cidr, 8, 0) # "10.0.0.0/24"
  subnet2   = cidrsubnet(local.vpc_cidr, 8, 1) # "10.0.1.0/24"
  first_ip  = cidrhost(local.subnet1, 10)
  netmask   = cidrnetmask(local.vpc_cidr)

  # -------------------------
  # 📁 Filesystem Functions
  # -------------------------
  script_content = file("${path.module}/scripts/init.sh")
  has_config     = fileexists("${path.module}/config.yaml")
  all_scripts    = fileset(path.module, "scripts/*.sh")

  # Render a user-data template with variables
  user_data = templatefile("${path.module}/templates/user-data.sh", {
    hostname = "${var.project_name}-server"
    port     = 8080
  })

  # -------------------------
  # 🔄 Type Conversion Functions
  # -------------------------
  is_public   = tobool(var.enable_public_access)
  number_test = try(tonumber("123"), 0)
  is_number   = can(tonumber("abc")) # false

  # Convert set → list → string for display
  tags_set  = toset(["app", "terraform", "infra", "app"])
  tags_list = tolist(local.tags_set)
  tags_str  = tostring(join(",", local.tags_list))
}

3️⃣ main.tf

(This simulates deployment logic — no real AWS resources needed, you can run this locally and observe outputs.)

terraform {
  required_version = ">= 1.6.0"
}

provider "aws" {
  region = var.region
}

# Example local resource to simulate usage of all functions
resource "null_resource" "example" {
  triggers = {
    build_version = local.build_version
    subnet        = local.subnet1
    netmask       = local.netmask
    has_config    = tostring(local.has_config)
    tags          = local.tags_str
  }

  provisioner "local-exec" {
    command = "echo '🚀 Deploying ${var.project_name} version ${local.build_version}'"
  }
}

# Save rendered user-data to file for demonstration
resource "local_file" "rendered_user_data" {
  content  = local.user_data
  filename = "${path.module}/rendered-user-data.sh"
}

4️⃣ outputs.tf

output "📅_date_time" {
  value = {
    now          = local.current_time
    formatted    = local.formatted_time
    version_tag  = local.build_version
    expires_at   = local.expire_time
  }
}

output "🌐_network_details" {
  value = {
    vpc_cidr  = local.vpc_cidr
    subnet1   = local.subnet1
    subnet2   = local.subnet2
    first_ip  = local.first_ip
    netmask   = local.netmask
  }
}

output "📁_filesystem_info" {
  value = {
    config_exists = local.has_config
    scripts_found = local.all_scripts
    user_data     = local.user_data
  }
}

output "🔄_type_conversions" {
  value = {
    is_public   = local.is_public
    number_test = local.number_test
    is_number   = local.is_number
    tags        = local.tags_str
  }
}

5️⃣ scripts/init.sh

#!/bin/bash
echo "Initializing system..."
echo "Date: $(date)"

6️⃣ templates/user-data.sh

#!/bin/bash
echo "🚀 Launching ${hostname}"
echo "Listening on port ${port}"

7️⃣ config.yaml

project: terraform-lab
environment: dev
owner: example@example.com

Run

# Initialize
terraform init
# Use console to test functions
terraform console
# Try these:
> cidrsubnet("10.0.0.0/16", 8, 0)
"10.0.0.0/24"
> cidrhost("10.0.1.0/24", 5)
"10.0.1.5"
> format("%s-%03d", "web", 5)
"web-005"
> timestamp()
"2025-10-22T09:38:22Z"
> formatdate("DD-MM-YYYY", timestamp())
"22-10-2025"
> md5("terraform")
"1b1ed905d54c18e3dd8828986c14be17"
> range(5)
tolist([
  0,
  1,
  2,
  3,
  4,
])
> exit
# Apply
terraform apply -auto-approve
# View outputs
terraform output

#Output look like this 
null_resource.example: Creating...
local_file.rendered_user_data: Creating...
null_resource.example: Provisioning with 'local-exec'...
local_file.rendered_user_data: Creation complete after 0s [id=2b67ae43a0bde8eaf87b28a86c7c9b9bf94cd3d5]
null_resource.example (local-exec): Executing: ["/bin/sh" "-c" "echo '🚀 Deploying terraform-lab version 20251022-0940'"]
null_resource.example (local-exec): 🚀 Deploying terraform-lab version 20251022-0940
null_resource.example: Creation complete after 0s [id=6127148458059718850]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:
date_time = {
  "expires_at" = "2025-10-29T09:40:25Z"
  "formatted" = "2025-10-22 09:40:25"
  "now" = "2025-10-22T09:40:25Z"
  "version_tag" = "20251022-0940"
}
filesystem_info = {
  "config_exists" = true
  "scripts_found" = toset([
    "scripts/init.sh",
  ])
  "user_data" = <<-EOT
  #!/bin/bash
  echo "🚀 Launching terraform-lab-server"
  echo "Listening on port 8080"
  EOT
}
network_details = {
  "first_ip" = "10.0.0.10"
  "netmask" = "255.255.0.0"
  "subnet1" = "10.0.0.0/24"
  "subnet2" = "10.0.1.0/24"
  "vpc_cidr" = "10.0.0.0/16"
}
type_conversions = {
  "is_number" = false
  "is_public" = true
  "number_test" = 123
  "tags" = "app,infra,terraform"
}

# Clean up
terraform destroy -auto-approve

📝 Summary

Today you learned:

  • ✅ Encoding functions (base64, json, yaml)

  • ✅ Crypto functions (md5, sha256, bcrypt)

  • ✅ Date/time functions (timestamp, formatdate)

  • ✅ Network functions (cidrsubnet, cidrhost)

  • ✅ Filesystem functions (file, templatefile)

🚀 Tomorrow’s Preview

Day 16: Dynamic Blocks for Flexible Resources

Tomorrow we’ll:

  • Master dynamic blocks

  • Create flexible security groups

  • Build dynamic ingress/egress rules

  • Use for_each in dynamic blocks

  • Handle complex nested blocks


← Day 14: Conditionals & Logic | Day 16: Dynamic Blocks →


Remember: Functions are powerful tools for data transformation and manipulation!

More from this blog

S

StackOps - Diary

33 posts

Welcome to the StackOps - Diary. We’re dedicated to empowering the tech community. We delve into cloud-native and microservices technologies, sharing knowledge to build modern, scalable solutions.