Terraform Data Sources Explained: Read External Information
Learn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
DevOps
Understand Terraform variables (inputs) and outputs. Learn types, validation, defaults, sensitive values
Variables are inputs — values passed into your configuration. Outputs are results — values exposed after terraform apply. Together they define how data flows in and out of Terraform modules.
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.micro"
}
resource "aws_instance" "web" {
instance_type = var.instance_type
}# String
variable "environment" {
type = string
default = "dev"
}
# Number
variable "instance_count" {
type = number
default = 3
}
# Boolean
variable "enable_monitoring" {
type = bool
default = true
}
# List
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Map
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
prod = "t3.xlarge"
}
}
# Object
variable "database" {
type = object({
engine = string
engine_version = string
instance_class = string
multi_az = bool
})
default = {
engine = "postgres"
engine_version = "16.3"
instance_class = "db.t3.micro"
multi_az = false
}
}
# Tuple
variable "subnet_config" {
type = tuple([string, number, bool])
# ["10.0.1.0/24", 1, true]
}# 1. CLI flag
terraform apply -var="instance_type=t3.large"
# 2. tfvars file
terraform apply -var-file="prod.tfvars"
# 3. Environment variable
export TF_VAR_instance_type="t3.large"
terraform apply
# 4. Auto-loaded files
# terraform.tfvars
# terraform.tfvars.json
# *.auto.tfvars
# *.auto.tfvars.json# prod.tfvars
environment = "prod"
instance_type = "t3.xlarge"
instance_count = 5
enable_monitoring = true-var and -var-file on CLI*.auto.tfvars (alphabetical order)terraform.tfvarsTF_VAR_* environment variablesdefault valuevariable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_type" {
type = string
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "Only t3 instance types are allowed."
}
}
variable "cidr_block" {
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}variable "db_password" {
type = string
sensitive = true # Hidden in plan/apply output
}terraform plan:
~ password = (sensitive value)Note: sensitive = true hides the value in CLI output but does NOT encrypt it in state.
# Required — no default, must be provided
variable "vpc_id" {
type = string
description = "VPC ID to deploy into"
}
# Optional — has a default
variable "enable_logging" {
type = bool
default = true
}output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP address"
value = aws_instance.web.public_ip
}terraform apply
# Outputs:
# instance_id = "i-0abc123def456"
# public_ip = "54.210.167.99"
# Query specific output
terraform output public_ip
# "54.210.167.99"
# JSON format (for scripts)
terraform output -jsonoutput "db_password" {
value = random_password.db.result
sensitive = true
}terraform output db_password
# (sensitive — use terraform output -raw db_password to see)
terraform output -raw db_password
# MyS3cur3P@ssoutput "bastion_ip" {
value = var.create_bastion ? aws_instance.bastion[0].public_ip : null
description = "Bastion host IP (null if bastion disabled)"
}This is where variables and outputs work together:
Root Module
├── variables.tf (inputs from user)
├── main.tf (calls child modules)
├── outputs.tf (exposes results)
│
├── modules/networking/
│ ├── variables.tf (inputs from root)
│ ├── main.tf (creates resources)
│ └── outputs.tf (exposes VPC ID, subnet IDs)
│
└── modules/compute/
├── variables.tf (inputs from root + networking outputs)
├── main.tf (creates resources)
└── outputs.tf (exposes instance IPs)# modules/networking/variables.tf
variable "vpc_cidr" {
type = string
}
variable "environment" {
type = string
}# modules/networking/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}# Root main.tf
module "networking" {
source = "./modules/networking"
vpc_cidr = var.vpc_cidr # Root variable → module input
environment = var.environment
}
module "compute" {
source = "./modules/compute"
vpc_id = module.networking.vpc_id # Module output → module input
subnet_ids = module.networking.private_subnet_ids
}
# Root outputs.tf
output "vpc_id" {
value = module.networking.vpc_id # Module output → root output
}Data flow:
User (tfvars) → Root variables → Module inputs → Resources
↓
User ← Root outputs ← Module outputs ← Resource attributesdescription — it shows up in terraform plan and documentationtype — catches errors earlysensitive — prevents accidental exposure in logsvariables.tf, outputs.tf, locals.tf# ❌ Too many individual variables
variable "db_engine" { ... }
variable "db_version" { ... }
variable "db_instance_class" { ... }
variable "db_multi_az" { ... }
# ✅ One object variable
variable "database" {
type = object({
engine = string
version = string
instance_class = string
multi_az = bool
})
}Variables are inputs (what goes into your config), outputs are results (what comes out). They form the interface of every Terraform module — variables define what the caller must provide, outputs define what the module exposes. Use types and validation to catch errors early, mark secrets sensitive, and use object types to group related inputs.
Learn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
Learn when to use Terraform depends_on for explicit resource dependencies. Understand implicit vs explicit dependencies, common use cases
Terraform for_each vs count explained with practical examples. Learn when to use each, how to migrate from count to for_each
Learn Terraform locals to reduce duplication, compute values, and organize complex configurations. Practical examples with naming conventions, tag maps