
Terraform import is the process of bringing existing cloud resources (created manually, by another tool, or before you adopted Terraform) under Terraform management by recording them in your state file. There are two ways to do it: the legacy terraform import command and the modern import {} block introduced in Terraform 1.5.
This guide covers both: when to use each, complete examples for AWS, Azure, and GCP, how to generate Terraform code from existing resources, how to import multiple resources at once, and the common errors that trip people up. Whether you're importing a single S3 bucket or migrating an entire estate of brownfield infrastructure, you'll find a working pattern below.
terraform import CLI. Import blocks are declarative, plan-previewable, and CI/CD-friendly. Pair them with terraform plan -generate-config-out=generated.tf to generate a draft resource block. The legacy CLI command writes to state directly, without a plan preview, and generates nothing.terraform plan first to see the imported state diff before applying. Mismatches between your HCL and the real resource cause drift on the first apply.moved blocks to refactor resource addresses without destroy/recreate.Terraform import takes an existing resource (an EC2 instance, an S3 bucket, an Azure VM, a GCS bucket, or any resource type your provider supports importing) and records it in your Terraform state file. After import, Terraform manages that resource the same as if it had been created by terraform apply from the start. You can then update it, plan changes against it, and destroy it through code.
Two important caveats up front:
-generate-config-out flag, covered below). You still have to write the matching resource block by hand, or use code generation.There are two ways to import existing resources into Terraform:
terraform import CLI command: available since Terraform 0.7. Imperative, one-off, writes to state directly without a plan preview.import {} block: introduced in v1.5.0. Declarative, plan-previewable, works cleanly in CI/CD, and can be paired with terraform plan -generate-config-out to generate draft HCL.The short version: use the import block for any new work. The CLI command is still useful for ad-hoc, interactive fixes, and it's worth knowing because most existing tutorials still reference it.
The terraform import command links a remote, pre-existing resource to a resource block in your Terraform configuration. It is the original Terraform import method and still works in current Terraform versions.
terraform import aws_instance.example i-1234567890abcdef0The syntax is terraform import <resource_address> <remote_id>. The resource address (e.g., aws_instance.example) must already exist in your configuration; the remote ID is the cloud provider's identifier for the live resource.
Here are the most useful flags for the terraform import command:
-config=path: Specifies the path to the directory containing your Terraform configuration files-input=true/false: Determines whether Terraform should ask for interactive input (set to false for automation)-lock=false: Disables state locking (generally not recommended in collaborative environments)-lock-timeout=0s: Sets a duration to retry acquiring a state lock before failing-no-color: Disables colorized output-var-file=path: Loads variable values from a .tfvars file (useful when your provider config references variables)-parallelism=n: Limits the number of concurrent operations (default is 10)-var 'foo=bar': Sets a variable from the command lineThese are the most common terraform import examples by provider. Each one links a live resource to a resource block already in your configuration. The pattern is always terraform import <RESOURCE_ADDRESS> <RESOURCE_ID>, but the ID format varies by provider.
terraform import aws_instance.web_server i-abcd1234AWS EC2 instance IDs start with i-. You can find them in the EC2 console or via aws ec2 describe-instances.
terraform import aws_s3_bucket.data_lake my-data-lakeNote: in AWS provider v4.0+, S3 bucket configuration was split into multiple resources (aws_s3_bucket, aws_s3_bucket_versioning, aws_s3_bucket_acl, etc.). You may need to import related S3 resources separately, depending on which parts of the bucket configuration you want Terraform to manage. Not every bucket needs every split-out resource.
terraform import aws_iam_role.lambda_execution my-lambda-execution-roleUse the role name (not the ARN) as the import ID. Attached policies and inline policies need separate imports. See streamlining AWS IAM role creation with Terraform for the full pattern.
terraform import aws_db_instance.primary my-primary-dbAzure resource IDs are full ARM paths:
terraform import azurerm_virtual_machine.app_server /subscriptions/xxx-xxx-xxx-xxx-xxx/resourceGroups/myRG/providers/Microsoft.Compute/virtualMachines/myVMFor importing entire Azure resource groups, aztfexport (covered below) is far faster than running individual terraform import commands.
terraform import google_compute_instance.app_server projects/my-project/zones/us-central1-a/instances/app-serverGCP IDs typically include project and zone. The simpler shorthand my-project/us-central1-a/app-server also works for most resources.
When using terraform import, a common challenge arises with computed attributes or default settings. Computed attributes are values determined by the cloud provider after resource creation (like timestamps, default security group IDs, or ARN components).
Use the ignore_changes lifecycle meta-argument to tell Terraform to disregard drift for specific attributes:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
lifecycle {
ignore_changes = [
default_security_group_id,
tags_all,
]
}
}Use ignore_changes sparingly. It hides drift, so it should be reserved for attributes you intentionally do not want Terraform to manage. Reaching for it on every imported resource turns it into a place where real configuration drift gets buried.
For attributes where you can reliably determine the default value, explicitly include that value in your HCL:
resource "aws_db_instance" "example" {
# ... other imported attributes
backup_retention_period = 7 # Explicitly set the provider's default
}Introduced in Terraform 1.5, the import {} block is a declarative alternative to the terraform import command, integrated directly into the configuration language. You write the import as code, run terraform plan to preview it, and terraform apply to commit. The same workflow as any other Terraform change.
Import blocks make import fit the normal Terraform workflow: write the import in code, plan it, apply it, review it in a PR like anything else. For more on the feature itself, see our Terraform import block deep-dive.
| Feature/Aspect | Import Block (Modern) | terraform import CLI (Legacy) |
|---|---|---|
| Preview | Yes, see plan before changes | No, writes to state directly |
| HCL Generation | Works with terraform plan -generate-config-out to write draft HCL |
No, write all HCL manually |
| Safety | High, part of plan/apply workflow | Low, easy to make mistakes |
| CI/CD Integration | Works well in pipelines | Risky and difficult |
| Version Control | Import definitions reviewable in PRs | No visibility into imports |
import {
to = aws_s3_bucket.legacy_bucket
id = "my-legacy-data-bucket"
}When you run terraform plan and terraform apply with an import block present, Terraform performs the import operation. You can also generate configuration automatically using:
terraform plan -generate-config-out=generated.tfVersion note: Basic
import {}blocks require Terraform 1.5+.for_eachinside import blocks was added later, so verify you're on Terraform 1.7+ (or a compatible OpenTofu version) before using this pattern.
For importing multiple resources of the same type:
locals {
buckets = {
"staging" = "staging-bucket"
"uat" = "uat-bucket"
"prod" = "production-bucket"
}
}
import {
for_each = local.buckets
to = aws_s3_bucket.app_data[each.key]
id = each.value
}
resource "aws_s3_bucket" "app_data" {
for_each = local.buckets
bucket = each.value
}To import resources into modules:
import {
to = module.servers.aws_instance.app_server
id = "i-1234567890abcdef0"
}There are two valid import-block workflows. Pick one before you start:
resource block in your config. Add the import {} block, run terraform plan, refine the HCL until the plan is clean, then apply.import {} block and run terraform plan -generate-config-out=generated.tf to have Terraform produce a draft resource block. Then clean up the generated HCL and apply.The steps below show workflow #2 (generate-config-out). For workflow #1, skip Step 2 and edit your existing resource block in Step 3 instead.
Create an import block in your configuration specifying the resource to import:
import {
to = aws_s3_bucket.legacy_bucket
id = "my-legacy-data-bucket"
}Run plan with configuration generation enabled:
terraform plan -generate-config-out=generated.tfTerraform will create a generated.tf file with configuration based on the live resource.
Do not blindly trust generated configuration. Review carefully:
arn, hosted_zone_id) and default valuesExample cleanup:
# BEFORE - Generated
resource "aws_s3_bucket" "legacy_bucket" {
bucket = "my-legacy-data-bucket"
bucket_domain_name = "my-legacy-data-bucket.s3.amazonaws.com" # Remove
hosted_zone_id = "Z3AQBSTGFYJSTF" # Remove
region = "us-east-1" # Remove
}
# AFTER - Cleaned
resource "aws_s3_bucket" "legacy_bucket" {
bucket = "my-legacy-data-bucket"
tags = {
Name = "Legacy Data Bucket"
Environment = "production"
}
}terraform planIdeally, the output should show 1 to import, 0 to change, 0 to destroy. If Terraform wants changes, adjust your HCL until the plan matches your intent. Generated config almost always needs some cleanup before the plan is clean.
terraform applyOnce approved, the resource is recorded in state. (You can still remove it later with terraform state rm if needed; nothing about the live resource changes.)
Verify the import succeeded:
terraform state show aws_s3_bucket.legacy_bucketYou can now remove the import block from your configuration. It's a one-time operation.
resource block must either already exist in your configuration or be generated with terraform plan -generate-config-outcount or keys for for_eachA common ask: "I have a hundred resources running in AWS already. Can Terraform read them and generate the HCL automatically?" The answer is yes, with caveats. None of these tools produces a final artifact. There are three common ways to generate Terraform code from existing resources in 2026:
terraform plan -generate-config-out (Built-in, Terraform 1.5+)Write an import {} block referencing the resource, then run:
terraform plan -generate-config-out=generated.tfTerraform reads the live resource, generates a resource block matching its current attributes, and writes it to generated.tf. This is the built-in Terraform approach. It works with providers and resource types that support Terraform import, but generated configuration quality varies. Some cloud services are represented by multiple Terraform resources, so full management may require separate imports, and HashiCorp's docs are explicit that each remote object should be imported to only one resource address.
Limitations: generated HCL is often verbose. It can include provider defaults, hardcoded values, and attributes your team would normally turn into variables or references. Treat the output as a starting point, not a final artifact.
aztfexport (Azure-only)For Azure, Microsoft's aztfexport tool can scan an entire resource group and generate HCL + a state file in one shot. Much faster than per-resource import calls for Azure migrations.
Terraformer, originally developed under the GoogleCloudPlatform GitHub organization, is a third-party CLI that walks AWS, GCP, Azure, Kubernetes, and ~20 other providers, generating HCL and state. It's the most automated option for full-estate bulk discovery but the generated code typically needs more cleanup than -generate-config-out, and the project is community-maintained. Verify it still supports your provider versions before relying on it.
# Example: generate Terraform for all EC2 instances in us-west-2
terraformer import aws --resources=ec2_instance --regions=us-west-2| Tool | Scope | Code Quality | Best For |
|---|---|---|---|
terraform plan -generate-config-out |
Single resource per import block | Faithful but verbose | Per-resource imports in regular workflow |
| aztfexport | Azure resource group / query | Good, needs refactoring | Bulk Azure migration |
| Terraformer | Multi-cloud, broad discovery | Rough, needs heavy cleanup | Initial brownfield audit |
Whichever method you use, always review the generated code before committing. Code generation can include provider defaults, hardcoded values that should be variables or references, and produce a main.tf no team would write by hand.
For Azure environments, Microsoft provides aztfexport (formerly aztfy), a command-line tool that scans existing Azure resources, generates the matching Terraform HCL code, and creates a state file in one pass. It's the fastest way to import existing Azure resources into Terraform at scale. For a hands-on walkthrough, see our guide to getting started with the Azure Terraform Export tool.
Azure Export for Terraform (aztfexport) is an open-source tool from Microsoft that:
azurerm and azapi Terraform providersUnder the hood, aztfexport maps Azure resource IDs to Terraform resource types, calls Terraform import on each, and writes the generated HCL and a mapping file. For normal use you don't need to think about that.
Recent versions of aztfexport (v0.13+) paired with Terraform v1.5+ can generate import {} blocks instead of running imports directly. This lets you review the imports in a PR and apply them through your normal plan/apply pipeline:
aztfexport resource-group --generate-import-block myRGThis produces an import.tf containing import {} blocks and a mapping file. Recommended for any team using Terraform 1.5 or later, since it makes the import auditable in CI/CD.
Check Microsoft's current installation docs before copying package-manager commands; the examples below show the common paths but Microsoft's repo URLs and supported distros change.
Linux (apt - Debian/Ubuntu):
curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc > /dev/null
sudo apt-add-repository https://packages.microsoft.com/ubuntu/20.04/prod
sudo apt-get update
sudo apt-get install aztfexportLinux/macOS (Homebrew):
brew install aztfexportWindows (winget):
winget install aztfexportExport a Resource Group:
aztfexport resource-group myRGUse the -n flag for non-interactive mode with large resource groups.
Export using Azure Resource Graph Query:
aztfexport query "resourceGroup =~ 'myRG' and type =~ 'microsoft.network/virtualnetworks'"Export a Single Resource:
aztfexport resource /subscriptions/your-sub-id/resourceGroups/myRG/providers/Microsoft.Compute/virtualMachines/myVMThe tool generates .tf files, a terraform.tfstate file, and a JSON mapping file (aztfexportResourceMapping.json).
It's important to note that Microsoft states the generated code is not intended to be fully reproducible from scratch. The generated output requires mandatory manual review and refactoring:
| Feature | aztfexport | terraform import (CLI) | terraform import (Block) |
|---|---|---|---|
| HCL Code Generation | Automated | Manual | Automated |
| State Import | Automated | Automated | Automated |
| Resource Discovery | Supported (RG, Query, Interactive) | Manual | Manual identification |
| Bulk Operations | High | Low | Medium |
| Manual Effort | Medium (refinement) | Very High | Medium |
If a resource already exists in your state file with a different address:
# Remove from old address
terraform state rm aws_instance.old_name
# Then import to new address
terraform import aws_instance.new_name i-1234567890abcdef0Error: Cannot import non-existent remote object
Error: Resource address does not exist in the configuration
Error: Error acquiring the state lock
Error: Invalid provider configuration
When importing resources with dependencies:
-target only as a temporary recovery tool, and only when you understand which dependencies you're skipping. -target is widely overused; reach for it last, not first.terraform import and the import {} block both do the same thing under the hood: they write the resource into your Terraform state file. The state file is what Terraform consults on every plan and apply to know what it manages. If a resource isn't in state, Terraform thinks it doesn't exist, even if it's running in your cloud account.
Terraform state is central to how import works. For a deeper look at state file structure, see Terraform state file best practices; for setting up the remote storage that holds it, see our guide to remote backends.
Configure the same backend you use for normal Terraform runs before importing. Imports write to whichever backend is active after terraform init; there's no separate import-only path. For remote backends like Terraform Cloud, Scalr, or S3 with DynamoDB locking, the import inherits the backend's normal locking, RBAC, and audit controls.
After importing, you might need to reorganize your state:
terraform state mv with -state and -state-outterraform state mv to rename without destroyingterraform state rm to remove without destroyingterraform state mv aws_instance.old_name aws_instance.new_name
terraform state mv aws_instance.standalone module.servers.aws_instance.serverWhen you're migrating dozens or hundreds of existing resources into Terraform, one-at-a-time CLI imports get painful fast. There are three workable patterns for terraform bulk imports.
Terraformer is a multi-cloud tool for bulk imports:
# Install Terraformer
go install github.com/GoogleCloudPlatform/terraformer@latest
# Import all EC2 instances in a region
terraformer import aws --resources=ec2_instance --regions=us-west-2For bulk imports in modern Terraform:
locals {
instances = {
"web" = "i-12345678"
"app" = "i-23456789"
"db" = "i-34567890"
}
}
import {
for_each = local.instances
to = aws_instance.servers[each.key]
id = each.value
}
resource "aws_instance" "servers" {
for_each = local.instances
# configuration...
}For large-scale imports using the CLI:
#!/bin/bash
for instance_id in i-12345678 i-23456789 i-34567890; do
terraform import aws_instance.server_${instance_id} ${instance_id}
doneTerraform's moved block feature (v1.1+) allows you to safely rename or relocate resources without destroying and recreating them. This is particularly useful when organizing imported resources.
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}resource "aws_security_group" "api_security_group" {
name = "api-security-group"
}
moved {
from = aws_security_group.sg
to = aws_security_group.api_security_group
}# After creating module/storage/main.tf with the resource
module "storage" {
source = "./modules/storage"
bucket_name = "application-logs"
}
moved {
from = aws_s3_bucket.logs
to = module.storage.aws_s3_bucket.logs
}locals {
servers = {
"web" = {}
"api" = {}
}
}
resource "aws_instance" "server" {
for_each = local.servers
}
moved {
from = aws_instance.server[0]
to = aws_instance.server["web"]
}
moved {
from = aws_instance.server[1]
to = aws_instance.server["api"]
}terraform plan to verify the moves are correctFor comprehensive details on moved blocks and advanced refactoring scenarios, see the dedicated article: Terraform Moved Blocks: Refactoring Without Pain
ignore_changes or explicit defaultsEnable debug logging:
export TF_LOG=TRACE
export TF_LOG_PATH=terraform.logInspect state:
terraform state list
terraform state show <resource_address>Validate configuration:
terraform validate
terraform fmtConsider alternatives when:
Each provider has unique import requirements:
Import works best as a controlled migration workflow: inventory the resources, write or generate the HCL, review the plan, apply the import, then refactor naming and module structure over time. For CI/CD specifically, prefer import {} blocks over the CLI command. They can be reviewed in a pull request and executed through the same plan/apply pipeline as any other Terraform change, with the same approvals and audit trail. The CLI terraform import command bypasses your pipeline entirely and modifies state out of band, which is usually what you don't want.
sensitive = true only suppresses values in plan/apply output; it does not redact them from the state file. Restrict state access, encrypt remote state, and avoid committing generated files that expose credentialsTerraform import is the process of bringing an existing cloud resource (one that was created outside of Terraform) under Terraform management by recording it in your state file. After import, Terraform manages the resource the same way it would any resource it created itself.
It reads an existing remote resource and writes an entry for it into your Terraform state file. The cloud resource itself is not modified. Terraform import does not generate the corresponding HCL configuration on its own (the legacy CLI command never has); you write that by hand or use terraform plan -generate-config-out with an import block to generate a starting point.
The basic syntax is terraform import <RESOURCE_ADDRESS> <RESOURCE_ID>. For example, to import an EC2 instance: terraform import aws_instance.web i-1234567890abcdef0. The resource block (aws_instance.web in this example) must already exist in your configuration. After running the command, run terraform plan to see whether your HCL matches the live resource. If not, edit your HCL until plan is clean.
The terraform import CLI command is imperative: you run it and it writes to state directly, without a plan preview. The import {} block is declarative: you add it to your .tf files, run terraform plan to preview what will be imported, then terraform apply to commit. The import block is plan-previewable, CI/CD-friendly, and can be paired with terraform plan -generate-config-out to generate draft HCL. It's the recommended approach in Terraform 1.5 and later.
Yes. Three options: (1) use import {} blocks with for_each (Terraform 1.7+) to import many resources of the same type in one apply; (2) use aztfexport for bulk Azure imports; (3) use third-party tools like Terraformer for multi-cloud bulk discovery. The for_each-with-import-block pattern is the cleanest for resources you already know about, while Terraformer is best for discovering resources you don't.
Not by itself. The legacy terraform import command only writes state. The modern import {} block can be paired with terraform plan -generate-config-out=generated.tf to generate draft HCL. Treat generated code as a starting point. It's faithful to the live resource but doesn't match your conventions, may include computed attributes you don't want managed, and rarely needs zero cleanup.
Write a resource block for the AWS resource you want to manage (e.g., aws_instance, aws_s3_bucket), then either run terraform import <address> <aws_id> or add an import {} block referencing the same address. Run terraform plan to verify the import works and your HCL matches. For AWS S3 specifically, provider v4.0+ split many bucket settings into separate resources (aws_s3_bucket_versioning, aws_s3_bucket_acl, etc.), so you may need to import related resources separately depending on what you want Terraform to manage.
Terraform will refuse and return an error: "Resource already managed by Terraform." If you need to move a resource to a new address, use terraform state mv instead of importing. If you need to re-import a resource (rare), first remove it from state with terraform state rm, then import to the new address.
Yes. Once terraform apply completes successfully and the resource is in state, the import {} block is no longer doing anything. You can remove it from your configuration. The resource block stays.
Yes. Import works with every Terraform backend: local, S3, GCS, Azure Blob, Terraform Cloud, Scalr, etc. For remote backends, import operations are executed against the remote state with the backend's standard locking and authorization rules. If you're running imports through a CI/CD pipeline, the import {} block approach is preferable since it integrates cleanly with the plan/apply workflow your remote backend already runs.
For one-off imports, the terraform import CLI command is fine. For anything you'd want to review, automate, or repeat, use an import {} block. Generate a draft of the HCL with -generate-config-out (or aztfexport --generate-import-block on Azure), clean it up by hand, get a clean plan, and apply.
The piece most people skip and regret: after the import succeeds, refactor. Rename resources to your team's conventions, move them into modules, and use moved blocks so you don't destroy and recreate anything along the way.
