Tutorial 8: Output Values
Learning Objectives
- Understand the purpose and benefits of output values
- Learn to define and use outputs in Terraform configurations
- Practice exposing important information from your infrastructure
- Integrate outputs with modules and remote state
What are Output Values?
Output values are a way to expose information about your infrastructure after Terraform has created or modified resources. They serve as the return values of a Terraform configuration and can be used by other configurations or displayed to users.
Purpose of Outputs
- Information Sharing: Share resource details with other configurations
- User Interface: Display important information to users
- Module Integration: Return values from modules
- Automation: Provide data for scripts and other tools
- Documentation: Self-document important infrastructure details
Outputs vs Variables
# Variables are inputs to your configuration
variable "instance_type" {
type = string
}
# Outputs are results from your configuration
output "instance_id" {
value = aws_instance.web.id
}
Output Value Syntax
Basic Output Block
output "output_name" {
description = "Description of the output value"
value = expression
sensitive = false # Optional, defaults to false
}
Output Components
- Name: Identifier for the output
- Description: Human-readable explanation
- Value: The expression to output
- Sensitive: Whether to hide the value in logs
Simple Output Examples
Basic Resource Attributes
# outputs.tf
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP address of the EC2 instance"
value = aws_instance.web.public_ip
}
output "instance_private_ip" {
description = "Private IP address of the EC2 instance"
value = aws_instance.web.private_ip
}
output "instance_arn" {
description = "ARN of the EC2 instance"
value = aws_instance.web.arn
}
URL Construction
output "website_url" {
description = "URL to access the website"
value = "http://${aws_instance.web.public_ip}"
}
output "ssh_connection" {
description = "SSH connection string"
value = "ssh ec2-user@${aws_instance.web.public_ip}"
}
output "website_endpoint" {
description = "Website endpoint with custom port"
value = "http://${aws_instance.web.public_ip}:8080"
}
Complex Output Examples
Lists and Maps
# Output list of instance IDs
output "instance_ids" {
description = "List of instance IDs"
value = aws_instance.web[*].id
}
# Output map of instance details
output "instance_details" {
description = "Map of instance details"
value = {
for instance in aws_instance.web :
instance.tags.Name => {
id = instance.id
public_ip = instance.public_ip
az = instance.availability_zone
}
}
}
# Output availability zones
output "availability_zones" {
description = "List of availability zones used"
value = data.aws_availability_zones.available.names
}
Database Connection Information
# Database outputs
output "database_endpoint" {
description = "Database endpoint"
value = aws_db_instance.main.endpoint
}
output "database_connection_string" {
description = "Database connection string"
value = "mysql://${aws_db_instance.main.username}:${var.db_password}@${aws_db_instance.main.endpoint}:${aws_db_instance.main.port}/${aws_db_instance.main.db_name}"
sensitive = true
}
output "database_info" {
description = "Database connection information"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
database = aws_db_instance.main.db_name
username = aws_db_instance.main.username
}
}
Load Balancer Outputs
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = aws_lb.web.dns_name
}
output "load_balancer_zone_id" {
description = "Hosted zone ID of the load balancer"
value = aws_lb.web.zone_id
}
output "load_balancer_url" {
description = "URL of the load balancer"
value = "https://${aws_lb.web.dns_name}"
}
output "target_group_arn" {
description = "ARN of the target group"
value = aws_lb_target_group.web.arn
}
Conditional Outputs
Using count
variable "create_load_balancer" {
type = bool
default = false
}
resource "aws_lb" "web" {
count = var.create_load_balancer ? 1 : 0
# ... configuration
}
output "load_balancer_dns" {
description = "DNS name of the load balancer (if created)"
value = var.create_load_balancer ? aws_lb.web[0].dns_name : null
}
# Alternative using try()
output "load_balancer_dns_safe" {
description = "DNS name of the load balancer (if created)"
value = try(aws_lb.web[0].dns_name, null)
}
Using for_each
variable "environments" {
type = list(string)
default = ["dev", "staging", "prod"]
}
resource "aws_instance" "web" {
for_each = toset(var.environments)
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
tags = {
Name = "web-${each.value}"
Environment = each.value
}
}
output "environment_instances" {
description = "Instance details by environment"
value = {
for env, instance in aws_instance.web :
env => {
id = instance.id
public_ip = instance.public_ip
url = "http://${instance.public_ip}"
}
}
}
Sensitive Outputs
Marking Outputs as Sensitive
output "database_password" {
description = "Database password"
value = var.db_password
sensitive = true
}
output "api_key" {
description = "API key for external service"
value = random_password.api_key.result
sensitive = true
}
output "ssl_certificate" {
description = "SSL certificate content"
value = tls_private_key.example.private_key_pem
sensitive = true
}
Viewing Sensitive Outputs
# Sensitive values are hidden in normal output
terraform output
# View specific sensitive output
terraform output database_password
# Show all outputs including sensitive (in JSON)
terraform output -json
Using Outputs
Viewing Outputs
# Show all outputs
terraform output
# Show specific output
terraform output instance_id
# Show output in JSON format
terraform output -json
# Show specific output in raw format (no quotes)
terraform output -raw website_url
Output Examples
$ terraform output
instance_id = "i-0123456789abcdef0"
instance_public_ip = "54.123.45.67"
website_url = "http://54.123.45.67"
$ terraform output instance_id
"i-0123456789abcdef0"
$ terraform output -raw website_url
http://54.123.45.67
Using Outputs in Scripts
#!/bin/bash
# deploy.sh
# Get outputs from Terraform
INSTANCE_IP=$(terraform output -raw instance_public_ip)
WEBSITE_URL=$(terraform output -raw website_url)
# Use in deployment script
echo "Deploying to instance: $INSTANCE_IP"
curl -f $WEBSITE_URL || echo "Website not responding"
# Update DNS or load balancer
aws route53 change-resource-record-sets \
--hosted-zone-id Z123456789 \
--change-batch "{
\"Changes\": [{
\"Action\": \"UPSERT\",
\"ResourceRecordSet\": {
\"Name\": \"app.example.com\",
\"Type\": \"A\",
\"TTL\": 300,
\"ResourceRecords\": [{\"Value\": \"$INSTANCE_IP\"}]
}
}]
}"
Module Outputs
Module with Outputs
# modules/web-server/outputs.tf
output "instance_id" {
description = "ID of the web server instance"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "security_group_id" {
description = "ID of the security group"
value = aws_security_group.web.id
}
output "web_server_info" {
description = "Complete web server information"
value = {
id = aws_instance.web.id
public_ip = aws_instance.web.public_ip
private_ip = aws_instance.web.private_ip
security_group_id = aws_security_group.web.id
availability_zone = aws_instance.web.availability_zone
}
}
Using Module Outputs
# main.tf
module "web_server" {
source = "./modules/web-server"
instance_type = "t2.micro"
environment = "production"
}
# Reference module outputs
output "web_instance_id" {
description = "Web server instance ID"
value = module.web_server.instance_id
}
output "web_public_ip" {
description = "Web server public IP"
value = module.web_server.public_ip
}
# Use module output in other resources
resource "aws_route53_record" "web" {
zone_id = data.aws_route53_zone.main.zone_id
name = "web.example.com"
type = "A"
ttl = 300
records = [module.web_server.public_ip]
}
Advanced Output Patterns
Computed Values
locals {
instance_count = length(aws_instance.web)
total_storage = sum([for i in aws_instance.web : i.root_block_device[0].volume_size])
environment_info = {
name = var.environment
region = var.region
instance_count = local.instance_count
total_storage = local.total_storage
created_at = timestamp()
}
}
output "deployment_summary" {
description = "Summary of the deployment"
value = local.environment_info
}
output "cost_estimate" {
description = "Estimated monthly cost"
value = {
compute_hours = local.instance_count * 24 * 30
storage_gb = local.total_storage
estimated_usd = local.instance_count * 8.76 + local.total_storage * 0.10
}
}
Dynamic Outputs with Functions
# Generate connection strings for all instances
output "ssh_connections" {
description = "SSH connection strings for all instances"
value = [
for instance in aws_instance.web :
"ssh -i ~/.ssh/key.pem ec2-user@${instance.public_ip}"
]
}
# Generate environment-specific URLs
output "environment_urls" {
description = "URLs for different environments"
value = {
for env, instance in aws_instance.web :
env => "https://${env}.example.com"
}
}
# Format database connection parameters
output "database_connections" {
description = "Database connection strings by type"
value = {
jdbc_url = "jdbc:mysql://${aws_db_instance.main.endpoint}:${aws_db_instance.main.port}/${aws_db_instance.main.db_name}"
python_url = "mysql+pymysql://${aws_db_instance.main.username}:${urlencode(var.db_password)}@${aws_db_instance.main.endpoint}:${aws_db_instance.main.port}/${aws_db_instance.main.db_name}"
node_config = {
host = aws_db_instance.main.address
port = aws_db_instance.main.port
database = aws_db_instance.main.db_name
username = aws_db_instance.main.username
}
}
sensitive = true
}
Infrastructure Documentation
output "infrastructure_map" {
description = "Complete infrastructure mapping"
value = {
vpc = {
id = aws_vpc.main.id
cidr_block = aws_vpc.main.cidr_block
region = var.region
}
subnets = {
for subnet in aws_subnet.public :
subnet.tags.Name => {
id = subnet.id
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
}
instances = {
for instance in aws_instance.web :
instance.tags.Name => {
id = instance.id
type = instance.instance_type
ami = instance.ami
public_ip = instance.public_ip
private_ip = instance.private_ip
subnet_id = instance.subnet_id
security_groups = instance.vpc_security_group_ids
}
}
load_balancers = var.create_load_balancer ? {
web = {
dns_name = aws_lb.web[0].dns_name
arn = aws_lb.web[0].arn
type = aws_lb.web[0].load_balancer_type
}
} : {}
}
}
Remote State and Outputs
Consuming Remote State Outputs
# In another configuration, reference outputs from remote state
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "vpc/terraform.tfstate"
region = "us-west-2"
}
}
# Use remote state outputs
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
subnet_id = data.terraform_remote_state.vpc.outputs.private_subnet_id
vpc_security_group_ids = [
data.terraform_remote_state.vpc.outputs.app_security_group_id
]
}
Organizing Outputs for Remote State
# vpc/outputs.tf - Expose values for other configurations
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "app_security_group_id" {
description = "Security group ID for applications"
value = aws_security_group.app.id
}
output "database_subnet_group_name" {
description = "Name of the database subnet group"
value = aws_db_subnet_group.main.name
}
Best Practices
1. Organize Outputs by Purpose
# Group related outputs
output "network_info" {
description = "Network configuration details"
value = {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.public[*].id
igw_id = aws_internet_gateway.main.id
}
}
output "compute_info" {
description = "Compute resource details"
value = {
instance_ids = aws_instance.web[*].id
public_ips = aws_instance.web[*].public_ip
private_ips = aws_instance.web[*].private_ip
}
}
2. Use Descriptive Names and Descriptions
# Good
output "web_server_public_ip" {
description = "Public IP address of the web server for SSH access"
value = aws_instance.web.public_ip
}
# Avoid
output "ip" {
value = aws_instance.web.public_ip
}
3. Include Connection Information
output "connection_info" {
description = "Information needed to connect to services"
value = {
ssh_command = "ssh -i ~/.ssh/key.pem ec2-user@${aws_instance.web.public_ip}"
website_url = "http://${aws_instance.web.public_ip}"
database_host = aws_db_instance.main.endpoint
database_port = aws_db_instance.main.port
}
}
4. Handle Missing Values Gracefully
output "optional_load_balancer" {
description = "Load balancer DNS (if created)"
value = var.create_load_balancer ? aws_lb.web[0].dns_name : "Not created"
}
output "backup_info" {
description = "Backup configuration"
value = {
enabled = var.enable_backups
bucket = var.enable_backups ? aws_s3_bucket.backups[0].id : null
schedule = var.enable_backups ? var.backup_schedule : null
}
}
5. Document Output Usage
output "api_endpoint" {
description = <<-EOF
API endpoint URL for the application.
Use this URL to configure client applications.
Example: curl ${aws_lb.api.dns_name}/health
EOF
value = "https://${aws_lb.api.dns_name}"
}
Testing and Validation
Validate Output Values
# Test outputs after apply
terraform apply
# Verify specific outputs
terraform output website_url
curl $(terraform output -raw website_url)
# Test database connection
terraform output -json database_info | jq '.host'
Output Testing Script
#!/bin/bash
# test-outputs.sh
echo "Testing Terraform outputs..."
# Get all outputs
terraform output -json > outputs.json
# Test web server
WEBSITE_URL=$(jq -r '.website_url.value' outputs.json)
echo "Testing website: $WEBSITE_URL"
curl -f "$WEBSITE_URL" || echo "Website test failed"
# Test SSH connection
INSTANCE_IP=$(jq -r '.instance_public_ip.value' outputs.json)
echo "Testing SSH to: $INSTANCE_IP"
ssh -o ConnectTimeout=5 -o BatchMode=yes ec2-user@$INSTANCE_IP exit || echo "SSH test failed"
# Test database connection
DB_HOST=$(jq -r '.database_endpoint.value' outputs.json)
echo "Testing database connection to: $DB_HOST"
nc -z "$DB_HOST" 3306 || echo "Database connection test failed"
echo "Output testing complete"
Key Takeaways
- Outputs expose important information about your infrastructure
- Use descriptive names and comprehensive descriptions
- Organize outputs logically by purpose or component
- Mark sensitive outputs appropriately
- Outputs enable integration between configurations and modules
- Use outputs to create self-documenting infrastructure
- Test outputs to ensure they provide correct information
- Structure outputs for easy consumption by scripts and other tools
Next Steps
- Complete Tutorial 9: Store Remote State
- Learn about local values and expressions
- Explore modules and their input/output patterns
- Practice with complex output scenarios