Reference #
Declaring a data source is the first step. The next step is using its value effectively — referencing the right attributes, understanding when values are available, and avoiding the traps that appear when a data source depends on resources that may not exist yet at plan time.
flowchart TD
A["data source\ndeclared"] --> B["Terraform\nReads the API"]
B --> C["Attribute\navailable"]
C --> D["Referenced\nwith data.X.Y.Z"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#f59e0b,stroke:#d97706,color:#fff
style C fill:#10b981,stroke:#059669,color:#fff
style D fill:#8b5cf6,stroke:#6d28d9,color:#fffHow to Reference Data Source Attributes #
References to data source attributes use the data.<TYPE>.<NAME>.<ATTRIBUTE> format.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# Reference data source attributes:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # String — the AMI ID
instance_type = "t3.micro"
}
resource "aws_launch_template" "web" {
image_id = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
# Reference other attributes from the same data source
description = "Template using ${data.aws_ami.ubuntu.name} (${data.aws_ami.ubuntu.creation_date})"
}
Data Sources for Reading Another Workspace’s State #
terraform_remote_state is a special data source that reads outputs from another Terraform workspace. This enables multi-workspace architectures that are separate yet connected.
# The "networking" workspace provides the VPC and subnets
# The "compute" workspace reads outputs from the "networking" workspace
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "production/networking/terraform.tfstate"
region = "ap-southeast-1"
}
}
# Use the outputs from the networking workspace
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
vpc_security_group_ids = [
data.terraform_remote_state.networking.outputs.app_security_group_id
]
}
# Or read all subnets at once
resource "aws_autoscaling_group" "app" {
vpc_zone_identifier = data.terraform_remote_state.networking.outputs.private_subnet_ids
# ...
}
Data Sources That Depend on Resources #
There are cases where a data source needs to read a resource just created in the same configuration. This requires depends_on.
# SCENARIO: Reading a policy just created to combine it
resource "aws_iam_policy" "s3_read" {
name = "s3-read-policy"
policy = data.aws_iam_policy_document.s3_read.json
}
resource "aws_iam_policy" "cloudwatch_write" {
name = "cloudwatch-write-policy"
policy = data.aws_iam_policy_document.cloudwatch_write.json
}
# This data source reads the policy just created above
data "aws_iam_policy" "s3_read" {
name = "s3-read-policy"
# Without depends_on, the data source may be evaluated before
# the aws_iam_policy.s3_read resource is finished being created
depends_on = [aws_iam_policy.s3_read]
}
# A more common pattern: avoid this situation with a direct reference
# Instead of reading via a data source, reference the resource directly
resource "aws_iam_role_policy_attachment" "app_s3" {
role = aws_iam_role.app.name
policy_arn = aws_iam_policy.s3_read.arn # Direct reference — cleaner
# Not: data.aws_iam_policy.s3_read.arn
}
flowchart LR
subgraph DATASOURCE["data source"]
DS["data.aws_vpc.selected"]
end
subgraph RESOURCES["Resources"]
SG["aws_security_group"]
SUB["aws_subnet"]
end
DS -->|"vpc_id"| SG
DS -->|"cidr_block"| SUB
style DATASOURCE fill:#e3f2fd,stroke:#1565c0
style RESOURCES fill:#e8f5e9,stroke:#2e7d32Data Source Patterns in Production Architectures #
# PATTERN 1: Dynamic AMI — always use the latest tested AMI
data "aws_ami" "app" {
most_recent = true
owners = ["self"] # AMIs created by this account itself (golden AMIs)
filter {
name = "name"
values = ["app-server-*"]
}
filter {
name = "tag:Status"
values = ["tested"] # Only AMIs that passed testing
}
}
# PATTERN 2: Lookup resources managed by another team
# The networking team manages the VPC and tags it
data "aws_vpc" "main" {
tags = {
Name = "main-vpc"
Environment = var.environment
ManagedBy = "networking-team"
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.main.id]
}
tags = {
Tier = "private"
}
}
# PATTERN 3: Lookup based on a consistent naming convention
locals {
# Team-agreed naming convention: <project>-<env>-<component>
vpc_name = "${var.project}-${var.environment}-vpc"
}
data "aws_vpc" "main" {
filter {
name = "tag:Name"
values = [local.vpc_name]
}
}
# PATTERN 4: Conditional data source — only query if needed
variable "use_existing_vpc" {
type = bool
default = false
}
data "aws_vpc" "existing" {
count = var.use_existing_vpc ? 1 : 0
tags = {
Name = "existing-vpc"
}
}
resource "aws_subnet" "app" {
vpc_id = var.use_existing_vpc ? data.aws_vpc.existing[0].id : aws_vpc.new.id
# ...
}
Reading Account and Region Attributes #
Data sources for account and region information are very useful for making configurations portable — no need to hardcode the account ID or region.
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
data "aws_partition" "current" {}
# Use for portable ARNs
resource "aws_iam_role_policy" "app" {
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = [
# Portable — no hardcoded account ID or region
"arn:${data.aws_partition.current.partition}:s3:::${var.bucket_name}/*"
]
}]
})
}
# Standard tags including account information
locals {
standard_tags = {
AccountId = data.aws_caller_identity.current.account_id
Region = data.aws_region.current.name
ManagedBy = "terraform"
}
}
Data Source Lifecycle and Refresh Behavior #
Data sources are refreshed during terraform plan and terraform apply. Understanding when and how data sources are refreshed matters for consistency.
# Data sources are refreshed EVERY time plan/apply runs
# This differs from managed resources, which are only refreshed
# if -refresh=true (the default)
# Example: data.aws_ami.latest will always look for the newest AMI
# If a new AMI is created between plan and apply, apply could use
# a different AMI than what was planned
# To pin a data source to a specific value:
# Use a specific filter or store the value in a variable
flowchart TD
A["terraform plan"] --> B["Refresh data sources"]
B --> C["Read from the API\n(ami-12345)"]
C --> D["Plan using\nami-12345"]
D --> E["Time passes...\na new AMI is created"]
E --> F["terraform apply"]
F --> G["Refresh data sources\nagain!"]
G --> H["Read from the API\n(ami-67890) ⚠️"]
H --> I["Apply using\nami-67890 ≠ plan"]
style A fill:#e3f2fd,stroke:#1565c0
style H fill:#fff3e0,stroke:#e65100
style I fill:#ffebee,stroke:#c62828Performance Optimization for Data Sources #
# Data sources calling the API can slow down plans
# if there are many data sources or the API is slow
# Tip: Use specific filters to reduce API calls
# BAD: Fetch all AMIs, then filter in Terraform
data "aws_ami" "all" {
most_recent = true
owners = ["self"]
# Returns ALL of our AMIs → a large API response
}
# GOOD: Filter as early as possible at the API level
data "aws_ami" "filtered" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["my-app-*"] # Filter at the API level → small response
}
filter {
name = "state"
values = ["available"] # Only available AMIs
}
}
Caching Data Source Results #
Data sources are called every time terraform plan or terraform apply runs. For data that rarely changes, this can slow down execution.
# Solution: store data source results in local values
# The data source is called once, but can be used many times
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
locals {
# Store in locals for easy reference
region = data.aws_region.current.name
account_id = data.aws_caller_identity.current.account_id
# Build an ARN pattern usable in many places
arn_prefix = "arn:aws:${local.region}:${local.account_id}"
# Common tags including info from the data sources
common_tags = {
Region = local.region
AccountId = local.account_id
ManagedBy = "terraform"
}
}
# If data sources are too slow due to API rate limits:
# 1. Reduce the number of data sources (combine filters)
# 2. Use -refresh=false to skip refresh during plan
terraform plan -refresh=false
# Careful: state isn't refreshed, may be inaccurate
# 3. Targeted refresh for specific resources
terraform plan -refresh-only -target=data.aws_ami.latest
Data Source Authentication Context #
Data sources use the credentials configured in the provider block. If the provider isn’t configured, the data source will fail.
# Data sources use credentials from:
# 1. Provider block (explicit configuration)
# 2. Environment variables (AWS_ACCESS_KEY_ID, etc.)
# 3. Shared credentials file (~/.aws/credentials)
# 4. IAM role (EC2 instance profile, ECS task role)
# 5. OIDC token (GitHub Actions, GitLab CI)
# Priority order:
# Explicit config > Env vars > Shared creds > IAM role > OIDC
# Data source with an explicit provider
provider "aws" {
region = "ap-southeast-1"
profile = "production" # Uses shared credentials
}
# This data source uses the provider above
data "aws_ami" "latest" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*"]
}
}
# Data source with a different provider
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
data "aws_ami" "latest_us" {
provider = aws.us_east
most_recent = true
owners = ["amazon"]
}
Summary #
- Reference format:
data.<TYPE>.<NAME>.<ATTRIBUTE>— consistent with the resource reference format but with adata.prefix.terraform_remote_stateto read another workspace’s outputs — useful for multi-workspace architectures but creates coupling that needs consideration.depends_onfor data sources that read resources in the same configuration — make sure the resource is finished being created before the data source is evaluated.- Direct references are better than reading via a data source if the resource is in the same configuration.
- Golden AMI pattern: use the
tag:Status = testedfilter to ensure only validated AMIs are used.aws_caller_identityandaws_regionfor making configurations portable — no need to hardcode the account ID or region name in the configuration.
← Previous: What is a Datasource? Next: Datasource Anti-Pattern →