| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1kreuzwerker/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:
resource → Terraform manages the object
data → Terraform queries information about an objectThis 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.
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:
data "docker_network" "lab" {
name = "tf-data-lab-net"
}- Type — provider-specific, written as
"docker_network"notdocker_network.lab - Local name — your label (
lab); combined with the type in references - Arguments — provider-specific filters such as
name,id, orfilterblocks
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 seedata.docker_network.lab: Read completebefore 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
<= readand 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:
mkdir -p ~/terraform-labs/terraform-data-sources && cd ~/terraform-labs/terraform-data-sourcesThe network must exist on the Docker host before the data source can find it:
docker network create tf-data-lab-netDocker prints the network ID on one line when creation succeeds.
Confirm the name is present:
docker network ls --filter name=tf-data-lab-net --format '{{.Name}}'Sample output:
tf-data-lab-netThat 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:
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
}
EOFDownload the Docker provider plugin for this directory:
terraform initSample 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:
terraform planSample 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:
terraform apply -auto-approveSample 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:
data.<TYPE>.<NAME>.<ATTRIBUTE>The practical flow is a short chain:
data source → returned attribute → resource argumentIn the lab configuration, networks_advanced.name takes data.docker_network.lab.name, and outputs expose the same lookup:
output "network_id" {
value = data.docker_network.lab.id
}
output "network_name" {
value = data.docker_network.lab.name
}Print the stored output values:
terraform outputSample 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:
docker inspect tf-data-sources-web --format '{{json .NetworkSettings.Networks}}'Sample 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:
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:
mkdir -p ~/terraform-labs/terraform-data-sources/depends-on-demo && cd ~/terraform-labs/terraform-data-sources/depends-on-demoWrite the demo module:
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
}
EOFInitialize the subdirectory:
terraform initOn the first plan the network does not exist yet, so Terraform defers the data read — the third timing case from earlier:
terraform planSample 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:
terraform apply -auto-approveSample 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:
terraform destroy -auto-approveReturn to the main lab directory:
cd ~/terraform-labs/terraform-data-sourcesData 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:
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:
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:
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:
mkdir -p ~/terraform-labs/terraform-data-sources/registry-demoMove into that directory:
cd ~/terraform-labs/terraform-data-sources/registry-demoWrite 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:
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
}
EOFInitialize the registry-demo directory:
terraform initSample 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:
terraform planSample 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:
cd ~/terraform-labs/terraform-data-sourcesCommon 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:
cp main.tf main.tf.goodPoint the data source at a network name that does not exist:
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"
}
EOFRun plan to surface the provider error:
terraform planSample 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:
mv main.tf.good main.tfThe 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:
terraform destroy -auto-approveSample 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:
docker network rm tf-data-lab-netConfirm the external network is gone:
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
- Terraform data sources overview — HashiCorp language documentation
- Terraform dependency management — implicit and explicit dependencies
- kreuzwerker/docker provider documentation —
docker_networkanddocker_registry_imagedata sources
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.

