Practical scenario: Burrito Barn application deployed on AWS (network, EC2, Secrets Manager, S3)
Table of Contents
- Overview
- Module 1 — Input Variables
- Module 2 — Data Types
- Module 3 — Local Values
- Module 4 — Output Values
- Flow Diagrams
- Reference Tables
- Best Practices and Common Pitfalls
1. Overview
Terraform relies on a structured information model in three layers:
| Layer | Terraform Construct | Function Analogy |
|---|---|---|
| Input | variable (input variable) | Parameter / argument |
| Internal | locals (local value) | Local variable |
| Output | output (output value) | Return value |
Input variables make a configuration reusable and dynamic. Local values avoid repetition and enable data transformations. Output values expose information to the terminal, parent modules, and other configurations via terraform_remote_state.
2. Module 1 — Input Variables
Variable Syntax
variable "<NAME>" {
description = "..."
type = <TYPE>
default = <VALUE>
sensitive = false
nullable = true
validation {
condition = <BOOLEAN_EXPRESSION>
error_message = "Error message shown to the user."
}
}
Reference in configuration: var.<NAME>
Available Arguments
| Argument | Required | Description |
|---|---|---|
description | Recommended | Automatic documentation (terraform-docs, Terraform Registry) |
type | Recommended | Data type validation; error if incompatible |
default | Optional | Makes variable optional; if absent, variable is required |
sensitive | Optional | Hides value in logs and plan; false by default |
nullable | Optional | Allows null as a valid value; true by default |
validation | Optional | Custom validation rule with error message |
Ways to Provide a Value
Terraform accepts variable values from multiple sources, applied in a strict precedence order.
1. Default value (default)
variable "region" {
type = string
default = "us-east-1"
}
2. terraform.tfvars file (auto-loaded)
# terraform.tfvars
region = "us-west-2"
instance_type = "t3.micro"
3. *.auto.tfvars file (auto-loaded)
# production.auto.tfvars
environment = "prod"
4. -var-file CLI option
terraform plan -var-file="values.tfvars"
5. -var CLI option
terraform plan -var "instance_type=t3.nano" -var "region=eu-west-1"
6. TF_VAR_<name> environment variable
# Bash / Zsh
export TF_VAR_api_key="MySecretAPIKey"
# PowerShell
$env:TF_VAR_api_key = "MySecretAPIKey"
7. Interactive prompt (last resort)
If no value is provided and there is no default, Terraform prompts the user at plan or apply time.
Variable Precedence
Rule: the source with the highest priority overrides all others.
flowchart TD
A[default in variable block\nLowest priority] --> B[Environment variable\nTF_VAR_name]
B --> C[terraform.tfvars\nor terraform.tfvars.json]
C --> D[*.auto.tfvars files\nor *.auto.tfvars.json\nalphabetical order]
D --> E[-var-file\nCLI option]
E --> F[-var\nCLI option\nHighest priority]
style A fill:#f9f,stroke:#333
style F fill:#9f9,stroke:#333
Burrito Barn Demo — Module 1
Refactored variables with type, description, validation:
# variables.tf — m1_solution
variable "api_key" {
description = "API key to be stored in Secrets Manager."
type = string
sensitive = true
}
variable "environment" {
description = "Deployment environment (e.g., dev, prod)."
type = string
default = "dev"
}
variable "instance_type" {
description = "Instance type for the EC2 instance."
type = string
}
variable "region" {
description = "AWS Region to deploy resources in."
type = string
default = "us-west-2"
}
variable "sg_port_number" {
description = "Port number for the security group."
type = number
default = 80
}
Passing api_key value via environment variable (PowerShell):
$env:TF_VAR_api_key = "MySecretAPIKey"
terraform plan
Referencing a variable in a resource:
resource "aws_instance" "web" {
instance_type = var.instance_type
user_data = templatefile("${path.module}/templates/user_data.sh", {
environment = var.environment
})
}
Passing a value to a child module:
# In the root module (main.tf)
module "storage" {
source = "./modules/s3_bucket"
bucket_prefix = var.bucket_prefix
}
3. Module 2 — Data Types
Primitive Types
| Type | Description | Example |
|---|---|---|
string | Sequence of Unicode characters | "us-east-1" |
number | Integer or decimal | 80, 3.14 |
bool | Boolean | true, false |
Collection Types
All elements of a collection type must be of the same data type.
| Type | Description | Example |
|---|---|---|
list(<TYPE>) | Ordered group of same-type elements | ["10.0.0.0/24", "10.0.1.0/24"] |
set(<TYPE>) | Unordered group of unique same-type elements | toset(["a", "b", "c"]) |
map(<TYPE>) | Key-value pairs with unique keys, same-type values | { subnet1 = "10.0.0.0/24" } |
Structural Types
Structural types allow mixing data types.
| Type | Description |
|---|---|
tuple([<TYPE>, ...]) | Ordered group of elements of different types; exact structure required |
object({ <KEY> = <TYPE>, ... }) | Key-value pairs with different types per key |
Type Expressions
# Primitives
variable "region" {
type = string
}
variable "instance_count" {
type = number
}
variable "create_nat_gateway" {
type = bool
}
# Collections
variable "allowed_ports" {
type = list(number) # [80, 443, 8080]
}
variable "subnet_cidrs" {
type = map(list(string))
# {
# public = ["10.0.0.0/24", "10.0.1.0/24"]
# private = ["10.0.2.0/24", "10.0.3.0/24"]
# }
}
# Tuple
variable "mixed_data" {
type = tuple([string, list(number), bool])
# ["us-east-1", [80, 443], true]
}
# Object
variable "vpc_config" {
type = object({
name = string
subnets = map(string)
create_nat_gateway = bool
})
}
Burrito Barn Demo — Module 2
vpc_network_info variable of type object:
variable "vpc_network_info" {
description = "Values for VPC module."
type = object({
vpc_cidr = string
public_subnets = map(string) # subnet name => CIDR
})
}
Value in terraform.tfvars:
vpc_network_info = {
vpc_cidr = "10.0.0.0/16"
public_subnets = {
subnet1 = "10.0.0.0/24"
subnet2 = "10.0.1.0/24"
}
}
4. Module 3 — Local Values
Why Use locals?
- Avoid repetition (DRY) — define a value once, reference it everywhere.
- Complex data transformations — isolate transformation logic in a
localsblock rather than inlining it in each resource.
Locals Syntax
locals {
# Simple value
naming_prefix = "${var.project}-${var.environment}"
# Dynamically constructed map
default_tags = merge(var.tags, {
Environment = var.environment
Project = var.project
})
# List
environments = ["dev", "staging", "prod"]
# Inline object
subnet_properties = {
cidr = "10.0.0.0/24"
az = "us-east-1a"
public = true
}
}
Reference in configuration:
resource "aws_vpc" "main" {
tags = local.default_tags
}
resource "aws_instance" "web" {
tags = {
Name = "${local.naming_prefix}-web"
}
}
Useful Transformation Functions
| Function | Description | Example |
|---|---|---|
merge(map1, map2, ...) | Merge multiple maps; last map’s keys win | merge(var.tags, { Env = "prod" }) |
lower(str) | Convert to lowercase | lower("BurritoBarn") → "burritobarn" |
upper(str) | Convert to uppercase | upper("dev") → "DEV" |
format(fmt, ...) | String formatting | format("%s-%s", var.project, var.env) |
join(delim, list) | Join list elements with separator | join("-", ["a","b"]) → "a-b" |
split(delim, str) | Split a string | split(",", "a,b,c") → ["a","b","c"] |
toset(list) | Convert list to set (deduplicates) | toset(["a","a","b"]) → {"a","b"} |
tolist(set) | Convert set to list | tolist(toset(["b","a"])) |
keys(map) | Return map keys as a list | keys({ a=1, b=2 }) → ["a","b"] |
values(map) | Return map values as a list | values({ a=1, b=2 }) → [1,2] |
length(val) | Length of string, list, map, or set | length(["a","b"]) → 2 |
lookup(map, key, default) | Look up key in map with default | lookup(var.tags, "Env", "dev") |
contains(list, val) | Check if list contains element | contains(["dev","prod"], var.env) |
slice(list, start, end) | Sublist from start (inclusive) to end (exclusive) | slice(azs, 0, 2) |
templatefile(path, vars) | Render template with variable substitution | templatefile("user_data.sh", { env = var.env }) |
nonsensitive(val) | Remove sensitive marker to allow display | nonsensitive(data.aws_ssm_parameter.amzn2.value) |
Burrito Barn Demo — Module 3
# locals.tf — m3_solution / m4_solution
locals {
# Default tags merged with user-provided ones
default_tags = merge(var.tags, {
Environment = var.environment
Project = var.project
})
# Common naming prefix for all resources
naming_prefix = "${var.project}-${var.environment}"
}
5. Module 4 — Output Values
Why Use outputs?
| Use Case | Description |
|---|---|
| Terminal display | After terraform apply, root module outputs are displayed and stored in state |
| Parent ↔ child module communication | Parent accesses child data via module.<name>.<output> |
| Cross-configuration sharing | Via terraform_remote_state, one config can read another’s outputs |
Useful commands:
terraform output # Display all outputs
terraform output ec2_public_ip # Display a specific output
terraform output -json # JSON output for automation
terraform output -raw ec2_public_ip # Raw output (string only)
Output Syntax
output "<NAME>" {
description = "..."
value = <EXPRESSION>
sensitive = false
ephemeral = false
depends_on = [<RESOURCE_OR_MODULE>]
}
Output Arguments
| Argument | Required | Description |
|---|---|---|
value | Yes | The expression whose value will be exposed |
description | Recommended | Documentation for terraform-docs and Terraform Registry |
sensitive | Optional | Hides value in terminal (false by default); stored in plain text in state |
ephemeral | Optional | Don’t write to state; only for child modules (false by default) |
depends_on | Optional | Explicit dependency when Terraform cannot infer it automatically |
Important:
sensitive = truein a root module output hides the terminal display but the value is still written to state. Protect access to the state file.
Output Use Cases
1. Display information in terminal
output "ec2_public_ip" {
description = "The public IP address of the EC2 instance."
value = aws_instance.web.public_ip
}
2. Expose child module data to parent module
# In modules/s3_bucket/outputs.tf
output "bucket_id" {
description = "The ID of the S3 bucket."
value = aws_s3_bucket.bucket.id
}
# In root module (main.tf or outputs.tf)
output "bucket_info" {
value = {
id = module.storage.bucket_id
}
}
3. Sensitive output
output "vpc_id" {
description = "The ID of the VPC."
value = module.networking.vpc_id
sensitive = true
}
4. Composite output (map/object)
output "bucket_info" {
description = "Information about the S3 bucket."
value = {
arn = module.storage.bucket_arn
id = module.storage.bucket_id
policy = module.storage.bucket_policy
}
}
6. Flow Diagrams
Global Flow: Variables → Locals → Outputs
flowchart LR
subgraph Sources["Value Sources"]
D[default]
ENV[TF_VAR_*\nenv vars]
TFVARS[terraform.tfvars\n*.auto.tfvars]
CLI[-var / -var-file]
PROMPT[Interactive prompt]
end
subgraph RootModule["Root Module"]
IV[Input Variables\nvar.*]
DS[Data Sources\ndata.*]
RES[Resources\nresource.*]
LOC[Local Values\nlocal.*]
OUT[Output Values\noutput.*]
end
subgraph ChildModule["Child Module"]
CIV[Input Variables]
CRES[Resources]
COUT[Output Values]
end
subgraph Consumers["Consumers"]
TERM[Terminal / CLI]
PARENT[Parent Module\nmodule.name.output]
REMOTE[Remote State\nterraform_remote_state]
end
Sources --> IV
IV --> LOC
IV --> RES
IV --> CIV
DS --> LOC
DS --> RES
LOC --> RES
LOC --> OUT
RES --> OUT
CIV --> CRES
CRES --> COUT
COUT --> OUT
OUT --> TERM
OUT --> PARENT
OUT --> REMOTE
style IV fill:#ddf,stroke:#33f
style LOC fill:#ffd,stroke:#fa0
style OUT fill:#dfd,stroke:#080
7. Reference Tables
Variable Types Table
| Category | Type | Example |
|---|---|---|
| Primitive | string | "us-east-1" |
| Primitive | number | 80, 3.14 |
| Primitive | bool | true, false |
| Collection | list(string) | ["a", "b", "c"] |
| Collection | set(string) | toset(["a", "b"]) |
| Collection | map(string) | { key = "value" } |
| Structural | tuple([string, number]) | ["us-east-1", 3] |
| Structural | object({ name = string }) | { name = "app", count = 2 } |
| Any | any | No type validation |
Value Source Precedence Table
| Priority | Source | How to Set |
|---|---|---|
| 1 (lowest) | default | In variable block |
| 2 | Environment variable | TF_VAR_name=value |
| 3 | terraform.tfvars | Key-value file |
| 4 | *.auto.tfvars | Alphabetical order |
| 5 | -var-file | terraform plan -var-file=file.tfvars |
| 6 (highest) | -var | terraform plan -var "name=value" |
8. Best Practices and Common Pitfalls
Best Practices
| Practice | Why |
|---|---|
Always specify type and description | Automatic validation + generated documentation |
| Sort variables and outputs alphabetically | Consistency, easier navigation |
Use sensitive = true for secrets | Avoids accidental exposure in CI/CD logs |
Don’t put secrets in committed terraform.tfvars | Risk of leaking in version control |
Use TF_VAR_ or a vault for secrets | Production security |
Define default only if reasonably universal | Avoids surprises in production |
Use locals to avoid repetition | DRY — one change updates the entire module |
Add description to each output | terraform-docs and Terraform Registry documentation |
Mark sensitive outputs with sensitive = true | Prevents accidental terminal display |
Use depends_on on outputs only when necessary | Implicit dependency graph is preferred |
Common Pitfalls
| Pitfall | Cause | Solution |
|---|---|---|
| Required variable without value → Terraform blocks | No value source available, no default | Provide value via -var, TF_VAR_, or .tfvars |
| Sensitive output visible in state | sensitive = true hides display, not state | Protect access to Terraform backend (e.g., S3 + encryption) |
Conflict between auto.tfvars | Two auto.tfvars files define the same variable | Alphabetical order applies; last one wins |
| Type mismatch | Variable typed number but string value passed | Check consistency between .tfvars value and type |
Circular reference in locals | Local A references local B which references A | Refactor; Terraform returns a clear error |
ephemeral = true on root module output | Root outputs are by definition written to state | Use ephemeral only in child modules |
Recommended File Structure
module/
├── main.tf # Main resources
├── variables.tf # All variable blocks (alphabetical order)
├── outputs.tf # All output blocks (alphabetical order)
├── locals.tf # All locals blocks
├── terraform.tf # required_providers, terraform block
└── terraform.tfvars # Values (DO NOT commit if contains secrets)
Search Terms
terraform · variables · outputs · infrastructure · ci/cd · devops · output · types · variable · barn · burrito · locals · syntax · value · arguments · auto-loaded · cli · data · flow · option · pitfalls · precedence · values