Terraform Cert Guide Chapter 3 Part 2: Outputs and Data Sources for All Clouds
In Part 1 we covered providers, resources, and variables. Now let’s talk about the other two pieces of the puzzle: outputs and data sources. These are the things that make your Terraform configs actually talk to each other and to existing infrastructure.
Terraform Outputs: Getting Info Back From Your Resources
So you just created a resource. Great. But how do you know what IP address it got? What ID was assigned? That’s where outputs come in.
Output values are basically return values. When Terraform creates something, it can tell you specific details about what it created. You define them in an output block, and they show up in your terminal after terraform apply.
But outputs aren’t just for your eyes. They have three real uses:
- Printing values to the CLI after apply so you can see what happened
- Passing values between resources when one resource depends on another
- Sharing data across configurations via remote state (more on that later in the book)
Azure Output Example
The book uses an Azure public IP address as the example. Say you create a public IP resource like this:
resource "azurerm_public_ip" "example" {
name = "acceptanceTestPublicIp1"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
allocation_method = "Static"
}
After creation, you can pull out several attributes: the id, the name, the resource_group_name, the actual ip_address, and the fqdn. You define these in a separate output.tf file:
output "ip_address" {
value = azurerm_public_ip.example.ip_address
}
output "id" {
value = azurerm_public_ip.example.id
}
Quick heads up: dynamic public IPs don’t actually get allocated until they’re attached to something like a VM or load balancer. If you need the IP before that, you’ll want to use a data source (we’ll get to those).
The book also shows attribute referencing, which is when one resource directly uses values from another. For example, an Azure load balancer can reference the public IP’s name and ID right in its own config block. This isn’t technically an “output” in the formal sense. It’s just one resource reading another’s attributes. But it works the same way under the hood.
AWS Output Example
For AWS, the book uses a VPC. You create one:
resource "aws_vpc" "terraform-vpc" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "default"
tags = {
Environment = "Terraform-lab"
}
}
Then you can output the ID and CIDR block:
output "id" {
value = aws_vpc.terraform-vpc.id
}
output "cidr_block" {
value = aws_vpc.terraform-vpc.cidr_block
}
And here’s the practical part. When you need to create a subnet inside that VPC, you just reference it directly:
resource "aws_subnet" "terraform-subnet" {
vpc_id = aws_vpc.terraform-vpc.id
cidr_block = "10.0.0.0/24"
}
See vpc_id = aws_vpc.terraform-vpc.id? That’s attribute referencing in action. Terraform figures out the dependency automatically.
GCP Output Example
Same concept, different cloud. The book creates a Google App Engine application and then pulls out the id and name:
output "id" {
value = google_app_engine_application.app.id
}
output "name" {
value = google_app_engine_application.app.name
}
Nothing new here. Once you understand the pattern for one cloud, it’s the same syntax everywhere. That’s one of Terraform’s strengths.
Output Optional Arguments
This is a small section but it matters for the certification exam. Outputs support three optional arguments:
description
Just a human-readable note about what the output is. Helps future-you (or your teammates) understand what they’re looking at:
output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."
}
sensitive
If your output contains something secret (like a database password), mark it as sensitive:
output "db_password" {
value = aws_db_instance.db.password
description = "The password for logging in to the database."
sensitive = true
}
This stops Terraform from printing the value in the CLI output. But here’s the catch that the book specifically calls out: sensitive values are still stored in plain text in the state file. Anyone with access to your state file can read them. So sensitive = true is not encryption. It’s just screen privacy.
Terraform 0.14+ improved this by also redacting sensitive values when they’re referenced elsewhere in the output. But the state file issue remains.
depends_on
You probably won’t use this often, but sometimes you need to explicitly tell Terraform that an output depends on something that isn’t obvious from the code:
output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."
depends_on = [
# Security group rule must exist before this IP is usable
aws_security_group_rule.local_access,
]
}
The book’s advice here is solid: use depends_on as a last resort. If you do use it, always leave a comment explaining why. Your future self will thank you.
Terraform Data Sources: Reading Existing Stuff
OK, this is where it gets really practical. Data sources let you read information about infrastructure that already exists, whether it was created by Terraform, by hand, by CloudFormation, by ARM templates, or whatever.
The book tells this great story about two colleagues, Matt and Bob, working at a shipping company. Bob already deployed a virtual network, some subnets, and VMs in Azure using ARM templates. Now Matt needs to create more VMs in those same networks, but he wants to use Terraform. How does he reference Bob’s existing infrastructure?
Data sources. That’s the answer.
Azure Data Source Example
Say you need to create a subnet in an existing virtual network. First, you use data blocks to read the existing resources:
data "azurerm_resource_group" "example" {
name = "Terraform-rg"
}
data "azurerm_virtual_network" "example" {
name = "production-vnet"
resource_group_name = data.azurerm_resource_group.example.name
}
Then you create your new subnet referencing that data:
resource "azurerm_subnet" "example" {
name = "terraform-subnet"
resource_group_name = data.azurerm_resource_group.example.name
virtual_network_name = data.azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
Notice the prefix data. when referencing data sources. That’s how Terraform knows you’re reading something that exists vs. creating something new.
AWS Data Source Example
Same pattern for AWS. You want to create a subnet in an existing VPC:
variable "vpc_id" {}
data "aws_vpc" "example" {
id = var.vpc_id
}
resource "aws_subnet" "example" {
vpc_id = data.aws_vpc.example.id
availability_zone = "us-west-2a"
cidr_block = cidrsubnet(data.aws_vpc.example.cidr_block, 4, 1)
}
The cidrsubnet function is a nice touch here. It automatically calculates a subnet CIDR from the VPC’s CIDR block. That’s built-in Terraform functionality doing the math for you.
GCP Data Source Example
For GCP, the book keeps it simple with a compute instance lookup:
data "google_compute_instance" "example" {
name = "Terraform-server"
zone = "us-central1-a"
}
Once you have this, you can pull any attribute from that existing instance and use it in your own resources.
Chapter Summary
Let me boil down what Chapter 3 Part 2 covers:
- Outputs are return values from your Terraform resources. Use them to print info, pass data between resources, or share across configs via remote state.
- Attribute referencing lets one resource directly use another’s attributes. Terraform handles the dependency graph for you.
- Output arguments include
description(documentation),sensitive(hide from CLI, but NOT from state file), anddepends_on(explicit dependency, use sparingly). - Data sources read existing infrastructure, no matter how it was created. Prefix with
data.when referencing them. - The syntax is the same across all clouds. Learn it once, use it everywhere.
If I had to pick the most important takeaway from this section, it’s data sources. In the real world, you almost never start from a blank slate. There’s always existing infrastructure, and data sources are how Terraform plays nice with it.
Previous: Chapter 3 Part 1: Providers and Resources | Next: Chapter 4 Part 1: Deep Dive - Backends and Provisioners
This is part of a series retelling “HashiCorp Infrastructure Automation Certification Guide” by Ravi Mishra (Packt, 2021). For the full text, grab the book - ISBN: 978-1-80056-597-5.