You inherit a production environment that was built by hand in a cloud console, and you must bring it under Terraform without recreating anything. How do you approach it?
answer
- inventory first, code second
- slices ordered by blast radius
- generated config is a draft
- empty plan is the acceptance test
- import never cascades to children
basics
~20 sAdopt in small slices ordered by blast radius, write or generate configuration for each slice, import it, and treat a plan reporting no changes as the acceptance test before moving on. Never apply a post-import plan that still proposes changes.
solid answer
~60 sI would treat it as an inventory problem, not a Terraform problem. First establish what exists and who depends on it, then adopt in slices — starting with something low-risk like a bucket or an IAM role, not the production database — so the workflow is proven before it touches anything that matters. For each slice: write the resource blocks, or use `import` blocks with `terraform plan -generate-config-out` to draft them and then clean the draft up by hand; apply; and require that the next plan reports no changes. That empty plan is the only real acceptance test, and it is where the work actually is — copied tags, unset defaults, nested blocks, and attributes the provider cannot read back all show up as diffs you have to reconcile in configuration rather than apply. I would also expect import not to cascade: an IAM role does not bring its attachments, a VPC does not bring its subnets, so each of those is its own entry. And I would be willing to decide *not* to adopt some things — a stateless service is often cheaper to rebuild from clean code and cut over than to reverse-engineer.
code
bash · 3 linesterraform plan -generate-config-out=generated.tf
terraform apply
terraform plan -detailed-exitcodego deeper
Know that existing infrastructure joins Terraform through import, that import changes nothing in the cloud, and that a plan showing no changes afterwards is what proves it worked.
Explain the loop concretely: write or generate configuration, import, plan, reconcile the diff in configuration rather than applying, and repeat — and know that import handles one object at a time with no cascade to children.
Show the operating judgment: order slices by blast radius, gate each on a clean plan in CI, anticipate tag and default diffs and unreadable attributes, and freeze console changes for the slice in flight.
Own the decision of what gets adopted at all versus rebuilt and cut over, the ownership boundaries between teams and tools, and the fact that the deliverable is an estate the pipeline can plan against — not a state file one engineer understands.
## Frame it correctly Adoption is a state problem wearing a configuration costume. Every object in that account either has a state entry or does not, and everything you do is about creating those entries safely. The infrastructure itself must not change at all: the success criterion for the whole project is that production is exactly the same at the end as at the start, and Terraform now knows about it. ## Inventory before code List what is actually there, per service and per region, and mark three things for each object: does the provider support importing it, does anything depend on it, and would recreating it be survivable. That last column drives the order of work. Objects fall out into three groups — adopt, rebuild, and leave alone (someone else's, or a console-managed service you should not fight). ## Slice by blast radius, easiest first Do not begin with the database. Begin with something whose failure is a shrug — a bucket, a log group, an IAM role — and drive one slice all the way through the pipeline. What you are proving in slice one is not "Terraform can import a bucket"; it is that your repository layout, backend, credentials, review process and CI plan step all work. Once that path is trusted, work upward in risk. Keep each slice a separate pull request. A reviewer can meaningfully check ten imports and their plan output; they cannot check four hundred. ## Write config, or generate a draft For each slice, declare the resources and pair them with `import` blocks: ```hcl import { to = aws_iam_role.deploy id = "deploy-role" } ``` Run `terraform plan -generate-config-out=generated.tf` to have Terraform draft the resource blocks from the live objects. Treat that file as a first draft: it is verbose, it hardcodes everything, it has no variables and no structure, and it will contain attributes you would never write. Edit it into something you would accept in review before merging. Generated configuration that goes in untouched is how a repository becomes unmaintainable in week two. ## The empty plan is the acceptance test After applying the imports, `terraform plan` must report no changes. Anything else means your configuration disagrees with reality, and applying it would push your reverse-engineered guesses into production. In CI you can assert this mechanically — `terraform plan -detailed-exitcode` returns a distinct exit code for "changes present" — so a slice cannot be declared done while it still drifts. The diffs you will fight are predictable: - **Tags** applied by a person, a policy or another tool, which your config omits. - **Provider-filled defaults** you left unset, which then read back as a change. - **Nested blocks** the object has and your configuration does not, or vice versa. - **Unreadable attributes** — passwords, some user data, certain write-only fields — which the provider cannot read back at all and which will therefore diff forever unless handled deliberately. Each one is fixed in configuration, not by applying. ## Import does not cascade One import equals one state entry. Importing a role does not import its policy attachments; importing a VPC does not import subnets, route tables, gateways or their associations; where a provider models firewall rules as separate resources, every rule is its own import. Miss one and the plan proposes creating it — which, on something like a security group rule, silently changes the security posture of a live system. The empty-plan gate catches this, which is exactly why it is non-negotiable. ## Know when not to adopt Adoption is not always the cheapest path. For a stateless service behind a load balancer, writing clean Terraform, standing up a parallel copy and cutting traffic over is often faster and leaves you with better code than reverse-engineering a hand-built one. Reserve import for the things that genuinely cannot be recreated: data stores, anything with a fixed address or identity, anything with a long-lived external dependency such as a DNS delegation or a third-party allowlist. ## Land it in the pipeline Once a slice is imported, it is subject to everything else the pipeline does — plan on the pull request, apply on merge, drift checks on a schedule. That is the actual deliverable. An adopted estate that only one person can plan against on their laptop has not been adopted; it has been moved from one undocumented place to another. ## The human part Announce a freeze on console changes for the slice you are adopting, however informal. Every manual edit during the window shows up as a mystery diff, and you will burn hours proving it was a person rather than a bug in your configuration. Where a freeze is impossible, adopt in the quiet window and re-plan immediately.
- Which resources would you deliberately refuse to import, and why?Anything cheap to recreate and hard to reverse-engineer — a stateless service is usually better rebuilt from clean configuration and cut over. I would also leave alone objects owned by another team or managed by a different tool, since importing them starts a fight over ownership that the plan will keep re-opening.
- After importing, one attribute diffs on every plan no matter what you write. What is going on?Usually the provider cannot read that attribute back from the API — passwords and some user-data or write-only fields behave this way — so Terraform compares your configuration against nothing and always sees a change. It has to be handled deliberately rather than applied away, and it is worth checking whether the provider offers a write-only or ephemeral form of that argument.
- How do you stop a colleague's console change from corrupting an in-flight adoption?Freeze console changes for the slice being adopted and say so explicitly, because every manual edit during the window appears as an unexplained diff that costs hours to attribute. Where a freeze is impossible, adopt during a quiet window and re-plan immediately so the gap between generating configuration and proving it clean stays small.
- Why import in small slices rather than generating the whole account at once?Because the acceptance test is a human reading a plan. Ten imports and their diff can be reviewed properly; four hundred cannot, and the one security-group rule you missed hides in the noise. Small slices also mean the first one proves your repository layout, backend, credentials and CI path before anything risky is touched.
saying these in an interview costs you the question
- Applies the post-import plan to "make the drift go away"
- Commits generated configuration without reading or restructuring it
- Assumes importing a parent brings its child resources along
- Starts adoption with the production database
- Skips the clean-plan gate because the imports succeeded