Almost every grown AWS environment holds two truths: the one in the console and the one in the repository. As long as they drift apart, every change carries a small risk and no environment is reproducible. I bring the two together, and in the direction that breaks nothing: what exists gets imported, not rebuilt.
What exists importedState with lockingDrift removedEnvironment reproducible
What you get
An inventory: what exists, who created it and who still needs it
What exists imported rather than recreated, in steps and without downtime
State in one place, with locking and versioning, separated per environment
Modules only where something genuinely repeats, not built in advance
A plan that runs in the pipeline and whose output can actually be read
Handover: your team creates infrastructure afterwards without asking me
Scope & working together
Inventory and a first import as a bounded two to four week engagement, further work according to scope. Operations stay with your team, which is why the handover is part of the work.
Remote from Germany. Straight with me, no agency in between.
The starting point
The most expensive road to Terraform is the one that looks faster.
When an environment grew in the console, there are two roads to infrastructure as code. The first: rebuild everything, switch over, tear down the old. It looks faster and it is a rewrite, with all the familiar consequences. It takes longer than estimated, it cannot be delivered in steps, and at the moment of switching everything depends on everything. The second road is the import: what exists gets described and brought under management without being touched. Since Terraform 1.5 that runs through import blocks in the configuration, and terraform plan -generate-config-out writes a first draft from the import that you tidy up rather than type.
The second finding is almost always the state. It sits on a laptop, or in a bucket without versioning, or there are three of them and nobody knows which one counts. The lock is missing too, so two concurrent runs overwrite each other. That is easier to fix today than it used to be: the S3 backend has locked itself since Terraform 1.11 through a .tflock object next to the state, switched on with use_lockfile = true, and a dedicated DynamoDB table is no longer needed because its arguments are deprecated. Versioning on the bucket, separate state per environment, encryption: half an hour of work, and the difference between a tool and a risk.
The third finding is drift, and it is a process problem rather than a technical one. As long as somebody changes things in the console "just quickly", the description drifts away, and the next apply wants to roll it back. No tool fixes that, only a decision: whatever is described as code is changed only through code, and anyone keeping write access in the console knows why. Alongside it a regular plan that reports deviations before somebody discovers them during an incident.
Does any of this sound familiar?
Nobody can say which resources in an account are actually needed.
Setting up a second environment takes days and never comes out quite the same.
The Terraform state sits on somebody machine or in a bucket without locking.
Part of it is described as code, the rest by hand, and nobody knows the boundary.
terraform plan shows changes that nobody asked for.
There are modules for everything, and nobody understands what a call does any more.
What I do
What happens along the way
01
Take stock before describing anything
InventoryDependenciesClearing up
The first question is which resources exist at all and which of them anybody still needs. In a grown environment a quarter regularly falls out: test instances, orphaned volumes, security groups with no target. Whatever can go does not get imported, because clearing up is cheaper than describing.
02
Import rather than recreate
Import blocksgenerate-config-outPlan must be empty
What exists comes under management through import blocks, area by area, in an order that respects dependencies: network and identity first, then data, then application. After each step terraform plan has to be empty. That is the acceptance criterion and at the same time the proof that the description matches reality.
03
State you can trust
S3 backenduse_lockfileSeparation per environment
One place for the state: S3 with versioning, encryption and locking through use_lockfile, separated per environment and per area. Separated also means small enough that a run takes minutes rather than half an hour, because a slow plan does not get read.
04
Modules once they are earned
Modulesmoved blocksInterfaces
Modules come out of repetition, not anticipation. Two identical things are a coincidence, three are a pattern. What a module encapsulates gets decided at its interface, and renames run through moved blocks so a refactor does not appear in the plan as delete and recreate.
05
Plan into the pipeline, drift onto the table
CI/CDDrift detectionOIDC
The plan runs in the delivery path: as a preview on every pull request, as an apply after the merge, with approval for the production stage. Alongside it a regular run that reports deviations. The pipeline obtains its permissions through OIDC rather than stored keys, and there is a page of its own for that.
06
Handover, so it belongs to you
DocumentationRunbookHandover
At the end the things that make the result last: a short guide on how to add a resource, the rule for console access, a runbook for a damaged state, and a handover session. Afterwards your team creates infrastructure without asking.
How long does a second environment take you today?
Tell me what is described as code today and what is not. You get an assessment before you commission anything.
Four steps, and after the second one a first area is under management. No step assumes you will commission the next one.
STEP 01
Inventory
What exists, which of it is needed, what is already described. The result is a map with order and effort, plus the list of what can go before the import. Read access is enough.
STEP 02
First area imported
One bounded area comes under management, with clean state and an empty plan as acceptance. That shows you on a real example what the approach costs and what it returns.
STEP 03
Onward in slices
Area by area, ordered by dependencies. Each slice is useful on its own, and between two slices the environment is always in a valid state.
STEP 04
Pipeline and handover
Plan and apply run in the delivery path, drift gets reported, the team gets the guide and a session on it.
The outcome
What is different afterwards
A second environment is an hour
When an environment comes out of code, the next one is a variable rather than a project. That pays for itself with the first test system somebody genuinely needs.
Changes are traceable
Who changed what and when sits in the repository rather than in CloudTrail. That is also the answer to the audit question, and it costs nothing extra.
No more guessing about what exists
Whatever is not in the code no longer exists. The clearing up alone regularly lowers the bill in grown accounts.
The plan gets read
Small states, short runs, output you can follow. A plan you check in two minutes is the difference between a control and a ritual.
Directly with me as a freelancer. No agency in between.
I am Tim Rutte. More than 20 years in software development, plus the HashiCorp certification for Terraform. Whatever infrastructure I build, I describe as code, because otherwise I cannot find it again myself. You talk to the person who touches your code, from the first call to the handover.
For most environments this is a reversible decision, because the language is the same, so it does not come first. Where things stand: HashiCorp changed the licence from MPL to BUSL 1.1 in August 2023, OpenTofu was forked from the last free version under the Linux Foundation in response, and HashiCorp now belongs to IBM. OpenTofu has since added capabilities of its own, among them state encryption, and GitLab moved its CI/CD templates onto it in 2025. If your legal team has a problem with BUSL, or you need encryption in the tool itself, we take OpenTofu. Otherwise Terraform, because the ecosystem and the documentation are larger.
Would CloudFormation or CDK not be the obvious choice on AWS?+
Sometimes yes, and then I say so. CDK is strong when your team writes TypeScript or Python anyway and the infrastructure grows close to the application; CloudFormation is right when you are AWS only and use Service Catalog or StackSets. Terraform is my default because it describes more than AWS, with Cloudflare, GitHub and Datadog in the same language, and because the plan is readable before it is applied. Anyone already on CDK gets no migration recommendation from me just because I prefer Terraform.
Do we have to rebuild everything?+
No, and that is the core of the approach. What exists gets imported rather than recreated: since Terraform 1.5 there are import blocks in the configuration for exactly that, and a first configuration draft can be generated from the import instead of typed. The acceptance criterion per step is an empty plan, which proves the description matches what is running, and nothing was touched.
What does introducing Terraform cost?+
Billed by time spent, with the inventory and a first import as a bounded two to four week engagement. Unlike the upgrade pages there is deliberately no fixed-price package here: the work between "nothing is described" and "half of it is described but the state is broken" is too different. After the inventory I can offer the rest at a fixed price; before it, that would be guesswork.
Our state sits on a laptop. How bad is that?+
Fixable, and usually the first thing done. The state moves into an S3 bucket with versioning and encryption, and since Terraform 1.11 the bucket handles locking itself through use_lockfile = true, so a dedicated DynamoDB table is no longer needed. After that the state is in one place, protected against concurrent runs, and recoverable from an earlier version if it comes to that.
We have drift. Will Terraform now roll everything back?+
Not unasked. The import describes the actual state rather than a desired one, which is why the first plan is empty. After that drift is a decision: whatever was changed in the console either gets adopted into the code or deliberately rolled back, item by item. What helps permanently is the rule behind it, namely that described resources are only changed through code, plus a regular run that reports deviations before they surface during an incident.
Do we need modules for everything straight away?+
No, and that is the most common mistake after an import. A module comes out of a repetition you have seen three times, not out of the expectation that three will happen. Modules built too early hide exactly the details you need while debugging, and their interfaces are wrong by then. I start flat and pull things together once the pattern shows.
Will you operate the infrastructure afterwards?+
No, and that is a decision. I bring the environment under management, set up the pipeline and hand over. Infrastructure whose description is maintained by an outsider is a problem exactly when speed matters. What remains are questions, which I am happy to answer, rather than a responsibility.