Provisioners, Import & Handling Drift

Terraform / Infrastructure as Code

Chapter 9 · Provisioners, Import & Handling Drift

Every prior chapter assumed clean, fully-managed resources. Real infrastructure has messier edges — a one-time bootstrap step with no native equivalent, or a resource that already existed before Terraform ever touched it. This chapter covers the deliberate escape hatches for exactly those cases.

Provisioners — local-exec and remote-exec

A provisioner block runs a command as a side effect of a resource's creation — local-exec on the machine running Terraform itself, remote-exec on the resource that was just created, via SSH or WinRM.

resource "aws_instance" "web" { provisioner "remote-exec" { inline = ["sudo systemctl start nginx"] } }

Why Provisioners Are Discouraged

A provisioner's command is imperative code embedded inside a declarative resource — the exact split Chapter 1 opened with. Terraform has no visibility into what that script actually did, can't record its effect in state, and can't detect or reconcile drift on it the way it can for a real resource attribute. HashiCorp's own guidance is to treat provisioners as a last resort — and per Chapter 1's Terraform-vs-Ansible distinction, using remote-exec to configure a server is arguably doing Ansible's job from inside Terraform, the exact mixing of responsibilities Chapter 1's warn-box already cautioned against.

When Provisioners Are Still Justified

A genuinely one-time bootstrap action with no native resource or provider API equivalent, or triggering an external signal/webhook right after creation. Not a substitute for real configuration management — if the goal is installing and maintaining software on a server, that's still Ansible's job.

Provisioners run once, on creation, by default
A provisioner fires when its resource is first created — not on every subsequent apply. Editing a provisioner's command doesn't re-run it against a resource that already exists; only creating a new one (or an explicit when = destroy provisioner, or a null_resource with triggers) causes it to run again.

Importing Existing Infrastructure

terraform import brings a resource created outside Terraform — by hand in a console, or by another tool — under Terraform's management, without destroying and recreating it.

$ terraform import aws_instance.web i-0abcdef1234567890
Import only creates the state entry
import populates state — it does not write the matching resource block for you. The resource block still has to be written by hand first, as a best guess at the real object's actual configuration, or the next plan will show Terraform trying to "fix" every attribute it thinks is wrong.

The Import Workflow in Practice

  1. Write a resource block matching the real object as closely as possible
  2. Run terraform import <address> <real-world-id>
  3. Run terraform plan — any remaining mismatch shows up as a planned change
  4. Adjust the resource block, repeat step 3, until plan reports no changes

Newer Terraform versions (1.5+) support a declarative import block plus config generation, letting Terraform draft the resource block for you — a materially easier modern alternative to hand-writing it from scratch, though the underlying iterate-until-plan-shows-nothing workflow is unchanged.

plan is the verification step, every time
Immediately after any import, run plan. It's the only reliable signal that the hand-written resource block genuinely matches reality — the same "plan before you trust anything" habit from Chapter 1, applied here to a freshly-imported resource specifically.

Resolving cloud1-11's Own Gotcha

cloud1-11 warned that a manual console fix gets silently reverted by the next automated apply. Now the real fix: if a manual change genuinely needs to persist, either (a) update the .tf configuration itself to match the new reality, so the next plan shows no diff, or (b) if the resource was created entirely outside Terraform, bring it under management with import first. Re-running apply repeatedly and hoping the drift stops happening is never a real fix — Terraform will keep reverting toward whatever the configuration says, every single time, until the configuration itself changes.

Hands-On Exercises

Exercise 1

Explain why embedding a local-exec/remote-exec provisioner breaks Terraform's own desired-state model from Chapter 1 — specifically, what can Terraform no longer track about the provisioner's effect?

📄 View solution
Exercise 2

A resource was created by hand in the AWS console before this project's Terraform configuration existed. Describe the two-step workflow needed to bring it under Terraform management safely, including the common surprise about what import alone does and doesn't do.

📄 View solution
Exercise 3

A manual console fix was applied during an incident to keep a service running. Explain the two legitimate options for stopping the automated apply from reverting it, and why "just don't run apply for a while" isn't a real fix.

📄 View solution

Chapter 9 Quick Reference

  • Provisioners — imperative escape hatch, discouraged, last-resort only
  • Provisioners run once, on creation, by default — not on every apply
  • terraform import — brings an existing resource under management without recreating it
  • Import only writes state — the resource block must still be written by hand (or via a config-generation import block on 1.5+)
  • Always plan immediately after an import to verify the resource block truly matches reality
  • Fixing drift for real: update configuration to match, or import — never just keep re-applying