Back to Terraform & IaC
Q8Question 8 of 13

Bringing existing/legacy AWS resources under Terraform management (write config → import → plan → iterate → commit), and why import alone does not produce clean config

Key Concepts
  • terraform import binds a real cloud resource to a Terraform address in state but does not generate configuration
  • The adoption workflow is: write HCL, import into state, run plan, iterate until plan shows zero changes, then commit
  • A clean plan proves configuration, state, and real infrastructure all agree
  • Terraform 1.5+ adds import blocks and -generate-config-out but the generated HCL is noisy and must be cleaned
  • Never import straight into production state on the first attempt; back up state and use terraform state rm to undo
  • Each resource type has its own import ID format: bucket name, instance ID, security group ID, or compound ID

Definition

Import fills state, not config: you still hand-write accurate HCL until terraform plan shows zero drift.
  • terraform import binds an already-existing real-world infrastructure object to a Terraform resource address in the state file, populating state only
  • Import does not generate configuration so you must author matching HCL by hand before or after importing
  • The adoption workflow is: write HCL that matches the live resource, run import, run plan, and iterate on the HCL until the plan shows no changes
  • A clean plan is the finish line, not a successful import, because it proves configuration, state, and real infrastructure all agree
  • Terraform 1.5+ adds import {} blocks and -generate-config-out to scaffold HCL, but the output is noisy and must be cleaned

Imagine the National Bank of India has an old safe-deposit vault that a contractor installed years ago without any paperwork. Terraform is the bank's official asset register. Importing is like writing the vault's serial number into the register so the bank now knows it owns the vault. But writing the serial number does not describe the vault: its size, lock type, and location. You still have to walk to the vault, measure it, and write an accurate description by hand. If your written description says "small vault" but the real vault is large, the next audit (the plan) will scream "mismatch." You keep fixing your description until the audit is silent.

Understanding the Concepts

What is the terraform import workflow?

Terraform tracks three things: the configuration (your HCL files), the state file (Terraform's record of what it manages), and the real infrastructure (what actually exists in AWS). A normal terraform apply keeps all three in sync. When a resource was created manually outside Terraform, the state file knows nothing about it, so Terraform would try to create a duplicate if you naively wrote config and applied. Import solves exactly this: it writes a state entry saying "this Terraform address corresponds to this real AWS object."

BASH
# The four-step adoption workflow
# Step 1: Write HCL matching the live resource
# Step 2: terraform import <address> <real-id>
# Step 3: terraform plan -- read every proposed change
# Step 4: Iterate on HCL until plan shows NO changes, then commit

How do you write the initial HCL for import?

You must have a resource block declared in your HCL before running import, even if it is a minimal shell. The resource address in the import command must match the address in your configuration.

HCL
# Step 1: Write a resource block for the existing S3 bucket
resource "aws_s3_bucket" "audit_logs" {
  bucket = "nbi-audit-logs-prod"

  tags = {
    Environment = "production"
    Team        = "compliance"
  }
}

resource "aws_s3_bucket_versioning" "audit_logs" {
  bucket = aws_s3_bucket.audit_logs.id

  versioning_configuration {
    status = "Enabled"
  }
}
BASH
# Step 2: Import the real bucket into state
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod

# Output:
# aws_s3_bucket.audit_logs: Importing from ID "nbi-audit-logs-prod"...
# aws_s3_bucket.audit_logs: Import prepared!
# Import successful!

Why does import alone not produce correct or clean config?

Import reads the live attributes into state, but your HCL is still whatever you typed. If your HCL omits versioning, server_side_encryption_configuration, or tags that the real bucket has, the first plan will propose to remove or change them, because Terraform believes your config is the desired truth.

BASH
# Step 3: Run plan and read every proposed change
terraform plan
TEXT
# Plan output shows drift because HCL is incomplete:
  # aws_s3_bucket.audit_logs will be updated in-place
  ~ resource "aws_s3_bucket" "audit_logs" {
      ~ tags = {
          - "CostCenter" = "COMPLIANCE-001" -> null   # Real tag not in HCL!
        }
    }
HCL
# Step 4: Fix the HCL to match reality
resource "aws_s3_bucket" "audit_logs" {
  bucket = "nbi-audit-logs-prod"

  tags = {
    Environment = "production"
    Team        = "compliance"
    CostCenter  = "COMPLIANCE-001"   # Added to match the real bucket
  }
}

You iterate until terraform plan shows No changes. Your infrastructure matches the configuration.

How does the Terraform 1.5+ import block work?

Newer Terraform adds an import {} block and a -generate-config-out flag that can scaffold HCL for you, but the generated HCL is noisy: it dumps every computed and default attribute.

HCL
# import block (Terraform 1.5+)
import {
  to = aws_s3_bucket.audit_logs
  id = "nbi-audit-logs-prod"
}
BASH
# Generate config scaffold
terraform plan -generate-config-out=generated.tf
TEXT
# generated.tf is noisy -- includes read-only and computed fields:
resource "aws_s3_bucket" "audit_logs" {
  arn                   = "arn:aws:s3:::nbi-audit-logs-prod"  # read-only!
  bucket                = "nbi-audit-logs-prod"
  bucket_domain_name    = "nbi-audit-logs-prod.s3.amazonaws.com"  # computed!
  hosted_zone_id        = "Z3AQBSTGFYJSTF"  # computed!
  # ... dozens more computed attributes
}

You still must strip read-only fields, replace hardcoded values with var. references, and group resources into modules so the config is reusable and reviewable.

What are the import ID formats for common resource types?

Each resource type documents its own import ID format. Using the wrong format causes the import to fail.

| Resource Type | Import ID Format | Example |
|---|---|---|
| aws_s3_bucket | Bucket name | nbi-audit-logs-prod |
| aws_instance | Instance ID | i-0abc123def456 |
| aws_security_group | Security group ID | sg-0abc123def456 |
| aws_db_instance | DB identifier | nbi-ledger-prod |
| aws_route_table_association | Compound ID | rtbassoc-abc123 |
| aws_iam_role | Role name | nbi-transfer-service-role |

BASH
# Each uses a different ID format
terraform import aws_instance.transfer_service i-0abc123def456
terraform import aws_security_group.payments sg-0abc123def456
terraform import aws_db_instance.ledger nbi-ledger-prod

What are common mistakes when importing?

The most common mistake is importing into production state on a first attempt. If the plan looks wrong, you need to undo the mapping.

BASH
# WRONG: Import directly into production state without a backup
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod
# If the plan is wrong, you are now stuck with a bad state entry

# CORRECT: Back up state first, then import
terraform state pull > backup.tfstate
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod
terraform plan

# If the plan looks wrong, undo the import without touching the real resource
terraform state rm aws_s3_bucket.audit_logs

Another mistake is forgetting that import only edits state. After importing, running terraform apply without checking the plan first can destroy or modify the resource if your HCL does not match.

What about importing resources inside modules?

When the resource you are importing belongs to a module, you use the full module path in the import address.

BASH
# Importing a VPC that belongs to the networking module
terraform import module.networking.aws_vpc.main vpc-abc123

# Importing an indexed resource (count or for_each)
terraform import 'module.networking.aws_subnet.private[0]' subnet-def456
terraform import 'module.networking.aws_subnet.private["ap-south-1a"]' subnet-ghi789

What should a senior engineer know?

A senior engineer never imports straight into production state. They take a backup with terraform state pull, import into a non-production workspace first, and verify the plan before touching production. They also know that terraform state rm undoes an import without destroying the real resource, because import only edits state.

The key insight is that a clean plan, not a successful import, is the finish line. A successful import only means state has a mapping. A clean plan means configuration, state, and real infrastructure all agree, and that is the moment adoption is safe to commit.

BASH
# The complete safe workflow
terraform state pull > backup.tfstate
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod
terraform plan
# Read every line of the plan
# Fix HCL until: No changes. Your infrastructure matches the configuration.
terraform plan -out=adopt.plan
terraform apply adopt.plan
git add . && git commit -m "Adopt nbi-audit-logs-prod bucket into Terraform"

What are common interview follow-up questions?

  • Can you import a resource that has no configuration block? No, the resource address must exist in HCL before importing.
  • Does import create anything in the cloud? No, it only writes to the state file.
  • What happens if you import the wrong real resource? Run terraform state rm to remove the mapping, then import the correct one.
  • Can you import the same real resource twice under different addresses? No, Terraform prevents duplicate state entries for the same real object.

Visual Overview

DIAGRAM
  Adoption Workflow
  ┌───────────────┐    ┌───────────────┐    ┌───────────────┐
  │  1. Write HCL │───▶│  2. Import    │───▶│  3. Plan      │
  │  (match live) │    │  (fill state) │    │  (check drift)│
  └───────────────┘    └───────────────┘    └───────┬───────┘
                                                    │
                                           Changes? │
                                            YES     │  NO
                                            ┌───────┴───────┐
                                            ▼               ▼
                                    ┌───────────────┐  ┌───────────┐
                                    │ 4. Fix HCL    │  │ 5. Commit │
                                    │ (iterate)     │  │ (done!)   │
                                    └───────┬───────┘  └───────────┘
                                            │
                                            └──── back to Step 3

  Three-way agreement (the finish line)
  ┌────────────┐   ┌────────────┐   ┌────────────┐
  │    HCL     │ = │   State    │ = │   Cloud    │
  │ (desired)  │   │ (recorded) │   │  (actual)  │
  └────────────┘   └────────────┘   └────────────┘
        Clean plan = all three agree

Key Takeaway

Bringing an existing or legacy AWS resource under Terraform management is a deliberate four-step workflow, and the single most important thing to understand is that importing only edits Terraform's state file: it never writes your configuration for you. First you author HCL that you believe matches the live resource. Second you run terraform import, which binds your resource address to the real object's import ID inside state so Terraform stops trying to create a duplicate. Third you run terraform plan and read every proposed change line by line. Fourth you iterate on the HCL until the plan reports no changes, and only then do you commit.

The reason import does not produce clean config is that import is a one-way read into state. Even the newer -generate-config-out feature, which does scaffold HCL, produces noisy output full of computed and default attributes that you still have to strip, and you still have to replace hardcoded values with variables and group resources into modules so the config is reusable and reviewable.

In the National Bank of India project, a contractor had manually created an S3 bucket called nbi-audit-logs-prod that stored transaction audit logs for accounts like Rajesh's SAV2001 and Priya's CUR3001, complete with versioning and AES-256 encryption. The team wrote a minimal resource block, imported the bucket into state, and the very first plan tried to strip versioning and encryption because the config had not mentioned them. They read that plan, added the versioning and encryption blocks, moved the bucket name into a variable, and iterated until the plan was silent.

Remember one sentence: a clean plan, not a successful import, is the finish line, because only a clean plan proves that configuration, state, and real infrastructure all agree.

---

Share
Use ← → arrow keys to navigate