RizTech Academy logo
RizTech Academy
Terraform at ScaleLesson 5 of 535 min

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 plan will 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-only shows 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 plan that 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 plan and 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

  1. Explain what drift is, how it arises, and why the next terraform plan might do something surprising.
  2. Describe how you detect drift and the two deliberate ways to resolve it.
  3. Explain three ways to prevent drift, including the IAM/access angle.
  4. Explain why you cannot just write config and apply for an existing resource, and what import does instead.
  5. Describe the import workflow and why a "no changes" plan afterwards is the goal.
  6. Explain what the moved block prevents when refactoring Terraform, and why that matters for stateful resources.

Official documentation

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