Variables, Outputs & Data Sources
Variables, outputs, locals, and data sources make Terraform configurations flexible, composable, and reusable. Input variables parameterize configs; locals compute reusable expressions; outputs expose values to users and parent modules; data sources read existing infrastructure. Mastering the type system, assignment precedence, and sensitive handling is essential for the Terraform Associate exam.
1. Input Variables — The Parameterization Mechanism
Input variables are Terraform's equivalent of function parameters. They make configurations reusable across environments without changing source code.
2. Variable Declaration — All Arguments
Every argument in a variable block is optional except the label (name):
1variable "instance_type" {
2 description = "EC2 instance type for web servers" # shown in docs/UI
3 type = string # type constraint
4 default = "t3.micro" # makes var optional
5 nullable = false # disallow null (Terraform >= 1.1)
6 sensitive = false # true = redact from logs/output
7
8 validation {
9 condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
10 error_message = "Must be t3.micro, t3.small, or t3.medium."
11 }
12}| Argument | Required? | Purpose |
|---|---|---|
description | No | Human-readable documentation |
type | No | Type constraint (defaults to any) |
default | No | Makes variable optional; omit to require value |
nullable | No | Whether null is a valid value (default true) |
sensitive | No | Redact from plan/apply output and state display |
validation block | No | Custom validation rules with error messages |
3. Variable Type System
The type system is a key exam topic. Know all primitive and complex types:
1# Primitive types
2variable "name" { type = string }
3variable "count" { type = number }
4variable "enabled" { type = bool }
5
6# Collection types — all elements must be same type
7variable "azs" { type = list(string) }
8variable "allowed_ports" { type = set(number) }
9variable "tags" { type = map(string) }
10
11# Structural types — elements can be different types
12variable "server_config" {
13 type = object({
14 instance_type = string
15 count = number
16 enable_monitoring = bool
17 tags = map(string)
18 })
19 default = {
20 instance_type = "t3.micro"
21 count = 1
22 enable_monitoring = true
23 tags = {}
24 }
25}
26
27variable "mixed_tuple" {
28 type = tuple([string, number, bool])
29 # Order matters: ["web", 3, true]
30}
31
32# any — no type checking; Terraform infers from the value
33variable "flexible" { type = any }| Type | Example Value | Notes |
|---|---|---|
string | "us-east-1" | UTF-8 text |
number | 42, 3.14 | Integer or float |
bool | true, false | |
list(string) | ["a", "b", "c"] | Ordered, indexed by integer |
set(string) | ["a", "b", "c"] | Unordered, unique elements |
map(string) | {key = "val"} | Key-value, keys are strings |
object({...}) | {name = "web", count = 2} | Named attributes, mixed types |
tuple([...]) | ["web", 2, true] | Ordered, mixed types, fixed length |
any | Anything | No constraint; inferred at runtime |
4. Variable Assignment Precedence
This is one of the most tested topics on the exam. Values are resolved in this order (highest wins):
1# All of these set var.environment:
2
3# 1. CLI flag — highest priority
4terraform apply -var="environment=prod"
5
6# 2. Named var file via flag
7terraform apply -var-file="prod.tfvars"
8
9# 3. Auto-loaded files (no flag needed):
10# - terraform.tfvars
11# - terraform.tfvars.json
12# - *.auto.tfvars
13# - *.auto.tfvars.json
14# (all auto files loaded; later alphabetical order wins if same var)
15
16# 4. Environment variable
17export TF_VAR_environment=prod
18terraform apply
19
20# 5. Default in variable declaration — lowest priority
21variable "environment" { default = "dev" }Key exam facts:
terraform.tfvarsis auto-loaded — no flag needed*.auto.tfvarsare auto-loaded — no flag needed- Custom
.tfvarsfiles (e.g.,prod.tfvars) must be passed with-var-file TF_VAR_prefix must match exactly (case-sensitive on Linux)
5. Sensitive Variables
Mark a variable sensitive = true to prevent Terraform from displaying its value in plan/apply output:
1variable "db_password" {
2 description = "RDS master password"
3 type = string
4 sensitive = true
5}
6
7resource "aws_db_instance" "main" {
8 password = var.db_password # redacted in plan output
9}Important caveats:
- The value IS stored in state (in plaintext by default) — encrypt your state
- If a sensitive variable is used in an output, that output must also be marked
sensitive = true terraform output db_passwordwill show(sensitive value)— use-jsonor-rawto extract
1# Extract sensitive output value (for scripting)
2terraform output -raw db_password
3terraform output -json | jq -r '.db_password.value'6. Local Values
Locals compute reusable expressions — they avoid repeating the same expression multiple times:
1locals {
2 # Computed values
3 env_prefix = "${var.environment}-${var.project}"
4 is_production = var.environment == "prod"
5
6 # Shared tag map (DRY — used across many resources)
7 common_tags = {
8 Environment = var.environment
9 Project = var.project
10 ManagedBy = "terraform"
11 Owner = var.team
12 }
13
14 # Conditional logic
15 instance_type = local.is_production ? "t3.medium" : "t3.micro"
16
17 # List manipulation
18 az_count = length(var.availability_zones)
19}
20
21resource "aws_instance" "web" {
22 instance_type = local.instance_type
23 tags = merge(local.common_tags, { Name = "${local.env_prefix}-web" })
24}Locals vs Variables:
| Aspect | variable | local |
|---|---|---|
| Set by | External caller (CLI, tfvars, env) | Only within the module |
| Purpose | External parameterization | Internal DRY / computed values |
| Reference | var.name | local.name |
| Can reference other locals? | No | Yes (but not circular) |
Shown in terraform plan? | Yes (as inputs) | No |
7. Output Values
Outputs expose values from a configuration — to the user after apply, or to a parent module when used as a child:
1output "instance_public_ip" {
2 description = "Public IP address of the web server"
3 value = aws_instance.web.public_ip
4 sensitive = false
5}
6
7output "db_connection_string" {
8 description = "Database connection string"
9 value = "postgresql://${aws_db_instance.main.endpoint}/${var.db_name}"
10 sensitive = true # required if value contains sensitive data
11}
12
13output "instance_ids" {
14 description = "All web server instance IDs"
15 value = aws_instance.web[*].id # splat expression
16 depends_on = [aws_security_group.web] # explicit dependency (rare)
17}| Argument | Purpose |
|---|---|
description | Documentation for the output |
value | The expression to expose (required) |
sensitive | Redact from display; required if referencing sensitive var |
depends_on | Explicit dependency — rarely needed |
Working with outputs:
1# Show all outputs
2terraform output
3
4# Get specific output (formatted)
5terraform output instance_public_ip
6
7# Get raw value (for scripting)
8terraform output -raw instance_public_ip
9
10# All outputs as JSON
11terraform output -json
12
13# In a child module, outputs are accessed as:
14# module.web_server.instance_public_ip8. Data Sources — Reading Existing Infrastructure
Data sources query existing resources — they do not create anything. They allow Terraform to use values from outside the current configuration:
1# Read latest Ubuntu AMI (external — from AWS)
2data "aws_ami" "ubuntu" {
3 most_recent = true
4 owners = ["099720109477"] # Canonical's AWS account
5
6 filter {
7 name = "name"
8 values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
9 }
10 filter {
11 name = "virtualization-type"
12 values = ["hvm"]
13 }
14}
15
16# Read data managed by another Terraform config (remote state)
17data "terraform_remote_state" "network" {
18 backend = "s3"
19 config = {
20 bucket = "my-terraform-state"
21 key = "network/terraform.tfstate"
22 region = "us-east-1"
23 }
24}
25
26# Read an SSM Parameter (secrets management pattern)
27data "aws_ssm_parameter" "db_password" {
28 name = "/prod/db/password"
29 with_decryption = true
30}
31
32resource "aws_instance" "web" {
33 ami = data.aws_ami.ubuntu.id
34 instance_type = "t3.micro"
35 subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
36}Data source lifecycle:
Data source with depends_on:
1# Data sources are normally read during plan.
2# Use depends_on when the data source depends on a resource
3# that must be created first.
4data "aws_iam_policy_document" "assume_role" {
5 depends_on = [aws_iam_role.lambda] # rare — usually unnecessary
6 statement {
7 effect = "Allow"
8 actions = ["sts:AssumeRole"]
9 principals {
10 type = "Service"
11 identifiers = ["lambda.amazonaws.com"]
12 }
13 }
14}9. Common Data Source Patterns
1# Pattern 1: Lookup existing VPC by tag
2data "aws_vpc" "main" {
3 filter {
4 name = "tag:Name"
5 values = ["production-vpc"]
6 }
7}
8
9# Pattern 2: All subnets in a VPC
10data "aws_subnets" "private" {
11 filter {
12 name = "vpc-id"
13 values = [data.aws_vpc.main.id]
14 }
15 filter {
16 name = "tag:Tier"
17 values = ["private"]
18 }
19}
20
21# Pattern 3: Current AWS account ID and region (no arguments needed)
22data "aws_caller_identity" "current" {}
23data "aws_region" "current" {}
24
25locals {
26 account_id = data.aws_caller_identity.current.account_id
27 region = data.aws_region.current.name
28 arn_prefix = "arn:aws:iam::${local.account_id}"
29}
30
31# Pattern 4: IAM policy document (always a data source, never a resource)
32data "aws_iam_policy_document" "s3_read" {
33 statement {
34 effect = "Allow"
35 actions = ["s3:GetObject", "s3:ListBucket"]
36 resources = ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]
37 }
38}
39
40resource "aws_iam_policy" "s3_read" {
41 name = "s3-read-policy"
42 policy = data.aws_iam_policy_document.s3_read.json
43}10. terraform_remote_state — Cross-Config References
The most important data source for sharing values between configurations:
1# network/outputs.tf
2output "vpc_id" { value = aws_vpc.main.id }
3output "private_subnet_ids" { value = aws_subnet.private[*].id }
4output "public_subnet_ids" { value = aws_subnet.public[*].id }
5
6# app/main.tf — reads network outputs
7data "terraform_remote_state" "network" {
8 backend = "s3"
9 config = {
10 bucket = "my-terraform-state"
11 key = "env/prod/network/terraform.tfstate"
12 region = "us-east-1"
13 }
14}
15
16resource "aws_lb" "web" {
17 # Reference network layer outputs directly
18 subnets = data.terraform_remote_state.network.outputs.public_subnet_ids
19}
20
21resource "aws_instance" "app" {
22 subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
23}Pattern comparison:
| Pattern | Use When |
|---|---|
terraform_remote_state | Both configs are Terraform-managed; you need outputs |
Regular data sources (aws_vpc, etc.) | Reading infra by tags/IDs, regardless of how it was created |
| Variable passing | Parent module passes values to child module |
11. Complete Example — Variables, Locals, Outputs, Data
1# variables.tf
2variable "environment" {
3 description = "Deployment environment"
4 type = string
5 validation {
6 condition = contains(["dev", "staging", "prod"], var.environment)
7 error_message = "environment must be dev, staging, or prod."
8 }
9}
10
11variable "instance_count" {
12 description = "Number of web servers"
13 type = number
14 default = 2
15}
16
17variable "allowed_cidrs" {
18 description = "CIDRs allowed to reach web servers"
19 type = list(string)
20 default = ["10.0.0.0/8"]
21}
22
23# locals.tf
24locals {
25 name_prefix = "${var.environment}-web"
26 is_prod = var.environment == "prod"
27 instance_type = local.is_prod ? "t3.medium" : "t3.micro"
28
29 common_tags = {
30 Environment = var.environment
31 ManagedBy = "terraform"
32 }
33}
34
35# data.tf
36data "aws_ami" "ubuntu" {
37 most_recent = true
38 owners = ["099720109477"]
39 filter { name = "name"; values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"] }
40}
41
42data "aws_vpc" "selected" {
43 filter { name = "tag:Environment"; values = [var.environment] }
44}
45
46data "aws_subnets" "private" {
47 filter { name = "vpc-id"; values = [data.aws_vpc.selected.id] }
48 filter { name = "tag:Tier"; values = ["private"] }
49}
50
51# main.tf
52resource "aws_instance" "web" {
53 count = var.instance_count
54 ami = data.aws_ami.ubuntu.id
55 instance_type = local.instance_type
56 subnet_id = data.aws_subnets.private.ids[count.index % length(data.aws_subnets.private.ids)]
57 tags = merge(local.common_tags, { Name = "${local.name_prefix}-${count.index}" })
58}
59
60# outputs.tf
61output "instance_ids" {
62 description = "Web server instance IDs"
63 value = aws_instance.web[*].id
64}
65
66output "instance_private_ips" {
67 description = "Private IP addresses"
68 value = aws_instance.web[*].private_ip
69}
70
71output "vpc_id" {
72 description = "VPC used for deployment"
73 value = data.aws_vpc.selected.id
74}12. Quick Reference
| Concept | Key Fact |
|---|---|
variable without default | Required — Terraform prompts if not provided |
variable with default = null | Optional — can be overridden or left null |
sensitive = true | Redacts from plan output; still in state |
terraform.tfvars | Auto-loaded — no -var-file flag needed |
*.auto.tfvars | Auto-loaded — no -var-file flag needed |
prod.tfvars | NOT auto-loaded — needs -var-file=prod.tfvars |
TF_VAR_name | Environment variable assignment (case-sensitive on Linux) |
| Precedence winner | -var CLI flag beats everything |
local.name | Computed within module; not exposed externally |
var.name | Set externally; exposed in module interface |
output purpose | Expose values to user and parent modules |
terraform output -raw | Get plain string value for scripting |
| Data source | Reads existing infra; never creates anything |
data.TYPE.NAME.attr | How to reference a data source attribute |
terraform_remote_state | Read outputs from another Terraform config |
data "aws_caller_identity" | Get current AWS account ID / ARN |
Practice Questions8
Q1. Which of the following are valid ways to pass a value for a Terraform input variable named `region`? (Select all that apply — more than one answer may be correct.)
Select one answer before revealing.
Q2. What happens when you declare a Terraform variable with `sensitive = true`?
Select one answer before revealing.
Q3. Which variable type would you use to store a list of CIDR blocks for allowed IP ranges?
Select one answer before revealing.
Q4. What is the difference between a Terraform `output` and a `local` value?
Select one answer before revealing.
Q5. What is the correct way to define a Terraform variable with object type for structured configuration?
Select one answer before revealing.
Q6. You are writing a Terraform module and want to expose an EC2 instance's public IP as an output. The instance is defined as `resource "aws_instance" "app"`. What is the correct output block?
Select one answer before revealing.
Q7. Which of the following are valid Terraform data types for variable declarations? (Select all that apply — more than one answer may be correct.)
Select one answer before revealing.
Q8. Hands-On: Debug the following Terraform configuration. Why does `terraform plan` show an error? ```hcl variable "environment" { type = string } resource "aws_s3_bucket" "logs" { bucket = "my-${var.environment}-logs" } resource "aws_s3_bucket_versioning" "logs" { bucket = aws_s3_bucket.logs.bucket versioning_configuration { status = "Enabled" } } output "bucket_name" { value = aws_s3_bucket.logs.bucket_name } ```
Select one answer before revealing.