DEV Community

Cover image for Terraform outputs.tf Explained: What It Is, When to Use It, and When to Skip It
Tanseer for AWS Community Builders

Posted on

Terraform outputs.tf Explained: What It Is, When to Use It, and When to Skip It

Who This Is For

If you are learning Terraform and have come across outputs.tf in project structures or tutorials but are not fully clear on what it actually does or why it exists, this blog is for you.

We will start from the basics and build up to how outputs work across modules, what they look like in practice, and the rules around where they can and cannot be used. Every term will be explained along the way.


What Is an Output in Terraform?

When Terraform creates infrastructure, it stores everything it knows about that infrastructure in a file called the state file. The state file is Terraform's database. It tracks every resource, every attribute, every ID — everything.

But the state file is not something you read directly. It is large, it is internal, and most of the values in it are not things you care about day to day.

Outputs are a way to surface specific values from that state file — the ones you actually need. You decide what gets exposed. Everything else stays internal.

A good way to think about it:

State file = the entire database.

Outputs = a view on top of that database that exposes only what you chose to make visible.

Outputs do not add new information. They do not change what Terraform tracks. They simply choose what to surface from what already exists.


The Three Contexts Where Outputs Are Used

Outputs are not just for one purpose. They serve three distinct purposes depending on where you use them. Understanding the difference between these three will make the entire concept click.


Context 1: Passing Values Between Modules in the Same Project

This is the most common use case, and the one you will use in almost every real Terraform project.

Imagine your project has two modules: a database module and a backend module. The database module creates an RDS instance and stores credentials in AWS Secrets Manager. The backend module is a Lambda function that needs to connect to that database. To do that, it needs the ARN (unique identifier) of the secret.

But here is the problem. Module internals are private. A resource defined inside modules/database/main.tf is not visible anywhere outside that module. It is intentionally hidden. You cannot just reference it from another module directly.

This is where outputs.tf comes in. The database module uses outputs.tf to explicitly expose the values it wants to share. The backend module then receives those values as input variables.

The wiring between them happens in your environment file, for example environments/dev/main.tf. That file is the connection layer. It takes what one module exposes and passes it as input to another.

The flow looks like this:

modules/database/outputs.tf
        |
        v
environments/dev/main.tf   (the wiring layer)
        |
        v
modules/backend/variables.tf
Enter fullscreen mode Exit fullscreen mode

Here is what that looks like in code:

# modules/database/outputs.tf
output "secret_arn" {
  value = aws_secretsmanager_secret.db_credentials.arn
}

output "secret_name" {
  value = aws_secretsmanager_secret.db_credentials.name
}
Enter fullscreen mode Exit fullscreen mode
# environments/dev/main.tf
module "database" {
  source = "../../modules/database"
}

module "backend" {
  source     = "../../modules/backend"
  secret_arn  = module.database.secret_arn
  secret_name = module.database.secret_name
}
Enter fullscreen mode Exit fullscreen mode
# modules/backend/variables.tf
variable "secret_arn" {}
variable "secret_name" {}
Enter fullscreen mode Exit fullscreen mode

The database module is a black box. variables.tf is what it accepts as input. main.tf is its internal implementation. outputs.tf is what it hands back out. Nothing inside the module leaks out unless you explicitly put it in outputs.tf.


Context 2: Displaying Values in the Terminal After Apply

Outputs defined in the root module, meaning the environment folder like environments/dev, are printed to the terminal after terraform apply completes.

This is purely for you as the person running the deployment. It is a convenience feature so you do not have to go hunting through the AWS console for values you commonly need.

Good candidates for terminal outputs are things like:

The API Gateway URL your backend is reachable at.

The Amplify URL where your frontend is deployed.

The RDS endpoint for your database.

# environments/dev/outputs.tf
output "api_gateway_url" {
  value = module.backend.api_url
}

output "amplify_url" {
  value = module.frontend.app_url
}
Enter fullscreen mode Exit fullscreen mode

After apply, these print cleanly to your terminal:

Outputs:

api_gateway_url = "https://abc123.execute-api.ap-south-1.amazonaws.com/dev"
amplify_url     = "https://main.abc123.amplifyapp.com"
Enter fullscreen mode Exit fullscreen mode

No digging through the console. The values are right there.


Context 3: Sharing Values Across Completely Separate Terraform Projects

Sometimes infrastructure is split across multiple completely separate Terraform projects, each with their own state file. A networking team might manage VPCs in one project. An application team might manage Lambda functions in another. The application team needs the VPC ID from the networking project.

This is handled using something called terraform_remote_state. It is a data source (a way to read information from outside the current project) that reads another project's state file and exposes its outputs.

data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "company-tfstate"
    key    = "networking/terraform.tfstate"
    region = "ap-south-1"
  }
}

# Now you can use:
data.terraform_remote_state.networking.outputs.vpc_id
Enter fullscreen mode Exit fullscreen mode

One important detail here: terraform_remote_state can only access values that were explicitly defined as outputs in the other project. It cannot reach into the raw state and pull arbitrary values. If the networking team did not put the VPC ID in their outputs.tf, you cannot get it from here.

This reinforces the same rule: outputs are the only gateway for values to leave a Terraform boundary, whether that boundary is a module or an entire separate project.

This pattern does not apply if your entire infrastructure lives in one root, which is the case for smaller projects. But it is a real and widely used pattern in team environments.


Where Outputs Cannot Be Used

This is important. Outputs work during the plan and apply phase, when Terraform is evaluating your configuration and building infrastructure. But not every file in your project is evaluated at that phase.

Two files are evaluated earlier, during the init phase, before any HCL evaluation happens:

backend.tf — This is where you configure where Terraform stores its state file, for example an S3 bucket. Terraform reads this file during terraform init, before it knows anything about your resources or variables. Everything here must be a hardcoded string. You cannot reference an output, a variable, or a local.

providers.tf — This is where you configure your cloud provider, for example the AWS region and profile. This is also read during init. Same restriction. Hardcode everything here.

Trying to use a variable or output in either of these files will cause Terraform to throw an error, and the reason is always the same: those values are not available yet at init time.

Here is a simple way to remember the two phases:

Init phase reads backend.tf and providers.tf statically. Downloads providers. Sets up the backend. No HCL logic allowed.

Plan and Apply phase evaluates everything else. Outputs, variables, locals, data sources, resource references — all of this works here.


Why You Cannot Just Skip outputs.tf and Read the State Directly

A common question when learning this is: the state file already has all the values, why do I need outputs at all?

There are two reasons.

Within a module, resources are private by design. Terraform intentionally encapsulates module internals so that modules are independent and reusable. You cannot reach into modules/database/main.tf from environments/dev/main.tf and reference a resource directly. The module must explicitly hand that value out through outputs.tf. This is the same reason functions in a programming language return values instead of letting callers read internal variables directly.

Across separate roots, terraform_remote_state only exposes outputs. There is no mechanism to read arbitrary resource attributes from another project's state file. Outputs are the only thing that crosses that boundary.


When outputs.tf Is Optional

Not every module needs an outputs.tf. The rule is straightforward.

If nothing downstream needs a value from your module, there is nothing to expose. The outputs file is optional.

A common example is a frontend module in a project. If the frontend module deploys an Amplify app and no other module needs the Amplify URL as an input, then outputs.tf in the frontend module is not needed. The URL might still be shown in the terminal via the root module's outputs, but the frontend module itself does not need to expose anything.

The question to ask before adding any output is:

Who is the consumer of this value, and how will they read it?

If the answer is nobody, the output does not belong. Adding outputs that nothing consumes is just noise in your codebase.


A Quick Reference

Here is everything in one place:

Outputs between modules: use outputs.tf in the source module, wire it in the environment's main.tf, receive it in the destination module's variables.tf.

Outputs in the terminal: define them in the root module's outputs.tf. They print after terraform apply.

Outputs across separate roots: use terraform_remote_state to read another project's outputs from its remote state file.

Outputs do not work in backend.tf or providers.tf because those are read during init before HCL evaluation.

You cannot skip outputs.tf because module internals are private and remote state only exposes outputs.

outputs.tf is optional when nothing downstream needs the module's values.


Conclusion

Outputs are one of those concepts in Terraform that seem small but are actually the backbone of how information flows through your infrastructure code. Once you understand that modules are black boxes and outputs are the only thing that leaves them, the entire system starts to make sense.

Before adding any output, always ask who the consumer is. That one question will keep your outputs intentional, your modules clean, and your project easy to follow.


Need Help?

If you are learning Terraform and have questions about project structure, modules, state management, or anything else, feel free to reach out.

Email me at khantanseer43@gmail.com


Top comments (0)