Terraform Data Sources

Explore data sources in Terraform — Terraform foundations.

Focus: explore data sources in terraform

Sponsored

You've spent hours crafting Terraform configurations with hard-coded AMI IDs, VPC IDs, and account numbers — then the next engineer on your team gets a cryptic error because a resource was recreated in another region. Hard-coding values that your infrastructure already knows is the #1 way to introduce drift, break reproducibility, and turn your IaC into a game of whack-a-mole. Data sources solve this by letting your configuration query the currently deployed world and use that information as input to your resources — no copy-paste, no guessing, no stale values.

The problem this lesson solves

When you write Terraform, you often need to know things like: "What's the latest Ubuntu AMI?" or "Which subnets exist in this VPC?" or "What's the current AWS account ID?". You could hard-code these values, but that creates several painful problems:

  • Stale values — AMIs change frequently; your hard-coded ID from last month might be deprecated or even deregistered.
  • Non-portable code — A configuration that works in your dev account fails in prod because the VPC ID is different.
  • Manual coordination — You have to ask your network team for the subnet IDs, then update the config every time they change.
  • Single point of failure — One typo in a hard-coded value can cause a full resource replacement.

Data sources give you a read-only query that fetches the latest state of your infrastructure at plan time. The result can be used as attributes (like data.aws_ami.ubuntu.id) or passed to other resources. This turns your configuration from a static document into a dynamic system that adapts to its environment.

Core concept / mental model

Think of Terraform as a chef preparing a meal. Resources are the ingredients you buy and cook — you create them from scratch. Data sources are like checking what's already in your pantry — you read what exists, without changing anything. Both are declared in your configuration, but they serve fundamentally different purposes:

  • Resource: aws_instance.web — Terraform will create, update, or destroy this.
  • Data source: data.aws_ami.ubuntu — Terraform will only read and expose it via attributes.

A data source is defined using the data block with a type (e.g., aws_ami) and a name (your local label). Inside, you can specify filters, arguments, and optionally a depends_on to control timing.

# This doesn't create an AMI; it looks up the most recent one matching your filters
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

Once declared, you access the data using data.<type>.<name>.<attribute> — for example, data.aws_ami.ubuntu.id. Data sources are evaluated during the plan, so they always reflect the current state of your cloud, not a cached version.

Pro tip: Visualization — Imagine a flow chart: data block -> Terraform queries the provider API -> returns attribute values -> these are injected into resource arguments. Nothing is created, only observed.

How it works step by step

  1. Declare the data source — Use the data block with a provider-specific type. Each provider (AWS, Azure, Google Cloud, etc.) offers its own catalog of data sources.
  2. Provide lookup arguments — Most data sources require filters or identifiers. For example, aws_ami needs a name filter or an owners list; aws_vpc needs a filter or id.
  3. Reference attributes — Once declared, use data.<TYPE>.<NAME>.<ATTRIBUTE> in resource blocks or output values.
  4. Terraform plans and applies — During terraform plan, Terraform makes a read-only API call to fetch the data, then computes the full configuration. The data source's values are stored in state (but never modified).
  5. Use the result — You can pass the ID, ARN, IP address, or any other exported attribute into your resources.

Data sources are read-only — Terraform will never attempt to modify or delete the underlying resource. If the external resource changes, the next plan will pick up those changes automatically.

Hands-on walkthrough

Let's build a real example. Assume you have an existing VPC in AWS and want to launch an EC2 instance inside it using the latest Ubuntu AMI from Canonical. You'll need two data sources: one for the AMI and one for the VPC. Here's the complete configuration:

# provider.tf
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

# data.tf
# Look up the latest Ubuntu 22.04 AMI from Canonical
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

# Look up the default VPC for this region
data "aws_vpc" "default" {
  default = true
}

# main.tf
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  subnet_id     = data.aws_subnets.default.ids[0]

  tags = {
    Name = "web-from-data-source"
  }
}

# We also need the subnet IDs from the VPC
data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
}

Run terraform init and then terraform plan. You should see output similar to:

$ terraform plan

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                          = "ami-0c7217cdde317cfec"
      + instance_type                = "t3.micro"
      + subnet_id                    = "subnet-0a1b2c3d"
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Notice that the AMI ID and subnet ID were dynamically resolved — no hard-coding. This makes your configuration portable across accounts and regions.

You can also export data source values as outputs to share with other teams:

# outputs.tf
output "ami_id" {
  value = data.aws_ami.ubuntu.id
}

output "vpc_id" {
  value = data.aws_vpc.default.id
}

Now run terraform apply and verify the outputs.

Pro tip: Use terraform console to interactively test data source lookups before committing them to a file. It's like a REPL for Terraform.

Compare options / when to choose what

Data sources aren't the only way to get information into Terraform. Here's a comparison:

Approach When to use Drawbacks
Data sources You need the current state of an existing resource or a value that changes frequently (e.g., AMI IDs). Requires a live API call; plan fails if no match is found.
Variables with defaults You need a simple, stable value that rarely changes (e.g., region, account ID). Can become stale; requires manual updates.
Terraform remote state You need outputs from a previously applied Terraform configuration (e.g., another module). Couples configurations; requires state sharing.
External data source You need to pull data from a non-Terraform-managed API or command line tool. Requires a separate executable; adds custom code.

Variations: Some providers offer specialized data sources like aws_caller_identity to get your account ID, or http data source to fetch any URL. You can also combine data sources to build complex lookups.

Troubleshooting & edge cases

Here are common errors and how to fix them:

  • Error: "no AMI found with the given criteria" — Your filter is too strict. Double-check the name pattern, owner ID, or region. Use terraform console to test filter variations.
  • Error: "can't find VPC" — If you're using filter on a non-default VPC, ensure you provide the correct vpc-id or other unique attributes. For default VPCs, default = true works only if exactly one exists.
  • Data source returns stale values — Remember, data sources are read only at plan time. If you apply and then the underlying resource changes, your next plan will update automatically, but not in the same apply.
  • Data source attributes are not exported — Not all attributes are exported. Check the provider documentation for which attributes are available.
  • depends_on misused — You might need to add depends_on to a data source if it depends on a resource created in the same configuration. For example, querying subnets created by a aws_subnet resource. Use it sparingly.

What you learned & what's next

You now understand the power of explore data sources in terraform. You can:

  • Explain what data sources are and how they differ from resources.
  • Declare data sources for AWS, reference their attributes, and use them in resources and outputs.
  • Connect data sources to existing infrastructure, making your configuration portable and avoiding hard-coded values.

This is a crucial foundation for building reusable modules and working with remote state. In the next lesson, you'll dive into modules — the building blocks for scalable infrastructure as code. With data sources, you can write modules that query the environment and adapt automatically.

Key takeaway: Data sources are read-only queries that turn your Terraform configuration from a static script into a dynamic, self-adjusting system.

Practice recap

Try this mini-exercise: Create a configuration that uses the aws_caller_identity data source to output your AWS account ID and the aws_region data source to output your current region. Then, add a data source that looks up the default security group for your VPC and outputs its ID. This will solidify your understanding before moving on to modules.

Common mistakes

  • Hard-coding AMI IDs or subnet IDs instead of using data sources — leads to drift and breakage when values change.
  • Using most_recent = true without a proper owner filter — you might get an unintended image from another vendor.
  • Forgetting that data sources are fetched at plan time — changes after plan won't appear until the next run.

Variations

  1. Use the aws_caller_identity data source to dynamically fetch your account ID instead of hard-coding it.
  2. Use the http data source to pull data from any REST API (e.g., dynamic IP lists) — perfect for security groups.
  3. Combine multiple data sources (e.g., VPC + subnets) to build complex lookup chains in a single configuration.

Real-world use cases

  • EC2 auto-scaling group that always launches the latest approved AMI — data source filters on the AMI name prefix.
  • Terraform module that attaches a security group to an existing VPC — data source looks up the VPC by tag or ID.
  • Multi-account setups where the same configuration runs in dev and prod — data sources pull region-specific values dynamically.

Key takeaways

  • Data sources are read-only queries that fetch current state from your provider — never modified.
  • Use data sources to avoid hard-coding volatile values like AMI IDs, subnet IDs, and account IDs.
  • Reference data source attributes with data.<TYPE>.<NAME>.<ATTRIBUTE>.
  • Data sources are evaluated at plan time, so they always reflect the latest API response.
  • Always use filters and most_recent to narrow results and avoid ambiguous matches.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.