Terraform

Terraform import Block Complete Guide: Safely Onboard Existing Assets with Declarative Imports

2026-04-19
NicheeLab Editorial Team

Historically, bringing existing cloud assets under Terraform management meant using the interactive CLI import. Since Terraform 1.5, you can do it declaratively with the import block.

This article covers the fundamentals of declarative imports, importing into for_each/count and module-scoped resources, handling diffs and generated configuration, and the exam-relevant points — all at a practical, hands-on level.

Declarative Import: Foundations and Exam Points

The import block is a declaration that associates an existing resource's provider-specific import ID with Terraform state. It is evaluated during plan/apply and performs the import against the target address. Data sources are out of scope; only managed resources can be imported.

Whereas the legacy terraform import (CLI) is an imperative procedure, the import block is captured as code — making code review, reproducibility, and CI/CD integration straightforward. After apply, you can delete the import block and the state is retained, but attempting to re-import the same address will error out.

The exam often asks about the following: which address to import into (count/for_each and module path specification), understanding import ID formats (provider-dependent), behavior when drift is detected (changes proposed in plan), the role and limits of generated configuration, and when to choose CLI vs. the declarative approach.

  • An import block must at minimum specify to and id
  • You can target resources inside modules using the fully qualified module.path.resource address
  • For individual instances created by for_each/count, specify the [key]/[index] explicitly
  • Diffs appearing at plan time are normal — leverage lifecycle.ignore_changes incrementally
  • Some resources are not import-supported by the provider (check the provider docs)

Basic Syntax of the import Block and a Minimal Example

The minimum configuration consists of to (the resource address to import into) and id (the import ID of the existing resource). The import ID format follows the provider documentation. For example, some AWS resources use ARNs, S3 buckets use their name, and VPCs use IDs like vpc-xxxx.

The apply flow is simple: the import is detected at plan time and registered into state at apply time. After that, any drift between the resource definition and reality is surfaced as a regular change plan.

  • Multiple import blocks can coexist within the same module
  • When the target resource definition does not yet exist, you can use the plan-time option to output a generated configuration skeleton (Terraform 1.5+)
  • After a successful import, the import block is no longer permanently required (the state is retained)

Declarative import flow

 Existing cloud asset      import block             Terraform state
        |                         |                        |
        |  Reference import ID    |                        |
        |------------------------>|  to=addr / id=...      |
        |                         |  (evaluated at         |
        |                         |   plan/apply time)     |
        |                         |----------------------->|
        |                         |   Associate to state   |
        |                         |<-----------------------|
        |                         |  Subsequent normal     |
        |                         |  drift detection       |

Minimal example (importing an AWS S3 bucket)

provider "aws" {
  region = var.region
}

resource "aws_s3_bucket" "example" {
  bucket = "my-existing-bucket-123"
  acl    = "private"
}

# Declarative import
import {
  to = aws_s3_bucket.example
  id = "my-existing-bucket-123"
}

# Example commands
# terraform init
# terraform plan
# terraform apply

Imports into for_each / count / Module-scoped Resources

When importing individual instances created by for_each or count, specify the key or index in the address. If you have many imports, you can declaratively batch them by using for_each on the import block itself (Terraform 1.5+).

For resources inside a module, specify the fully qualified address including the module. path. The same applies to deeply nested hierarchies, e.g. module.a.module.b.resource["key"].

  • For count: specify the index, e.g. aws_instance.web[0]
  • For for_each: specify the key, e.g. aws_security_group.sg["admin"]
  • Use for_each on the import block to codify large batches of imports, ensuring reviewability and reproducibility
  • Module-scoped: use the fully qualified address, e.g. to = module.net.aws_vpc.main

Concrete address examples

# Import the 0th instance created by count
resource "aws_instance" "web" {
  count         = 2
  ami           = var.ami
  instance_type = "t3.micro"
}

import {
  to = aws_instance.web[0]
  id = "i-0ab1c2d3e4f5"
}

# Import a specific for_each key
resource "aws_security_group" "sg" {
  for_each = {
    admin = "sg-admin"
    app   = "sg-app"
  }
  name = each.value
  vpc_id = var.vpc_id
}

import {
  to = aws_security_group.sg["admin"]
  id = "sg-0123456789abcdef0"
}

# Batch-declare imports with for_each on the import block
variable "buckets" {
  type = map(string) # key=logical name, value=actual bucket name
}

resource "aws_s3_bucket" "b" {
  for_each = var.buckets
  bucket   = each.value
}

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

# Import a module-scoped resource
module "net" {
  source = "./modules/network"
}

import {
  to = module.net.aws_vpc.main
  id = "vpc-0abc123def456"
}

Diffs, Generated Configuration, and Safe Migration Steps

Immediately after an import, the existing resource and the current resource definition often disagree, producing many changes in the plan. A pragmatic incremental migration prioritizes safe ingestion first, temporarily suppresses excessive drift with lifecycle.ignore_changes as needed, and then removes those suppressions once the configuration converges.

In Terraform 1.5+, when the target resource definition does not yet exist, you can use a plan-time option to emit a generated configuration. Note that this is a skeleton and is not guaranteed to be a perfect match. Review the output and adopt only the attributes you actually need.

  • Import first → back up state → analyze the meaning of diffs → apply in small steps
  • Keep ignore_changes minimal and remove it incrementally
  • Treat generated configuration as a starting point — watch out for provider defaults and unnecessary attributes
  • Get the import ID exactly right — incorrect IDs cause apply errors or unintended drift

Example of temporarily suppressing drift

resource "aws_s3_bucket" "example" {
  bucket = "my-existing-bucket-123"

  lifecycle {
    ignore_changes = [
      acl,                   # Respect the current ACL for now
      versioning,            # Reconcile later in a planned step
    ]
  }
}

# Once converged, remove ignore_changes and re-run plan/apply

Comparison with the terraform import CLI and When to Use Each

For small, one-off cases the CLI is usually fine in practice, but the import block is superior in team development settings where reproducibility matters. It survives as reviewable code, which also improves auditability. On the other hand, the CLI's lightweight nature is useful when you need to verify something interactively and quickly.

Auto-generating configuration and expressively handling many simultaneous imports are strengths of the import block. Consider using both, aligned with your existing IaC standards and operational structure.

  • Team development, CI/CD, audit-focused → centered on the import block
  • Spot work, experimentation → try quickly with the CLI
  • Both arrive at the same state outcome, but they differ significantly in traceability and reproducibility
Aspectimport block (declarative)terraform import (CLI)Notes
Declarativeness / HistorySurvives as code; easy to review in PRs and auditHistory depends on operation logs; reproducibility depends on peopleThe import block fits naturally into standard organizational code review
Batch executionCan declare large batches of imports with for_eachRequires shell loops or similarEffective at reducing human error
Generated configurationEasy to obtain a skeleton via the plan-time generation optionConfiguration is mostly prepared manuallySkeletons require review
Module hierarchySpecify naturally with fully qualified addressesAddressing is possible, but the procedure tends to scatterThe deeper the hierarchy, the more the declarative approach wins
Automation / CIDrop-in to CI as-isRequires additional implementation for execution ordering and safety controlsDifferences in how easy it is to re-run on failure
Learning costStays within the Terraform languageRequires CLI flags and case-by-case explanationChoose based on the team's proficiency

Pitfalls and Troubleshooting

Common stumbling blocks include trying to re-import an address that is already in state (which errors out), incorrect import IDs, and mixing up providers, regions, or aliases. Start with terraform state list to understand the current situation, then review the address, ID, and provider selection.

An import does not modify the resource itself. It only creates the state association; actual changes only happen during the subsequent drift reconciliation. If the blast radius is large, back up the target workspace before proceeding incrementally.

  • On a 'resource already managed' error, either remove the import block or correct the instance address
  • Import ID format is provider-dependent — confirm ARN vs. name beforehand
  • Watch out for provider alias/region mix-ups (an asset with the same name in a different region)
  • When diffs are large, converge safely with ignore_changes and scope splitting
  • Back up with terraform state pull before working, in case state gets corrupted

Minimal commands to inspect the current situation

# List all addresses currently in state
terraform state list

# Show the details of a specific address
terraform state show aws_s3_bucket.example

# Back up state if needed
terraform state pull > state-backup.jsonl

Check Your Understanding

Pro

問題 1

In Terraform 1.5+, you want to manage 5 existing S3 buckets under for_each within the same module. Which is the appropriate way to import them declaratively, preserving reproducibility with the fewest steps?

  1. Specify for_each on the import block and map to to each key's address
  2. Manually run terraform import (CLI) five times, then rewrite as for_each later
  3. Use the moved block to move unmanaged buckets under for_each management
  4. Reference them as data sources, then let apply automatically register them into state

正解: A

With declarative imports, you can specify for_each on the import block itself and map to each instance address with to, e.g. aws_s3_bucket.resource[each.key]. Iterating the CLI is inferior in reproducibility and reviewability; moved is for moving between existing state entries, not for unmanaged → managed import. Data sources are out of scope for imports.

Frequently Asked Questions

Can I delete the import block after the import completes?

Yes. The import block is a declaration that instructs Terraform to import a resource during plan/apply; once the import succeeds, the association with state persists. The resource is then managed via the normal resource definition, so the import block is no longer required. Leaving it in place can even cause errors on subsequent applies because the same address is already under management, so the common practice is to delete it after completion.

Does an import change the existing resource? Can it cause downtime or replacement?

The import itself only associates the resource with Terraform state and does not modify the existing resource. Changes are only proposed during apply when the plan detects a drift. Stops or replacements can occur when the configuration diverges significantly from reality, so always review the plan carefully.

I don't know the format of the import ID. What should I do?

Import IDs are defined per resource by each provider. For example, AWS resources may require an ARN, a resource name, or a resource ID such as vpc-xxxx. Check the resource documentation for the target provider and supply the exact value in the required format.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.