Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Terraform Built-in Functions: Zero-Fluff, Hands-On Guide**
Source: https://www.fatskills.com/cloud-application-developer/chapter/tech-terraform-built-in-functions-zero-fluff-hands-on-guide

TECH **Terraform Built-in Functions: Zero-Fluff, Hands-On Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~8 min read

Terraform Built-in Functions: Zero-Fluff, Hands-On Guide

(file, lookup, element, merge, jsonencode/decode)


1. What This Is & Why It Matters

You’re a cloud engineer maintaining a Terraform codebase for a multi-environment (dev/stage/prod) AWS deployment. Your team just inherited a legacy module where hardcoded values are scattered across files, and secrets are accidentally committed to Git. Worse, the same VPC CIDR block is duplicated in three places—meaning a typo in one file breaks everything.

Built-in functions are your escape hatch. They let you: - Dynamically read files (file) instead of hardcoding secrets or configs.
- Safely fetch values from maps (lookup) without crashing if a key is missing.
- Slice and dice lists (element) to pick the right subnet or AMI.
- Merge configurations (merge) to combine dev/stage/prod settings without copy-pasting.
- Encode/decode JSON (jsonencode/jsondecode) to pass structured data to APIs (e.g., IAM policies, Lambda environment variables).

What breaks if you ignore this?
- Security risks: Hardcoded secrets in .tf files get leaked.
- Brittle code: A single typo in a duplicated value (e.g., us-east-1a vs us-east-1b) breaks deployments.
- Wasted time: Manually updating the same value in 10 places.

Superpower: You’ll write DRY (Don’t Repeat Yourself), dynamic, and self-healing Terraform code that adapts to environments without manual edits.


2. Core Concepts & Components


file(path)

  • Definition: Reads the contents of a file at path and returns it as a string.
  • Production insight: Use this to load external configs (e.g., user_data scripts, IAM policies) without hardcoding them in .tf files. ⚠️ Never commit files with secrets—use sops or AWS Secrets Manager instead.

lookup(map, key, [default])

  • Definition: Fetches the value for key in map. Returns default (or null) if the key doesn’t exist.
  • Production insight: Critical for environment-specific configs (e.g., lookup(var.instance_sizes, var.environment, "t3.micro")). Prevents crashes when a key is missing.

element(list, index)

  • Definition: Returns the item at index in list. Wraps around if index is out of bounds (e.g., element(["a", "b"], 2) returns "a").
  • Production insight: Useful for round-robin resource distribution (e.g., picking subnets or AZs). Avoids hardcoding indices.

merge(map1, map2, ...)

  • Definition: Combines multiple maps into one. Later maps overwrite earlier ones if keys collide.
  • Production insight: Merge base configs with environment overrides (e.g., merge(local.common_tags, { Environment = "prod" })). Keeps code DRY.

jsonencode(value) / jsondecode(string)

  • Definition:
  • jsonencode: Converts a Terraform value to a JSON string.
  • jsondecode: Parses a JSON string into a Terraform value.
  • Production insight: Required for APIs expecting JSON (e.g., IAM policies, Lambda env vars). jsonencode ensures valid JSON syntax.


3. Step-by-Step Hands-On


Prerequisites

  • Terraform installed (>= 1.0.0).
  • AWS CLI configured with credentials.
  • A text editor (VS Code, Vim, etc.).

Task: Deploy an EC2 Instance with Dynamic Configs

You’ll: 1. Load user_data from an external file.
2. Use lookup to pick an instance type based on environment.
3. Use element to select a subnet from a list.
4. Merge tags with environment-specific overrides.
5. Pass a JSON IAM policy to the instance.


Step 1: Set Up the Project

mkdir tf-functions-demo && cd tf-functions-demo
touch main.tf variables.tf outputs.tf user_data.sh


Step 2: Write user_data.sh

#!/bin/bash
echo "Hello from $(hostname)" > /tmp/hello.txt


Step 3: Define Variables (variables.tf)

variable "environment" {
  description = "Deployment environment (dev/stage/prod)"
  type        = string
  default     = "dev"
}

variable "instance_sizes" {
  description = "Map of instance sizes per environment"
  type        = map(string)
  default = {
dev = "t3.micro"
stage = "t3.small"
prod = "m5.large" } } variable "subnets" { description = "List of subnet IDs" type = list(string) default = ["subnet-12345678", "subnet-87654321"] # Replace with real subnet IDs } variable "common_tags" { description = "Tags applied to all resources" type = map(string) default = {
Terraform = "true"
Owner = "team-infra" } }


Step 4: Deploy the EC2 Instance (main.tf)

provider "aws" {
  region = "us-east-1"
}

# Load user_data from file
data "template_file" "user_data" {
  template = file("${path.module}/user_data.sh")
}

# Lookup instance size based on environment
locals {
  instance_size = lookup(var.instance_sizes, var.environment, "t3.micro")
}

# Pick a subnet using element (round-robin)
locals {
  subnet_id = element(var.subnets, 0) # Change index to distribute across subnets
}

# Merge common tags with environment-specific tags
locals {
  tags = merge(var.common_tags, {
Environment = var.environment }) } # Create IAM policy as JSON locals { iam_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:ListBucket"]
Resource = ["arn:aws:s3:::example-bucket"]
}
] }) } # Deploy EC2 instance resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2 (us-east-1) instance_type = local.instance_size subnet_id = local.subnet_id user_data = data.template_file.user_data.rendered tags = local.tags # Attach IAM policy (simplified for demo) iam_instance_profile = aws_iam_instance_profile.example.name } # IAM role and profile (simplified) resource "aws_iam_role" "example" { name = "ec2-role-${var.environment}" assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}
] }) } resource "aws_iam_role_policy" "example" { name = "ec2-policy-${var.environment}" role = aws_iam_role.example.id policy = local.iam_policy } resource "aws_iam_instance_profile" "example" { name = "ec2-profile-${var.environment}" role = aws_iam_role.example.name }


Step 5: Deploy and Verify

terraform init
terraform plan -var="environment=stage" # Test with stage environment
terraform apply -var="environment=stage" -auto-approve

Verify success:
1. Check the EC2 instance in the AWS Console:
- Tags should include Environment = "stage".
- Instance type should be t3.small (for stage).
2. SSH into the instance and check /tmp/hello.txt:
bash
ssh -i your-key.pem ec2-user@<public-ip>
cat /tmp/hello.txt


4. ? Production-Ready Best Practices


Security

  • Never hardcode secrets: Use file + sops or AWS Secrets Manager.
  • Least privilege IAM policies: Use jsonencode to generate minimal policies.
  • Validate inputs: Use can(lookup(...)) to check if a key exists before using it.

Cost Optimization

  • Dynamic instance sizing: Use lookup to pick instance types based on environment (e.g., t3.micro for dev, m5.large for prod).
  • Spot instances: Combine element with aws_instance’s spot_price to distribute across AZs.

Reliability & Maintainability

  • DRY configs: Use merge to combine base and environment-specific settings.
  • Round-robin resource distribution: Use element to cycle through subnets/AZs.
  • Immutable infrastructure: Use jsonencode to generate unique resource names (e.g., name = "ec2-${md5(jsonencode(var.tags))}").

Observability

  • Tag everything: Use merge to apply consistent tags (e.g., Environment, Owner).
  • Log user_data output: Add echo "User data executed at $(date)" >> /var/log/user-data.log to user_data.sh.


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Hardcoding secrets in .tf files Secrets leaked in Git history Use file + sops or AWS Secrets Manager.
Forgetting default in lookup Terraform crashes if key is missing Always provide a default: lookup(var.map, "key", "default_value").
Using element with empty lists Terraform error: "index out of range" Check list length first: length(var.list) > 0 ? element(var.list, 0) : null.
Merging maps with conflicting keys Later maps silently overwrite earlier ones Document key collisions and use merge intentionally.
Invalid JSON in jsonencode AWS API rejects the policy Test JSON with jq: echo '{"key": "value"}' | jq ..


6. ? Exam/Certification Focus


Typical Question Patterns

  1. lookup vs try:
  2. Question: "How do you safely fetch a value from a map that might not exist?"
  3. Trap: try(var.map.key, "default") works, but lookup is more explicit.
  4. Answer: Use lookup(var.map, "key", "default").

  5. element vs list[index]:

  6. Question: "How do you pick the 3rd subnet from a list without crashing if the list has only 2 items?"
  7. Trap: var.subnets[2] crashes if the list is too short.
  8. Answer: element(var.subnets, 2) wraps around.

  9. jsonencode for IAM policies:

  10. Question: "How do you pass a JSON policy to an IAM role?"
  11. Trap: Hardcoding JSON strings leads to syntax errors.
  12. Answer: Use jsonencode({ Version = "2012-10-17", Statement = [...] }).

  13. merge for tags:

  14. Question: "How do you combine base tags with environment-specific tags?"
  15. Trap: Copy-pasting tags leads to drift.
  16. Answer: merge(local.common_tags, { Environment = var.env }).

7. ? Hands-On Challenge

Challenge: Write a Terraform config that: 1. Reads a JSON file (config.json) with environment-specific settings (e.g., {"dev": {"instance_type": "t3.micro"}}).
2. Uses jsondecode to parse the file.
3. Uses lookup to fetch the instance type for the current environment (var.environment).
4. Deploys an EC2 instance with the correct type.

Solution:


locals {
  config = jsondecode(file("${path.module}/config.json"))
  instance_type = lookup(local.config[var.environment], "instance_type", "t3.micro")
}

resource "aws_instance" "challenge" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = local.instance_type
}

Why it works: - jsondecode parses the JSON file into a Terraform map.
- lookup safely fetches the instance type, defaulting to t3.micro if missing.


8. ? Rapid-Reference Crib Sheet

Function Example Use Case ⚠️ Trap
file(path) file("user_data.sh") Load external scripts/configs. Never commit files with secrets.
lookup(map, key) lookup(var.sizes, "prod", "m5.large") Fetch values from maps. Always provide a default.
element(list, i) element(var.subnets, 0) Pick items from lists (wraps around). Fails if list is empty.
merge(map1, map2) merge(local.tags, { Env = "prod" }) Combine maps (later keys overwrite). Silent overwrites.
jsonencode(val) jsonencode({ Key = "value" }) Convert Terraform to JSON. Invalid JSON breaks APIs.
jsondecode(str) jsondecode(file("config.json")) Parse JSON into Terraform. Fails if JSON is malformed.


9. ? Where to Go Next

  1. Terraform Docs: Built-in Functions – Official reference.
  2. Terraform Best Practices: DRY Configs – Real-world patterns.
  3. AWS IAM Policy JSON Reference – For jsonencode examples.
  4. SOPS: Secrets Management – Secure alternative to file for secrets.


ADVERTISEMENT