Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 007 / 100

Terraform outputs: a Terraform & IaC guide for developers

Share values between configurations and people.

Expona Admin8 min read

Why Terraform outputs matter

Outputs are how Terraform lets you extract and share values from your infrastructure once it's been created. Whether you're passing a database endpoint to your application team, capturing load balancer URLs for DNS records, or connecting multiple infrastructure stacks together, Terraform outputs: a Terraform & IaC guide for developers will show you how to do this reliably and securely. Without outputs, you'd be manually hunting through the AWS console for resource IDs and connection strings—a practice that breaks at scale.

In this guide, we'll walk through how to expose IDs, URLs and ARNs, mark sensitive data appropriately, retrieve output values, and wire stacks together through remote state. By the end, you'll have a pattern you can apply across all your infrastructure code.

Expose IDs, URLs and ARNs

The most common use case for outputs is surfacing the identifiers and endpoints that other parts of your system need to know about. Think of an RDS database instance: your application needs the endpoint address. A Lambda function needs an S3 bucket name. A microservice needs the ARN of an IAM role.

Let's say we've just created an RDS database and want to share its endpoint with the team:

hcl
resource "aws_db_instance" "primary" {
  identifier     = "production-postgres"
  engine         = "postgres"
  engine_version = "15.3"
  instance_class = "db.t3.medium"
  allocated_storage = 100
  
  db_name  = "appdb"
  username = "postgres"
  password = random_password.db_password.result
  
  skip_final_snapshot = false
  final_snapshot_identifier = "production-postgres-final-snapshot"
}

output "database_endpoint" {
  description = "RDS endpoint for application configuration"
  value       = aws_db_instance.primary.endpoint
}

output "database_port" {
  description = "RDS port"
  value       = aws_db_instance.primary.port
}

output "database_name" {
  description = "Database name"
  value       = aws_db_instance.primary.db_name
}

When you run terraform apply, Terraform will display these outputs at the end. The team can then grab the endpoint and port for their connection strings. Notice we're keeping the actual password separate—we'll cover that in the next section.

Here's another practical example with an API Gateway and Lambda:

hcl
resource "aws_api_gateway_deployment" "api" {
  rest_api_id = aws_api_gateway_rest_api.main.id
  stage_name  = "prod"
}

output "api_base_url" {
  description = "Base URL of the deployed API"
  value       = "${aws_api_gateway_deployment.api.invoke_url}/v1"
}

output "api_gateway_id" {
  description = "API Gateway ID for cross-stack reference"
  value       = aws_api_gateway_rest_api.main.id
}

We exposed both the full URL for developers to test with, and the API ID for other stacks that might need to add more resources to the same gateway.

Mark secrets as sensitive

Outputs aren't just for URLs and IDs. Sometimes you need to output credentials, API keys, or connection strings that contain secrets. Terraform has a built-in mechanism to prevent these from being logged or displayed unnecessarily: the sensitive attribute.

When you mark an output as sensitive, Terraform will redact it from the console output and from state file displays—though it's still stored in the state file itself, so guard that carefully.

hcl
resource "random_password" "db_password" {
  length  = 32
  special = true
}

resource "aws_db_instance" "primary" {
  identifier     = "production-postgres"
  engine         = "postgres"
  engine_version = "15.3"
  instance_class = "db.t3.medium"
  allocated_storage = 100
  
  db_name  = "appdb"
  username = "postgres"
  password = random_password.db_password.result
  
  skip_final_snapshot = false
  final_snapshot_identifier = "production-postgres-final-snapshot"
}

output "database_password" {
  description = "Database password - store in secrets manager"
  value       = random_password.db_password.result
  sensitive   = true
}

output "database_connection_string" {
  description = "Full connection string for app"
  value       = "postgres://postgres:${random_password.db_password.result}@${aws_db_instance.primary.endpoint}/appdb"
  sensitive   = true
}

With sensitive = true, running terraform apply won't print these values to the terminal. You'll see <sensitive> instead. However, you can still retrieve them programmatically using terraform output, which we'll cover next. This pattern keeps secrets out of logs and CI/CD console output while still making them accessible to the systems that need them.

Read with terraform output

Once your infrastructure is deployed, you need a way to retrieve those outputs so you can use them. The terraform output command does exactly this. It reads your state file and prints the current value of any output.

From the command line, you can grab a single output:

hcl
terraform output database_endpoint

This is useful for scripts. In a CI/CD pipeline, you might do something like:

hcl
DB_ENDPOINT=$(terraform output -raw database_endpoint)
DB_PORT=$(terraform output -raw database_port)
echo "Host=$DB_ENDPOINT; Port=$DB_PORT" > app-config.env

The -raw flag strips the quotes and newlines, giving you just the value. Without it, Terraform formats the output as JSON.

You can also get all outputs as JSON:

hcl
terraform output -json

This is handy when you need to pass multiple values downstream or log them for debugging. Just remember: if an output is marked sensitive = true, it will show as <sensitive> in the console but the actual value is still in the state file. Never commit your state file to version control.

Other stacks read outputs via remote state

One of the most powerful patterns in infrastructure-as-code is the ability to have one Terraform stack produce outputs that another stack consumes. This keeps your code modular and loosely coupled.

Imagine you have a network stack that creates a VPC, and an application stack that needs to deploy into that VPC. The network stack outputs the subnet IDs; the application stack reads them via terraform_remote_state.

First, the network stack (in its own directory, with its own state file) outputs the subnet IDs:

hcl
# networking/main.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = {
    Name = "prod-vpc"
  }
}

resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  
  tags = {
    Name = "prod-private-subnet-${count.index + 1}"
  }
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

The networking stack stores its state in an S3 backend. Now the application stack reads those outputs:

hcl
# application/main.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/application/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-locks"
  }
}

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key    = "prod/networking/terraform.tfstate"
    region = "eu-west-1"
  }
}

resource "aws_instance" "app_server" {
  count           = 2
  ami             = data.aws_ami.ubuntu.id
  instance_type   = "t3.small"
  subnet_id       = data.terraform_remote_state.network.outputs.private_subnet_ids[count.index]
  security_groups = [aws_security_group.app.id]
  
  tags = {
    Name = "app-server-${count.index + 1}"
  }
}

output "instance_ids" {
  value = aws_instance.app_server[*].id
}

The key line is data.terraform_remote_state.network.outputs.private_subnet_ids. This pulls the remote state file from S3 and accesses the outputs directly. If the networking team updates the VPC and applies changes, the application stack can immediately reference the new subnet IDs on its next run.

This pattern scales well. You might have separate stacks for databases, caching, networking, and each application. Each one reads the outputs it needs from its dependencies via remote state. The result is infrastructure that's modular, testable, and easy to reason about.

Common mistakes

  • Forgetting to commit your outputs to version control. Store your Terraform code in Git, but not your state files. Document your outputs in comments or a README so the team knows what's available.
  • Outputting secrets without marking them sensitive. If you output a password or API key without sensitive = true, it will be printed to console and logged in CI/CD. Always use the sensitive flag for any credential.
  • Assuming outputs persist after destroy. Once you run terraform destroy, those outputs are gone. If you need them later, you'll need to restore from backups or re-read them from the destroyed resources' snapshots.
  • Not validating remote state paths. When using terraform_remote_state, a typo in the S3 key or region will fail silently or give confusing errors. Double-check your paths and test in a non-production environment first.
  • Mixing manual changes with Terraform outputs. If someone modifies a resource outside of Terraform (via the console or API), the outputs will still reflect the old values until you run terraform refresh or terraform apply.

Key takeaways

  • Outputs expose resource IDs, URLs, and ARNs so other parts of your system can reference them without manual lookup.
  • Mark outputs containing passwords, API keys, or other secrets with sensitive = true to prevent them from being displayed in logs or console output.
  • Use terraform output to retrieve values programmatically in scripts and CI/CD pipelines; -raw strips formatting for easier parsing.
  • Connect multiple Terraform stacks by using data.terraform_remote_state to read outputs from one stack into another, enabling modular and loosely coupled infrastructure code.
  • Store state files in remote backends (S3 with locking) and treat them as sensitive data; never commit them to version control.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles