TT Lab
Get started
Learn Learning paths Courses

Terraform/OpenTofu Fundamentals

Blocks That Create and Blocks That Read

Continue in TT Lab

In one line

A resource owns its target and a data only reads it. This one-word difference separates the state shape, the planning time, and the destruction scope.

Why this distinction was needed

The early configuration language had only resources. But real infrastructure is not all built by one team. The network has already been built by the infrastructure team, the certificates are issued by the security team, and the company has only one account number. To use these, you need their values, and if you write them as a resource just to get the values, the tool mistakes them for its own. The moment it believes it owns them, destroy tries to delete someone else's network, and drift detection tries to revert what another team changed.

So a "read-only block" arose separately. A data block neither creates, changes, nor deletes its target. An entry does appear in the state, but that is not a deed of ownership but a copy of the last value read. So even if you run destroy, the original remains, and the plan shows only read, not create or destroy.

How it works

If you open the state file, every entry has a mode. The value is either managed or data. Whether tofu state list adds the data. prefix when it prints an address is also decided here.

tofu state list
# data.local_file.seed      ← mode = "data"
# local_file.copy           ← mode = "managed"

When the read happens is the second fork. The default is the plan stage. If all the arguments are known values, the tool reads while making the plan and embeds the value it read directly in the plan. So a reviewer knows what will go in just by looking at the plan.

There are two cases where the read is pushed back to apply time. When an argument depends on an attribute of a resource that has not yet been created, and when depends_on is attached to the data source. The latter is pushed back even if the original is already on disk — because it is a declaration that it will read only after the dependency is done. In this case the plan shows this.

  # data.local_file.back will be read during apply
      + content = (known after apply)

When a value becomes unknown, the attributes of downstream resources that use that value also all become unknown. The worth of plan review drops by that much, so when attaching depends_on to a data source, you should ask once more whether it is truly needed.

The third fork is that it reads again. A managed resource catches drift by comparing the value written in the state with the real object, but a data source has nothing to compare against. It reads fresh on every plan and passes that value downstream. If the original changes, it is caught not as "the state diverged" but as "the input changed," and if that value is an attribute that triggers recreation, the downstream is replaced entirely.

The failure when it cannot be read also differs in timing. If the file is missing, it cannot get as far as apply and the plan stops right there.

Error: Read local file data source error
+Original Error: open ./absent.txt: no such file or directory

There is one sibling whose name is confusing. terraform_data is not a data block but a managed resource provided by the built-in provider. Its mode in the state is managed, changing its value produces update or replace in the plan, and destroy makes it disappear. Many people read it as a data source because of the name, but the purpose of this block is not to read a value but to leave a "marker that makes something happen again when it changes" in the state.

What you see in the field

The most common accident is writing something someone else made as a resource. If you declare a target that already exists as a resource, the tool tries to create it anew; if the names collide, the apply fails, and if they do not collide, one more identical thing is created. The latter is worse — duplicates pile up with no one seeing a failure. Write what you only need to read as data, and if you truly must own something that already exists, bring it in with import.

The second most common is the plan being plastered with unknowns. The more a team has a review culture, the more it sets a rule such as "we approve only if all values are visible in the plan," and an unapprovable plan comes out because of depends_on attached to a single data source. In such a case it is better to move the dependency not to the data source but to the resource that uses the value.

The third is the opposite direction. It is the case where the original quietly changes and the downstream gets replaced. A replacement shows up in the plan though not one character of the configuration file has changed. If you look for the cause in the code, you will never find it. First look at what original the data source reads and who can change it.

What you will do in the next lab

You go through eight steps with the Pod's OpenTofu and the local, random, and null providers. You read one file as data and copy it, separate two entries by mode in the state, and confirm that the original remains after destroy. Then you read a file whose name is decided at apply time to see how the plan differs, see what gets pushed back if you attach depends_on to an existing file, and see where the plan stops when the original is missing. In the last two steps, you place the mode of terraform_data side by side with a data source to compare, and confirm that changing the original makes the downstream get replaced.