Stage 4 · Provision
Terraform Fundamentals
Variables, Outputs & Data Sources
Type constraints, validation, sensitive values, and dynamic blocks — making configurations flexible and safe.
Input Variables
Input variables are parameters for your Terraform configuration. They let you customize behavior without editing resource blocks. Variables can come from variable definition files (.tfvars), environment variables, CLI flags, or interactive prompts.
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}The description field documents the variable's purpose. The default value makes the variable optional. Without a default, Terraform prompts for a value or reads from a .tfvars file.
Variable Types
Terraform supports string, number, bool, list, map, set, object, tuple, and any types. Type constraints catch errors at plan time before any API calls are made.
variable "subnets" {
description = "Subnet configuration"
type = list(object({
cidr = string
az = string
}))
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
variable "enable_features" {
description = "Feature flags"
type = object({
monitoring = bool
logging = bool
encryption = bool
})
}The object type enforces a specific shape. Callers must provide all fields with the correct types. The map(string) type allows any string keys with string values — flexible but unvalidated.
Validation Rules
Validation blocks enforce constraints on variable values. Terraform evaluates them before any resource changes. Use them to catch misconfigurations early.
variable "cidr_block" {
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "replica_count" {
type = number
validation {
condition = var.replica_count >= 2 && var.replica_count <= 10
error_message = "Must be between 2 and 10 replicas."
}
}
variable "region" {
type = string
validation {
condition = can(regex("^us-east-|^us-west-", var.region))
error_message = "Must be a US region."
}
}The condition expression must return true for valid inputs. can() wraps functions that would throw errors on invalid input, returning false instead of failing. regex() tests string patterns.
Outputs
Outputs expose values from your configuration. They display after terraform apply, can be referenced by other modules, and are stored in the state file. Outputs are the primary interface between root modules and child modules.
output "instance_public_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "database_connection" {
description = "Database connection string"
value = "postgresql://user:pass@${aws_db_instance.main.endpoint}"
sensitive = true
}
output "all_subnet_ids" {
description = "All subnet IDs"
value = module.vpc.private_subnet_ids
}sensitive = true prevents the value from appearing in terraform plan or apply output. The value is still stored in state and accessible to other modules. Use sensitive for passwords, tokens, and connection strings.
Data Sources
Data sources query existing infrastructure that Terraform does not manage. They let you reference resources created outside Terraform or in a different state file without importing them.
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
data "aws_vpc" "existing" {
tags = {
Name = "production-vpc"
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
subnet_id = data.aws_vpc.existing.id
}Data sources are read-only. They query the provider's API and return the results. If no resource matches, Terraform fails. Use depends_on or lifecycle prevent_destroy carefully with data sources.
Dynamic Blocks
Dynamic blocks generate repeated nested blocks from a list or map. They are the HCL equivalent of a loop inside a resource block.
variable "ingress_rules" {
default = [
{ port = 80, cidr = "0.0.0.0/0" },
{ port = 443, cidr = "0.0.0.0/0" },
]
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [ingress.value.cidr]
}
}
}The dynamic block iterates over var.ingress_rules and generates an ingress block for each item. The iterator variable is ingress, and each item is accessed via ingress.value. This avoids repeating nearly-identical blocks.
Dynamic blocks work well for nested blocks, but for_each on a resource or module is usually cleaner for creating multiple independent resources. Use dynamic blocks only when the target resource type does not support for_each.
Setting a variable as sensitive only hides it from plan and apply output. The actual value is stored in plaintext in the state file. For real secrets, use Vault, SOPS, or another secrets manager.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.