When a team grows fast, the cloud account usually becomes a museum of hand-built resources. A terraform import checklist is how you stop treating that museum like a live system and start bringing it under control without breaking what already works.
This is not about purity. It is about reducing drift, making changes repeatable, and removing the quiet risk that nobody knows who owns the load balancer, the database, or the IAM policy that keeps prod alive.
- Why legacy cloud assets drift
- Terraform import checklist prep
- Executing the import safely
- Validating state after import
- Operating model after the import
Why legacy cloud assets drift
Most drift starts the same way: someone needed a change fast, the change was small, and the ticket was not worth waiting on. A security group rule gets added in the console. A DNS record changes during an incident. A managed database is resized from the provider UI because the deploy window is closed. None of that is irrational. It is how teams ship under pressure.
The problem is what happens next. The change is real, but the source of truth is not. Terraform still thinks the old value exists. Six months later, a routine plan shows a replacement nobody expected, or a module upgrade fails because the imported resource was never modeled correctly. That is why a terraform import checklist matters. It is not a ceremony. It is a control surface.
I usually separate legacy assets into three buckets before touching state: safe to import, safe to recreate, and leave alone for now. If a resource is stateful, customer-facing, or entangled with IAM, assume import first. If it is ephemeral and trivial, recreate it cleanly. If ownership is unclear, stop and map it before making the Terraform file prettier. Beauty is not the goal. Predictability is.
A concrete example: a team with 140 cloud resources might discover that only 60 are managed in code. Of the remaining 80, maybe 20 are worth importing immediately: load balancers, autoscaling groups, RDS instances, KMS keys, IAM roles, and Route 53 records. The rest can be scheduled later. That sequencing lowers the blast radius and keeps the work finite.
This is also where Terraform drift detection for cloud environments earns its place. Detection tells you what is off. Import closes the loop and makes the desired state durable.
Terraform import checklist prep
Before you run terraform import, build the inventory. Not a rough spreadsheet. A real map of resource type, provider, identifier, owner, and whether the object can be recreated. For each item, capture the cloud ID exactly as the provider expects it. AWS, Azure, and GCP all have different shapes here, and guessing wastes time.
Then lock the environment down. Freeze unrelated changes for the import window. If you are using Terraform Cloud or remote state in S3 with locking, verify the lock is working. If you are not using state locking, fix that first. Importing into a moving target is how teams end up with conflicting state and a long night.
At this stage, write the resource blocks before the import, even if they are incomplete. The import command binds state to configuration, not to wishful thinking. I prefer a minimal resource block with the correct name, provider, and required arguments, then refine it after the import. That keeps the work honest.
A simple workflow looks like this:
terraform init
terraform plan
terraform import aws_lb.app arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/app-lb/50dc6c495c0c9188
terraform state show aws_lb.app
terraform plan
That sequence matters. state show tells you what Terraform believes exists. The final plan tells you whether the configuration actually matches. If the plan proposes changes you did not expect, do not paper over them with ignore_changes until you understand why they appear. That rule saves teams from hard-to-debug drift later.
This is also where internal ownership becomes visible. If nobody can say who owns a resource, put it in a quarantine list. The best terraform import checklist is not only technical. It forces a decision about ownership, lifecycle, and whether the resource should exist at all.
Executing the import safely
Import one resource at a time when the resource has real blast radius. I know batch import scripts are tempting. They are also how people accidentally map the wrong VPC, the wrong security group, or the wrong database. For low-risk resources, batching can work. For anything stateful, slow down. The goal is not speed. The goal is confidence.
After each import, run terraform state show and compare it to the cloud console or provider CLI. Look for fields that Terraform does not manage, fields that are computed, and fields that are defaults in the provider but explicit in code. Those differences are where the first surprise usually hides. A classic one is a security group rule that exists in the account but not in code because the provider models it differently than the console does.
Resource type matters. An ALB is usually easy. A database is not. A database may carry subnet group references, parameter groups, security group attachments, backup settings, and maintenance windows. Importing it cleanly means modeling the dependencies in the right order. If you import the database before the subnet group and parameter group exist in code, the plan will be noisy and the team will lose trust in the result.
For complex objects, I like a dependency table:
- Network primitives: VPC, subnets, route tables, security groups
- Shared services: KMS keys, IAM roles, parameter groups
- Stateful services: databases, queues, buckets with lifecycle rules
- Entry points: load balancers, DNS, WAF, CDN
Import from the bottom up. Dependencies first. Interfaces last. That order keeps plans stable and makes the output understandable to the next engineer who opens the repo.
If you need a reference on adjacent infrastructure work, Terraform state locking for safer infrastructure changes is the companion piece. Locking does not make imports correct, but it does keep the state from being edited by two people at once.
Validating state after import
Once the resource is in state, the work is not done. The real test is whether a clean plan comes back empty or nearly empty. A good import should reduce uncertainty, not create a new class of noise. If Terraform wants to replace the resource, that is a signal that the resource block does not match reality closely enough.
In practice, validation falls into four checks. First, run terraform plan with no changes and inspect the diff. Second, compare any computed attributes that Terraform cannot own. Third, verify tags, names, and lifecycle rules. Fourth, confirm that nothing outside Terraform still mutates the resource on a schedule, such as a security automation job or a platform script. If another tool is still writing to the same object, drift will return.
A useful trick is to classify plan noise into three categories: harmless, expected, and dangerous. Harmless noise includes provider-computed fields. Expected noise includes values you intentionally left out because the cloud provider sets them. Dangerous noise includes anything that would recreate a production resource or alter traffic. Dangerous noise gets fixed before the import is considered complete.
Example: after importing an AWS ALB, the plan may show differences in idle timeout, deletion protection, or access logs. Those are not cosmetic. They change behavior. If the team cannot explain each one, the import is not ready for merge. The same is true for a GCP load balancer, an Azure app gateway, or a managed queue. The provider names change. The discipline does not.
This is also a good moment to test with a targeted apply in a non-production environment first, if one exists. If the imported resource has a sibling in staging, compare the plan shape. Divergence between environments often exposes hidden console edits or stale assumptions in modules. That is useful information, not a nuisance.
Operating model after the import
An import project succeeds when the team stops needing console edits for routine changes. That requires a small operating model, not just a one-time cleanup. New resources must be provisioned through code. Console changes need an explicit exception path. Drift detection should run on a schedule. And the team needs one owner for the module, even if many people can read it.
I usually recommend three follow-up rules. First, every imported resource gets documented in the repo with a short note explaining why it was imported and any known caveats. Second, every future change goes through the same review path as a new resource. Third, the team revisits the imported objects after one or two deploy cycles to remove temporary compatibility code. Otherwise, imports become permanent technical debt in disguise.
There is also a human side. Imported infrastructure can expose weak spots in naming, tagging, and IAM boundaries. That is good. It means the cloud account is telling the truth. If a tag policy is inconsistent or a role is overbroad, fixing it during import is cheaper than discovering it during an incident review or a compliance audit. If you care about auditability, this pairs naturally with SOC 2 evidence collection for engineering teams.
For teams that do not have enough senior bandwidth to do this cleanly, the right answer is not to rush. It is to schedule a focused engagement and ship the boring, necessary work once. That is exactly the kind of problem we handle in our Sprint, Build, or Fractional engagements, and when the situation calls for a narrow cleanup, the application takes ten minutes on application.
A messy cloud account taxes every deploy, every incident, and every audit. If your team needs to bring unmanaged infrastructure under control, it is worth applying for an engagement. We take three engagements a quarter, and a Sprint is often the right shape when the goal is a clean import, a safe plan, and a repo the next engineer can trust.




