Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 021 / 100

Data sources in Terraform for Terraform & IaC developers

Read existing infrastructure.

Expona Admin8 min read

Data sources in Terraform for Terraform & IaC developers

Data sources are one of the most underused features in Terraform, yet they solve a critical problem: how do you reference infrastructure that already exists or is managed outside your current state file? Whether you're looking up an AMI ID, finding a VPC created by another team, or querying your AWS account for resource details, data sources let you read existing infrastructure without managing it. Understanding when and how to use them separates teams that fight with hardcoded IDs from those that build flexible, maintainable Terraform configurations.

What data sources do

A data source queries your cloud provider's API and returns information about existing resources. Unlike resources, data sources don't create anything—they only read. During terraform plan and terraform apply, Terraform fetches the current state of the resource you're querying and makes that data available to your configuration.

The fundamental syntax mirrors resource blocks:

hcl
data "aws_ami" "ubuntu" {
  most_recent = true

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

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

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

You reference the returned data the same way you reference computed attributes from resources: data.TYPE.NAME.ATTRIBUTE.

Look up AMIs, VPCs and accounts

The most common use case for data sources is retrieving information about resources that change frequently or have many variants. AMI IDs are a perfect example: they're region-specific, regularly patched, and hardcoding them defeats the purpose of infrastructure-as-code.

Finding the right AMI

Instead of maintaining a spreadsheet of AMI IDs across regions, query AWS for the latest approved image:

hcl
data "aws_ami" "app_server" {
  most_recent = true

  filter {
    name   = "name"
    values = ["my-company-app-*"]
  }

  filter {
    name   = "state"
    values = ["available"]
  }

  owners = ["self"]
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.app_server.id
  instance_type = "t3.medium"
}

This works well when your organization maintains a shared AMI pipeline. Every time you run Terraform, it grabs the latest version. You can filter by tags, name patterns, owner, or state to narrow results.

Referencing existing VPCs and subnets

Many teams inherit VPCs created outside Terraform, or they have centralized networking managed by a different team. Rather than importing those resources into your state file, use data sources:

hcl
data "aws_vpc" "main" {
  tags = {
    Name = "production"
  }
}

data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.main.id]
  }

  tags = {
    Tier = "private"
  }
}

resource "aws_security_group" "app" {
  name   = "app-sg"
  vpc_id = data.aws_vpc.main.id
}

This approach keeps your configuration clean: you're not managing the VPC, just consuming it. If the VPC is deleted or modified elsewhere, Terraform will either error or adapt depending on your configuration.

Querying account metadata

Sometimes you need your account ID, current region, or other AWS-wide information:

hcl
data "aws_caller_identity" "current" {}

data "aws_region" "current" {}

resource "aws_s3_bucket" "logs" {
  bucket = "app-logs-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.name}"
}

This pattern eliminates region and account assumptions from your code, making it portable across environments.

Reference resources managed elsewhere

In larger organizations, infrastructure spans multiple Terraform projects. Your application Terraform module needs to reference a load balancer provisioned by the platform team, or a database created in a separate stack. Data sources bridge that gap.

Cross-stack references without sharing state

If the other team tags their resources predictably, you can discover them:

hcl
data "aws_lb" "platform" {
  tags = {
    ManagedBy = "platform-team"
    Environment = "prod"
  }
}

resource "aws_lb_target_group" "app" {
  load_balancer_arn = data.aws_lb.platform.arn
  name              = "app-targets"
  port              = 8080
  protocol          = "HTTP"
  vpc_id            = data.aws_lb.platform.vpc_id
}

This works because the data source queries AWS's API in real-time. You're not reading from another team's state file—you're asking AWS directly. If they update the load balancer, your next plan reflects that.

Reading outputs from other stacks

For tighter coordination, you can use the terraform_remote_state data source to reference outputs from another state file. We'll cover this in more depth in the "Prefer remote state outputs" section, but the pattern is:

hcl
data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "terraform-state"
    key    = "network/prod.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
}

This approach requires the other stack to explicitly output the values you need, but it's explicit and version-controlled—both teams know the contract between stacks.

Avoid data sources that change every plan

Data sources are powerful, but they come with a risk: if you query something that changes constantly, your Terraform plan becomes unstable. You'll see different results on each run, making it hard to reason about what's actually changing.

The unstable data source problem

Consider querying EC2 instances by a tag:

hcl
data "aws_instances" "web" {
  filter {
    name   = "tag:Role"
    values = ["web-server"]
  }
}

resource "aws_security_group_rule" "allow_web" {
  for_each                 = toset(data.aws_instances.web.ids)
  type                     = "ingress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  source_security_group_id = each.value
  security_group_id        = aws_security_group.alb.id
}

If someone manually launches or terminates an instance with that tag, your next terraform plan shows different rules. This breaks the principle that infrastructure changes should flow through Terraform. You're now dependent on manual AWS operations.

When to avoid data sources

Use data sources for relatively static infrastructure: VPCs, subnets, security groups managed by other teams, or metadata like AMIs. Avoid them for:

  • Instance counts that vary outside Terraform
  • Resources created by autoscaling groups
  • Infrastructure provisioned by CI/CD pipelines that don't use Terraform
  • Any resource whose existence depends on transient operational decisions

When you're tempted to use a data source for something dynamic, ask: does this resource change outside Terraform? If yes, either bring it into your Terraform project or document that dependency clearly in your runbooks.

Prefer remote state outputs for your own stacks

When you control both stacks, remote state outputs are more reliable than querying AWS's API. You're reading from a single source of truth—your state file—rather than the current state of AWS, which may not reflect what Terraform thinks you have.

State outputs vs. data sources

Imagine a networking stack that creates a VPC and outputs its ID:

hcl
# networking/main.tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "prod-vpc"
  }
}

output "vpc_id" {
  value = aws_vpc.main.id
}

Your application stack can consume this in two ways:

hcl
# Option 1: Remote state (preferred for your own stacks)
data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "terraform-state"
    key    = "network/prod.tfstate"
    region = "us-east-1"
  }
}

# Option 2: Query AWS directly
data "aws_vpc" "main" {
  tags = {
    Name = "prod-vpc"
  }
}

resource "aws_instance" "app" {
  # Both work, but remote state is more predictable
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
}

Remote state outputs are more reliable because they're in sync with the networking stack's last successful apply. If someone manually deletes the VPC in AWS, the data source query might fail mysteriously, whereas remote state still has the reference—making the error clearer during planning.

Structure for cross-stack communication

Organize your outputs explicitly for consumption by other stacks:

hcl
# networking/outputs.tf
output "vpc_id" {
  description = "VPC ID for app stack consumption"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "Private subnet IDs for app deployment"
  value       = aws_subnet.private[*].id
}

output "app_security_group_id" {
  description = "Security group for app servers"
  value       = aws_security_group.app.id
}

Document what you're exporting and why. This makes it clear to other teams what's safe to depend on. If you refactor the networking stack, you know which outputs might break downstream consumers.

For state files stored in different AWS accounts or regions, always use terraform_remote_state over querying AWS directly. It's faster (no API calls) and guaranteed to be consistent with your state.

Common mistakes

  • Hardcoding filter values: Avoid queries like "find the VPC tagged with my team's name" if another team might provision it differently. Instead, establish a naming or tagging convention and document it as a prerequisite.
  • Using data sources for everything: New users often reach for data sources instead of importing existing resources. If you manage a resource in Terraform, import it; don't query it every run.
  • Depending on manual tags: Tags set outside Terraform can disappear or change. Use data sources only for infrastructure managed by stable, documented processes.
  • Mixing remote state and data source queries: If you have a choice, pick one pattern per relationship. Mixing makes debugging confusing.
  • Forgetting to handle missing data: If a data source query returns no results, Terraform errors. Add validation or default values where appropriate.

Key takeaways

  • Data sources read existing infrastructure without managing it—use them to reference resources outside your Terraform project.
  • Query AWS directly for AMIs, VPCs, subnets, and other infrastructure managed by other teams or automated processes.
  • Avoid data sources for resources that change frequently outside Terraform; they make your plans unstable.
  • Prefer terraform_remote_state outputs when both stacks are under your control; it's more reliable than querying AWS's API.
  • Tag resources predictably and document your expectations so data source queries remain stable.
  • Use data sources to keep your Terraform configurations portable across regions and accounts.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles