Terraform Outputs Explained

Published:
August 13, 2026

Terraform outputs let you expose values from your configuration so you can display them after deployment, pass them to parent modules, or use them in automation workflows.

Outputs allow you to expose some of these values outside the module where they are defined. You can display them in the terminal, pass them to another module, or use them in scripts and automated tasks.

This article will explain how Terraform outputs work and how to use them when provisioning and managing  Bare Metal Cloud infrastructure.

A detailed explanation of Terraform Outputs.

What Are Terraform Output Values?

Outputs are named values a Terraform module makes available to users, parent modules, or external tools.

A value from the module becomes an output when you reference it in the value argument of an output block. Values from these sources are usually used as outputs:

  • Managed resources. Terraform providers return attributes for the infrastructure that Terraform creates and manages. Useful values you can expose as outputs include hostnames, resource IDs, IPs, and subnets.
  • Data sources. Providers use data sources to look up and return information about existing infrastructure. This includes network IDs, application image IDs, or provider account details, all of which can be exposed as outputs.
  • Input variables. These variables are passed into a Terraform module through a .tfvars file or directly by a user, for example, with a terminal command. Values such as the server location, operating system, server type, or environment name can also be made available as outputs.
  • Local values. Local values are defined inside the configuration and can combine or organize other values into data Terraform can reuse. They are often used for standardized server names, labels, or reorganized lists and maps.
  • Child module outputs. A child module can make values available to the module that calls it. These outputs usually include server IDs, lists of IP addresses, load balancer addresses, or network IDs.
  • Terraform expressions. Terraform can calculate new values during a run using functions, conditions, arithmetic, or for expressions. For example, you might create maps that pair server names with IP addresses or resource counts and expose them as outputs.
  • Literal values. These are fixed values written directly in an output block, such as environment names or deployment messages.
  • Terraform information. Terraform keeps track of details about the current configuration, like workspace names, module paths, or the root directory path. These details can also be exposed as outputs.

You usually define output blocks in a separate outputs.tf file within your Terraform module. After you declare a value as an output, other external systems or modules can use them outside the module where you defined them.

Output Block Syntax

You can declare an output using an output block. For most basic outputs, you only need a label, a description, and a value:

output "public_ip_addresses" {
  description = "A list of public IP addresses assigned to the server"
  value = pnap_server.server.public_ip_addresses
}

This example block contains the following elements:

  • output. Tells Terraform that the block defines an output value.
  • public_ip_addresses. This is the name (label) of the output. Terraform uses this name when you reference the output from another module or run a command like terraform output public_ip_addresses.
  • description. An optional argument that explains what the output returns and how to use it.
  • value. Defines the expression that Terraform evaluates and returns. It is the only required argument in a basic output block.

Terraform output blocks also support several optional arguments, such as:

Argument or blockDescription
typeTells Terraform that the output value must use a specific data type.
sensitiveIf you set this argument to true, Terraform will not display the value in the standard CLI output.
ephemeralPrevents a child-module output from being stored in a plan or state file.
depends_onYou can use this argument to add a dependency to the block if Terraform does not detect one automatically.
deprecatedWarns users that a child-module output should no longer be used.
preconditionYou can add a requirement that must be met before Terraform exposes the output.

Note: If you are new to Terraform and unsure about block syntax and using optional arguments, learn the basics of HashiCorp Configuration Language (HCL).

Outputs in Infrastructure as Code

Without an output, a resource attribute can only be used within the same module. In an environment where you run your infrastructure as code, you need the information your configuration creates after the resource is deployed.

Outputs provide a clear way to choose which information you want to make available outside a module.

For example, if your configuration creates a new server, the deployment may finish successfully, but you still need the server's public IP address before you can connect to it. You could open the provider portal and look for the address manually, but an output value can display it immediately after you run the terraform apply command.

This makes output indispensable for:

  • Showing important deployment information to the person running Terraform.
  • Allowing a parent module to use values produced by a child module.
  • Passing infrastructure data to scripts and CI/CD pipelines.
  • Sharing selected root module values with another Terraform configuration through remote state.

Note: The examples in this guide use Terraform to manage phoenixNAP Bare Metal Cloud as infrastructure as code. If you want to automate Bare Metal Cloud deployments, see our Terraform provisioning guide.

Use Cases: CLI Display, Inter-Module Passing, and Automation

Some of the most common ways to use Terraform outputs in practice include:

Displaying Values in the CLI

After you run terraform apply, Terraform shows outputs from the root module in the terminal. You can also check the current output values anytime by running:

terraform output

This command reads the output values from the current state and lists all outputs from the root module. To display only one output, add its name:

terraform output public_ip_addresses

The terraform output command does not show outputs hidden inside child modules. To make a child module's output visible in the CLI, the root module needs to expose it with its own output block.

Passing Values Between Modules

A child module can use outputs to expose its resource attributes to the parent module. For example, a module named server may define this output:

output "server_id" {
  description = "The ID of the created server."
  value = pnap_server.server.id
}

The parent module can reference this output value using the following format:

module.[module_name].[output_name]

For this example, the reference would be:

module.server.server_id

The parent module can then pass the server ID to another module, use it in a resource, or make it available as a root output:

output "server_id" {
  description = "The ID returned by the server module."
  value = module.server.server_id
}

Outputs create a clear connection between modules, so the parent module does not need to know how the child module creates its resources.

Passing Values to Automation Tools

Scripts and CI/CD pipelines often need deployment results before moving to the next step. For example, a pipeline might need a new server's IP address before it can run a configuration script or perform a connectivity test.

Use the -json option for lists, maps, objects, and other complex values:

terraform output -json

This command returns all root module outputs in JSON format, which scripts can process more reliably than Terraform's regular output.

To get a single simple value without extra formatting, use the -raw option:

terraform output -raw server_id

The -raw option supports values that Terraform can convert directly to strings, including strings, numbers, and Boolean values.

Provisioning Bare Metal Cloud with Terraform Outputs

Bare Metal Cloud (BMC) instances are physical dedicated servers that give you direct access to hardware without virtualization. Many organizations use several BMC instances to handle demanding workloads or to separate different parts of their applications. Terraform makes it easier to set up and manage this kind of infrastructure.

This configuration uses the pnap_server resource to create a server. It sends values such as the hostname, operating system, server type, and location to Bare Metal Cloud:

resource "pnap_server" "server" {
  hostname = var.hostname
  os = var.os
  type = var.server_type
  location = var.location
  ssh_keys = var.ssh_keys
}

After the server is provisioned, Bare Metal Cloud returns additional details like the server ID and its public and private IP addresses. The pnap_server resource allows you to use these attributes in the rest of the configuration.

Exporting Provisioned BMC Server IDs and Public IP Addresses

Every Bare Metal Cloud server gets a unique ID when it is created. To expose this ID, use the following output:

output "bmc_server_id" {
  description = "The unique ID of the provisioned Bare Metal Cloud server."
  value = pnap_server.server.id
}

You can also expose all public IP addresses assigned to the server:

output "bmc_public_ip_addresses" {
  description = "A list of public IP addresses assigned to the Bare Metal Cloud server."
  value = pnap_server.server.public_ip_addresses
}

The first output returns the server ID as a string. The second output provides a list, since a server can have more than one public IP address. Terraform does not know the server's ID or IP until the server is actually created. Run the apply command to provision the server and get these outputs:

terraform apply

When the apply command finishes, Terraform displays the outputs:

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
 
Outputs:

bmc_public_ip_addresses = [ 
  "114.0.217.10", 
]

bmc_server_id = "76b1945567890defjam1989"

The address and ID shown above are just examples. The actual values come from the server created in your Bare Metal Cloud account. If some values are usually used together, you can group them into a single output:

output "bmc_server_details" { 
  description = "Main details of the provisioned Bare Metal Cloud server." 
  value = { 
    id = pnap_server.server.id 
    hostname = pnap_server.server.hostname 
    location = pnap_server.server.location 
    status = pnap_server.server.status 
    public_ip_addresses = pnap_server.server.public_ip_addresses 
    private_ip_addresses = pnap_server.server.private_ip_addresses 
  } 
}

Include only the attributes you actually need. This makes the output easier to read and helps you avoid sharing sensitive information by accident.

Returning BMC Network Subnets, Assigned VLANs, and Gateway Attributes

Outputs can also expose information about networks connected to a server.

Note: The current pnap_server schema includes a shared gateway address and separate details for public networks, private networks, and IP blocks.

In this example, the pnap_private_network data source returns details about an existing Bare Metal Cloud private network by using its ID:

variable "private_network_id" {
  description = "The ID of the private network assigned to the server."
  type = string
}

data "pnap_private_network" "application" {
  id = var.private_network_id
}

The data source provides information about the network, including its CIDR range and VLAN ID. The gateway information comes from the server's network configuration.

You can group these related values into one output or expose them separately, as in this example:

output "bmc_private_network_subnet" { 
  description = "The CIDR range of the Bare Metal Cloud private network." 
  value = data.pnap_private_network.application.cidr }
 
output "bmc_private_network_vlan_id" { 
  description = "The VLAN ID of the Bare Metal Cloud private network."
  value = data.pnap_private_network.application.vlan_id }

output "bmc_gateway_address" { 
  description = "The gateway address assigned to the Bare Metal Cloud server." 
  value = try( 
    pnap_server.server.network_configuration[0].gateway_address, null 
  ) 
}

In this example:

  • cidr. Identifies the network range.
  • vlan_id. Shows the VLAN assigned to the network.
  • gateway_address. Returns the gateway assigned to the provisioned server.

When you run terraform apply, Terraform displays the network information:

bmc_gateway_address = "10.20.30.1"
bmc_private_network_subnet = "10.20.30.0/24"
bmc_private_network_vlan_id = 120

The try function returns null if Terraform cannot get the gateway address.

Handling Bare Metal Server SSH Credentials and API Access Keys

Bare Metal Cloud servers use public SSH keys for access. Install the public key on your server, but keep the matching private key safe on your system or in a secure secrets manager.

When you manage a public SSH key through the pnap_ssh_key resource, you can expose useful information about the key without revealing any private credentials:

output "bmc_ssh_key_metadata" {
  description = "Details about the SSH public key used by the server."

  value = {
    id = pnap_ssh_key.admin.id
    name = pnap_ssh_key.admin.name
    fingerprint = pnap_ssh_key.admin.fingerprint
  }
}

The output only includes information about the public SSH key, like its ID, name, and fingerprint. For example:

bmc_ssh_key_metadata = { 
  "fingerprint" = "SHA256:AcanTLoupE908932isec" 
  "id" = "6jimmyjammy67890abef52345" 
  "name" = "admin-key" }

You need to be more careful with Bare Metal Cloud API credentials. The phoenixNAP provider can read the client ID and secret from environment variables:

export PNAP_CLIENT_ID="your-client-id"
export PNAP_CLIENT_SECRET="your-client-secret"

This way, the provider can use those credentials without putting them directly in the provider block:

provider "pnap" {}

Terraform does allow you to expose sensitive values as outputs, but you usually do not need to do this with provider credentials. Marking an output as sensitive only hides it from normal CLI output. Terraform still stores the value in the state file, so you should continue protecting state appropriately.

Using for Expressions to Restructure Multi-Server Bare Metal Metadata

When you use count or for_each, Terraform manages several server instances from the same resource block. In these cases, it is often easier to create one structured output instead of a separate output for every server.

For example, this resource creates multiple email servers:

variable "server_count" { 
  description = "The number of Bare Metal Cloud servers to create." 
  type = number 
  default = 2 
} 

resource "pnap_server" "server" { 
  count = var.server_count

  hostname = "email-server-${count.index + 1}" 
  os = var.os 
  type = var.server_type
  location = var.location 
  ssh_keys = var.ssh_keys
}

You can use a for expression to organize useful information about each server in one output:

output "bmc_servers" {
  description = "The provisioning details for my Bare Metal Cloud servers."
  value = {
     for server in pnap_server.server :
  server.hostname => {
     id = server.id
     location = server.location
     status = server.status
     public_ip_addresses = server.public_ip_addresses
     private_ip_addresses = server.private_ip_addresses
     }
  }
}

The hostname is used as the key for each server. Each key points to an object with the server's ID, location, status, and assigned IP addresses.

After you run terraform apply, the output would follow this general structure:

bmc_servers = { 

  "email-server-1" = { 
    id = "76b1945567890defjam1989" 
    location = "PHX" 
    status = "powered-on" 
    public_ip_addresses = ["114.0.33.20"] 
    private_ip_addresses = ["10.20.30.10"] 
  }
 
  "email-server-2" = { 
      id = "88b1945567890defrec1919" 
      location = "ASH" 
      status = "powered-on" 
      public_ip_addresses = ["114.0.113.22"] 
      private_ip_addresses = ["10.20.30.11"] 
  } 
}

This structure gives each server a clear name, making the output easier for both people and automation tools to use as you add more servers.

The value used as the map key must be unique. In the example, each hostname must be different. If two servers have the same hostname, Terraform cannot create a map for both and will return an error.

Extracting Outputs via Terraform CLI and CI/CD Pipelines

Once Terraform has access to output values, you can retrieve them from the command line using the terraform output command or pass them to other tools in a CI/CD pipeline.

Querying Outputs with terraform output Commands

The terraform output command lets you retrieve output values stored in the current Terraform state file. To display all outputs from the root module, enter:

terraform output

Terraform lists each output and its current value. To get a specific output, add its name to the command:

terraform output bmc_server_id

This is an example output:

"76b1945567890defjam1989"

You can use the same command for other outputs. For example, to retrieve the public IP addresses from the earlier example, enter:

terraform output bmc_public_ip_addresses

If you need to remove terminal colors from the results, use the -no-color option:

terraform output -no-color

This helps when you need to save the results to a text file or send them to a log.

The terraform output command only shows outputs defined in the root module. If you need a value from a child module, the root module must expose it first before you can get it with this command. For example, if a child module has an output named server_id, the root module can expose it like this:

output "bmc_server_id" {
  description = "The server ID returned by the BMC server module."
  value = module.server.server_id
}

After that, you can get the value as usual:

terraform output bmc_server_id

Note: Terraform commands are short and clearly describe what they do, but there are a lot of them. Keep this Terraform commands cheat sheet nearby when working in the terminal.

Extracting Raw and JSON Formatted Values for Automation Scripts

The default terraform output format is intended for humans reading the results in a terminal. For scripts and automation, you should use the -raw or -json options.

Use -raw if you need to display a single string, number, or Boolean value:

terraform output -raw bmc_server_id

Unlike the default output, this result does not have quotation marks:

76b1945567890defjam1989

This makes it easy to assign the value to a shell variable:

SERVER_ID="$(terraform output -raw bmc_server_id)"
echo "Provisioned server: $SERVER_ID"

The -raw option does not work with complex values like lists, maps, or objects. For those data types, use the -json option:

terraform output -json bmc_servers

You can also save the JSON output to a file:

terraform output -json bmc_servers > bmc-servers.json

Or you can send it straight to a JSON tool such as jq:

terraform output -json bmc_servers |
jq -r 'to_entries[] | "\(.key): \(.value.id)"'

This command prints the hostname and ID for each server. You can use the same method to get a value from a list. For example, the following commands save the first public IP address to a variable:

PRIMARY_IP="$(
  terraform output -json bmc_public_ip_addresses |
  jq -r '.[0]'
)"

Both -raw and -json can reveal sensitive output values in plain text. Do not print their results to unprotected logs or save them in files other users can access.

Troubleshooting Errors with Terraform Outputs

If Terraform runs into an issue, it displays an error message that tells you which block or expression is causing the problem.

Most output errors happen because of an incorrect reference, a value that is missing or is not in the right format, or a dependency that Terraform cannot find.

Resolving Sensitive Output Errors

Terraform returns an error if an output refers to a value that is already marked as sensitive somewhere in the configuration, but you have not marked it as sensitive in the output block. The error message looks like this:

Error: Output refers to sensitive values.

For example, this output block will cause the error if pnap_client_secret is marked as sensitive:

output "bmc_api_secret" {
  description = "The BMC API client secret."
  value = var.pnap_client_secret
}

To fix the issue, add the sensitive = true argument to the output block:

output "bmc_api_secret" {
  description = "The BMC API client secret."
  value = var.pnap_client_secret
  sensitive = true
}

Sensitive values are now only hidden in normal terminal output. They can still appear in the state file or show up in plain text when you run the terraform output -raw or terraform output -json commands.

Note: By default, Terraform stores the state file in the local project directory. If you want to keep state somewhere else, see our Terraform backends guide to learn how to configure a backend block.

Resolving Circular Dependencies and Cycle Errors

Terraform works out the order for handling resources and modules based on their dependencies. A cycle error occurs when two parts of the configuration depend on each other, so Terraform cannot decide which one should come first. Here is an example:

module "network" {
  source = "./modules/network"
  server_id = module.server.server_id
}

module "server" {
  source = "./modules/server"
  network_id = module.network.network_id
}

In this setup, the network module needs the server ID, but the server module also needs the network ID. This means neither module can finish before the other one starts.

If you run the terraform validate command, Terraform may return a cycle error similar to this:

Error: Cycle: module.network, module.server

To break the cycle, decide which part of the infrastructure needs to exist first. In most cases, the network should be created before the server:

module "network" {
  source = "./modules/network"
}

module "server" {
  source = "./modules/server"
  network_id = module.network.network_id
}

If the network needs to connect to the server, manage that connection in a separate resource or module after both main resources are created.

Handling Missing, Null, or Undeclared Module Outputs

A module reference will fail if the parent module tries to access an output that the child module does not expose. For example:

output "server_ip" {
  value = module.server.public_ip
}

If the server module does not have a public_ip output, this expression fails with the following error:

Error: Unsupported attribute

To fix this issue, update the child module to expose the required value:

output "public_ip_addresses" {
  description = "List of public IP addresses assigned to the server."
  value = pnap_server.server.public_ip_addresses
}

Then reference the same output name in the parent module:

output "server_ip_addresses" {
  description = "List of public IP addresses returned by the server module."
  value = module.server.public_ip_addresses
}

Output names are case-sensitive. Double-check for spelling differences such as public_ip, public_ips, and public_ip_addresses.

Problems can also occur when an output exists but does not contain the expected value. For example, trying to retrieve the first item from an empty list:

output "primary_public_ip" {
  value = module.server.public_ip_addresses[0]
}

If public_ip_addresses is empty, Terraform returns an error:

Error: Invalid index

You can use the try function to provide a fallback value:

output "primary_public_ip" {
  description = "The first public IP address assigned to the server."
  value = try(
    module.server.public_ip_addresses[0],
    null
  )
}

If the list is empty, the expression returns null instead of causing an invalid index error. Before using null as a fallback, think about whether it is acceptable for the value to be missing. For example, a private-only server may not need a public IP address, while a public web server probably does.

Best Practices for Terraform Outputs

Terraform outputs should only return the specific values that users, other modules, scripts, or external tools actually need. Apply the following best practices to keep your configuration in check as it grows.

Standardizing Output Naming Conventions for Reusable Modules

Terraform recommends descriptive names in lowercase with underscores between words. Examples of good reusable output names are:

  • server_id
  • hostname
  • network_id
  • vlan_id
  • gateway_address

The name should describe what the output contains and not how the module created it, so you should avoid names like:

  • result
  • value
  • server_info
  • temp_ip
  • output1

In a reusable child module, short names like server_id and public_ip_addresses give out enough information because the module name adds context:

module.server.server_id
module.server.public_ip_addresses

At the root level, you can add a prefix if it helps prevent confusion:

output "bmc_server_id" {
  value = module.server.server_id
}

Try to use the same name for the same type of value across related modules. For example, avoid using server_id in one module, instance_id in another, and machine_id in a third if they all mean the same thing.

Minimizing State File Bloat and Unnecessary Output Exposure

A good output gives users, modules, and automation workflows only the values they need, without exposing unnecessary data.

Terraform stores output values in its state file. Small values, like an IP address, add very little data. However, large maps, full resource objects, generated documents, or repeated lists can make the state file harder to read and manage.

Do not use patterns like this unless you have a specific reason to expose the entire resource:

output "complete_server_resource" {
  value = pnap_server.server
}

This example exposes every available attribute that Terraform can return. It also connects users of the output to the provider's internal resource structure.

Instead, return a smaller object that only contains the values you need:

output "server_connection_details" {
  description = "Server details required by deployment automation."
  value = {
    id = pnap_server.server.id
    hostname = pnap_server.server.hostname
    public_ip_addresses = pnap_server.server.public_ip_addresses
  }
}

This approach makes the output's purpose clear and lowers the risk that future provider changes will break your scripts and modules. Only create an output if someone or something actually needs it, not just because the attribute exists.

Pairing Custom Preconditions with Descriptive Error Messages

A precondition checks if an output meets a requirement before Terraform exposes it or saves it to state. For example, a server meant for public access should have at least one public IP address. The example below checks for a public IP before returning the first address:

output "bmc_primary_public_ip" {
  description = "The primary public IP address of the BMC server."
  value = pnap_server.server.public_ip_addresses[0]

  precondition {
    condition = length(pnap_server.server.public_ip_addresses) > 0
    error_message = "The BMC server has no public IP address. Check that public networking is enabled."
  }
}

If the condition is not met, Terraform shows the error message and stops the operation. The message The BMC server has no public IP address. Check that public networking is enabled. clearly explains the problem and what to do next.

Make sure your error message is useful and that it explains:

  • Which value is missing or invalid.
  • Why the value is required.
  • What the user should check or change.

Do not use generic messages like:

Invalid output.

You do not need to add a precondition to every output. Only use one when the output must meet a requirement for the configuration or tools to work correctly.

Conclusion

This guide covered how outputs work, when to use them, and how to make them useful to other modules, scripts, and automation tools.

Kubernetes is a good example. Terraform outputs can pass useful infrastructure values to tools like Helm, which help deploy and manage containerized applications. To learn more, find out how to use Terraform outputs in a Kubernetes environment.

Was this article helpful?
YesNo