Using variables, data sources, locals, and interpolation to replace hard-coded values so Terraform configurations are reusable, adaptable, and safe across environments, accounts, and regions
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.
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.
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
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 expressiongreeting = "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 environmentname = "server-${data.aws_region.current.name}-${var.environment}"# Example bucket name that includes account ID for global uniquenessbucket_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 type
Use case
Example
string
Names, ARNs, single values
variable "app" { type = string }
number
Sizing, counts, ports
variable "instance_count" { type = number }
bool
Feature toggles
variable "enable_monitoring" { type = bool }
list
Ordered collections (subnets, zones)
variable "subnets" { type = list(string) }
map
Key/value pairs for tags or mappings
variable "tags" { type = map(string) }
object
Structured compound inputs
variable "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.