Terraform Output Values : Complete Guide & Examples
Tags: terraform, aws, devops, cloud
Published: 2022-05-15 19:08:02

This blog post will deep dive into how Terraform handles output and how we can leverage and use output values efficiently across our Terraform projects. Output values allow us to share data between modules and workspaces while also providing us the flexibility to pass values to external systems for automation purposes.

You have come to the right place if you are new to Terraform! Spacelift has curated a ton of valuable material, tutorials, and [blog posts around Terraform](https://spacelift.io/blog/terraform) and how industry experts use it on its Spacelift blog. 

## Output vs Input Values

Input variables permit us to customize Terraform configurations without hardcoding any values. This way, we can reuse [Terraform modules](https://spacelift.io/blog/what-are-terraform-modules-and-how-do-they-work) while assigning custom values based on our needs. Usually, we refer to them as just **variables** in the context of Terraform.

To define input variables, we must declare them using a variable block:

```
variable "aws_region" {
  description = "AWS region"
  type        = string
}
 
variable "ec2_instance_type" {
  description = "Instance type for EC2 instances"
  type        = string
  default     = "t2.small"
}
```

The variable’s name is the label we set following the variable keyword. For every variable, we have the option to set some arguments such as _default_, _type_, _description_, _validation_, _sensitive_, and _nullable_. Check the official documentation about these arguments and how to set them in detail [here](https://www.terraform.io/language/values/variables#arguments). 

After declaring our input variables, we can utilize them in modules by referencing them like this **var.<variable_name>** where <variable_name> matches the label following the _variable_ keyword. For example, to reference the variable _ec2_instance_type_ that we defined above:

```
resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.ec2_instance_type
}
```
On the other hand, output values empower us to export helpful information from our Terraform projects that we have defined and provisioned with Terraform. In the context of Terraform, we refer to output values as just **outputs** for simplicity. 

Combining input and output variables, we get the flexibility to customize, automate, reuse and share our Terraform code easily. Input variables are similar to function arguments in traditional programming, while output variables work similarly to the return values of a function. Both are equally important to make our Terraform projects functional and facilitate data’s incoming and outgoing flow.

## Terraform Outputs Use Cases

More specifically, output values are quite helpful in certain use cases:

* We can expose information from child modules to a parent module using outputs.
* Even more, from a root module, we can print outputs in the command line or pass these output values to external systems for automation purposes. 
When we use a remote state, we can access the root module outputs by other configurations using the [terraform_remote_state](https://www.terraform.io/language/state/remote-state-data) data source. Output values from child modules aren’t accessible.

## Declaring and Using Output Values

In order to define an output value, we have to use the **output** block:

```
output "instance_public_ip" {
  description = "Public IP of EC2 instance"
  value       = aws_instance.web_server.public_ip
}
```

In the above example, we define an output value with the name _instance_public_ip_. This way, we can pass the value to the parent module or display it to the end-user if it’s an output of the root module.

The value argument, which is the returned output value, takes an expression referencing other resources or module attributes. Terraform only renders and displays outputs when executing _terraform apply_ and not when executing _terraform plan_. 

To use outputs of nested modules from parent modules, we have to reference them as: 

`module.<module_name>.<output_value_name>`

For example, to reference the output value _instance_public_ip_ that we have declared above in a module named _aws_web_server_instance_ from its parent module, we have to use: 

```
module.aws_web_server_instance.instance_public_ip
```

Let’s examine how we can use all this in a real-world example. In [this GitHub repository](https://github.com/spacelift-io-blog-posts/Blog-Technical-Content/tree/master/terraform-output), we define the Terraform configuration for this example’s infrastructure. To follow along, you will need to [install Terraform](https://spacelift.io/blog/how-to-install-terraform), have an [AWS account](https://aws.amazon.com/console/) ready, and [authenticate with your AWS keys](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) via the command line. Note that you might be charged a few dollars in your AWS account if you follow along.

In this example, we create the necessary infrastructure for a webserver. For the needs of this demo, we split our Terraform configuration into three modules, the root one and two child modules responsible for handling VPC-related resources and EC2 instance-related resources.

The project structure looks like this:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/60759qvjsecacoo29v57.png)

For each module, we define a _main.tf_ file that handles the main functionality of the module. 

Variable’s declarations and default values are populated in _variables.tf_ files, while for the root module, we also use a _terraform.tfvars_ file to set some variable values.

A good practice is to define our outputs in separate _outputs.tf_ files, as you can see in the above example project structure. By declaring output values in an **outputs.tf** file per module, we improve the clarity of our modules as it’s easier for users to understand what outputs to expect from them quickly. 

The root module utilizes and configures the aws provider and then just simply calls two child modules _aws_web_server_vpc_ and _aws_web_server_instance_ in main.tf of the top directory. 

_root module main.tf_

```
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.16.0"
    }
  }
}
 
provider "aws" {
  region = var.aws_region
}
 
module "aws_web_server_vpc" {
  source = "./modules/aws-web-server-vpc"
}
 
module "aws_web_server_instance" {
  source            = "./modules/aws-web-server-instance"
  ec2_instance_type = var.ec2_instance_type
  vpc_id            = module.aws_web_server_vpc.vpc_id
  subnet_id         = module.aws_web_server_vpc.subnet_id
}
```

We notice that when calling the module _aws_web_server_instance_, we are passing two expressions using output values from the _aws_web_server_vpc_ module with the notation _module.<module_name>.<output_value_name>_ we have seen earlier. 

_root module outputs.tf_

```
output "vpc_id" {
  description = "ID of the vpc"
  value       = module.aws_web_server_vpc.vpc_id
}
 
output "instance_id" {
  description = "ID of EC2 instance"
  value       = module.aws_web_server_instance.instance_id
}
 
output "instance_public_ip" {
   description = "Public IP of EC2 instance"
   value       = module.aws_web_server_instance.instance_public_ip
}
```
We define three output values for our root module, and we expect to see them at the command line after our infrastructure is provisioned. Checking the value parameter of each block, we notice that all of them are coming from output values of the two child modules, and by declaring them as output values of the root module, we are able to pass them through to the command line.

_root module variables.tf_

```
variable "aws_region" {
  description = "AWS region"
  type        = string
}
 
variable "ec2_instance_type" {
  description = "Instance type for EC2 instances"
  type        = string
  default     = "t2.small"
}
```

_root module terraform.tfvars_

```
aws_region        = "us-east-1"
ec2_instance_type = "t2.nano"
```
Let’s examine next our two child modules and how we use output values to pass parameters between them.

_aws-web-server-vpc module main.tf_

```
resource "aws_vpc" "web_server" {
  cidr_block       = var.vpc_cidr_block
  instance_tenancy = "default"
 
  tags = {
    Name = var.vpc_name
  }
}
 
resource "aws_subnet" "web_server" {
  vpc_id                  = aws_vpc.web_server.id
  cidr_block              = var.subnet_cidr_block
  map_public_ip_on_launch = true
  availability_zone       = var.aws_az
 
  tags = {
    Name = var.subnet_name
  }
}
 
resource "aws_internet_gateway" "web_server" {
  vpc_id = aws_vpc.web_server.id
 
  tags = {
    Name = var.igw_name
  }
}
 
resource "aws_route_table" "web_server" {
  vpc_id = aws_vpc.web_server.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.web_server.id
  }
 
  tags = {
    Name = var.rt_name
  }
}
 
resource "aws_route_table_association" "web_server" {
  subnet_id      = aws_subnet.web_server.id
  route_table_id = aws_route_table.web_server.id
}
```

In the above module, we define some resources necessary for the networking layer of our infrastructure.

_aws-web-server-vpc module variables.tf_

```
variable "vpc_cidr_block" {
  description = "CIDR block for webserver VPC"
  type        = string
  default     = "10.0.0.0/16"
}
 
variable "vpc_name" {
  description = "Name of the vpc"
  type        = string
  default     = "web_server"
}
 
variable "subnet_cidr_block" {
  description = "CIDR block for the webserver subnet"
  type        = string
  default     = "10.0.0.0/24"
}
 
variable "subnet_name" {
  description = "Name for the webserver subnet"
  type        = string
  default     = "web_server"
}
 
variable "aws_az" {
  description = "Availability Zone for the webserver subnet"
  type        = string
  default     = "us-east-1a"
}
 
variable "igw_name" {
  description = "Name for the Internet Gateway of the webserver vpc"
  type        = string
  default     = "web_server"
}
 
variable "rt_name" {
  description = "Name for the route table of the webserver vpc"
  type        = string
  default     = "web_server"
}
```

_aws-web-server-vpc module outputs.tf_

```
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.web_server.id
}
 
output "subnet_id" {
  description = "ID of the VPC subnet"
  value       = aws_subnet.web_server.id
}
```

The two outputs we export here from this module are passed to the _aws-web-server-instance_ module as parameters in order to create the EC2 instance inside the vpc and subnet that we have just created. We saw how this was handled in the main.tf file of the root module. The output value _vpc_id_ is passed along as an output of the root module and should be printed in the command line after we apply the plan.

Finally, the Terraform configuration for the _aws-web-server-instance_ module uses the passed info from the _aws-web-server-vpc_ module. It creates and configures the web server instance accordingly.  

aws-web-server-instance module main.tf

```
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}
 
resource "aws_security_group" "web_server" {
  name        = var.ec2_security_group_name
  description = var.ec2_security_group_description
  vpc_id      = var.vpc_id
 
  ingress {
    description      = "Allow traffic on port 80 from everywhere"
    from_port        = 80
    to_port          = 80
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
 
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
 
  tags = {
    Name = var.ec2_security_group_name
  }
}
 
resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.ec2_instance_type
 
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.web_server.id]
 
  tags = {
    Name = var.ec2_instance_name
  }
 
  user_data = <<-EOF
   #!/bin/bash
   sudo yum update -y
   sudo yum install httpd -y
   sudo systemctl enable httpd
   sudo systemctl start httpd
   echo "<html><body><div>This is a test webserver!</div></body></html>" > /var/www/html/index.html
   EOF
}
```

_aws-web-server-instance module variables.tf_

```
variable "ec2_instance_name" {
  description = "Name for web server EC2 instance"
  type        = string
  default     = "web_server"
}
 
variable "ec2_instance_type" {
  description = "Instance type for web server EC2 instance"
  type        = string
  default     = "t2.micro"
}
 
variable "ec2_security_group_name" {
  description = "Security group name for web server EC2 instance"
  type        = string
  default     = "web_server"
 
}
 
variable "ec2_security_group_description" {
  description = "Security group description for web server EC2 instance"
  type        = string
  default     = "Allow traffic for webserver"
}
 
variable "vpc_id" {
  description = "VPC id for web server EC2 instance"
  type        = string
}
 
variable "subnet_id" {
  description = "Subnet id for web server EC2 instance"
  type        = string
}
```

The two output values that we pass through the root module are also defined in this module’s outputs.tf file.

```
output "instance_id" {
  description = "ID of EC2 instance"
  value       = aws_instance.web_server.id
}
 
output "instance_public_ip" {
  description = "Public IP of EC2 instance"
  value       = aws_instance.web_server.public_ip
}
```

Time to wrap up everything and execute the plan to provision our demo infrastructure.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3nm0gvf71i4elaocrydc.png)

Our _terraform plan_ shows 7 new resources to be added and displays the changes to our three output values declared in the root module. Let’s go ahead and apply the plan.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c8p6m5nhyvf5k9f4031b.png)

As expected, the three outputs declared in the root module are displayed at the command line, sweet! 

We could use these values to automate other parts of our systems and process, but for now, we can get the value from _instance_public_ip_ and head to _http://<instance_public_ip>_, and we should see our demo web server up and running.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b74xb6ka3gloxxo3ku4u.png)

Great! Everything works as expected.

## Terraform Output Command

Output values are stored in the state Terraform file. Since we have successfully applied our plan, we can now access these output values at will. We can leverage the _terraform output_ command for this purpose.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dc8ii8bh5hzmc51izhg4.png)

Additionally, we can query individual output values by name like this

_terraform output <output_value_name>_


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ajidpev73ess0tl4cms8.png)

To get the raw value without quotes, use the **-raw** flag.

_terraform output -raw <output_value_name> _

To get the JSON-formatted output, we can use the **-json** flag.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zpa8stld5kqed0gcmzfj.png)

This is quite useful when we want to pass the outputs to other tools for automation since JSON is way easier to handle programmatically. Note that Terraform does not protect sensitive output values when using the _-json_ flag.

When we are done, let’s go ahead and delete all these resources to avoid paying for them. 

From the top of our repository, execute:

```
terraform destroy
```

## Output Values Options & Arguments

When defining output values, we have a couple of options that might help us better define and organize them. 

The argument **description** is optional, but it is always considered good practice to include it in our output declarations to document their purpose. This argument should briefly explain each output’s intent and should be used as a helper description for the users of the module. We have already seen examples like this since we defined the description argument in all our output block declarations in our previous demo.

In cases where we want to handle sensitive values and suppress them in command line output, we can declare an output value as **sensitive**. Terraform will redact the values of sensitive outputs when planning, applying, destroying, or querying outputs to avoid printing them to the console. In practice, this is a good use case when we would like to pass values to other Terraform modules or automation tools without exposing them to the intermediate users.

```
output "example_password" {
  description = "An example DB password"
  value       = aws_db_instance.database.password
  sensitive   = true
}
```
Note that Terraform won’t redact sensitive output values when you query a specific output by name. After we apply a plan with an output declared as sensitive, the console displays a message with the value redacted.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxiwijwobdwqu3mite6i.png)

These values are still recorded in the state files, so anyone who can access them can also access any sensitive values of our Terraform configuration. 

The **depends_on** argument on output declarations is used to define dependencies explicitly when this is necessary. Most of the time, Terraform handles this automatically, but there are some rare uses cases where you might find this option handy when it’s not the case. Consider including a comment when you use this option to explain why this is necessary.

## Terraform Remote State Data Source

Occasionally, we might need to share data between different Terraform configurations with separate states. This is where the **terraform_remote_state** data sources come into play. We can retrieve the root module outputs from another Terraform configuration using this data source. This built-in data source is available without any extra configuration needed. 

Following up on our previous example, let’s say that we would like to create a new subnet in the vpc of our _aws-web-server-vpc_ module. This time, the new subnet needs to be defined in a completely separate Terraform configuration that has its own state. We can leverage the _terraform_remote_state_ to get the value of the _vpc_id_ defined as an output of our previous example’s root module. In this case, we use the local backend to reach the state of another configuration in the local machine. The backend could be any remote backend that points to a Terraform state in a real-world scenario.

```
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.16.0"
    }
  }
}
 
provider "aws" {
  region = "us-east-1"
}
 
data "terraform_remote_state" "terraform_output" {
  backend = "local"
 
  config = {
    path = "../terraform-output/terraform.tfstate"
  }
}
 
resource "aws_subnet" "test_terraform_remote_state_subnet" {
  vpc_id            = data.terraform_remote_state.terraform_output.outputs.vpc_id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1b"
 
  tags = {
   Name = "test_terraform_remote_state_subnet"
  }
}
```

Note that only the output values of the root module are accessible from the remote state. If we want to pass values from nested modules, we have to configure a passthrough output value declaration as we defined earlier in the root module of our previous example. 

Although this option is handy for some use cases, it also has some caveats. To use this data source, the user must have access to the entire state snapshot, which could potentially expose sensitive data. Check out the official docs to find [alternative ways to share data between configurations](https://www.terraform.io/language/state/remote-state-data#alternative-ways-to-share-data-between-configurations).

Key Points
We have seen how Terraform handles and exports output values between modules and the different options for outputs configuration. Even more, we compared input and output variables and examined multiple use cases where the use of outputs is helpful. Finally, we went through a complete example of using output values in our Terraform configuration between different modules and printing them to the console. 

Thank you for reading, and I hope you enjoyed this “Terraform Outputs” blog post as much as I did.