Tutorial 18: Output Values and Dependencies
Learning Objectives
By the end of this tutorial, you will be able to:
- Create and manage output values effectively
- Use outputs to expose important resource attributes
- Implement output dependencies and data sharing
- Design module interfaces with strategic outputs
- Handle sensitive data in outputs securely
Prerequisites
- Completed Tutorial 8: Output Values
- Understanding of Terraform modules and resources
- Knowledge of resource dependencies
Introduction
Output values serve as the return values of a Terraform module, allowing you to expose information about your infrastructure for use by other configurations, modules, or external systems. This tutorial covers advanced output patterns and best practices.
Basic Output Patterns
Simple Resource Outputs
# main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 1)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet-${count.index + 1}"
Type = "public"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "List of IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "public_subnet_cidrs" {
description = "List of CIDR blocks of the public subnets"
value = aws_subnet.public[*].cidr_block
}
output "internet_gateway_id" {
description = "ID of the Internet Gateway"
value = aws_internet_gateway.main.id
}
Computed and Transformed Outputs
# outputs.tf
output "subnet_availability_zone_map" {
description = "Map of subnet IDs to their availability zones"
value = {
for subnet in aws_subnet.public :
subnet.id => subnet.availability_zone
}
}
output "subnet_details" {
description = "Detailed information about all subnets"
value = [
for i, subnet in aws_subnet.public : {
id = subnet.id
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
index = i
name = subnet.tags.Name
}
]
}
output "network_summary" {
description = "Summary of network configuration"
value = {
vpc = {
id = aws_vpc.main.id
cidr_block = aws_vpc.main.cidr_block
region = data.aws_region.current.name
}
subnets = {
count = length(aws_subnet.public)
public_ids = aws_subnet.public[*].id
total_cidrs = aws_subnet.public[*].cidr_block
}
connectivity = {
internet_gateway_id = aws_internet_gateway.main.id
has_internet_access = true
}
}
}
data "aws_region" "current" {}
Advanced Output Patterns
Conditional Outputs
# variables.tf
variable "create_nat_gateway" {
description = "Whether to create NAT Gateway"
type = bool
default = false
}
variable "enable_vpn" {
description = "Whether to enable VPN connection"
type = bool
default = false
}
# main.tf
resource "aws_subnet" "private" {
count = var.create_nat_gateway ? length(var.availability_zones) : 0
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-private-subnet-${count.index + 1}"
Type = "private"
}
}
resource "aws_nat_gateway" "main" {
count = var.create_nat_gateway ? length(var.availability_zones) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-nat-gateway-${count.index + 1}"
}
}
resource "aws_eip" "nat" {
count = var.create_nat_gateway ? length(var.availability_zones) : 0
domain = "vpc"
tags = {
Name = "${var.project_name}-eip-nat-${count.index + 1}"
}
}
resource "aws_vpn_gateway" "main" {
count = var.enable_vpn ? 1 : 0
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-vpn-gateway"
}
}
# outputs.tf
output "private_subnet_ids" {
description = "List of IDs of the private subnets"
value = var.create_nat_gateway ? aws_subnet.private[*].id : []
}
output "nat_gateway_ids" {
description = "List of IDs of the NAT Gateways"
value = var.create_nat_gateway ? aws_nat_gateway.main[*].id : []
}
output "nat_gateway_ips" {
description = "List of Elastic IP addresses of the NAT Gateways"
value = var.create_nat_gateway ? aws_eip.nat[*].public_ip : []
}
output "vpn_gateway_id" {
description = "ID of the VPN Gateway"
value = var.enable_vpn ? aws_vpn_gateway.main[0].id : null
}
# Conditional complex outputs
output "network_configuration" {
description = "Complete network configuration"
value = {
vpc_id = aws_vpc.main.id
cidr_block = aws_vpc.main.cidr_block
public_subnets = {
ids = aws_subnet.public[*].id
cidrs = aws_subnet.public[*].cidr_block
}
private_subnets = var.create_nat_gateway ? {
ids = aws_subnet.private[*].id
cidrs = aws_subnet.private[*].cidr_block
} : null
nat_gateways = var.create_nat_gateway ? {
ids = aws_nat_gateway.main[*].id
ips = aws_eip.nat[*].public_ip
} : null
vpn_gateway = var.enable_vpn ? {
id = aws_vpn_gateway.main[0].id
} : null
}
}
Multi-Module Outputs
# modules/vpc/outputs.tf
output "vpc_info" {
description = "VPC information"
value = {
id = aws_vpc.main.id
cidr_block = aws_vpc.main.cidr_block
default_security_group_id = aws_vpc.main.default_security_group_id
}
}
output "subnet_info" {
description = "Subnet information"
value = {
public = {
ids = aws_subnet.public[*].id
cidrs = aws_subnet.public[*].cidr_block
azs = aws_subnet.public[*].availability_zone
}
private = var.create_private_subnets ? {
ids = aws_subnet.private[*].id
cidrs = aws_subnet.private[*].cidr_block
azs = aws_subnet.private[*].availability_zone
} : null
}
}
# modules/security/outputs.tf
output "security_group_ids" {
description = "Security group IDs"
value = {
web_tier = aws_security_group.web.id
app_tier = aws_security_group.app.id
db_tier = aws_security_group.db.id
}
}
# Root module main.tf
module "vpc" {
source = "./modules/vpc"
project_name = var.project_name
vpc_cidr = var.vpc_cidr
availability_zones = var.availability_zones
create_nat_gateway = var.create_nat_gateway
}
module "security" {
source = "./modules/security"
vpc_id = module.vpc.vpc_info.id
vpc_cidr = module.vpc.vpc_info.cidr_block
environment = var.environment
}
# Root module outputs.tf
output "infrastructure_summary" {
description = "Complete infrastructure summary"
value = {
vpc = module.vpc.vpc_info
subnets = module.vpc.subnet_info
security_groups = module.security.security_group_ids
# Computed values from multiple modules
total_subnets = (
length(module.vpc.subnet_info.public.ids) +
(module.vpc.subnet_info.private != null ? length(module.vpc.subnet_info.private.ids) : 0)
)
has_private_subnets = module.vpc.subnet_info.private != null
}
}
Sensitive Outputs
Handling Sensitive Data
# main.tf
resource "random_password" "db_password" {
length = 16
special = true
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-database"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_encrypted = true
db_name = var.database_name
username = var.database_username
password = random_password.db_password.result
skip_final_snapshot = true
tags = {
Name = "${var.project_name}-database"
}
}
# outputs.tf
output "database_endpoint" {
description = "RDS instance endpoint"
value = aws_db_instance.main.endpoint
}
output "database_port" {
description = "RDS instance port"
value = aws_db_instance.main.port
}
# Sensitive outputs
output "database_password" {
description = "RDS instance master password"
value = random_password.db_password.result
sensitive = true
}
output "database_connection_string" {
description = "Database connection string"
value = format(
"mysql://%s:%s@%s:%d/%s",
var.database_username,
random_password.db_password.result,
aws_db_instance.main.endpoint,
aws_db_instance.main.port,
var.database_name
)
sensitive = true
}
# Non-sensitive derived outputs
output "database_info" {
description = "Database connection information (non-sensitive)"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
database_name = var.database_name
username = var.database_username
# Don't include password here
}
}
Working with Sensitive Outputs
# When using sensitive outputs in other resources
resource "kubernetes_secret" "db_credentials" {
metadata {
name = "database-credentials"
namespace = "default"
}
data = {
host = aws_db_instance.main.endpoint
port = tostring(aws_db_instance.main.port)
database = var.database_name
username = var.database_username
password = random_password.db_password.result
}
type = "Opaque"
}
# Output for external consumption (still sensitive)
output "kubernetes_secret_name" {
description = "Name of the Kubernetes secret containing database credentials"
value = kubernetes_secret.db_credentials.metadata[0].name
}
Output Dependencies and Data Flow
Explicit Dependencies
# Creating explicit dependencies through outputs
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[count.index % length(aws_subnet.public)].id
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "${var.project_name}-web-${count.index + 1}"
}
}
resource "aws_lb" "main" {
name = "${var.project_name}-lb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.lb.id]
subnets = aws_subnet.public[*].id
tags = {
Name = "${var.project_name}-load-balancer"
}
}
resource "aws_lb_target_group" "web" {
name = "${var.project_name}-web-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 10
}
tags = {
Name = "${var.project_name}-web-target-group"
}
}
resource "aws_lb_target_group_attachment" "web" {
count = length(aws_instance.web)
target_group_arn = aws_lb_target_group.web.arn
target_id = aws_instance.web[count.index].id
port = 80
}
# outputs.tf
output "application_endpoints" {
description = "Application access endpoints"
value = {
load_balancer = {
dns_name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
url = "http://${aws_lb.main.dns_name}"
}
instances = [
for i, instance in aws_instance.web : {
id = instance.id
private_ip = instance.private_ip
public_ip = instance.public_ip
az = instance.availability_zone
url = "http://${instance.public_ip}"
}
]
}
# This output depends on all instances and load balancer being ready
depends_on = [
aws_lb_target_group_attachment.web
]
}
Cross-Module Dependencies
# modules/database/outputs.tf
output "database_config" {
description = "Database configuration for applications"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
database_name = aws_db_instance.main.db_name
security_group_id = aws_security_group.database.id
}
}
output "database_credentials" {
description = "Database credentials (sensitive)"
value = {
username = aws_db_instance.main.username
password = random_password.db_password.result
}
sensitive = true
}
# modules/application/main.tf
resource "aws_instance" "app" {
count = var.instance_count
ami = var.app_ami
instance_type = var.instance_type
subnet_id = var.private_subnet_ids[count.index % length(var.private_subnet_ids)]
vpc_security_group_ids = [
aws_security_group.app.id,
var.database_security_group_id # From database module
]
user_data = templatefile("${path.module}/user_data.sh", {
db_endpoint = var.database_endpoint
db_port = var.database_port
db_name = var.database_name
})
tags = {
Name = "${var.project_name}-app-${count.index + 1}"
}
}
# Root main.tf
module "database" {
source = "./modules/database"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
allowed_cidr_blocks = [module.vpc.vpc_cidr_block]
}
module "application" {
source = "./modules/application"
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
database_endpoint = module.database.database_config.endpoint
database_port = module.database.database_config.port
database_name = module.database.database_config.database_name
database_security_group_id = module.database.database_config.security_group_id
# This ensures database is ready before application deployment
depends_on = [module.database]
}
# Root outputs.tf
output "deployment_info" {
description = "Complete deployment information"
value = {
database = {
endpoint = module.database.database_config.endpoint
port = module.database.database_config.port
name = module.database.database_config.database_name
}
application = {
instances = module.application.instance_details
load_balancer = module.application.load_balancer_info
}
deployment_timestamp = timestamp()
}
}
Dynamic Output Generation
Outputs Based on Resource Counts
# variables.tf
variable "environments" {
description = "Environment configurations"
type = map(object({
instance_count = number
instance_type = string
enable_monitoring = bool
}))
}
# main.tf
resource "aws_instance" "environment" {
for_each = {
for env_name, env_config in var.environments :
env_name => env_config
}
count = each.value.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = each.value.instance_type
tags = {
Name = "${each.key}-instance-${count.index + 1}"
Environment = each.key
}
}
# outputs.tf
output "environment_instances" {
description = "Instance information by environment"
value = {
for env_name, env_config in var.environments :
env_name => {
count = env_config.instance_count
instances = [
for i in range(env_config.instance_count) : {
id = aws_instance.environment[env_name][i].id
public_ip = aws_instance.environment[env_name][i].public_ip
private_ip = aws_instance.environment[env_name][i].private_ip
}
]
monitoring_enabled = env_config.enable_monitoring
}
}
}
output "all_instance_ips" {
description = "All instance IP addresses across environments"
value = flatten([
for env_name, env_config in var.environments : [
for i in range(env_config.instance_count) :
aws_instance.environment[env_name][i].public_ip
]
])
}
output "environment_summary" {
description = "Summary statistics by environment"
value = {
for env_name, env_config in var.environments :
env_name => {
total_instances = env_config.instance_count
instance_type = env_config.instance_type
total_cost_estimate = env_config.instance_count * local.instance_costs[env_config.instance_type]
}
}
}
locals {
instance_costs = {
"t3.micro" = 8.76 # USD per month
"t3.small" = 17.52
"t3.medium" = 35.04
"t3.large" = 70.08
}
}
Output for External Systems
JSON Output for Scripts
# outputs.tf
output "infrastructure_json" {
description = "Infrastructure configuration in JSON format for external tools"
value = jsonencode({
vpc = {
id = aws_vpc.main.id
cidr_block = aws_vpc.main.cidr_block
region = data.aws_region.current.name
}
subnets = [
for subnet in aws_subnet.public : {
id = subnet.id
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
type = "public"
}
]
security_groups = [
for sg in [aws_security_group.web, aws_security_group.app] : {
id = sg.id
name = sg.name
rules = [
for rule in sg.ingress : {
from_port = rule.from_port
to_port = rule.to_port
protocol = rule.protocol
cidr_blocks = rule.cidr_blocks
}
]
}
]
instances = [
for instance in aws_instance.web : {
id = instance.id
type = instance.instance_type
availability_zone = instance.availability_zone
public_ip = instance.public_ip
private_ip = instance.private_ip
security_groups = instance.vpc_security_group_ids
}
]
load_balancer = {
dns_name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
}
created_at = timestamp()
})
}
# For Ansible inventory
output "ansible_inventory" {
description = "Ansible inventory in JSON format"
value = jsonencode({
web_servers = {
hosts = {
for i, instance in aws_instance.web :
"web-${i + 1}" => {
ansible_host = instance.public_ip
private_ip = instance.private_ip
instance_id = instance.id
}
}
vars = {
ansible_user = "ubuntu"
ansible_ssh_private_key_file = var.ssh_private_key_path
}
}
_meta = {
hostvars = {
for i, instance in aws_instance.web :
"web-${i + 1}" => {
ec2_id = instance.id
ec2_public_ip = instance.public_ip
ec2_private_ip = instance.private_ip
ec2_instance_type = instance.instance_type
ec2_availability_zone = instance.availability_zone
}
}
}
})
}
Best Practices
1. Output Naming and Documentation
# Good: Descriptive names and documentation
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "List of IDs of the public subnets"
value = aws_subnet.public[*].id
}
# Better: Include usage context
output "vpc_id" {
description = "ID of the VPC - use this to associate resources with the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of public subnets - use these for resources requiring internet access"
value = aws_subnet.public[*].id
}
2. Output Organization
# Group related outputs
output "network_config" {
description = "Network configuration details"
value = {
vpc_id = aws_vpc.main.id
vpc_cidr = aws_vpc.main.cidr_block
public_subnets = aws_subnet.public[*].id
private_subnets = aws_subnet.private[*].id
nat_gateway_ips = aws_eip.nat[*].public_ip
}
}
output "security_config" {
description = "Security configuration details"
value = {
web_sg_id = aws_security_group.web.id
app_sg_id = aws_security_group.app.id
db_sg_id = aws_security_group.db.id
}
}
3. Sensitive Data Handling
# Always mark sensitive outputs
output "database_password" {
description = "Database master password"
value = random_password.db_password.result
sensitive = true
}
# Provide non-sensitive alternatives when possible
output "database_password_length" {
description = "Length of the database password"
value = length(random_password.db_password.result)
}
Key Takeaways
- Strategic Outputs: Design outputs to support module interfaces and external integrations
- Documentation: Always provide clear descriptions for outputs
- Sensitive Handling: Properly mark and handle sensitive data in outputs
- Dependencies: Use outputs to create explicit dependencies between resources
- Transformation: Transform raw resource attributes into useful formats
- Organization: Group related outputs for better maintainability
Next Steps
- Tutorial 19: Learn about resource meta-arguments (count, for_each)
- Practice designing module interfaces with outputs
- Experiment with output transformations and conditional logic
- Review the Terraform Output Values Documentation