What is a Datasource? #

Resources and data sources both use information from the provider, but their roles are opposite. Resources create infrastructure and Terraform is responsible for their lifecycle. Data sources read information from existing infrastructure — which may have been created by another Terraform workspace, by another team, or even manually. Data sources are Terraform’s way of reaching beyond its own configuration to get the information it needs.

flowchart TD
    A["data source block\nin the Terraform config"] -->|"Read"| B["☁️ Cloud Provider\nAPI"]
    B -->|"Return"| C["📋 Existing\nResource Data"]
    C --> D["🔗 Referenced in\nother resources"]

    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

Resource vs Data Source: The Fundamental Difference #

# resource — Terraform OWNS this
# Created, updated, and deleted by Terraform
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  # This VPC exists because Terraform created it
  # If this block is removed from the configuration → the VPC is deleted
}

# data — Terraform READS this
# Already exists outside, Terraform only queries
data "aws_vpc" "existing" {
  id = "vpc-0abcdef1234567890"
  # This VPC existed before Terraform — created manually or by another team
  # Terraform only reads it, never modifies or deletes it
}
LIFECYCLE COMPARISON:

resource:
  terraform apply  → resource is CREATED
  change the config → resource is UPDATED
  remove from .tf   → resource is DELETED

data:
  terraform plan/apply → data is READ
  change a filter      → DIFFERENT data is read
  remove from .tf       → nothing changes in the infrastructure

How Data Sources Work in the Execution Plan #

Data sources are evaluated during terraform plan — before any create/update/destroy operation on resources.

EVALUATION ORDER:

flowchart TD
    Start["terraform plan starts"] --> Step1["1. Evaluate all data sources<br/>(query the provider API right away)"]
    Step1 --> Step2["2. Use data source values<br/>to calculate the resource plan"]
    Step2 --> Step3["3. Show the execution plan"]
# Data sources are evaluated first, then the resources using them are planned

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

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

resource "aws_instance" "web" {
  # At plan time, data.aws_ami.ubuntu.id is already known
  # (not "known after apply" like a resource)
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
}

The Most Commonly Used Data Sources #

# 1. Latest AMI
data "aws_ami" "ubuntu_22_04" {
  most_recent = true
  owners      = ["099720109477"]

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

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

# 2. Current AWS account information
data "aws_caller_identity" "current" {}

data "aws_region" "current" {}

# Use in the configuration:
resource "aws_s3_bucket" "logs" {
  bucket = "logs-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.name}"
}

# 3. Available availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "public" {
  count             = 3
  availability_zone = data.aws_availability_zones.available.names[count.index]
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
}

# 4. A VPC or resource managed by another Terraform
data "aws_vpc" "shared_services" {
  tags = {
    Name = "shared-services-vpc"
    Environment = "production"
  }
}

# 5. A secret from AWS Secrets Manager
data "aws_secretsmanager_secret_version" "api_key" {
  secret_id = "production/myapp/api-key"
}

# 6. An IAM policy document
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "instance" {
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
flowchart LR
    A["🏗️ Manually\nCreated VPC"] -->|"Existing"| B["data.aws_vpc\nLookup"]
    B -->|"vpc_id, cidr_block"| C["🔗 Used by\nNew Subnet"]
    D["🏗️ Manually\nCreated AMI"] -->|"Existing"| E["data.aws_ami\nLookup"]
    E -->|"image_id"| F["🔗 Used by\nNew Instance"]

    style A fill:#6b7280,stroke:#4b5563,color:#fff
    style B fill:#3b82f6,stroke:#1e40af,color:#fff
    style C fill:#10b981,stroke:#059669,color:#fff
    style D fill:#6b7280,stroke:#4b5563,color:#fff
    style E fill:#3b82f6,stroke:#1e40af,color:#fff
    style F fill:#10b981,stroke:#059669,color:#fff

When to Use Data Sources #

USE DATA SOURCES FOR:
  ✓ Reading resources managed by another Terraform workspace
    (a VPC from the networking team, an IAM role from the security team)
  ✓ Getting dynamic information that changes
    (latest AMI, available AZs, latest RDS engine version)
  ✓ Reading secrets from AWS Secrets Manager or Vault
  ✓ Getting information about the active account or region
  ✓ Reading resources created manually that won't be Terraformed
  ✓ Building IAM policy documents programmatically

DON'T USE DATA SOURCES FOR:
  ✗ Resources whose lifecycle you want to manage with Terraform
    → Use a resource block, not a data block
  ✗ Working around cross-workspace dependencies with implicit coupling
    → Consider passing values explicitly via variables

Data Sources with Wrong Filters #

One of the most common mistakes with data sources is a filter that returns more than one result or no result at all.

# ANTI-PATTERN: Overly broad filter — may return >1 result
data "aws_instance" "web" {
  filter {
    name   = "instance-state-name"
    values = ["running"]  # There are many running instances!
  }
  # Error: Your query returned more than one result. Please try a more specific search criteria.
}

# CORRECT: A filter specific enough to return exactly 1 result
data "aws_instance" "web" {
  filter {
    name   = "tag:Name"
    values = ["web-server-production"]
  }

  filter {
    name   = "instance-state-name"
    values = ["running"]
  }
}


Data Source vs Variable: When to Use Which #

The choice between a data source and a variable depends on the value’s source and the flexibility needed.

flowchart TD
    A["Need the value of\na resource that exists?"] --> B{"Value source?"}
    B -->|"From another\nTerraform"| C["terraform_remote_state\ndata source"]
    B -->|"From the cloud\nprovider"| D["data.aws_ami\ndata.aws_vpc\netc."]
    B -->|"From the user\nin the CLI"| E["variable\nwith a default"]
    B -->|"From a file\n.env/.tfvars"| F["variable\nwith validation"]

    style A fill:#e3f2fd,stroke:#1565c0
    style C fill:#e8f5e9,stroke:#2e7d32
    style D fill:#fff3e0,stroke:#e65100
    style E fill:#f3e5f5,stroke:#6a1b9a
    style F fill:#fce4ec,stroke:#c62828
# DATA SOURCE: Fetch from the cloud API
data "aws_ami" "latest" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*"]
  }
}

# VARIABLE: Provided by the user or tfvars
variable "ami_id" {
  type    = string
  default = "ami-12345"
}

# When a data source is better:
# - The value changes over time (latest AMI, availability zones)
# - The value depends on resources already existing in the cloud
# - You need information the user doesn't know

# When a variable is better:
# - The value is known and specific
# - You need custom validation
# - The value differs per environment

Avoiding Over-Fetching with Data Sources #

# ANTI-PATTERN: Fetching too much data
data "aws_instances" "all" {
  # Fetches ALL instances in the account → expensive API calls
  # No filter → a very large response
}

# CORRECT: Filter as early as possible
data "aws_instances" "running" {
  filter {
    name   = "instance-state-name"
    values = ["running"]
  }
  
  filter {
    name   = "tag:Environment"
    values = [var.environment]
  }
}

Data Source Patterns for Multi-Environment #

A common pattern: using data sources to adapt the configuration to the currently active environment.

# Fetch info about an existing VPC (shared infrastructure)
data "aws_vpc" "main" {
  filter {
    name   = "tag:Environment"
    values = [var.environment]
  }
}

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

# Use data sources for conditional resources
resource "aws_instance" "web" {
  count         = var.environment == "production" ? 3 : 1
  ami           = data.aws_ami.latest.id
  instance_type = var.environment == "production" ? "m5.large" : "t3.micro"
  subnet_id     = data.aws_subnets.private.ids[0]
}
flowchart LR
    A["terraform workspace\nselect production"] --> B["data.aws_vpc\n(tag:Environment=prod)"]
    B --> C["data.aws_subnets\n(vpc-id=xxx)"]
    C --> D["resource.aws_instance\n(count=3)"]

    style A fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32

Data Source Error Handling #

Data sources can fail for various reasons. It’s important to handle errors correctly.

# Common errors:

# 1. Resource not found
# Error: no matching AMI found
# → Check the filter, make sure the resource actually exists

# 2. Multiple matches
# Error: multiple VPCs matched; use additional constraints
# → Add a more specific filter

# 3. Permission denied
# Error: UnauthorizedOperation
# → Make sure the IAM policy has read permission for the resource type

# 4. API rate limit
# Error: Throttling
# → Reduce data sources, use caching, or retry
# Tip: use try() for graceful degradation
locals {
  # If the data source fails, use a default
  vpc_cidr = try(data.aws_vpc.main.cidr_block, "10.0.0.0/16")
  
  # If the data source returns empty
  subnet_ids = length(data.aws_subnets.private.ids) > 0     ? data.aws_subnets.private.ids     : [aws_subnet.fallback.id]
}

Data Source Lifecycle #

DATA SOURCE LIFECYCLE:

terraform init
  └── Download providers

terraform plan
  ├── Read the data source (API call)
  ├── Store the result in state
  ├── Use the result to compute resource arguments
  └── Plan resource changes

terraform apply
  ├── Read the data source again (refresh)
  ├── Apply resource changes
  └── Update state

terraform destroy
  └── Delete managed resources (data sources are NOT deleted)
# Data sources are ALWAYS refreshed during plan/apply
# This can be slow if there are many data sources

# Speed up: use -refresh=false (not recommended)
terraform plan -refresh=false

# Better: reduce the number of data sources
# or use a caching mechanism

Summary #

  • Data sources read, resources create — data sources never change infrastructure, they only read information from what already exists.
  • Data sources are evaluated at plan time — their values are known before any resource is created or changed.
  • Use them for dynamic information — the latest AMI, available AZs, current account info — values that change and can’t be hardcoded.
  • Use them for cross-boundary reads — resources from other workspaces, resources managed by other teams, manually created resources.
  • Filters must be specific enough to return exactly one result — overly broad filters cause errors at plan time.
  • Data sources don’t create ownership coupling — Terraform can’t delete or modify a resource it only reads via a data source.

← Previous: Sensitive Output   Next: Reference →

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