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:#fff

How 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:#2e7d32

Data 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:#c62828

Performance 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 a data. prefix.
  • terraform_remote_state to read another workspace’s outputs — useful for multi-workspace architectures but creates coupling that needs consideration.
  • depends_on for 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 = tested filter to ensure only validated AMIs are used.
  • aws_caller_identity and aws_region for 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 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact