Drift, imports and adopting existing resources
Two situations break the clean "Terraform is the source of truth" picture, and every real project hits them: drift (someone changed infrastructure outside Terraform, so reality no longer matches the code) and existing resources (infrastructure that already exists and needs to be brought under Terraform's management). Handling both without causing an outage is a practical skill this lesson covers. It closes the Terraform-at-scale module.
Drift: when reality and code disagree
Drift is when the real infrastructure differs from what your Terraform code and state say it should be — because someone made a change outside Terraform (a manual tweak in the AWS console, an emergency fix, another tool). Drift is a problem because it breaks Terraform's core assumption that the code is the source of truth:
- The next
terraform planwill show the drift — Terraform sees reality does not match the code and proposes to change reality back to match the code (undoing the manual change), or, if the manual change altered something the code manages, a confusing diff. - If the manual change was important (an emergency fix), Terraform's next apply might silently revert it — a real risk.
Detecting and resolving drift:
- Detect it with
terraform plan— a plan that shows unexpected changes to things you did not edit is drift (something changed outside Terraform). Run plans regularly (in CI) so drift surfaces early, not as a surprise during an unrelated change.terraform plan -refresh-onlyshows drift specifically without proposing config changes. - Resolve it deliberately. Either bring the change into code (update the Terraform to match the manual change you want to keep, so code and reality agree), or let Terraform revert it (apply, so reality returns to what the code says) if the manual change was unwanted. The wrong move is to ignore drift — it compounds, and eventually an apply does something surprising.
The cultural point: drift comes from changing infrastructure outside Terraform, so the fix is discipline — once infrastructure is managed by Terraform, change it through Terraform, not by hand. The occasional emergency console fix is understandable, but it must be reconciled back into code promptly, or the code stops being the source of truth and the whole benefit of IaC erodes.
Preventing drift
Better than resolving drift is preventing it:
- Change infrastructure only through Terraform. Make it the team norm; reserve manual console changes for genuine emergencies.
- Restrict who can change infrastructure manually. Use IAM (the IAM lesson) so most people cannot make ad-hoc production changes in the console — the infrastructure is changed by the Terraform pipeline's role, not by individuals clicking around. This both prevents drift and improves security.
- Run drift detection in CI. A scheduled
terraform planthat alerts on unexpected diffs catches drift early.
The combination — change only through Terraform, restrict manual access, detect drift automatically — keeps code and reality aligned, which is the precondition for trusting your IaC.
Importing existing resources
The second situation: infrastructure that already exists (created manually, or by another tool, or before you adopted Terraform) and that you now want Terraform to manage. You cannot just write the code and apply — Terraform would try to create a new resource, colliding with the existing one. Instead you import the existing resource into Terraform's state:
- Write the resource block in your Terraform to match the existing resource's configuration.
- Import it so Terraform's state maps that block to the existing real resource:
terraform import aws_s3_bucket.legacy my-existing-bucket-name
or, in newer Terraform, an import block in the config (which shows up in the plan and is applied
declaratively):
import {
to = aws_s3_bucket.legacy
id = "my-existing-bucket-name"
}
- Run
terraform planand iterate: after import, the plan compares your written config to the real resource, and you adjust your code until the plan shows no changes — meaning your code now accurately describes the existing resource, and Terraform manages it without wanting to alter it.
The goal of an import is a clean plan (no changes) afterwards — proving Terraform has correctly adopted the resource. This is how you bring an existing AWS estate under Terraform gradually: import resources one at a time, write matching config, confirm a no-change plan, and now that resource is managed as code. It is painstaking but safe, and it is how most teams migrate from ClickOps or another tool to Terraform without recreating everything.
The moved block and refactoring safely
One related tool: when you refactor your Terraform (rename a resource, move it into a module), Terraform
would otherwise see the old address gone and the new one new — and plan to destroy and recreate the
resource (dangerous for stateful things). The moved block tells Terraform "this is the same resource,
just at a new address," so it updates state without destroying anything:
moved {
from = aws_instance.app
to = module.compute.aws_instance.app
}
This lets you restructure your code (extract modules, rename things) without Terraform destroying and
recreating the underlying resources — essential when refactoring a live infrastructure codebase. Between
import (adopt existing resources), drift discipline (keep code and reality aligned), and moved (refactor
safely), you can evolve a real Terraform estate without causing outages — which is what operating IaC at scale
actually requires.
Check your work
Drift = real infrastructure differs from code/state (someone changed it outside Terraform). Problem: next
plan proposes to revert it (may silently undo an emergency fix) or shows a confusing diff. Detect with
plan (esp. -refresh-only), run regularly in CI; resolve deliberately — bring the change into code (keep it)
or apply to revert (unwanted). Don't ignore drift; it compounds.
Prevent drift: change infrastructure only through Terraform; restrict manual console access via IAM (change via the pipeline's role, not individuals); run drift detection in CI. Keeps code = reality (the precondition for trusting IaC).
Import existing resources: you can't just apply (Terraform would create a duplicate). Write a matching
resource block, import it (terraform import or an import block) into state, then iterate plan until
no changes (code accurately describes the real resource). How you adopt an existing AWS estate gradually.
moved block: when refactoring (rename/move into a module), tells Terraform it's the same resource at a
new address → updates state without destroy-and-recreate. Lets you restructure live code safely. Together:
import (adopt), drift discipline (align), moved (refactor) evolve a real estate without outages.
Practice
- Explain what drift is, how it arises, and why the next
terraform planmight do something surprising. - Describe how you detect drift and the two deliberate ways to resolve it.
- Explain three ways to prevent drift, including the IAM/access angle.
- Explain why you cannot just write config and apply for an existing resource, and what
importdoes instead. - Describe the import workflow and why a "no changes" plan afterwards is the goal.
- Explain what the
movedblock prevents when refactoring Terraform, and why that matters for stateful resources.
Official documentation
- Terraform — Import — Bringing existing resources under management.
- Terraform — Manage resource drift — Detecting and resolving drift.
- Terraform — Refactoring (
movedblocks) — Moving resources without recreating them.
Next: the Deployment Pipelines module.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship