/Variables, Outputs & Data Sources
Concept
Medium

Variables, Outputs & Data Sources

11 min read·variablesoutputslocalsdata-sourcestype-systemsensitivetfvarsvariable-precedenceterraform-associate

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.

Rendering diagram…

2. Variable Declaration — All Arguments

Every argument in a variable block is optional except the label (name):

hcl
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}
ArgumentRequired?Purpose
descriptionNoHuman-readable documentation
typeNoType constraint (defaults to any)
defaultNoMakes variable optional; omit to require value
nullableNoWhether null is a valid value (default true)
sensitiveNoRedact from plan/apply output and state display
validation blockNoCustom validation rules with error messages

3. Variable Type System

The type system is a key exam topic. Know all primitive and complex types:

hcl
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 }
TypeExample ValueNotes
string"us-east-1"UTF-8 text
number42, 3.14Integer or float
booltrue, 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
anyAnythingNo 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):

Rendering diagram…
bash
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.tfvars is auto-loaded — no flag needed
  • *.auto.tfvars are auto-loaded — no flag needed
  • Custom .tfvars files (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:

hcl
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_password will show (sensitive value) — use -json or -raw to extract
bash
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:

hcl
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:

Aspectvariablelocal
Set byExternal caller (CLI, tfvars, env)Only within the module
PurposeExternal parameterizationInternal DRY / computed values
Referencevar.namelocal.name
Can reference other locals?NoYes (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:

hcl
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}
ArgumentPurpose
descriptionDocumentation for the output
valueThe expression to expose (required)
sensitiveRedact from display; required if referencing sensitive var
depends_onExplicit dependency — rarely needed

Working with outputs:

bash
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_ip

8. 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:

hcl
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:

Rendering diagram…

Data source with depends_on:

hcl
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

hcl
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:

hcl
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:

PatternUse When
terraform_remote_stateBoth 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 passingParent module passes values to child module

11. Complete Example — Variables, Locals, Outputs, Data

hcl
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

ConceptKey Fact
variable without defaultRequired — Terraform prompts if not provided
variable with default = nullOptional — can be overridden or left null
sensitive = trueRedacts from plan output; still in state
terraform.tfvarsAuto-loaded — no -var-file flag needed
*.auto.tfvarsAuto-loaded — no -var-file flag needed
prod.tfvarsNOT auto-loaded — needs -var-file=prod.tfvars
TF_VAR_nameEnvironment variable assignment (case-sensitive on Linux)
Precedence winner-var CLI flag beats everything
local.nameComputed within module; not exposed externally
var.nameSet externally; exposed in module interface
output purposeExpose values to user and parent modules
terraform output -rawGet plain string value for scripting
Data sourceReads existing infra; never creates anything
data.TYPE.NAME.attrHow to reference a data source attribute
terraform_remote_stateRead outputs from another Terraform config
data "aws_caller_identity"Get current AWS account ID / ARN

Practice Questions8

medium

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.

medium

Q2. What happens when you declare a Terraform variable with `sensitive = true`?


Select one answer before revealing.

easy

Q3. Which variable type would you use to store a list of CIDR blocks for allowed IP ranges?


Select one answer before revealing.

medium

Q4. What is the difference between a Terraform `output` and a `local` value?


Select one answer before revealing.

hard

Q5. What is the correct way to define a Terraform variable with object type for structured configuration?


Select one answer before revealing.

easy

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.

medium

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.

hard

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.