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:

  1. Printing values to the CLI after apply so you can see what happened
  2. Passing values between resources when one resource depends on another
  3. 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), and depends_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.

About

About BookGrill.net

BookGrill.net is a technology book review site for developers, engineers, and anyone who builds things with code. We cover books on software engineering, AI and machine learning, cybersecurity, systems design, and the culture of technology.

Know More