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 modulemodule.net.data...is not reachable from outside. countandfor_eachwork on data blocks, givingdata.aws_ami.x[0]anddata.aws_ami.x["key"].provider = aws.westpicks 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.
| Situation | Behavior |
|---|---|
| All arguments known at plan time | read during plan, values appear in the diff |
| Any argument depends on an unknown value | deferred to apply, shows as (known after apply) |
depends_on present on the data block | always deferred to apply |
terraform apply -refresh=false | uses the values cached in state, no re-read |
terraform plan -refresh-only | re-reads data sources and resources, proposes no changes |
terraform apply -refresh-only | writes the drift it found into state |
terraform destroy | read if still needed by something being destroyed |
terraform refreshis deprecated. Useterraform 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_parameteroraws_secretsmanager_secret_versionread puts the secret in your state file. Use an ephemeral resource, or accept it and encrypt the backend. - A data source with
count = 0still parses, but every downstream reference must useone()or an index, so a toggle is easier to read as atry()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.externalanddata.httpmake 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_stategrants read access to every secret in it. Prefer publishing the specific values (SSM,tfe_outputs).
| Related | Where |
|---|---|
count, for_each, depends_on, lifecycle | Meta-Arguments |
| Ephemeral resources and write-only arguments | Resources |
List Resources and terraform query (1.14+) | Resources, Commands |
| Backends, locking, remote state | State |