AskLearn
Loading...
← Back to Terraform Course
AdvancedConfiguration

Output Dependencies

Complex output patterns

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

  1. Strategic Outputs: Design outputs to support module interfaces and external integrations
  2. Documentation: Always provide clear descriptions for outputs
  3. Sensitive Handling: Properly mark and handle sensitive data in outputs
  4. Dependencies: Use outputs to create explicit dependencies between resources
  5. Transformation: Transform raw resource attributes into useful formats
  6. 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

Additional Resources