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.
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.
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.
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
- Write a
resourceblock matching the real object as closely as possible - Run
terraform import <address> <real-world-id> - Run
terraform plan— any remaining mismatch shows up as a planned change - Adjust the resource block, repeat step 3, until
planreports 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. 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
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?
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.
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 solutionChapter 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
planimmediately 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