Advanced

Terraform Deep Dive

Enterprise Terraform — importing resources, remote state, CI/CD, multi-environment and sensitive data.

Advanced course by Ned Bellavance (Ned in the Cloud, HashiCorp Ambassador) Exercises: github.com/ned1313/Deep-Dive-Terraform


Table of Contents

  1. Overview and Prerequisites
  2. Working with Existing Resources — Import
  3. Team Collaboration — State and Remote Backend
  4. CI/CD Pipeline with Terraform
  5. Supporting Multiple Environments
  6. Connecting Multiple Configurations
  7. Configuration Management with Ansible
  8. Managing Sensitive Data
  9. Reference — Terraform Functions
  10. Reference — Meta-arguments
  11. Reference — Terraform Environment Variables
  12. Advanced HCL Patterns

1. Overview and Prerequisites

Course Positioning

This course targets those who already have Terraform basics and want to tackle enterprise use cases: importing existing resources, remote state, CI/CD pipelines, multi-environment management, sharing information between configurations, configuration management, and securing sensitive data.

Training Global Architecture

flowchart TD
    A[Terraform Deep Dive] --> B[Importing existing resources]
    A --> C[Remote State & Collaboration]
    A --> D[CI/CD Pipeline]
    A --> E[Multi-environments]
    A --> F[Connecting configurations]
    A --> G[Configuration Management]
    A --> H[Sensitive data]

    B --> B1[terraform import command]
    B --> B2[import block HCL]

    C --> C1[Terraform Cloud]
    C --> C2[State backends]
    C --> C3[terraform state commands]

    D --> D1[GitHub Actions]
    D --> D2[Terraform Cloud VCS workflow]
    D --> D3[Pull Requests & Speculative Plans]

    E --> E1[OSS Workspaces]
    E --> E2[Separate Directories]
    E --> E3[Git Branches]
    E --> E4[Separate Repositories]

    F --> F1[terraform_remote_state]
    F --> F2[tfe_outputs data source]

    G --> G1[Ansible Playbooks]
    G --> G2[user_data]
    G --> G3[terraform_data resource]

    H --> H1[AWS Secrets Manager]
    H --> H2[SSM Parameter Store]
    H --> H3[sensitive argument]

Technical Prerequisites

SkillRequired Level
Terraform CLI (plan, apply, destroy)Mastery
HCL — variables, outputs, resources, data sourcesMastery
Provider pluginsKnowledge
AWS (VPC, EC2, IAM)Basic knowledge
Git / GitHubBasic knowledge

Technologies Used in the Course

  • AWS — primary cloud provider (VPC, EC2, Security Groups, Secrets Manager, SSM)
  • GitHub — source control for Terraform code
  • Terraform Cloud — remote state, remote execution, workspaces, CI/CD
  • Ansible — configuration management for EC2 instances

2. Working with Existing Resources — Import

Context — Brownfield vs Greenfield

In practice, introducing Terraform into an existing environment is the norm (brownfield). Already-deployed resources must be brought into Terraform management without destroying or recreating them.

Globomantics Environment Architecture

flowchart TB
    subgraph vpc["VPC us-east-1 — 10.42.0.0/16"]
        igw[Internet Gateway]
        rt[Route Table]
        sg["Security Group NoIngress"]
        subgraph az1["AZ us-east-1a"]
            subnet1["Public Subnet 10.42.10.0/24"]
        end
        subgraph az2["AZ us-east-1b"]
            subnet2["Public Subnet 10.42.11.0/24"]
        end
        igw --> rt
        rt --> subnet1
        rt --> subnet2
    end

The terraform import Command

# Basic syntax
terraform import <ADDR> <ID>

# Example — import an AWS VPC
terraform import aws_vpc.main vpc-0a1b2c3d4e5f

# Import a resource into a module
terraform import module.main.aws_vpc.this[0] vpc-0a1b2c3d4e5f

Import command process:

  1. Terraform updates the state with the resource at the specified address
  2. It does not generate the resource block in the code — you must write it yourself
  3. You run terraform plan to verify no changes are required
  4. If changes appear, you fix the code until No changes

The import Block (Terraform 1.5+)

The import block is the declarative, modern approach introduced in Terraform 1.5.

# imports.tf

# Import the VPC
import {
  to = module.main.aws_vpc.this[0]
  id = "vpc-0a1b2c3d4e5f"
}

# Import subnets
import {
  to = module.main.aws_subnet.public[0]
  id = "subnet-0a1b2c3d"
}

import {
  to = module.main.aws_subnet.public[1]
  id = "subnet-0e5f6g7h"
}

# Import a security group (outside module)
import {
  to = aws_security_group.no_ingress
  id = "sg-0a1b2c3d"
}

Automatic configuration code generation:

# Terraform generates the missing resource blocks in generated.tf
terraform plan -generate-config-out=generated.tf

# Once satisfied with the result, apply the import
terraform apply

import command vs import block Comparison

Criterionterraform importimport block
VersionAll versionsTerraform 1.5+
Execution planNoYes
Code generationNoYes (-generate-config-out)
ReversibleNoYes (remove the block)
IdempotentNoYes
VCS managementNoYes
Multi-resourceOne per commandMultiple blocks

Usage in a Module

# resources.tf — using the vpc module from the public registry
module "main" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name             = "globo-dev"
  cidr             = var.cidr_block
  azs              = data.aws_availability_zones.available.names
  public_subnets   = [for k, v in var.public_subnets : v.cidr_block]
  
  tags = local.common_tags
}

# Resource outside module
resource "aws_security_group" "no_ingress" {
  name        = "no-ingress-sg"
  description = "Security group that blocks all ingress"
  vpc_id      = module.main.vpc_id

  tags = local.common_tags
}

3. Team Collaboration — State and Remote Backend

Terraform State Anatomy

The Terraform state is a JSON file that links configuration to real resources. It contains:

{
  "version": 4,
  "terraform_version": "1.5.0",
  "resources": [
    {
      "module": "module.main",
      "mode": "managed",
      "type": "aws_vpc",
      "name": "this",
      "instances": [
        {
          "index_key": 0,
          "attributes": {
            "id": "vpc-0a1b2c3d",
            "cidr_block": "10.42.0.0/16",
            "tags": { "Environment": "globo-dev" }
          }
        }
      ]
    }
  ],
  "outputs": {
    "vpc_id": {
      "value": "vpc-0a1b2c3d",
      "type": "string"
    }
  }
}

State Inspection Commands

# List all resources in the state
terraform state list

# Show details of a resource
terraform state show module.main.aws_vpc.this[0]

# Export the complete state as JSON
terraform state pull

# Move a resource (rename)
terraform state mv aws_instance.old aws_instance.new

# Remove a resource from state without destroying the real resource
terraform state rm aws_instance.obsolete

Deprecated Commands and Their Replacements

Deprecated commandRecommended replacement
terraform refreshterraform plan -refresh-only / terraform apply -refresh-only
terraform taint <resource>terraform apply -replace="<resource>"
terraform untaint <resource>Modify the code, terraform apply
terraform import <addr> <id>import {} block in HCL

Backends — Comparison

flowchart LR
    subgraph local["Local Backend"]
        direction TB
        L1[terraform.tfstate]
        L2[terraform.tfstate.backup]
    end

    subgraph remote["Remote Backends"]
        direction TB
        R1[Terraform Cloud / Enterprise]
        R2[AWS S3 + DynamoDB]
        R3[Azure Blob Storage]
        R4[GCS]
        R5[Consul]
    end

    subgraph features["Features"]
        direction TB
        F1[State Locking]
        F2[Workspaces]
        F3[Encryption]
        F4[Access Control]
    end

    R1 -- "✅ All" --> features
    R2 -- "✅ Locking via DynamoDB" --> features
    local -- "❌ None" --> features

Backend Configuration — Syntax

# backend.tf — Classic S3 backend (with DynamoDB locking)
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}
# backend.tf — Terraform Cloud with the cloud block (Terraform 1.2+)
terraform {
  cloud {
    organization = "deep-dive-globo"

    workspaces {
      name = "web-network-dev"
      # Or use tags for multiple workspaces:
      # tags = ["network", "development"]
    }
  }
}

Important: The backend block and cloud block do not support variable interpolation. All values must be literals or passed via partial configuration at terraform init time.

Partial Configuration — Dynamic Backend

# Provide values at init time (useful for pipelines)
terraform init \
  -backend-config="bucket=my-state-bucket" \
  -backend-config="key=prod/terraform.tfstate" \
  -backend-config="region=us-east-1"

Remote State — Collaboration Architecture

sequenceDiagram
    participant Dev1 as Developer 1
    participant Dev2 as Developer 2
    participant TFC as Terraform Cloud
    participant AWS as AWS

    Dev1->>TFC: terraform plan (acquires lock)
    TFC->>AWS: Refresh state
    AWS-->>TFC: Current attributes
    TFC-->>Dev1: Plan (proposed changes)
    
    Dev2->>TFC: terraform plan (while Dev1 is active)
    TFC-->>Dev2: ❌ Error: State locked by Dev1
    
    Dev1->>TFC: terraform apply (confirms)
    TFC->>AWS: Apply changes
    AWS-->>TFC: Confirmation
    TFC->>TFC: Release lock
    TFC-->>Dev1: ✅ Apply complete
    
    Dev2->>TFC: terraform plan (retry, lock free)
    TFC-->>Dev2: ✅ Plan available

4. CI/CD Pipeline with Terraform

Infrastructure as Code Principles in a Pipeline

When Terraform manages production workloads, software development principles apply:

  1. Source Control — all code in Git / GitHub
  2. Continuous Integration (CI) — automatic validation on each commit
  3. Continuous Delivery (CD) — automatic application after approval

Terraform Automation Considerations

# .gitignore — Files to exclude from version control
.terraform/
terraform.tfstate
terraform.tfstate.backup
*.tfvars          # May contain secrets
crash.log
override.tf
override.tf.json

Exception: The .terraform.lock.hcl file (provider lock file) must be committed to ensure provider version consistency across all users and build servers.

Environment Variables for Automation

# Tell Terraform it's in an automation context
export TF_IN_AUTOMATION=true

# Disable interactive prompts (pipeline cannot respond)
export TF_INPUT=false

# Logging level
export TF_LOG=INFO                  # ERROR | WARN | INFO | DEBUG | TRACE
export TF_LOG_PROVIDER=ERROR        # Separate provider plugin logging
export TF_LOG_PATH=/tmp/terraform.log

# Plugin cache to speed up builds
export TF_PLUGIN_CACHE_DIR=/home/runner/.terraform.d/plugins

# Terraform variables directly as env vars (TF_VAR_ prefix)
export TF_VAR_region="us-east-1"
export TF_VAR_billing_code="8675309"

GitHub Actions — CI Workflow for Terraform

# .github/workflows/terraform.yml
name: Terraform CI

on:
  push:
    branches: ["**"]

env:
  TF_LOG: INFO
  TF_INPUT: false

jobs:
  terraform:
    name: Terraform Validate & Format
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: "~1.5"

      - name: Terraform Init
        run: terraform init -backend=false   # No backend for CI only

      - name: Terraform Format Check
        id: fmt
        run: terraform fmt -check -recursive
        continue-on-error: true

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color
        if: success() || failure()          # Runs even if fmt failed

Complete CI/CD Workflow with Terraform Cloud VCS

sequenceDiagram
    participant Dev as Developer
    participant GH as GitHub
    participant GHA as GitHub Actions (CI)
    participant TFC as Terraform Cloud (CD)
    participant AWS as AWS

    Dev->>Dev: git checkout -b feature/add-subnet
    Dev->>Dev: Modifies HCL code
    Dev->>GH: git push origin feature/add-subnet
    
    GH->>GHA: Trigger push event
    GHA->>GHA: terraform fmt -check
    GHA->>GHA: terraform validate
    GHA-->>GH: ✅ CI Passed

    Dev->>GH: Creates Pull Request (feature → development)
    GH->>TFC: Trigger Speculative Plan
    TFC->>AWS: Refresh state
    AWS-->>TFC: Current state
    TFC-->>GH: Plan displayed in PR (read-only)
    
    Note over Dev,GH: Team reviews the plan
    
    Dev->>GH: Merge Pull Request
    GH->>TFC: Trigger Standard Run
    TFC->>TFC: terraform plan
    TFC-->>Dev: ⏸️ Awaiting manual approval
    Dev->>TFC: Approves the plan
    TFC->>AWS: terraform apply
    AWS-->>TFC: Resources created/modified
    TFC-->>Dev: ✅ Apply complete

5. Supporting Multiple Environments

The Four Approaches Compared

quadrantChart
    title Terraform Multi-Environment Approaches
    x-axis "Low operational complexity" --> "High operational complexity"
    y-axis "Low isolation" --> "High isolation"
    quadrant-1 "Maximum separation"
    quadrant-2 "Strong isolation, heavy management"
    quadrant-3 "Simple but risky"
    quadrant-4 "Good balance"
    OSS Workspaces: [0.15, 0.25]
    Separate Directories: [0.45, 0.65]
    Git Branches: [0.55, 0.70]
    Separate Repositories: [0.85, 0.85]

OSS Workspaces — State Isolation

# Create and navigate between workspaces
terraform workspace list
terraform workspace new development
terraform workspace select development
terraform workspace show           # Shows the current workspace
# Use terraform.workspace in code
locals {
  env_name = terraform.workspace
  
  # Different configuration per environment
  instance_type = {
    default     = "t3.micro"
    development = "t3.small"
    staging     = "t3.medium"
    production  = "t3.large"
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.instance_type[terraform.workspace]

  tags = {
    Name        = "${terraform.workspace}-web"
    Environment = terraform.workspace
    Workspace   = terraform.workspace
  }
}

Workspace Isolation — Terraform Cloud vs OSS

FeatureOSS WorkspacesTerraform Cloud Workspaces
Separate state
Separate backend❌ (shared)✅ (isolated per workspace)
Access control✅ (RBAC)
VCS integration✅ (branch-based)
Per-workspace variables
Per-workspace credentials
Recommended use caseShort-lived testsLong-term environments

Separate Directories Approach

network_config/
├── environments/
│   ├── development/
│   │   ├── main.tf           → calls the network module
│   │   ├── variables.tf
│   │   ├── terraform.tfvars  → dev-specific values
│   │   └── backend.tf        → S3 backend dev bucket
│   ├── staging/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── production/
│       ├── main.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── modules/
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf
# environments/development/terraform.tfvars
region       = "us-east-1"
environment  = "development"
billing_code = "DEV-001"
cidr_block   = "10.42.0.0/16"

public_subnets = {
  "public-1" = { cidr_block = "10.42.10.0/24" }
  "public-2" = { cidr_block = "10.42.11.0/24" }
}

Promoting Changes Between Branches

# 1. Develop on a feature branch
git checkout main
git pull
git checkout -b feature/add-third-subnet

# 2. Modify code
# ... HCL modifications ...
terraform fmt
terraform validate

# 3. Commit and push
git add .
git commit -m "Add third public subnet and billing_code tag"
git push -u origin feature/add-third-subnet

# 4. PR feature → development (triggers CI + Speculative Plan)
# 5. Merge after approval (triggers Apply in the dev workspace)

# 6. Promotion dev → staging
git checkout staging
git pull
# Create PR development → staging in GitHub

# 7. Promotion staging → production
# Create PR staging → production in GitHub

# 8. Sync main branch (source of truth)
# PR production → main

6. Connecting Multiple Configurations

Why Separate Configurations?

  • Complexity — fewer resources per configuration = faster refresh
  • Blast radius — a change cannot impact all infrastructure
  • Separation of responsibilities — network team, security, application

Information Sharing Options

flowchart LR
    subgraph net["Network Configuration"]
        NO[Outputs:\nvpc_id\npublic_subnets]
    end

    subgraph options["Sharing Mechanisms"]
        O1[terraform_remote_state\ndata source]
        O2[tfe_outputs\ndata source]
        O3[AWS SSM\nParameter Store]
        O4[AWS Route 53\nTXT Records]
        O5[Manual variables]
    end

    subgraph app["Application Configuration"]
        AI[Inputs used]
    end

    net --> O1 --> app
    net --> O2 --> app
    net --> O3 --> app
    net --> O4 --> app
    net --> O5 --> app

    style O2 fill:#00c853,color:#fff
    style O1 fill:#ff6d00,color:#fff

terraform_remote_state — Full State Access

# Access the complete state of the network configuration
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "globo-terraform-state"
    key    = "network/development/terraform.tfstate"
    region = "us-east-1"
  }
}

# Usage
resource "aws_instance" "web" {
  subnet_id = data.terraform_remote_state.network.outputs.public_subnets[0]
  vpc_security_group_ids = [
    data.terraform_remote_state.network.outputs.security_group_id
  ]
}

⚠️ Risk: This approach exposes the entire state (including sensitive data) to the calling configuration. Not recommended if teams don’t share the same access level.

tfe_outputs — Limited Access to Outputs (Terraform Cloud)

# terraform.tf — Add the TFE provider
terraform {
  required_providers {
    tfe = {
      source  = "hashicorp/tfe"
      version = "~> 0.45"
    }
  }
}

# datasources.tf — Retrieve only the exposed outputs
data "tfe_outputs" "networking" {
  organization = var.tfe_organization
  workspace    = var.tfe_workspace_name
}

# Non-sensitive outputs are in nonsensitive_values
locals {
  vpc_id         = data.tfe_outputs.networking.nonsensitive_values.vpc_id
  public_subnets = data.tfe_outputs.networking.nonsensitive_values.public_subnets
}

7. Configuration Management with Ansible

Terraform Limitations for OS Configuration

Terraform operates on a Get → Test → Set cycle via provider APIs. For OS configuration or application deployment, no provider API is available. This is where a configuration management tool comes in.

Configuration Management Tools Comparison

ToolModelAgent requiredLanguageUse case
AnsiblePush / PullNo (SSH)YAMLAd-hoc config, deployment
PuppetPullYesPuppet DSLLarge fleets, compliance
ChefPullYesRuby DSLExisting Ruby environments
SaltStackPush + PullOptionalYAML + PythonLarge scale

user_data + templatefile Pattern

# resources.tf — EC2 instance with Ansible user_data
resource "aws_instance" "web" {
  count = length(local.public_subnets)

  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = var.instance_type
  subnet_id              = local.public_subnets[count.index]
  vpc_security_group_ids = [aws_security_group.web.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2_profile.name

  # Trigger replacement if user_data changes
  user_data_replace_on_change = true

  # templatefile() reads the .tftpl file and substitutes variables
  user_data = templatefile("${path.module}/templates/userdata.tftpl", {
    ansible_repo_url = var.ansible_repo_url
    playbook_path    = "playbooks/webserver.yml"
    environment      = var.environment
  })

  tags = merge(local.common_tags, {
    Name = "${var.prefix}-web-${count.index + 1}"
  })
}
#!/bin/bash
# templates/userdata.tftpl

# Install dependencies
yum install -y git
amazon-linux-extras install ansible2 -y

# Clone the Ansible repository
git clone ${ansible_repo_url} /opt/ansible
cd /opt/ansible

# Run the playbook
ansible-playbook ${playbook_path} \
  --connection=local \
  -e "environment=${environment}"

The terraform_data Resource (Replacement for null_resource)

# OLD APPROACH — null_resource (deprecated)
resource "null_resource" "app_config" {
  triggers = {
    instance_ids = join(",", aws_instance.web[*].id)
  }

  provisioner "remote-exec" {
    # ...
  }
}

# NEW APPROACH — terraform_data (Terraform 1.4+)
resource "terraform_data" "app_config" {
  # triggers_replace is a list (not a map like null_resource)
  triggers_replace = [
    join(",", aws_instance.web[*].private_dns),
    var.site_name
  ]

  # Store arbitrary data in the state
  input = {
    instance_count = length(aws_instance.web)
    updated_at     = timestamp()
  }

  provisioner "local-exec" {
    command = "echo 'Config updated for ${self.input.instance_count} instances'"
  }
}

null_resource vs terraform_data Comparison

Aspectnull_resourceterraform_data
Provider requirednull providerBuilt-in Terraform
triggersmap(string)Arbitrary list
Data storageNoinput argument
StatusDeprecatedActive (Terraform 1.4+)
Plugin to downloadYesNo

8. Managing Sensitive Data

Sensitive Data Lifecycle in Terraform

flowchart LR
    subgraph inputs["Input data"]
        V1[Input Variables\nsensitive = true]
        V2[ENV Variables\nTF_VAR_*]
        V3[.tfvars files\n⚠️ Do not commit]
    end

    subgraph processing["Terraform Processing"]
        P1[HCL Configuration]
        P2[Terraform State\n⚠️ Contains everything\nin plaintext]
        P3[Console Output\nmasked if sensitive]
    end

    subgraph outputs["Outputs"]
        O1[Output Values\nsensitive = true]
        O2[Logs\nmasked if TF_LOG]
    end

    subgraph best["Best Practices"]
        B1[AWS Secrets Manager]
        B2[HashiCorp Vault]
        B3[SSM Parameter Store]
        B4[Machine Identity\nIAM Role]
    end

    inputs --> processing
    processing --> outputs
    best -.->|Prevent Terraform\nfrom touching secrets| processing

The sensitive Argument

# variables.tf — Mark a variable as sensitive
variable "api_key" {
  type        = string
  description = "API key for the SaaS application"
  sensitive   = true    # Masked in console outputs
}

variable "db_password" {
  type      = string
  sensitive = true
}

# outputs.tf — Mark an output as sensitive
output "private_key_pem" {
  value     = tls_private_key.web.private_key_pem
  sensitive = true    # Doesn't display with terraform output
}

# To explicitly display a sensitive output:
# terraform output -raw private_key_pem

⚠️ Critical limitation: sensitive = true does not prevent plaintext storage in Terraform state. It is only visual masking in the console.

sequenceDiagram
    participant TF as Terraform
    participant SM as AWS Secrets Manager
    participant IAM as AWS IAM
    participant EC2 as EC2 Instance

    Note over TF,SM: Setup phase (one time)
    TF->>SM: Creates the secret (only ID transmitted)
    TF->>IAM: Creates IAM Role + Policy (GetSecretValue)
    TF->>EC2: Launches instance with iam_instance_profile
    TF->>EC2: Passes secret_id via input variable (not the value!)

    Note over EC2,SM: Runtime phase (Terraform never sees the value)
    EC2->>SM: GetSecretValue(secret_id)
    SM->>IAM: Verifies EC2 role permissions
    IAM-->>SM: Authorized
    SM-->>EC2: Secret value (encrypted in transit)
    EC2->>EC2: Uses the value locally
# create_secrets_manager/main.tf
resource "aws_secretsmanager_secret" "api_key" {
  name        = "${var.prefix}-api-key"
  description = "API key for the SaaS application"

  tags = var.common_tags
}

resource "aws_secretsmanager_secret_version" "api_key" {
  secret_id     = aws_secretsmanager_secret.api_key.id
  secret_string = var.api_key    # var.api_key is marked sensitive
}

# IAM Role for EC2 instances
resource "aws_iam_role" "ec2_role" {
  name = "${var.prefix}-ec2-role"

  assume_role_policy = data.aws_iam_policy_document.ec2_assume.json
}

output "secret_id" {
  value       = aws_secretsmanager_secret.api_key.id
  description = "Secret ID — to pass as input variable to the application"
  # We pass the ID (not the value) to the application configuration
}

output "ec2_role_name" {
  value = aws_iam_role.ec2_role.name
}

9. Reference — Terraform Functions

String Functions

FunctionSignatureDescriptionExample
formatformat(spec, args...)Format a stringformat("Hello, %s!", var.name)
joinjoin(sep, list)Join a listjoin(",", ["a","b","c"]) → "a,b,c"
splitsplit(sep, str)Split a stringsplit(",", "a,b,c") → ["a","b","c"]
replacereplace(str, sub, rep)Replacereplace("abc", "b", "X") → "aXc"
trimspacetrimspace(str)Remove spacestrimspace(" hello ") → "hello"
lower / upperlower(str)Caselower("HELLO") → "hello"
substrsubstr(str, off, len)Substringsubstr("hello", 1, 3) → "ell"
regexregex(pattern, str)Regexregex("[0-9]+", "abc123")
templatefiletemplatefile(path, vars)File templatesee below

Collection Functions

FunctionDescriptionExample
lengthLength of a list/maplength(var.subnets)
mergeMerge mapsmerge(local.common_tags, {Name="web"})
concatConcatenate listsconcat(list1, list2)
flattenFlatten nested listsflatten([[1,2],[3,4]]) → [1,2,3,4]
distinctRemove duplicatesdistinct(["a","b","a"]) → ["a","b"]
tosetConvert to settoset(["a","b","a"])
tolistConvert to listtolist(var.my_set)
tomapConvert to maptomap({a="1", b="2"})
keysKeys of a mapkeys({a=1, b=2}) → ["a","b"]
valuesValues of a mapvalues({a=1, b=2}) → [1, 2]
lookupValue from a maplookup(var.ami_map, var.region, "default")
zipmapCreate map from two listszipmap(["a","b"], [1, 2]) → {a=1, b=2}
elementElement by index (cyclic)element(["a","b","c"], 5) → "c"
sliceSub-listslice(["a","b","c","d"], 1, 3) → ["b","c"]
indexIndex of an elementindex(["a","b","c"], "b") → 1
containsExistence in a listcontains(["a","b"], "a") → true

Numeric and Conditional Functions

FunctionDescriptionExample
max / minMaximum / Minimummax(1, 5, 3) → 5
ceil / floorRound up / downceil(1.2) → 2
absAbsolute valueabs(-5) → 5
coalesceFirst non-nullcoalesce("", null, "found") → "found"
coalescelistFirst non-emptycoalescelist([], ["a","b"]) → ["a","b"]
tryEvaluate without errortry(var.optional_config.key, "default")
canTest if evaluation possiblecan(var.obj.key) → bool

Type and Encoding Functions

FunctionDescription
jsonencode / jsondecodeJSON serialization
yamlencode / yamldecodeYAML serialization
base64encode / base64decodeBase64 encoding
filebase64Read file and encode to Base64
fileRead a file as string
filemd5MD5 hash of a file
md5 / sha256Hash of a string
tostring / tonumber / toboolType conversion
typeType of a value

10. Reference — Meta-arguments

Meta-arguments Overview

mindmap
  root((Terraform\nMeta-arguments))
    count
      Fixed number of instances
      count.index for the index
    for_each
      Iterate over map or set
      each.key and each.value
    depends_on
      Explicit dependencies
      List of resources
    lifecycle
      create_before_destroy
      prevent_destroy
      ignore_changes
      replace_triggered_by
    provider
      Select a provider
      Provider alias

count — Multiple Instances by Index

# Create N identical instances
resource "aws_instance" "web" {
  count = var.instance_count

  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"
  subnet_id     = var.public_subnets[count.index % length(var.public_subnets)]

  tags = {
    Name  = "${var.prefix}-web-${count.index + 1}"
    Index = count.index
  }
}

# Reference a specific instance
output "first_instance_id" {
  value = aws_instance.web[0].id
}

# Reference all instances (splat expression)
output "all_instance_ids" {
  value = aws_instance.web[*].id
}

for_each — Multiple Instances by Key

# for_each on a map (recommended for stable resources)
variable "public_subnets" {
  type = map(object({
    cidr_block        = string
    availability_zone = string
  }))
  default = {
    "public-1" = { cidr_block = "10.42.10.0/24", availability_zone = "us-east-1a" }
    "public-2" = { cidr_block = "10.42.11.0/24", availability_zone = "us-east-1b" }
    "public-3" = { cidr_block = "10.42.12.0/24", availability_zone = "us-east-1c" }
  }
}

resource "aws_subnet" "public" {
  for_each = var.public_subnets

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone

  tags = {
    Name = "${var.prefix}-${each.key}"
  }
}

# for_each on a set of strings
resource "aws_iam_user" "team" {
  for_each = toset(["alice", "bob", "carol"])

  name = each.key    # each.key == each.value for sets
  path = "/team/"
}

lifecycle — Lifecycle Control

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type

  lifecycle {
    # Create the new instance BEFORE destroying the old one
    # Useful for zero-downtime deployments
    create_before_destroy = true

    # Prevent accidental destruction
    # terraform destroy will return an error
    prevent_destroy = true

    # Ignore changes on certain attributes
    # Useful when an app modifies tags outside of Terraform
    ignore_changes = [
      tags["LastModified"],
      user_data,
    ]

    # Replace this resource if another changes
    replace_triggered_by = [
      terraform_data.app_config.id,
      aws_launch_template.web.latest_version
    ]
  }
}

depends_on — Explicit Dependencies

# depends_on is needed when Terraform cannot automatically detect
# a dependency (e.g.: configuration dependencies, not data)
resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  # Instance must wait for routing and gateway to be ready
  # even if it doesn't directly reference these resources
  depends_on = [
    aws_route.public_internet,
    aws_internet_gateway_attachment.main
  ]
}

provider — Provider Alias for Multi-Region / Multi-Account

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

# Alternative provider with alias
provider "aws" {
  alias  = "eu_west"
  region = "eu-west-1"
}

provider "aws" {
  alias   = "prod_account"
  region  = "us-east-1"
  profile = "production"
}

# Use a specific provider in a resource
resource "aws_instance" "eu_server" {
  provider = aws.eu_west
  ami      = data.aws_ami.amazon_linux_eu.id
  # ...
}

# Pass a provider to a module
module "network_eu" {
  source = "./modules/network"
  
  providers = {
    aws = aws.eu_west
  }
}

11. Reference — Terraform Environment Variables

VariableDescriptionValues
TF_IN_AUTOMATIONAutomation modeAny value
TF_INPUTDisable interactive promptsfalse
TF_LOGTerraform core logging levelERROR, WARN, INFO, DEBUG, TRACE
TF_LOG_PROVIDERProvider logging levelSame
TF_LOG_PATHLog fileAbsolute path to a file
TF_PLUGIN_CACHE_DIRPlugin cachePath to a directory
TF_VAR_<name>Variable valueVariable’s value
TF_CLI_ARGSGlobal CLI argumentse.g.: -no-color
TF_CLI_ARGS_planArguments for terraform plane.g.: -refresh=false
TF_DATA_DIR.terraform directoryAlternative path
TF_WORKSPACEActive workspaceWorkspace name
TF_REGISTRY_DISCOVERY_RETRYRegistry discovery retryNumber

12. Advanced HCL Patterns

Dynamic Blocks

dynamic blocks allow programmatic generation of configuration blocks, avoiding repetition in resources that accept repeated nested blocks.

# Example — Security Group with dynamic rules
variable "ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = [
    { port = 80,  protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTP" },
    { port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTPS" },
    { port = 22,  protocol = "tcp", cidr_blocks = ["10.0.0.0/8"], description = "Internal SSH" },
  ]
}

resource "aws_security_group" "web" {
  name        = "${var.prefix}-web-sg"
  description = "Security group for web servers"
  vpc_id      = var.vpc_id

  # Dynamic block for ingress rules
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = local.common_tags
}

for Expressions and Collection Transformations

locals {
  # for expression on a list → list
  upper_names = [for name in var.names : upper(name)]

  # for expression with condition (filter)
  large_instances = [
    for inst in var.instances : inst
    if inst.instance_type != "t3.micro"
  ]

  # for expression on a map → map
  instance_ids = {
    for k, v in aws_instance.web : k => v.id
  }

  # Transform a list to a map (key = value)
  subnet_map = {
    for subnet in aws_subnet.public : subnet.availability_zone => subnet.id
  }

  # Group by a property (for expression with ellipsis)
  instances_by_az = {
    for inst in aws_instance.web :
    inst.availability_zone => inst.id...
  }
}

Variable Validation

variable "environment" {
  type        = string
  description = "Deployment environment"

  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "Environment must be development, staging, or production."
  }
}

variable "cidr_block" {
  type        = string
  description = "CIDR block for the VPC"

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "Value must be a valid CIDR block (e.g.: 10.0.0.0/16)."
  }
}

variable "instance_count" {
  type        = number
  description = "Number of EC2 instances"

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "Instance count must be between 1 and 10."
  }
}

moved Block — Refactoring Without Destruction

# When renaming a resource, Terraform wants to destroy and recreate it
# The moved block allows clean refactoring

moved {
  from = aws_security_group.no_ingress
  to   = aws_security_group.baseline
}

# Move from root to a module
moved {
  from = aws_vpc.main
  to   = module.network.aws_vpc.this
}

# Move from a count resource to for_each
moved {
  from = aws_instance.web[0]
  to   = aws_instance.web["primary"]
}

Sources and additional resources


Search Terms

terraform · deep · dive · infrastructure · ci/cd · devops · state · configuration · functions · architecture · comparison · import · backend · block · cloud · environment · reference · sensitive · access · automation · collaboration · collection · command · commands

Interested in this course?

Contact us to book it or get a custom training plan for your team.