Terraform Cheatsheet

Data Sources

Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

What Data Sources Are

A data source reads something Terraform does not manage and exposes its attributes for use elsewhere. It never creates, updates or destroys anything.

data "aws_ami" "al2023" {          # data "<TYPE>" "<LOCAL NAME>"
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami = data.aws_ami.al2023.id     # data.<TYPE>.<NAME>.<ATTRIBUTE>
}
  • Address form is data.aws_ami.al2023, and inside a module module.net.data... is not reachable from outside.
  • count and for_each work on data blocks, giving data.aws_ami.x[0] and data.aws_ami.x["key"].
  • provider = aws.west picks an aliased provider, same as on a resource.
  • A data block that finds nothing is an error, not an empty result. Plural data sources (aws_subnets, aws_instances, aws_vpcs) return possibly-empty lists instead.
  • Data sources still cost API calls on every plan. A config with 200 of them is a slow config.

When Data Sources Read

Timing is the thing people get wrong. A data source is read as late as it has to be and as early as it can be.

SituationBehavior
All arguments known at plan timeread during plan, values appear in the diff
Any argument depends on an unknown valuedeferred to apply, shows as (known after apply)
depends_on present on the data blockalways deferred to apply
terraform apply -refresh=falseuses the values cached in state, no re-read
terraform plan -refresh-onlyre-reads data sources and resources, proposes no changes
terraform apply -refresh-onlywrites the drift it found into state
terraform destroyread if still needed by something being destroyed

terraform refresh is deprecated. Use terraform apply -refresh-only, which shows you the drift and asks before writing it to state. The old command wrote state silently.

Adding depends_on to a data source forces a deferred read, which turns everything downstream unknown at plan time. That is sometimes exactly what you want, and sometimes it wrecks an otherwise readable plan.

# Needed: the API will not return the record until the zone exists,
# and nothing in the arguments references the zone resource.
data "aws_route53_zone" "main" {
  name       = "example.com"
  depends_on = [aws_route53_zone.main]
}

If you can express the dependency as a reference instead, do that. vpc_id = aws_vpc.main.id orders the read correctly with no depends_on and keeps the plan concrete where it can be.

Common AWS Data Sources

# Account, region and partition context
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
data "aws_partition" "current" {}

# Availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

# Existing network, looked up by tag
data "aws_vpc" "main" {
  tags = { Name = "production" }
}

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

# DNS zone
data "aws_route53_zone" "main" {
  name         = "example.com"
  private_zone = false
}
# IAM policy documents: build JSON in HCL instead of a heredoc
data "aws_iam_policy_document" "s3_access" {
  statement {
    sid       = "ReadWriteObjects"
    actions   = ["s3:GetObject", "s3:PutObject"]
    resources = ["${aws_s3_bucket.app.arn}/*"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_s3_bucket_policy" "app" {
  bucket = aws_s3_bucket.app.id
  policy = data.aws_iam_policy_document.s3_access.json
}
# Secrets. Both land in plaintext in state, see the gotchas below.
data "aws_ssm_parameter" "db_password" {
  name = "/myapp/prod/db_password"   # decrypted by default
}

data "aws_secretsmanager_secret_version" "db" {
  secret_id = "myapp/prod/db"
}

locals {
  db = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}

Other Providers

# Google
data "google_compute_image" "debian" {
  family  = "debian-12"
  project = "debian-cloud"
}
data "google_client_config" "current" {}
data "google_project" "current" {}
data "google_container_cluster" "main" {
  name     = "prod-cluster"
  location = "us-central1"
}

# Azure
data "azurerm_client_config" "current" {}
data "azurerm_resource_group" "main" {
  name = "prod-rg"
}
data "azurerm_key_vault_secret" "db" {
  name         = "db-password"
  key_vault_id = data.azurerm_key_vault.main.id
}

# Kubernetes
data "kubernetes_namespace" "system" {
  metadata { name = "kube-system" }
}
data "kubernetes_service" "ingress" {
  metadata {
    name      = "ingress-nginx-controller"
    namespace = "ingress-nginx"
  }
}

Utility and Built-in Data Sources

# Another root module's outputs. Reads the WHOLE state file.
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "acme-tf-state"
    key    = "shared/network/terraform.tfstate"
    region = "us-east-1"
  }
}

# An HTTP GET
data "http" "my_ip" {
  url = "https://checkip.amazonaws.com"
}

locals {
  my_cidr = "${chomp(data.http.my_ip.response_body)}/32"
}
# Run a program and parse its stdout as a flat JSON object.
# Last resort: it runs on every plan, on every machine, including CI.
data "external" "git_sha" {
  program = ["bash", "${path.module}/scripts/git-sha.sh"]
}

# Read a file from disk (hashicorp/local)
data "local_file" "ca" {
  filename = "${path.module}/certs/ca.pem"
}

For a secret you do not want in state, use an ephemeral resource (1.10+) instead of a data source. ephemeral "aws_secretsmanager_secret_version" "db" {} is fetched, used, and never persisted.

Filtering and Picking One Result

# Multiple filter blocks are ANDed together
data "aws_ami" "app" {
  most_recent = true
  owners      = ["self"]

  filter {
    name   = "tag:Project"
    values = ["myapp"]
  }
  filter {
    name   = "state"
    values = ["available"]
  }
}

# Plural data source plus one(): fails loudly if not exactly one match
data "aws_vpcs" "selected" {
  tags = { Name = "production" }
}

locals {
  vpc_id = one(data.aws_vpcs.selected.ids)
}

most_recent = true silently changes the answer whenever a new image is published, which means a plan can propose replacing every instance on a day you changed nothing. Pin the AMI in a variable for production, or attach ignore_changes = [ami] deliberately.

Gotchas

  • Data source results land in state in plaintext. A aws_ssm_parameter or aws_secretsmanager_secret_version read puts the secret in your state file. Use an ephemeral resource, or accept it and encrypt the backend.
  • A data source with count = 0 still parses, but every downstream reference must use one() or an index, so a toggle is easier to read as a try() on a local.
  • Do not use a data source to read something the same configuration creates. Reference the resource directly, otherwise you get a chicken-and-egg cycle or a stale read.
  • A data source cannot depend on a value only known after apply without going unknown itself. That cascade is often the reason a plan is full of (known after apply).
  • data.external and data.http make your plan depend on the machine and the network. Both break in CI in ways that are painful to debug.
  • Reading a whole remote state through terraform_remote_state grants read access to every secret in it. Prefer publishing the specific values (SSM, tfe_outputs).
RelatedWhere
count, for_each, depends_on, lifecycleMeta-Arguments
Ephemeral resources and write-only argumentsResources
List Resources and terraform query (1.14+)Resources, Commands
Backends, locking, remote stateState