Skip to main content
Keeping Terraform code reusable means avoiding hard-coded values. This guide explains how to replace static literals with dynamic values using variables, data sources, locals, and interpolation. These patterns make your configurations adaptable across accounts, regions, and environments, reduce repetition, and lower the risk of errors.
Use variables, data sources, and locals to assemble names, tags, and arguments dynamically so a single set of Terraform files can be reused across environments (dev/stage/prod), accounts, and regions.
Why use dynamic values?
  • Reusability: One configuration can handle many environments.
  • Maintainability: Update variable defaults or data lookups instead of editing every resource.
  • Safety: Avoid accidental drift caused by manually changing literal values.
  • Uniqueness: Build unique names (for S3, role names, etc.) programmatically.
Quick recap: variables Variables let you pass values into Terraform modules and root modules. Terraform supports types like string, number, bool, list, map, and object, and variables can be set using defaults, -var/.tfvars files, or environment variables.
The image is a recap of Terraform variables, featuring three sections: dynamic inputs, variable types, and keeping code DRY, each with brief explanations.
Brief reminder: data sources Data sources (data blocks) allow Terraform to read information from providers—such as the latest AMI, the current account ID, or regions—so you can reference existing infrastructure or provider metadata without creating or managing it.
The image is a graphic titled "Data Sources - Reap," explaining how to use data sources in Terraform to pull external information. It highlights provider-specific information, common use cases, and that data sources do not make changes to infrastructure.
Static (anti-pattern) vs. dynamic (recommended) Below is a direct comparison that highlights why dynamic values are preferable. Static example (what to avoid)
resource "aws_instance" "example" {
  ami           = "ami-12345678"
  instance_type = "t2.micro"
}

resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "hardcoded-vpc"
  }
}

resource "github_repository" "example" {
  name        = "my-static-repo"
  description = "A static name"
  visibility  = "public"
}

resource "github_team" "example" {
  name        = "hardcoded-team"
  description = "A team with a name"
}
Dynamic example using data sources, locals, variables, and interpolation
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
}

locals {
  instance_type = "t3.micro"
  env           = "production"
}

variable "app" {
  type    = string
  default = "xyz"
}

resource "aws_instance" "example" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.instance_type

  tags = {
    Name = "${var.app}-${local.env}-server"
  }
}

resource "aws_vpc" "example" {
  cidr_block = var.vpc_cidr

  tags = {
    Name = "vpc-${local.env}-${data.aws_region.current.name}-${var.network}"
  }
}
What’s happening in the dynamic example:
  • The AMI is discovered via data.aws_ami.ubuntu.id instead of a literal AMI ID.
  • A local holds the instance type so the value is defined once and reused.
  • Tags and names are assembled with interpolation to include variables, locals, and data source values.
Interpolation (string templating) Interpolation in Terraform uses the ${...} syntax to evaluate expressions and embed results into strings. Simple interpolation examples
variable "name" {
  type    = string
  default = "Bryan"
}

# Using interpolation in a string expression
greeting = "Hello, ${var.name}!"
bio = "My name is ${var.name}, I'm your instructor!"
# Evaluates to: My name is Bryan, I'm your instructor!
Constructing names and identifiers from data + variables
variable "environment" {
  type    = string
  default = "dev"
}

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

# Example resource name assembled from region and environment
name = "server-${data.aws_region.current.name}-${var.environment}"
# Example bucket name that includes account ID for global uniqueness
bucket_name = "s3-${data.aws_caller_identity.current.account_id}-backups"
# Example evaluated value: s3-123456789012-backups
S3 bucket names must be globally unique across all AWS accounts. Include account IDs, environment prefixes (e.g. dev, prod), or timestamps in bucket names to avoid naming collisions.
Best practices and patterns
  • Use locals for values repeated across resources (instance types, common tags).
  • Use data blocks for provider metadata (AMIs, regions, account IDs).
  • Prefer interpolation or the newer expression forms over hard-coded strings.
  • Validate variable inputs with validation blocks to prevent invalid configurations.
  • For global resources (S3), ensure deterministic uniqueness using account IDs, regions, timestamps, or hashes.
Quick reference: Terraform variable types and common usage
Variable typeUse caseExample
stringNames, ARNs, single valuesvariable "app" { type = string }
numberSizing, counts, portsvariable "instance_count" { type = number }
boolFeature togglesvariable "enable_monitoring" { type = bool }
listOrdered collections (subnets, zones)variable "subnets" { type = list(string) }
mapKey/value pairs for tags or mappingsvariable "tags" { type = map(string) }
objectStructured compound inputsvariable "db_config" { type = object({ engine = string, version = string }) }
Practical next steps
  • Practice by converting an existing static Terraform module into a dynamic one: replace literals with var.*, local.*, and data.*.
  • Create small lab folders for AWS, Azure, and GitHub:
    • Add a README with instructions.
    • Add starter Terraform files and exercises to modify interpolation and data lookups.
  • Experiment with naming patterns to ensure uniqueness across accounts and regions.
Links and references

Watch Video

Practice Lab