Terraform Data Sources with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
kreuzwerker/docker 3.9.0
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform and Docker — Terraform lab environment on Ubuntu
Privilege Normal user (Docker socket access required)
Scope Terraform data sources — data block syntax, querying existing infrastructure, attribute references, feeding data into resources, resource vs data comparison, evaluation timing, depends_on, brief count and for_each, provider alias syntax, and common read errors. Does not cover remote state data sources, full resource CRUD, provider alias depth, count or for_each mechanics, precondition depth, or cloud inventory tutorials.
Related guides Terraform resources
Terraform providers
terraform plan command
terraform apply command
Terraform Associate certification course

A Terraform data source lets you read information that already exists outside your current configuration — or that another team manages — and use those values in resources, locals, and outputs. The central distinction is simple:

text
resource → Terraform manages the object
data     → Terraform queries information about an object

This walkthrough uses the Docker provider on Ubuntu. You create a network outside Terraform, look it up with data "docker_network", attach a container to that network, and verify the result with docker inspect. Work in ~/terraform-labs/terraform-data-sources/ throughout.

NOTE
Use the Terraform lab environment on Ubuntu with Docker Engine running. Run terraform init before your first plan in a new directory. For managed objects and the resource block, see Terraform resources.

How Terraform data sources work

Provider developers publish both resources (objects Terraform can manage) and data sources (lookups Terraform can perform). Every data source belongs to a provider the same way resources do — if you use kreuzwerker/docker, you get data "docker_network", data "docker_registry_image", and other read operations documented in that provider.

A data block queries information without provisioning an associated infrastructure object through that block. Calling it a "read-only resource" is imprecise: Terraform never treats a data source as something to create, update, or destroy in state the way it does a managed resource.

Typical uses include looking up a VPC or subnet your platform team already created, resolving a container image digest before you pass it to a resource, or reading tags and names you need as arguments elsewhere in the module.

data block syntax

The block header has three parts — the keyword data, a type string the provider defines, and a local name you choose for references inside the module:

hcl
data "docker_network" "lab" {
  name = "tf-data-lab-net"
}
  • Type — provider-specific, written as "docker_network" not docker_network.lab
  • Local name — your label (lab); combined with the type in references
  • Arguments — provider-specific filters such as name, id, or filter blocks

Data blocks support depends_on, count, for_each, and provider, plus a lifecycle block for precondition and postcondition. Later sections cover the ones you meet most often on Associate exam tasks.

Resource vs data source

Behavior resource data
Manages an object Yes — create, update, delete through Terraform No associated provisioning through the data block
Reads information Yes, as part of managing state Primary purpose
Configuration keyword resource data
Reference prefix resource_type.local_name data.data_type.local_name

A managed docker_network resource and a data "docker_network" lookup can return similar attributes, but only the resource block puts Terraform in charge of the network lifecycle. The Terraform resources guide covers the managed side; this lesson covers the read side.

When Terraform reads a data source

Terraform evaluates a data source when its arguments are known and any dependencies are satisfied. HashiCorp documents three common cases:

  • Known arguments, no blocking dependency — independent lookups (such as an external Docker network that already exists) usually complete during terraform plan. You see data.docker_network.lab: Read complete before resource actions are proposed.
  • Unknown argument values — when a data source argument references a value not yet computed, Terraform may postpone the read until apply or until the value becomes known.
  • Dependency with pending changes — when a data source depends on a resource still being created in the same run, the plan shows <= read and a note that the data source will be read during apply.

Avoid absolute rules like "data sources always run only during plan." Watch the plan output: immediate Read complete means the lookup already succeeded; will be read during apply means Terraform is waiting on a dependency or unknown value. The deferred-read demo in a later section shows what that looks like in practice.


Query existing infrastructure with a data source

The strongest data-source pattern is: something exists already, Terraform reads it, then other configuration consumes the returned attributes. Here the network is created with the Docker CLI before Terraform runs.

Create the external Docker network

Create an isolated working directory for this lesson:

bash
mkdir -p ~/terraform-labs/terraform-data-sources && cd ~/terraform-labs/terraform-data-sources

The network must exist on the Docker host before the data source can find it:

bash
docker network create tf-data-lab-net

Docker prints the network ID on one line when creation succeeds.

Confirm the name is present:

bash
docker network ls --filter name=tf-data-lab-net --format '{{.Name}}'

Sample output:

output
tf-data-lab-net

That external object is what the data source will query.

Configure and initialize the lookup

Write the root module. The data block looks up the existing network; the docker_container resource is what Terraform creates:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

data "docker_network" "lab" {
  name = "tf-data-lab-net"
}

resource "docker_image" "nginx" {
  name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}

resource "docker_container" "web" {
  name  = "tf-data-sources-web"
  image = docker_image.nginx.image_id

  networks_advanced {
    name = data.docker_network.lab.name
  }

  ports {
    internal = 8080
    external = 8093
  }
}

output "network_id" {
  value = data.docker_network.lab.id
}

output "network_name" {
  value = data.docker_network.lab.name
}
EOF

Download the Docker provider plugin for this directory:

bash
terraform init

Sample output:

output
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0"...
- Installing kreuzwerker/docker v3.9.0...
- Installed kreuzwerker/docker v3.9.0 (self-signed, key ID 0DCE698927DAF8EC)

Terraform has been successfully initialized!

Read the data source during plan

Run a plan to see Terraform query the network before proposing container changes:

bash
terraform plan

Sample output:

output
data.docker_network.lab: Reading...
data.docker_network.lab: Read complete after 0s [id=343981b1b9980bdfa0f1c195e45ad345da29bda0936906e63b3ddb12ef65fc24]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

  # docker_container.web will be created
  + resource "docker_container" "web" {
      + name = "tf-data-sources-web"
      ...
      + networks_advanced {
          + name = "tf-data-lab-net"
        }
      + ports {
          + external = 8093
          + internal = 8080
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + name = "ghcr.io/nginx/nginx-unprivileged:alpine"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + network_id   = "343981b1b9980bdfa0f1c195e45ad345da29bda0936906e63b3ddb12ef65fc24"
  + network_name = "tf-data-lab-net"

The Reading... / Read complete lines show an immediate plan-time read — the case described in the timing section above. networks_advanced.name comes from data.docker_network.lab.name rather than a hard-coded string.

Apply and verify the result

Create the image and container Terraform manages:

bash
terraform apply -auto-approve

Sample output:

output
data.docker_network.lab: Reading...
data.docker_network.lab: Read complete after 0s [id=343981b1b998...]

docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 0s [id=sha256:334d92979f15...]
docker_container.web: Creating...
docker_container.web: Creation complete after 1s [id=f3a7b079196e...]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

network_id = "343981b1b9980bdfa0f1c195e45ad345da29bda0936906e63b3ddb12ef65fc24"
network_name = "tf-data-lab-net"

Only the image and container count as created resources. The network was already on the host.

Reference data source attributes in resources and outputs

Use this reference form anywhere an expression is allowed:

text
data.<TYPE>.<NAME>.<ATTRIBUTE>

The practical flow is a short chain:

text
data source  →  returned attribute  →  resource argument

In the lab configuration, networks_advanced.name takes data.docker_network.lab.name, and outputs expose the same lookup:

hcl
output "network_id" {
  value = data.docker_network.lab.id
}

output "network_name" {
  value = data.docker_network.lab.name
}

Print the stored output values:

bash
terraform output

Sample output:

output
network_id = "343981b1b9980bdfa0f1c195e45ad345da29bda0936906e63b3ddb12ef65fc24"
network_name = "tf-data-lab-net"

The network_id value matches the Docker network ID Terraform read during plan and apply.

Confirm the running container joined the external network:

bash
docker inspect tf-data-sources-web --format '{{json .NetworkSettings.Networks}}'

Sample output:

output
{"tf-data-lab-net":{"NetworkID":"343981b1b9980bdfa0f1c195e45ad345da29bda0936906e63b3ddb12ef65fc24","IPAddress":"172.19.0.2",...}}

The NetworkID matches data.docker_network.lab.id and terraform output network_id. Terraform read the network; the container resource consumed that information.


Data source dependencies

Implicit dependencies

When a resource argument references a data source attribute, Terraform infers that the read must complete before that resource is configured. In the lab, networks_advanced.name = data.docker_network.lab.name creates an implicit dependency — no depends_on required.

HashiCorp recommends letting expression references drive ordering whenever possible. Explicit depends_on is for hidden dependencies Terraform cannot infer from the data source's arguments.

Deferred reads and depends_on

Use depends_on on a data source only when a real dependency exists that Terraform cannot infer from its arguments — for example, when the lookup must wait for a side effect that is not reflected in any expression the data block uses. Unnecessary depends_on makes plans more conservative without adding safety.

The configuration below is an intentional teaching demo, not a pattern to copy in production. It creates a network and then re-queries that same network through a data source only to make deferred evaluation visible:

hcl
resource "docker_network" "created" {
  name = "tf-data-created-net"
}

data "docker_network" "created" {
  name       = "tf-data-created-net"
  depends_on = [docker_network.created]
}

When Terraform itself creates the network, reference docker_network.created.id or docker_network.created.name directly instead of looking up the same object through a data source.

Switch to a fresh subdirectory so the main lab state stays intact:

bash
mkdir -p ~/terraform-labs/terraform-data-sources/depends-on-demo && cd ~/terraform-labs/terraform-data-sources/depends-on-demo

Write the demo module:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

resource "docker_network" "created" {
  name = "tf-data-created-net"
}

data "docker_network" "created" {
  name       = "tf-data-created-net"
  depends_on = [docker_network.created]
}

output "network_id" {
  value = data.docker_network.created.id
}
EOF

Initialize the subdirectory:

bash
terraform init

On the first plan the network does not exist yet, so Terraform defers the data read — the third timing case from earlier:

bash
terraform plan

Sample output:

output
Terraform will perform the following actions:

  # data.docker_network.created will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "docker_network" "created" {
      + name = "tf-data-created-net"
      ...
    }

  # docker_network.created will be created
  + resource "docker_network" "created" {
      + name = "tf-data-created-net"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

The <= read symbol and the apply-time comment are what you were looking for in the timing section. After apply, the read runs once the resource exists:

bash
terraform apply -auto-approve

Sample output:

output
docker_network.created: Creating...
docker_network.created: Creation complete after 3s [id=eab2ff5569a9...]
data.docker_network.created: Reading...
data.docker_network.created: Read complete after 0s [id=eab2ff5569a9...]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Clean up the depends-on demo before continuing:

bash
terraform destroy -auto-approve

Return to the main lab directory:

bash
cd ~/terraform-labs/terraform-data-sources

Data source meta-arguments

HashiCorp documents these meta-arguments on data blocks: count, for_each, depends_on, provider, and a lifecycle block containing precondition and postcondition.

count and for_each

count and for_each work on data blocks the same way they work on resource blocks. With count, reference indexed instances such as data.docker_network.lab[0].name:

hcl
data "docker_network" "lab" {
  count = 1
  name  = "tf-data-lab-net"
}

output "network_name" {
  value = data.docker_network.lab[0].name
}

for_each uses a map or set of strings and produces data.type.name["key"] addresses. Full meta-argument behavior belongs in a dedicated lesson; this page only confirms data blocks accept count and for_each.

Provider aliases

When a provider has multiple configurations — for example, two Docker hosts — point a data source at the correct one with the provider meta-argument:

hcl
data "docker_network" "lab" {
  provider = docker.secondary
  name     = "tf-data-lab-net"
}

The alias must be declared in a provider "docker" block with alias = "secondary". See Terraform providers for alias setup and when multiple provider configurations are appropriate.

Preconditions and postconditions

Data sources can include lifecycle preconditions and postconditions that validate lookup results — the same mechanism resources use:

hcl
data "docker_network" "lab" {
  name = "tf-data-lab-net"

  lifecycle {
    postcondition {
      condition     = self.name == "tf-data-lab-net"
      error_message = "Expected the lab network name."
    }
  }
}

Custom conditions and validation rules are covered in the course lesson on validation and checks; this page only notes that data blocks support them.


Another data source example: query registry metadata

Data sources are not limited to local infrastructure. data "docker_registry_image" queries registry metadata without managing a local image resource. Create a separate subdirectory so this read does not interfere with the main lab state:

bash
mkdir -p ~/terraform-labs/terraform-data-sources/registry-demo

Move into that directory:

bash
cd ~/terraform-labs/terraform-data-sources/registry-demo

Write a complete root module. The provider exposes sha256_digest specifically for the image content digest, so use that attribute when you need the registry digest:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

data "docker_registry_image" "nginx" {
  name = "ghcr.io/nginx/nginx-unprivileged:alpine"
}

output "registry_digest" {
  value = data.docker_registry_image.nginx.sha256_digest
}
EOF

Initialize the registry-demo directory:

bash
terraform init

Sample output:

output
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0"...
- Installing kreuzwerker/docker v3.9.0...

Terraform has been successfully initialized!

Run plan to see a remote read with no infrastructure changes:

bash
terraform plan

Sample output:

output
data.docker_registry_image.nginx: Reading...
data.docker_registry_image.nginx: Read complete after 2s [id=sha256:334d92979f15...]

Changes to Outputs:
  + registry_digest = "sha256:334d92979f15aaecd5dd50af5105e1230e2bb70765d45b1e2f964e7c5eda81c3"

sha256_digest is the provider's explicit content-digest attribute. A docker_image resource is what actually manages the local image object.

Return to the main lab directory when you are done:

bash
cd ~/terraform-labs/terraform-data-sources

Common Terraform data source errors

Symptom Likely cause Fix
No matching object / not found Wrong name, ID, or filter; object deleted outside Terraform Confirm the object exists with the provider CLI or console; fix the lookup argument
Multiple matches where one expected Filter too broad Tighten filters or switch to a data source that accepts a unique ID
Invalid filter argument Typo or unsupported filter key for that data source Compare arguments with the provider documentation for the data source type
Missing provider configuration Provider block absent or wrong alias Add provider configuration or set provider = alias.name on the data block
Attribute not available Field does not exist on that data source or requires a newer provider version Read the provider schema; upgrade the provider constraint if needed
Dependency value unknown Data source argument references a value not known until apply Restructure references or use depends_on only when Terraform cannot infer the dependency

Reproduce a missing-object error

Save the working configuration before you break it on purpose:

bash
cp main.tf main.tf.good

Point the data source at a network name that does not exist:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

data "docker_network" "lab" {
  name = "tf-data-missing-net"
}
EOF

Run plan to surface the provider error:

bash
terraform plan

Sample output:

output
data.docker_network.lab: Reading...

Planning failed. Terraform encountered an error while generating this plan.

Error: Could not find docker network: Error response from daemon: network tf-data-missing-net not found

  with data.docker_network.lab,
  on main.tf line 14, in data "docker_network" "lab":
  14: data "docker_network" "lab" {

Restore the working module before cleanup:

bash
mv main.tf.good main.tf

The error is expected when the target object is missing. With main.tf restored, destroy can plan against the container and image again.


Cleanup

Destroy Terraform-managed objects in the main directory:

bash
terraform destroy -auto-approve

Sample output:

output
docker_container.web: Destroying...
docker_container.web: Destruction complete after 1s
docker_image.nginx: Destroying...
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.

Remove the Docker network you created outside Terraform:

bash
docker network rm tf-data-lab-net

Confirm the external network is gone:

bash
docker network ls --filter name=tf-data-lab-net --format '{{.Name}}'

An empty result means the network was removed. The data source never managed that network, so terraform destroy does not delete it — you remove it manually when the lab is finished.


References


Summary

Terraform data sources answer lookup questions your module should not hard-code. A data block asks the provider to read an existing object or external record; returned attributes flow into output blocks, resource arguments, and locals through the data.TYPE.NAME.ATTRIBUTE reference form. In the Docker lab you created a network with the CLI, queried it with data "docker_network", attached a container using data.docker_network.lab.name, and verified the join with docker inspect — without putting that network under Terraform management.

Evaluation timing matters as much as syntax. Known arguments on an object that already exists usually produce Read complete during plan; unknown values or dependencies with pending changes defer the read until apply, which the plan surfaces as <= read. Use depends_on on a data source only when Terraform cannot infer a real dependency from its arguments — not as a default. When Terraform creates an object itself, reference the resource attributes directly rather than re-querying the same object through a data source.

count, for_each, provider aliases, and lifecycle conditions all apply to data blocks when you need them. The registry example shows that lookups are not limited to local infrastructure. For production modules, treat missing lookups as hard failures during plan when possible, fix filter typos early, and prefer unique IDs over broad name searches when the provider allows it. Next, deepen managed-object workflows in Terraform resources or provider configuration in Terraform providers.


Frequently Asked Questions

1. What is a Terraform data source?

A data source is a configuration block that asks a provider to look up existing information and expose it as attributes you can reference elsewhere. Terraform does not create, update, or delete the underlying object through a data block.

2. What is the difference between a Terraform resource and a data source?

A resource block declares an object Terraform manages through create, update, and delete operations. A data block only queries information. You reference managed objects as resource_type.name.attribute and queried values as data.data_type.name.attribute.

3. How do you reference a Terraform data source attribute?

Use the form data.TYPE.NAME.ATTRIBUTE, where TYPE is the data source type, NAME is the local label in the data block header, and ATTRIBUTE is a field the provider documents for that data source.

4. When does Terraform read a data source?

Terraform reads a data source during planning or apply when its arguments are known and its dependencies are satisfied. Independent lookups often complete during plan; unknown argument values or dependencies with pending changes can defer the read until apply.

5. Do Terraform data sources support count and for_each?

Yes. Meta-arguments count and for_each work on data blocks the same way they work on resources, producing indexed instances such as data.docker_network.lab[0].name. This lesson shows a count example only; full meta-argument mechanics belong in a dedicated lesson.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)