How to import already-running infrastructure into OpenTofu using a declarative import block or tofu import, then refactor without destroy using the moved block and tofu state mv, rm, list, and show so state always stays in sync with reality.

In the previous episode 8 we covered advanced expressions, built-in functions, and loops — how count, for_each, depends_on, lifecycle, and for expressions plus ternary make your HCL dynamic and concise. But there's one assumption we've been silently holding: all infrastructure is created from scratch by OpenTofu itself.
Reality on the ground is almost always different. When you join a team, most of the infrastructure already exists — built through the AWS console, created by someone who has left, or the result of legacy tooling. Simply re-declaring all those resources in HCL isn't enough: OpenTofu would see "two versions" and want to recreate everything. Terrible.
In this episode we cover state manipulation & import workflows — how to bring existing infrastructure into OpenTofu state without destroying it, then refactor code without destroy using the moved block and the tofu state commands. With these skills, you can adopt IaC for systems that have been running for months.
Importing means telling OpenTofu: "this resource already exists in the cloud with a specific ID, don't create a new one — just record it in state." After that, tofu plan treats the resource as part of the configuration and computes the difference toward the declared condition.
There are two ways: an imperative CLI and a declarative block. The CLI way is direct, a single command:
tofu import aws_s3_bucket.data produksi-data-bucketThe declarative way is preferred for production because it's recorded in code and can be reviewed in a pull request:
import {
to = aws_s3_bucket.data
id = "produksi-data-bucket"
}Once the import block is written, run tofu plan to see the changes that will happen, then tofu apply to record the resources into state. Idempotent and repeatable — that's the advantage of the declarative approach over typing CLI commands in a terminal.
Tip
Write the resource definition in main.tf first (e.g. bucket attributes along with tags), then add the import block. During apply, OpenTofu imports and immediately reconciles attribute differences. Fewer surprises than importing into an undefined resource.
moved BlockOnce infrastructure is adopted, refactoring needs almost certainly arrive: renaming resources, moving resources into modules, or reorganizing structure. If you only change names in the code, OpenTofu will offer destroy then create — killing the old resource and making a new one. For stateless resources that might be fine; for databases and production instances, it's a disaster.
The moved block solves this by telling OpenTofu that the new address is just the result of a renaming, not a new resource:
moved {
from = aws_instance.web
to = aws_instance.web_server
}
moved {
from = aws_instance.web_server
to = module.web.aws_instance.web
}The first example moves an address between resource names; the second shows moving a resource into a module. Once the moved block is added, tofu plan no longer shows a destroy — what appears is just a line telling you the state was moved.
Warning
Don't remove moved blocks too quickly. Keep them for a few apply cycles so all team members — including those who rarely run OpenTofu — get the chance to run their state migrations. Removing the block before the team syncs state risks triggering an unwanted destroy.
tofu stateFor one-off or diagnostic state changes, OpenTofu provides the tofu state subcommand. The four most commonly used:
tofu state list
tofu state show aws_instance.web_server
tofu state mv aws_instance.web_server aws_instance.web
tofu state rm aws_instance.bekastofu state list lists all recorded resource addresses.tofu state show details one resource's attributes — useful when verifying an import result.tofu state mv moves a state entry — a quick alternative to the moved block for ad-hoc changes.tofu state rm removes an entry from state.The fundamental difference between a moved block and tofu state mv: moved is part of the configuration, recorded in Git, and processed automatically every time you run tofu plan or tofu apply; tofu state mv is a one-time operation on the machine that runs it — other team members must do the same or accept a moved later.
Warning
tofu state rm does not delete the resource in the cloud — it only releases it from state. As a result, on the next tofu plan, OpenTofu considers the resource gone and offers to create it again. Make sure you truly know what you're removing, and combine this with the state encryption from episode 6 if your state holds sensitive data.
Here's a safe sequence for adopting legacy infrastructure:
main.tf with attributes matching the real-world condition.import block in import.tf for each resource.tofu init if there are new providers or backends, then tofu plan.tofu apply to record state.moved blocks and keep them for a few cycles before cleaning up.tofu state list that each resource is at its expected address.In episode 9 we mastered state management and import workflows:
import blocks (declarative) and tofu import (CLI) bring existing infrastructure into state without destroy.moved blocks handle renaming and moving into modules without recreate.tofu state list, show, mv, and rm for diagnostics and one-off state manipulation.These capabilities make OpenTofu safe to apply to long-running production systems. In the next episode, episode 10, we'll cover Reusable Modules & OpenTofu Registry — wrapping infrastructure into reusable modules with the main.tf, variables.tf, and outputs.tf structure, then consuming them from the registry, Git, or a local path. See you there!