Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

Terraform / IaC

Importing Existing Cloud Resources into Terraform Without Recreating Them

We bring hand-built AWS and Azure resources under Terraform control using reviewed import blocks, then write configuration until the plan shows no changes. Resources are never destroyed or recreated, so running workloads are unaffected, and the work proceeds in small, ordered batches.

Key takeaways

  • Importing adds a resource to state; it does not write code, so the code still has to match reality.
  • The finish line is a plan with zero changes, not a successful import command.
  • Import blocks put the import itself into pull request review, unlike one-off CLI commands.
  • Order matters: import foundations (network, identity) before the things that depend on them.
  • Generated configuration is a starting draft, not a finished module.

The problem with adopting what already runs

Most companies do not start with Terraform. They start with a console, a deadline and a working product. Two years later production holds hundreds of resources that nobody can recreate from scratch, and the one engineer who remembers how they were made is on holiday.

You cannot simply write Terraform for these resources and apply it. Terraform would try to create duplicates, or fail on name conflicts. The right move is to import existing resources into Terraform, so state records that these objects already exist and the code describes them. It is one of the first things we do when moving a team off ClickOps, and a major part of our Terraform and infrastructure as code service. Our blog post on moving from ClickOps to Terraform on AWS covers the wider journey.

What good looks like

  • Every important resource is in state and described by readable code.
  • Imports were reviewed in pull requests, not run as private commands.
  • The plan after import shows no changes.
  • Imported resources are organised into modules and stacks, not dumped in one file.
  • Nothing was recreated, so there was no downtime.

How it works in practice

Import blocks, reviewed like any change

An import block lives in your configuration. The plan shows the adoption before it happens:

import {
  to = aws_db_instance.orders
  id = "orders-prod"
}

resource "aws_db_instance" "orders" {
  identifier        = "orders-prod"
  engine            = "postgres"
  instance_class    = "db.m6g.large"
  allocated_storage = 200
  # ...remaining arguments written to match the live instance

  lifecycle {
    prevent_destroy = true
  }
}

Run terraform plan. If the plan shows changes to aws_db_instance.orders, your code does not yet describe reality. Edit the code, not the database, until the plan reports only the import. Then apply, and remove the import block in a follow-up if you wish.

Generating a first draft

For a resource with many arguments, you can ask Terraform to draft the configuration:

terraform plan -generate-config-out=generated.tf

Treat the output as scaffolding. It is correct but noisy, includes defaults, and uses none of your conventions. We review it, remove noise, replace hard-coded values with variables, and move it into modules. For Azure, aztfexport can export resource groups into configuration and state, with the same caveat: tidy before you trust it.

Ordering the work

Resources have dependencies, and importing in the wrong order creates confusing plans. Our usual sequence:

  1. Networking: VPC or VNet, subnets, route tables, gateways.
  2. Identity: IAM roles and policies, managed identities, role assignments.
  3. Data: databases, buckets, storage accounts, key vaults.
  4. Compute and platform: clusters, instances, scale sets.
  5. Edge: load balancers, DNS, certificates.

Once the foundations are in code, they can be wrapped into reusable modules using moved blocks.

Decisions and trade-offs

Import or rebuild. For stateless resources that are cheap to recreate, such as a load balancer behind DNS, it can be quicker and cleaner to write fresh code and cut over. For databases and anything holding data, import is the safe path.

Hand-written versus generated code. Generated code is fast but long-winded. Hand-written code is slower but readable. We usually generate, then rewrite the important parts.

One big import or many small ones. Small batches keep every plan reviewable and failures contained. A single huge import is hard to review and hard to roll back.

When not to import. Resources owned by another system, for example load balancers created by a Kubernetes controller or resources managed by a vendor tool, should stay with their owner. Importing them creates a tug of war over the same object. For clusters themselves see Kubernetes consulting.

A worked example of one batch

Take a team with a hand-built production VPC. We start by listing the subnets, route tables, internet gateway and NAT gateways, and grouping them as one batch. Each object gets a resource block and an import block, and the first plan almost always shows differences: missing tags, a route written differently, an attribute the console set implicitly. We fix the code, not the network, and re-run the plan until it reports only the imports.

Before merging, a second engineer reads the plan with the live console open beside it. That review is where wrong addresses and forgotten route table associations surface. After the apply, the plan is run again and must be empty. Only then does the next batch begin. The slowest part is rarely the import itself: it is working out why the live setting differs from what anyone remembers choosing.

Tagging and ownership at the same time

Importing is a cheap moment to add consistent tags such as owner, environment and cost centre. Doing it inside the same pull request turns a one-off clean-up into a standing convention, and it makes later cost reports far easier to read.

Failure modes we avoid

  • Importing without matching code. The next apply modifies production to match half-written code. We never apply without a clean plan.
  • Forgotten sub-resources. Some cloud objects are separate resources in Terraform, such as bucket policies or security group rules. Missing one shows up as a perpetual diff.
  • Import into the wrong address. A typo adopts a resource under the wrong name. Reviewing the plan catches this.
  • No state backup. We snapshot the state before every batch, as described in our page on remote state.
  • Sensitive values. Imported databases can write secrets into state, so backend access is restricted first.

Delivery and handover

You receive the imported resources in your repository, with modules where patterns repeat, a record of what was deliberately left out, and a list of resources we recommend deleting. Afterwards the drift checks keep it honest.

How to tell it is working

A plan against the imported stacks is empty. The number of resources in the cloud that are not in state is falling, and you can see that number. New environments are built from code rather than copied by hand. If you have an estate waiting to be adopted, get in touch.

References

How we deliver it

  1. 01

    Inventory what exists

    We list resources by account or subscription, group them by dependency and risk, and mark what should be imported, replaced or deleted.

  2. 02

    Plan the batches

    Imports run in layers, starting with networking and identity, then data stores, then compute and edge resources.

  3. 03

    Write or generate config

    We draft resource blocks by hand or with generated configuration, then tidy them into your naming and module standards.

  4. 04

    Import through pull requests

    Each batch uses import blocks, so the plan shows exactly what will be adopted before anything is applied.

  5. 05

    Reach a clean plan

    We adjust code until the plan shows no changes, and only then merge.

  6. 06

    Protect what you imported

    Critical resources get prevent_destroy and the pipeline becomes the only way to change them.

Tools and technologies

TerraformOpenTofuterraform importimport blocksterraform plan generate-config-outaztfexportAWS Resource ExplorerAWS CLIAzure Resource GraphAzure CLI

Frequently asked questions

How do you import an existing resource into Terraform?
Write a resource block for it, then add an import block giving the resource address and the cloud ID, and run plan and apply. Terraform records the real resource in state. The older terraform import command does the same without review. After importing, adjust the code until the plan shows no changes.
Does importing change or restart my infrastructure?
Importing itself only writes to state and does not touch the resource. Risk appears afterwards: if your code differs from reality, the next apply will change the resource to match the code. That is why we require a zero-change plan before any apply, and keep a backup of the state.
Can Terraform write the configuration for imported resources?
Partly. Recent Terraform versions can generate draft configuration for import blocks using the generate-config-out option on plan. The output is verbose and unopinionated, so we treat it as a starting point and rewrite it into clean, reusable code. On Azure, aztfexport can also scaffold code and state.
How long does it take to import an existing environment?
It depends on the number of resources, how tangled their dependencies are, and how much of the estate you want in modules. A small environment can be done in days, while a large, undocumented one is staged over weeks. We estimate after an inventory, not before.
Should we import everything?
Not necessarily. Unused resources are better deleted. Short-lived or tool-managed resources, such as those created by Kubernetes controllers, may not belong in Terraform. We decide with you what is worth owning, since every imported resource becomes something you maintain.

Talk to an engineer about terraform import existing resources

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.