Back to all posts
Infrastructure as Code Migration: A Practical Playbook
Cloud & DevOpsAugust 28, 2026

Infrastructure as Code Migration: A Practical Playbook

Infrastructure as Code Migration: A Practical Playbook

Most teams do not adopt infrastructure as code from a clean slate. They inherit a subscription full of resources someone clicked into existence over two or three years: a hand-tuned load balancer, a database with firewall rules nobody documented, an app service with three environment variables that only one former employee understood. An infrastructure as code migration is the process of bringing that reality under version-controlled, reviewable, repeatable definitions, usually with Terraform, without breaking production along the way. This article covers how to sequence that migration, where teams lose weeks importing state incorrectly, and when the manual setup you already have is still the right call.

Why Manually Provisioned Infrastructure Stops Scaling

Console-driven infrastructure works fine for a single environment run by one or two engineers who remember what they built. It stops working once any of three things happen: a second environment needs to match the first, a second engineer needs to change something without breaking it, or an audit needs to answer what exists and why. At that point the console becomes a liability rather than a convenience, because nothing in it is reviewable, diffable, or reproducible.

The failure mode is rarely a single outage. It is slow drift: staging quietly stops matching production, a firewall rule gets added directly to fix an incident and never gets backported anywhere, and six months later nobody can say with confidence what a disaster recovery rebuild would actually produce. The common mistake here is treating this as a tooling problem to solve later rather than a standing operational risk. The consequence, when a real incident forces a rebuild from documentation instead of code, is a recovery that takes days instead of minutes and still does not match what was actually running. Infrastructure as code does not eliminate operational risk, but it converts invisible drift into a diffable pull request, which is the precondition for catching it at all.

Choosing a Migration Strategy: Import vs. Rebuild vs. Strangler

There are three realistic ways to bring existing infrastructure under code, and the right one depends on how much you trust the current state and how much downtime risk the business will tolerate. Picking the wrong one for a given resource is the single most common planning mistake in this kind of migration: teams default to whichever strategy they used successfully once, on a different kind of resource, and then get surprised when it does not fit.

ImportRebuildStranglerDowntime riskNone, if the import matches reality exactlyRequires a cutover windowNone, applies only to new resourcesSpeed to full coverageSlow, resource by resourceFast for the migrated sliceVery slow, spread across monthsBest fitDatabases and anything holding stateStateless compute and networkingSmall teams without a dedicated migration windowCommon mistakeWriting config from documentation instead of the live resourceUnderestimating the cutover mechanism's own complexityNever actually reaching full coverage

  1. Import. Write Terraform resource blocks that match what already exists, then run terraform import to bind them to real resource IDs without recreating anything. Lowest risk, no downtime, but slow: every resource has to be described by hand, and any mismatch between the written configuration and the real resource shows up as a disruptive plan the moment someone runs terraform apply. This is the wrong choice when the resource changes often, since the manual description work has to be redone every time reality moves.

  2. Rebuild. Stand up a parallel environment from code, validate it, then cut traffic over and decommission the old one. Clean end state with no imported drift, but it requires an environment the business can afford to run twice during the cutover, and a cutover mechanism (DNS, load balancer weighting, or a blue-green swap) that already works. This is the wrong choice for anything stateful where a second copy of the data is expensive or slow to create.

  3. Strangler migration. Bring new resources under code as they are created, and import or rebuild existing ones opportunistically whenever they need to change anyway. Slowest to reach full coverage, but it spreads the work across normal maintenance instead of demanding a dedicated migration sprint. This is the wrong choice when there is a compliance deadline forcing full coverage by a fixed date, since opportunistic migration has no guaranteed completion point.

Most mid-size engagements end up mixing these: stateless compute and networking get rebuilt because recreating them is cheap and low-risk, while databases and anything holding customer data gets imported because the downtime and data-migration cost of a rebuild is not worth it.

State Management: The Part Most Teams Get Wrong

Terraform state is the file that maps your configuration to real resource IDs, and it is also the single most common source of migration incidents. Two mistakes account for most of them.

Local State Without Locking

The first is local state. A state file sitting on one engineer's laptop or in an unversioned S3 bucket with no locking means two people can run apply at the same time, or a laptop can be lost with the only record of what maps to what. Remote state with locking (an S3 backend with DynamoDB locking, or Terraform Cloud) is not an optimization to add later; it is a prerequisite for letting more than one person touch the migration. Skipping it is not a shortcut, it is a bet that no two people will ever run apply in the same window, and that bet loses eventually.

Monolithic State Files

The second is importing into a monolithic state file. Teams that import their entire subscription into one state file end up with a plan step that touches everything, which means every change carries the blast radius of every resource in the account. Splitting state by environment and by bounded domain (networking, data tier, compute per service) keeps a bad plan contained to the part of the system it actually concerns, and it lets different owners work on different domains without stepping on each other's locks. The operational consequence of skipping this split shows up months later, when a routine networking change requires a plan review of the entire production database tier simply because they share one state file.

State as Sensitive Data

State also needs to be treated as sensitive data, not incidental output. A Terraform state file frequently contains resource attributes that amount to secrets: database connection strings, generated passwords, private keys. State stored in a backend without encryption at rest, or in a state file committed to a general-purpose repository instead of a locked-down backend, is a credential leak waiting to be found. Encrypt the backend, restrict who can read state directly, and never pass secrets through variables that Terraform will happily write into plan output in plain text.

Wiring the Migration Into CI/CD and Policy

The Review Pipeline

A migration that ends with engineers running terraform apply from their own machines has not actually fixed the underlying problem, it has just moved the console-driven risk into a different tool. The point of the migration is a pipeline where plan runs automatically on every pull request, the plan output is attached for review, and apply only runs after a human approval on a protected branch. Without that pipeline, state can still drift from what was reviewed, because nothing stops someone from running apply locally with an uncommitted local change.

Policy as Code and Module Versioning

Policy as code (tools like Sentinel or Open Policy Agent evaluating a plan before it is allowed to apply) is worth adding once the pipeline exists, not before. Common rules include blocking public storage buckets, requiring encryption on new databases, and requiring specific tags for cost allocation. Adding policy checks before the pipeline itself is reliable is solving a problem the team does not have yet while the actual problem, unreviewed applies, stays unsolved. Module versioning matters for the same reason: pin shared modules to a specific tagged version rather than a branch, so a change to a shared networking module does not silently reapply across every environment that consumes it the next time anyone runs plan.

A Phased Migration Playbook

The sequence below has held up across engagements with existing production traffic and no tolerance for a migration-caused incident.

  1. Inventory before writing any code. Export what actually exists (cloud provider CLI list commands, or a tool like Terraformer for a first-pass draft) rather than trusting internal documentation, which is almost always stale.

  2. Start with the lowest-risk domain. Networking and DNS are usually safest: they change rarely and a misconfiguration is easy to plan-diff before applying. Save the data tier for last.

  3. Import into isolated state, then run a plan with zero diff before touching anything else. If terraform plan shows changes immediately after import, the written configuration does not actually match reality yet. Fix that before any real apply, not after.

  4. Put the new configuration through the same review process as application code. A pull request, a plan output attached to it, and a second reviewer who is not the author.

  5. Migrate the data tier last, and treat it as a separate project with its own rollback plan. Import database and storage resources without letting Terraform manage anything that could trigger a destructive replace, such as a forced database recreation from a changed identifier.

  6. Decommission console access only after a full apply-from-clean-checkout has been proven in a non-production environment. Keeping console write access open during migration is reasonable; removing it before the migration is proven is how teams end up locked out of their own fix path mid-incident.

Common Anti-Patterns During IaC Migration

A handful of mistakes show up repeatedly enough to call out directly, beyond the ones already tied to a specific stage above.

  1. Migrating everything in one state file to save time up front. This trades a faster migration for a permanently larger blast radius on every future change.

  2. Treating the migration as a one-time project instead of a new operating model. If new resources still get created by hand after the migration "finishes," drift starts accumulating again immediately.

  3. Ignoring existing tags and naming conventions when writing new configuration. Inconsistent naming between imported and newly created resources makes the eventual full-coverage state harder to reason about, not easier.

When Infrastructure as Code Is Not Worth It Yet

A single environment run by one engineer, with infrequent changes and no compliance requirement to prove what exists, does not need a formal IaC migration yet. The cost of writing and maintaining Terraform configuration is real, and for a small enough footprint the console remains faster to operate day to day. The signal to invest is not team size on its own; it is the appearance of a second environment that needs to match the first, a second engineer who needs to change infrastructure without breaking it, or an audit or compliance requirement that demands a reviewable record of what exists. Once any of those show up, the cost of not having infrastructure as code starts compounding faster than the cost of building it.

Infrastructure as Code Migration: Frequently Asked Questions

Does a migration require downtime?

Not if the import strategy is used correctly for stateful resources. Downtime risk comes almost entirely from the rebuild strategy's cutover step, and that risk can be scoped to non-critical resources first.

How long does a typical migration take?

It depends far more on how many resources hold state and how tolerant the team is of a dedicated migration window than on raw resource count. A strangler approach with no dedicated window has no fixed timeline by design, since it completes opportunistically. A focused import-and-rebuild push has a definable end point, but the data tier's rollback planning is usually the pacing factor, not the Terraform authoring itself.

Should Terraform state live in the same repository as application code?

Usually not. Infrastructure changes on a different cadence and needs different review gates than application code, and a separate repository makes it easier to restrict who can apply infrastructure changes.

The teams that get through an infrastructure as code migration without an incident are the ones that treat state management and review process as the actual deliverable, not the Terraform files themselves. That work spans cloud infrastructure and DevOps engineering as much as it does application delivery, and it reflects the kind of full ownership we bring to engagements like the multi-country Azure microservices platform we built and continue to operate end to end. Start with the domain that has the least tolerance for surprise, prove a zero-diff import before anything else, and expand coverage only as fast as the review process can actually absorb it.

#azure#cloud-migration#devops#azure-devops#Architecture
Infrastructure as Code Migration: A Practical Playbook | Brain Space