Loading Now

Managing Infrastructure Data with Terraform Outputs

Managing Infrastructure Data with Terraform Outputs

The outputs in Terraform may seem straightforward initially, yet they become essential as your infrastructure expands beyond just a few resources. These outputs link your Terraform modules to the wider infrastructure ecosystem, enabling you to retrieve crucial details such as IP addresses, database connection strings, or load balancer endpoints for use in other parts of your infrastructure or applications. In this article, we will explore Terraform outputs—from elementary usage to more sophisticated patterns, common challenges you may encounter, and how to incorporate these outputs into extensive infrastructure workflows.

Understanding Terraform Outputs

Terraform outputs act as return values for your configurations. When you execute terraform apply, any specified outputs will be shown in your terminal and stored in the Terraform state file. Unlike variables that send data into your configuration, outputs retrieve data from your resources post-creation or modification.

The basic syntax follows this simple structure:

output "example_output" {
  description = "Explanation of this output"
  value       = resource.example.attribute
  sensitive   = false
}

Terraform processes outputs during the planning stage but only reveals them after a successful apply. The values are recorded in your state file and can be requested later using the terraform output commands or accessed by other Terraform configurations via remote state data sources.

It’s vital to note that outputs are assessed in order of dependency. If your output refers to a resource dependent on others, Terraform ensures those prerequisites are established first, making outputs dependable for sharing data throughout your infrastructure stack.

Step-by-Step Implementation Guide

Let’s begin with a practical illustration that showcases outputs in action. We will set up a basic web server infrastructure and extract valuable information:

# main.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-west-2a"
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public.id

  tags = {
    Name = "web-server"
  }
}

# outputs.tf
output "vpc_id" {
  description = "Identifier of the VPC"
  value       = aws_vpc.main.id
}

output "instance_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}

output "instance_private_ip" {
  description = "Private IP of the web server"
  value       = aws_instance.web.private_ip
}

output "ssh_connection_command" {
  description = "SSH command to access the instance"
  value       = "ssh -i your-key.pem ec2-user@${aws_instance.web.public_ip}"
}

After executing terraform apply, you will receive output similar to the following:

Outputs:

instance_private_ip = "10.0.1.45"
instance_public_ip = "54.202.123.45"
ssh_connection_command = "ssh -i your-key.pem [email protected]"
vpc_id = "vpc-0123456789abcdef0"

For more intricate scenarios, outputs can be derived from computed values:

output "server_info" {
  description = "All-inclusive server details"
  value = {
    id         = aws_instance.web.id
    public_ip  = aws_instance.web.public_ip
    private_ip = aws_instance.web.private_ip
    az         = aws_instance.web.availability_zone
    type       = aws_instance.web.instance_type
  }
}

output "database_connection_string" {
  description = "Database connection string"
  value       = "postgresql://${aws_db_instance.main.username}:${var.db_password}@${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
  sensitive   = true
}

To retrieve outputs post-deployment, use the following commands:

# Display all outputs
terraform output

# Display a specific output
terraform output instance_public_ip

# Display output in JSON
terraform output -json

# Display sensitive outputs (requires explicit flag)
terraform output -json database_connection_string

Real-World Use Cases and Examples

Outputs are particularly useful in modular infrastructure designs. Below is a common scenario where a networking module supplies VPC data to an application module:

# modules/networking/main.tf
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
}

# modules/networking/outputs.tf
output "vpc_id" {
  description = "VPC Identifier for other modules"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "Array of private subnet IDs"
  value       = aws_subnet.private[*].id
}

output "vpc_cidr_block" {
  description = "CIDR block of the VPC"
  value       = aws_vpc.main.cidr_block
}

Then utilize these outputs in your application module:

# modules/application/main.tf
resource "aws_security_group" "app" {
  name   = "app-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr_block]
  }
}

resource "aws_instance" "app" {
  count                  = 2
  ami                    = var.ami_id
  instance_type          = "t3.medium"
  subnet_id              = var.private_subnet_ids[count.index]
  vpc_security_group_ids = [aws_security_group.app.id]
}

In your root configuration:

# main.tf
module "networking" {
  source = "./modules/networking"
  
  vpc_cidr               = "10.0.0.0/16"
  private_subnet_cidrs   = ["10.0.1.0/24", "10.0.2.0/24"]
  availability_zones     = ["us-west-2a", "us-west-2b"]
}

module "application" {
  source = "./modules/application"
  
  vpc_id              = module.networking.vpc_id
  vpc_cidr_block      = module.networking.vpc_cidr_block
  private_subnet_ids  = module.networking.private_subnet_ids
  ami_id              = "ami-0c02fb55956c7d316"
}

Another advantageous use case is generating configuration files for external tools, such as creating an Ansible inventory file:

output "ansible_inventory" {
  description = "Ansible inventory file content"
  value = templatefile("${path.module}/inventory.tpl", {
    web_servers = aws_instance.web[*].public_ip
    db_servers  = aws_instance.db[*].private_ip
  })
}
# inventory.tpl
[web]
%{ for ip in web_servers ~}
${ip}
%{ endfor ~}

[database]
%{ for ip in db_servers ~}
${ip}
%{ endfor ~}

Remote State and Cross-Stack Communication

One of the primary advantages of outputs is their ability to share data between distinct Terraform configurations using remote state. This approach is indispensable for large organisations where different teams oversee various aspects of the infrastructure.

Begin by setting up remote state in your foundational stack:

# foundation/main.tf
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "foundation/terraform.tfstate"
    region = "us-west-2"
  }
}

resource "aws_vpc" "shared" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_route53_zone" "internal" {
  name = "internal.company.com"
  vpc {
    vpc_id = aws_vpc.shared.id
  }
}

# foundation/outputs.tf
output "shared_vpc_id" {
  description = "Shared VPC ID for application stacks"
  value       = aws_vpc.shared.id
}

output "internal_zone_id" {
  description = "ID of the internal DNS zone"
  value       = aws_route53_zone.internal.zone_id
}

output "vpc_cidr" {
  description = "CIDR block of the VPC"
  value       = aws_vpc.shared.cidr_block
}

Then access these outputs in your application stack:

# application/main.tf
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "application/terraform.tfstate"
    region = "us-west-2"
  }
}

data "terraform_remote_state" "foundation" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key    = "foundation/terraform.tfstate"
    region = "us-west-2"
  }
}

resource "aws_instance" "app" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.app.id
}

resource "aws_subnet" "app" {
  vpc_id     = data.terraform_remote_state.foundation.outputs.shared_vpc_id
  cidr_block = "10.0.10.0/24"
}

resource "aws_route53_record" "app" {
  zone_id = data.terraform_remote_state.foundation.outputs.internal_zone_id
  name    = "app.internal.company.com"
  type    = "A"
  ttl     = 300
  records = [aws_instance.app.private_ip]
}

Advanced Output Patterns and Techniques

For complicated infrastructures, you may need to manipulate and format output data. Terraform’s built-in functions simplify this process:

# Advanced output examples
output "instance_map" {
  description = "Mapping of instance names to IPs"
  value = {
    for instance in aws_instance.cluster :
    instance.tags.Name => {
      public_ip  = instance.public_ip
      private_ip = instance.private_ip
      az         = instance.availability_zone
    }
  }
}

output "load_balancer_targets" {
  description = "Configuration for load balancer target group"
  value = [
    for instance in aws_instance.web : {
      id   = instance.id
      ip   = instance.private_ip
      port = 80
    }
  ]
}

output "environment_config" {
  description = "All-encompassing environment configuration"
  value = {
    vpc = {
      id         = aws_vpc.main.id
      cidr       = aws_vpc.main.cidr_block
      dns_domain = aws_route53_zone.private.name
    }
    database = {
      endpoint = aws_rds_instance.main.endpoint
      port     = aws_rds_instance.main.port
      name     = aws_rds_instance.main.db_name
    }
    cache = {
      endpoint = aws_elasticache_cluster.main.cache_des[0].address
      port     = aws_elasticache_cluster.main.cache_des[0].port
    }
  }
}

output "kubeconfig" {
  description = "Kubernetes configuration file"
  value = templatefile("${path.module}/kubeconfig.tpl", {
    cluster_name     = aws_eks_cluster.main.name
    cluster_endpoint = aws_eks_cluster.main.endpoint
    cluster_ca       = aws_eks_cluster.main.certificate_authority[0].data
  })
  sensitive = true
}

For outputs that might not always exist, leverage conditional expressions:

output "database_endpoint" {
  description = "Database endpoint if enabled"
  value       = var.enable_database ? aws_rds_instance.main[0].endpoint : null
}

output "ssl_certificate_arn" {
  description = "ARN of the SSL certificate"
  value       = var.use_ssl ? aws_acm_certificate.main[0].arn : "SSL not enabled"
}

Integration with External Systems

Outputs become significantly more powerful when linked with external systems. Here are some prevalent integration patterns:

Generating configuration for monitoring tools:

output "prometheus_targets" {
  description = "Configuration for Prometheus scrape targets"
  value = yamlencode({
    static_configs = [{
      targets = [
        for instance in aws_instance.web :
        "${instance.private_ip}:9090"
      ]
      labels = {
        environment = var.environment
        service     = "web"
      }
    }]
  })
}

output "grafana_dashboard_vars" {
  description = "Variables for Grafana dashboards"
  value = jsonencode({
    environment = var.environment
    region      = var.aws_region
    vpc_id      = aws_vpc.main.id
    instances   = [
      for instance in aws_instance.web : {
        id   = instance.id
        name = instance.tags.Name
        az   = instance.availability_zone
      }
    ]
  })
}

Creating DNS records for service discovery:

output "consul_service_config" {
  description = "Configuration for Consul service discovery"
  value = {
    services = [
      for instance in aws_instance.web : {
        name    = "web-service"
        address = instance.private_ip
        port    = 80
        tags    = ["web", var.environment]
        checks = [{
          http     = "http://${instance.private_ip}/health"
          interval = "10s"
        }]
      }
    ]
  }
}

Common Pitfalls and Troubleshooting

Even seasoned Terraform practitioners may encounter output-related issues. Below are common problems and their remedies:

Circular Dependencies: This occurs when you attempt to output a value depending on a resource that subsequently references the output. Terraform will flag this during the planning phase:

# This results in a circular dependency
resource "aws_security_group" "web" {
  name = "web-sg"
  
  ingress {
    cidr_blocks = [data.terraform_remote_state.network.outputs.admin_cidr]
  }
}

# Should the network stack attempt to output something dependent on this security group
# you will face: "Cycle: output.admin_cidr, aws_security_group.web"

Solution: Break the cycle by reorganising your dependencies or utilising separate Terraform operations.

Sensitive Data Exposure: Outputs containing sensitive data will appear in plain text unless designated as sensitive:

# Incorrect - password visible in terraform output
output "database_password" {
  value = random_password.db_password.result
}

# Correct - password concealed but still programmatically accessible
output "database_password" {
  value     = random_password.db_password.result
  sensitive = true
}

Type Mismatches in Remote State: Type mismatches may lead to puzzling errors when consuming outputs from remote state:

# If the remote state outputs a string but you expect a list
resource "aws_instance" "web" {
  count                  = length(data.terraform_remote_state.network.outputs.subnet_ids)
  subnet_id              = data.terraform_remote_state.network.outputs.subnet_ids[count.index]
  vpc_security_group_ids = [data.terraform_remote_state.network.outputs.security_group_id]
}

Utilise the terraform console command to check output types:

$ terraform console
> data.terraform_remote_state.network.outputs.subnet_ids
[
  "subnet-12345",
  "subnet-67890"
]
> type(data.terraform_remote_state.network.outputs.subnet_ids)
tuple([
  string,
  string,
])

Stale Remote State: Outputs from remote state can become outdated if the source stack hasn’t been recently applied. Always ensure the source stack is current when troubleshooting cross-stack issues.

Large Output Values: Terraform enforces limits on output sizes. For very substantial outputs (like full configuration files), consider saving to a file and outputting only the file path:

resource "local_file" "large_config" {
  content = templatefile("${path.module}/large_config.tpl", {
    instances = aws_instance.cluster[*]
    # ... numerous variables
  })
  filename = "${path.module}/generated/cluster_config.json"
}

output "config_file_path" {
  description = "Path to the generated configuration file"
  value       = local_file.large_config.filename
}

Comparison with Alternative Approaches

Although Terraform outputs are the conventional method for extracting infrastructure data, understanding alternatives and their appropriate use cases is advisable:

Method Optimal For Advantages Disadvantages
Terraform Outputs Standard inter-stack communication Natively integrated with Terraform, type-safe, stored in state Requires remote state configuration, limited by state backend
AWS Parameter Store Cross-service communication in AWS Encrypted storage, detailed permissions, versioned Exclusive to AWS, necessitates additional API calls
Consul KV Store Multi-cloud service discovery Real-time updates, decentralised, TTL support Additional infrastructure requirements, operational complexity
External Data Sources Querying existing infrastructure Real-time data, compatible with any API Performance impact, external dependencies

An example employing AWS Parameter Store as an alternative to outputs is as follows:

# Store infrastructure data in Parameter Store
resource "aws_ssm_parameter" "vpc_id" {
  name  = "/infrastructure/vpc/id"
  type  = "String"
  value = aws_vpc.main.id
  
  tags = {
    ManagedBy = "terraform"
    Stack     = "foundation"
  }
}

resource "aws_ssm_parameter" "database_endpoint" {
  name  = "/infrastructure/database/endpoint"
  type  = "SecureString"
  value = aws_rds_instance.main.endpoint
  
  tags = {
    ManagedBy = "terraform"
    Stack     = "foundation"
  }
}

# Consume in another stack
data "aws_ssm_parameter" "vpc_id" {
  name = "/infrastructure/vpc/id"
}

resource "aws_subnet" "app" {
  vpc_id     = data.aws_ssm_parameter.vpc_id.value
  cidr_block = "10.0.20.0/24"
}

Best Practices and Performance Considerations

Adhere to these guidelines to enhance the maintainability and performance of your outputs:

  • Employ descriptive names and descriptions: This will assist your future self and colleagues when resolving issues across stacks.
  • Group related outputs: Instead of creating several individual outputs, use objects to consolidate related values.
  • Designate sensitive outputs correctly: Even data that may not seem sensitive at present should be considered for potential sensitivity in the future.
  • Document output structures: For complex object outputs, elucidate the expected format in comments or separate documentation.
  • Version your output structures: When altering output formats, consider backward compatibility or provide migration instructions.
# Well-structured: Grouped, documented output
output "database_config" {
  description = <<-EOT
    Database configuration object that includes:
    - endpoint: Endpoint for the RDS instance (string)
    - port: Port for the database (number)
    - name: Name of the database (string)
    - security_group_id: ID of the database security group (string)
  EOT
  
  value = {
    endpoint          = aws_rds_instance.main.endpoint
    port              = aws_rds_instance.main.port
    name              = aws_rds_instance.main.db_name
    security_group_id = aws_security_group.db.id
  }
}

# Poor practice: Dispersed outputs
output "db_endpoint" { value = aws_rds_instance.main.endpoint }
output "db_port" { value = aws_rds_instance.main.port }
output "db_name" { value = aws_rds_instance.main.db_name }
output "db_sg" { value = aws_security_group.db.id }

In terms of performance, be mindful that outputs are evaluated with each plan/apply execution. Complex template functions or external data sources included in outputs may degrade your Terraform execution speed. Pre-calculate complex values in locals when feasible:

locals {
  server_config = {
    for instance in aws_instance.cluster :
    instance.tags.Name => {
      ip = instance.private_ip
      az = instance.availability_zone
      # Intensive computation here
      health_check_url = "https://${instance.private_ip}:8443/health?token=${random_password.health_token.result}"
    }
  }
}

output "server_config" {
  description = "Pre-calculated server configuration"
  value       = local.server_config
  sensitive   = true
}

Remember that outputs are an instrumental tool for constructing manageable, modular infrastructures. They lay the groundwork for creating reusable Terraform modules and facilitate team collaboration on extensive infrastructure projects. The key is to start simply with fundamental outputs and progressively adopt more advanced patterns as your infrastructure complexity increases.

For comprehensive details about Terraform outputs, refer to the official Terraform documentation as well as explore the Terraform Registry for practical examples within community modules.



This article combines insights and resources from a range of online platforms. We extend our gratitude toward all the original authors, publishers, and websites. Though diligent efforts have been made to credit the source material appropriately, any unintended omission does not imply copyright infringement. All trademarks, logos, and images mentioned belong to their respective owners. Should you believe any content within this article violates your copyright, please promptly notify us for review and appropriate action.

This article is constructed for informational and educational purposes and does not infringe upon the rights of copyright proprietors. If any copyrighted materials have been utilised without proper attribution or in violation of copyright regulations, it is unintentional and will be corrected as soon as we are informed.
Please note that the republication, redistribution, or reproduction of any part or all of the contents in any format is barred without explicit written consent from the author and website owner. For permissions or further inquiries, please reach out to us.