Skip to content

Fix: Terraform Import Error — Resource Not Importable or State Conflict

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix Terraform import errors — terraform import syntax, import blocks (Terraform 1.5+), state conflicts, provider-specific import IDs, and importing existing infrastructure.

The Problem

terraform import fails with an error:

terraform import aws_s3_bucket.my_bucket my-existing-bucket
# Error: Resource already managed by Terraform
# Error: Cannot import non-existent remote object
# Error: error reading S3 Bucket (my-existing-bucket): NoSuchBucket

Or the import succeeds but terraform plan still shows changes:

terraform plan
# aws_s3_bucket.my_bucket will be updated in-place
# ~ versioning {
#     ~ enabled = false -> true
#   }
# Plan: 0 to add, 1 to change, 0 to destroy.

Or import blocks in Terraform 1.5+ don’t generate a resource config as expected:

terraform plan -generate-config-out=generated.tf
# Error: Unsupported argument

Why This Happens

terraform import brings existing infrastructure under Terraform management, but it only writes to the state file — it does NOT generate the Terraform configuration. The CLI command predates the language entirely: it was designed when state was the source of truth and HCL files were assumed to be hand-written to match. Every modern import problem traces back to this split.

The split surfaces in three different ways. First, the resource block has to exist in HCL before the command runs, because Terraform validates the import against the resource type and provider schema. Without a block, the command rejects the target address. Second, the import ID format is set by the provider, not by Terraform, so a working pattern for aws_s3_bucket is wrong for aws_db_instance even though both are AWS resources. Third, the imported state captures the live resource exactly, but your HCL captures your intent — and intent rarely matches a resource that someone else created by hand.

Import blocks (Terraform 1.5, June 2023) close part of the gap by making the operation declarative. They run during apply, integrate with state locking, and can generate a skeleton HCL file with -generate-config-out. But the underlying provider-specific ID format and post-import drift are still your problem.

  • Missing resource configuration — you must write the resource block in your .tf files before importing. Without it, Terraform doesn’t know what to manage.
  • Wrong import ID format — every resource type has its own import ID format. aws_s3_bucket uses just the bucket name, but aws_db_instance uses the DB instance identifier, and some resources use ARNs. Using the wrong format causes Cannot import non-existent remote object.
  • State conflict — the resource is already in the state file (from a previous import or terraform apply). Re-importing over an existing state entry requires removing it first.
  • Config drift after import — the import writes current state, but if your .tf config doesn’t match the actual resource configuration, Terraform will show a plan to change the resource.

Fix 1: Correct terraform import Syntax

The basic import command requires an existing resource block in your config:

# Syntax: terraform import <resource_type>.<resource_name> <import_id>
terraform import aws_s3_bucket.my_bucket my-existing-bucket-name

# With a module
terraform import module.storage.aws_s3_bucket.my_bucket my-existing-bucket-name

# With a resource that has a count index
terraform import 'aws_s3_bucket.buckets[0]' bucket-name-0
# Note: quote the address to prevent shell interpretation of brackets

# With a resource that uses for_each
terraform import 'aws_s3_bucket.buckets["production"]' production-bucket-name

Write the resource block BEFORE importing:

# main.tf — add this before running terraform import
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-existing-bucket-name"

  # Add other required/known attributes
  # You don't need all attributes — Terraform fills in the rest from state
  # But attributes in the config must match the real resource
}

Fix 2: Find the Correct Import ID

Every resource type has a specific import ID format. Check the provider docs:

# AWS resources — common import ID formats

# S3 bucket — just the bucket name
terraform import aws_s3_bucket.example my-bucket-name

# EC2 instance — instance ID
terraform import aws_instance.example i-1234567890abcdef0

# RDS instance — DB identifier (not ARN)
terraform import aws_db_instance.example my-db-instance

# IAM role — role name
terraform import aws_iam_role.example my-role-name

# IAM policy — policy ARN
terraform import aws_iam_policy.example arn:aws:iam::123456789:policy/MyPolicy

# Security group — group ID
terraform import aws_security_group.example sg-0123456789abcdef0

# VPC — vpc ID
terraform import aws_vpc.example vpc-0123456789abcdef0

# Route53 record — format: zone_id_RECORD_NAME_RECORD_TYPE
terraform import aws_route53_record.example Z1234567890ABC_example.com_A

Look up the import ID format in the provider registry:

# Open the provider docs for the resource
# URL pattern: registry.terraform.io/providers/hashicorp/<provider>/latest/docs/resources/<resource>

# Or use the Terraform CLI
terraform providers schema -json | jq '.provider_schemas."registry.terraform.io/hashicorp/aws".resource_schemas."aws_s3_bucket"'

Google Cloud and Azure examples:

# GCP — Cloud SQL instance
terraform import google_sql_database_instance.example projects/my-project/instances/my-instance

# Azure — Resource Group
terraform import azurerm_resource_group.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg

# Azure — Virtual Machine
terraform import azurerm_virtual_machine.example /subscriptions/.../resourceGroups/mygroup/providers/Microsoft.Compute/virtualMachines/myvm

Fix 3: Use Import Blocks (Terraform 1.5+)

Terraform 1.5 introduced import blocks as a declarative alternative to the CLI command. They’re reproducible, reviewable, and can generate config:

# import.tf — define what to import
import {
  id = "my-existing-bucket-name"
  to = aws_s3_bucket.my_bucket
}

# The resource block must still exist (or be generated)
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-existing-bucket-name"
}
# Run terraform plan to see what will be imported
terraform plan

# Apply to execute the import
terraform apply
# After apply, the import block can be removed (it's a one-time operation)

Generate resource config automatically (Terraform 1.5+):

# Step 1: Write just the import block (no resource block yet)
# import.tf
cat > import.tf << 'EOF'
import {
  id = "my-existing-bucket-name"
  to = aws_s3_bucket.my_bucket
}
EOF

# Step 2: Generate the resource configuration
terraform plan -generate-config-out=generated_resources.tf

# Step 3: Review the generated config, then apply
cat generated_resources.tf
terraform apply

Note: -generate-config-out requires Terraform 1.5+. The generated config is a starting point — review it for accuracy before committing.

Fix 4: Fix State Conflicts

If the resource is already in state, re-importing requires removing it first:

# List resources in state
terraform state list

# Check current state of a resource
terraform state show aws_s3_bucket.my_bucket

# Remove from state (does NOT destroy the actual resource)
terraform state rm aws_s3_bucket.my_bucket

# Now you can re-import
terraform import aws_s3_bucket.my_bucket my-bucket-name

Moving resources within state (renamed resource blocks):

# If you renamed the resource block, use state mv instead of rm + import
# Old: resource "aws_s3_bucket" "old_name"
# New: resource "aws_s3_bucket" "new_name"

terraform state mv aws_s3_bucket.old_name aws_s3_bucket.new_name
# Updates state without touching the real resource

Fix 5: Fix Config Drift After Import

After importing, terraform plan often shows changes because your config doesn’t fully match the actual resource. Align the config with the real state:

# Step 1: Import the resource
terraform import aws_s3_bucket.my_bucket my-bucket-name

# Step 2: See what Terraform wants to change
terraform plan
# Note every attribute it wants to change

# Step 3: Update your resource block to match current state
# Either: add the attributes to your config
# Or: accept the changes (Terraform will apply them on next apply)

Example — fixing versioning drift:

# BEFORE — config doesn't specify versioning
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-bucket-name"
}
# terraform plan shows: ~ versioning { enabled = false -> true }

# AFTER — config matches actual state
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-bucket-name"
}

resource "aws_s3_bucket_versioning" "my_bucket" {
  bucket = aws_s3_bucket.my_bucket.id
  versioning_configuration {
    status = "Enabled"  # Matches what's actually in AWS
  }
}
# terraform plan now shows: No changes.

Note: AWS provider v4+ splits many S3 attributes into separate resources (aws_s3_bucket_versioning, aws_s3_bucket_acl, etc.). Import each sub-resource separately.

Fix 6: Bulk Import with for_each

Import many similar resources at once using import blocks with for_each (Terraform 1.7+):

# Import multiple S3 buckets
locals {
  buckets = {
    "logs"    = "my-logs-bucket-prod"
    "backups" = "my-backups-bucket-prod"
    "assets"  = "my-assets-bucket-prod"
  }
}

import {
  for_each = local.buckets
  id       = each.value
  to       = aws_s3_bucket.buckets[each.key]
}

resource "aws_s3_bucket" "buckets" {
  for_each = local.buckets
  bucket   = each.value
}
terraform plan   # Shows N imports
terraform apply  # Imports all at once

Version History: Import Across Terraform Releases

The terraform import CLI command has existed since Terraform 0.7 (2016), but the developer experience around it has changed substantially. Understanding which features your version has saves a lot of guessing.

Terraform 0.12 (2019) introduced the modern HCL syntax that import targets still use today. Address quoting rules — single quotes around aws_s3_bucket.buckets[0] on shells that expand brackets — date from this era. If you are reading old runbooks, they may use unquoted addresses that no longer work on modern bash or zsh.

Terraform 1.1 (December 2021) added moved blocks. These are not imports, but they replaced the terraform state mv workflow for renames done inside HCL. If you refactor a module and rename aws_s3_bucket.old to aws_s3_bucket.new, a moved { from = ..., to = ... } block in HCL is preferable to the imperative state mv shown in Fix 4 — the move is reviewable in code review and runs as part of apply.

Terraform 1.5 (June 2023) added the headline feature: declarative import blocks with -generate-config-out. This is the version covered in Fix 3. Before 1.5, importing 20 resources meant 20 separate CLI invocations, often interleaved with HCL edits to add the resource blocks. After 1.5, you write the import blocks once, run terraform plan -generate-config-out=..., review the generated HCL, and apply. The whole operation is gitable and reviewable.

Terraform 1.5 also added removed blocks as the counterpart to import. removed { from = ..., lifecycle { destroy = false } } removes a resource from state without destroying it — the declarative version of terraform state rm. The CLI command still works and is fine for one-off cleanup, but removed blocks are the right choice when the change should land in git.

OpenTofu 1.6 (January 2024) forked from Terraform 1.5.5 and shipped with full import block and -generate-config-out support. The behavior matches Terraform 1.5 closely. Where OpenTofu diverges in later versions, it tends to add features rather than remove them — for example, OpenTofu’s state encryption and provider mirroring features have no equivalent in Terraform 1.6 or 1.7. For pure import operations, OpenTofu 1.6+ is a drop-in replacement.

Terraform 1.7 (January 2024) added for_each to import blocks, shown in Fix 6. Before 1.7, bulk importing 50 buckets meant 50 import blocks generated by a script. With 1.7, one block with for_each over a locals map does the same job. OpenTofu added matching support in 1.7 (March 2024).

Terraform 1.8 (April 2024) allowed providers to offer multiple identity attributes for imports — important for resources where the natural ID is composite (zone + record name + type, for instance). Older Terraform versions force you to encode the entire composite as a single string with provider-specific separators, which is brittle when separators clash with valid characters in the IDs.

The practical guidance: if you are still on Terraform 0.13 or 0.14 doing imports, the upgrade to 1.5+ is the single largest quality-of-life jump in the tool’s history for this workflow. Below 1.5, every import is a small manual project. At 1.5+, it is a code change.

Still Not Working?

Provider authenticationterraform import must be able to authenticate to the provider (AWS, GCP, Azure) to verify the resource exists. Run terraform init and ensure credentials are configured before importing.

Import ID requires sub-components — some resources have compound IDs. For example, aws_iam_role_policy_attachment requires role-name/policy-arn. Check the provider documentation’s “Import” section carefully.

Resource doesn’t support import — not every resource type supports terraform import. If a resource has no “Import” section in its docs, it can’t be imported. You’ll need to delete the real resource and recreate it with Terraform, or use terraform state to manually craft a state entry (advanced, error-prone).

Terraform Cloud / Enterprise — when using Terraform Cloud, run terraform import in a local workspace that mirrors the remote state, then push the state. Or use the Terraform Cloud API to run the import remotely.

Sensitive attributes blank out after import — provider read functions sometimes cannot retrieve sensitive values (database passwords, API keys, generated secrets) from the cloud API. After import, those attributes are empty in state, and the next plan wants to set them from your HCL. Set the value in HCL with sensitive = true and the corresponding variable, then run terraform apply -refresh-only once to absorb the value into state without triggering downstream changes.

Import block runs but state stays empty — this happens when the resource address in the to: argument does not match any resource block in HCL. The plan succeeds (because import blocks don’t require the resource at plan time on some versions), but apply silently does nothing. Run terraform state list after apply to confirm the address landed. If it didn’t, double-check spelling and module paths.

Generated config uses default attribute values-generate-config-out emits every attribute the provider exposes, including ones at their default. The generated file is verbose. Trim defaults manually before committing, or run terraform plan to confirm a no-op, then prune attributes one at a time and re-plan until the file is minimal.

For related Terraform issues, see Fix: Terraform Error Acquiring State Lock, Fix: Terraform Plan Error Invalid Reference, Fix: Terraform Resource Already Exists, and Fix: Terraform Failed to Install Provider.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles