Importing Existing Infrastructure into Terraform
Jul 14, 2026 · 8 min read
Importing Existing Infrastructure into Terraform
Infrastructure doesn’t always start its life in Terraform. Many legacy environments were built by someone clicking through a cloud console, using legacy scripts to provision resources, or making manual changes during an outage or to quickly remediate a security vulnerability.
By the time someone decides to manage it with infrastructure as code (IaC), starting over isn’t realistic — it’s already live. Terraform’s import functionality solves that by bringing existing resources under Terraform management without modifying or recreating them.
Why IaC / Terraform
Before getting into the mechanics of import, it’s worth justifying the destination: why bring infrastructure under IaC at all, and why Terraform.
- Repeatable instead of manual. Console clicks and one-off scripts are difficult to reproduce consistently and often lead to misconfigurations and inconsistencies. With Terraform, infrastructure is defined as code, allowing every environment to be provisioned from the same version-controlled configuration.
- A single source of truth. Terraform stores your infrastructure configuration in version-controlled code, eliminating reliance on tribal knowledge, outdated documentation, or manually inspecting cloud resources. Every team member can review, audit, and understand exactly how the environment is intended to be configured.
- Plan before apply. Manual changes often require you to make updates and hope they have the intended effect. The
terraform plancommand provides a detailed preview of exactly what will change before anything is modified. Resource additions, updates, and deletions are all visible up front instead of being discovered after apply. - One tool across providers. Whether you’re deploying resources to AWS, Azure, Google Cloud, or another supported provider, Terraform provides a consistent workflow, state management, and configuration language across every platform.
- Built-in drift detection. Terraform compares your configuration to the actual infrastructure every time you run
terraform plan. If someone makes a manual change outside of Terraform, the next plan identifies it as configuration drift, allowing you to either roll back the change or codify it in your configuration. - Reusable modules. Terraform modules allow you to package common infrastructure patterns into reusable building blocks. Whether you create your own or use community modules from the Terraform Registry, modules can be parameterized with input variables, allowing each deployment to be customized while reusing the same underlying code.
Terraform isn’t the only IaC tool available. Provider-native tools such as AWS CloudFormation and Azure Bicep are developed and maintained by their respective cloud providers, which often means they support newly released services and features sooner than Terraform providers. If your organization is committed to a single cloud platform and wants the tightest possible integration with that provider’s ecosystem and roadmap, CloudFormation or Bicep are excellent choices. This article sticks with Terraform, though, and the rest of it covers how to bring existing resources under its management without recreating them — the same concept applies to CloudFormation, Bicep, and other IaC tools, just with different mechanics.
Why Import Instead of Recreate
For existing production infrastructure, recreating resources is rarely an option — they may already be serving live traffic, and destroying and rebuilding them risks downtime or data loss.
Terraform import provides a safe alternative. Instead of recreating infrastructure, import lets Terraform begin managing resources that already exist, without changing them, so they can be brought under infrastructure-as-code management without impacting production workloads.
Once imported, the resource can be managed like any other Terraform resource. The next step is ensuring your Terraform configuration accurately reflects the existing infrastructure.
Import Methods
Terraform offers a few ways to bring a resource into state, from a CLI command to declarative blocks that fit into a normal plan/apply cycle. Which one to reach for depends on how many resources you’re importing, whether the resource lives inside a module, and how much HCL you’re willing to write by hand.
Option 1: The CLI (terraform import)
The original import workflow is a CLI command along with a resource block that you create manually:
resource "aws_s3_bucket" "example" {
bucket = "my-existing-bucket"
}
terraform import aws_s3_bucket.example my-existing-bucket
This imports the existing bucket into Terraform’s state under the resource address aws_s3_bucket.example. The infrastructure itself is left unchanged.
After the import is complete, run terraform plan. Terraform will compare your configuration to the existing resource and report any differences between the two. Update the resource block to reflect the current infrastructure, then run terraform plan again. Repeat the process until the plan reports No changes.
⚠️ Note: terraform import requires the resource block to already exist in your configuration before you run it. If it does not, the import will fail.
Option 2: Import Blocks
Terraform 1.5 introduced import blocks, which move the import operation into the plan/apply lifecycle instead of a separate imperative command:
import {
to = aws_s3_bucket.example
id = "my-existing-bucket"
}
Running terraform plan shows the import as part of the plan output, and terraform apply performs it — reviewable and easy to incorporate into a normal CI/CD pipeline rather than as a one-off command from someone’s laptop.
To have Terraform generate an initial resource configuration instead of writing one by hand, run:
terraform plan -generate-config-out=generated.tf
Terraform reads the resource’s current state through the provider and writes a corresponding resource block to the new generated.tf file using the values it can retrieve.
Review and clean up the generated configuration, then move the resource block into the appropriate .tf file for your project. Run terraform plan again to verify the configuration. Once terraform apply has run and a subsequent plan reports No changes, you can delete the temporary generated.tf file. The import block can also be removed after the import succeeds.
✅ For complex resources, this is significantly faster than manually recreating dozens of configuration attributes.
⚠️ Review the generated file before trusting it. Terraform cannot retrieve sensitive values such as passwords or secret keys, so those attributes must be supplied manually or referenced from a secrets manager. The generated configuration is also a starting point rather than a finished module and may include computed or deprecated attributes that are better omitted from your final configuration.
The path passed to -generate-config-out must point to a file that does not already exist. Terraform returns an error rather than overwriting an existing file.
Option 3: Multiple Resources at Once
Terraform 1.7 added for_each support to import blocks, which is useful when you have a batch of similar resources to bring in together:
locals {
buckets_to_import = {
logs = "my-company-logs-bucket"
backups = "my-company-backups-bucket"
}
}
import {
for_each = local.buckets_to_import
to = aws_s3_bucket.this[each.key]
id = each.value
}
The workflow is identical to importing a single resource using import blocks and -generate-config-out, except Terraform imports and generates configuration for multiple resources in a single operation.
Option 4: Importing Into a Module
If the resource belongs inside a module rather than the root configuration, simply prefix the import address with the module path. The resource itself is defined inside the module just like any other Terraform resource.
# modules/storage/main.tf
resource "aws_s3_bucket" "example" {
bucket = var.bucket_name
}
The import block then references the resource through the module:
import {
to = module.storage.aws_s3_bucket.example
id = "my-existing-bucket"
}
The same addressing convention applies to the CLI:
terraform import module.storage.aws_s3_bucket.example my-existing-bucket
This also works with for_each, allowing multiple module instances to be imported using the appropriate module instance address.
Common Gotchas
- Related resources aren’t imported automatically. For example, importing a VPC doesn’t pull in its subnets, route tables, or gateways. Every resource must be imported individually.
- Import doesn’t create dependencies. For example, if you import an EC2 instance and a security group separately, Terraform doesn’t magically know the instance should reference that security group. References between imported resources must still be expressed in your Terraform configuration.
- Not every resource type supports import. It’s rare, but check the
Importsection on the provider’s registry docs before planning a migration around a specific resource. - Generated configurations omit sensitive values. Providers can’t read back sensitive values such as passwords, secret keys, or other write-only attributes. Add these manually or reference them from a secrets manager before applying the configuration to avoid unexpected changes in future plans.
- Already-imported resources can still be moved. If you import into the wrong address or later refactor into modules,
terraform state mvmoves the state without re-importing or modifying the underlying infrastructure.
Closing Thoughts
Terraform import makes adopting infrastructure as code practical for existing environments, not just greenfield deployments. The introduction of import blocks and -generate-config-out has transformed what was once a tedious, error-prone process of manually transcribing resource settings into a workflow that is largely automated and far easier to review.