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.
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.
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.
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 applyWhen 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"].
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"
}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.
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/applyFor 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.
| Aspect | import block (declarative) | terraform import (CLI) | Notes |
|---|---|---|---|
| Declarativeness / History | Survives as code; easy to review in PRs and audit | History depends on operation logs; reproducibility depends on people | The import block fits naturally into standard organizational code review |
| Batch execution | Can declare large batches of imports with for_each | Requires shell loops or similar | Effective at reducing human error |
| Generated configuration | Easy to obtain a skeleton via the plan-time generation option | Configuration is mostly prepared manually | Skeletons require review |
| Module hierarchy | Specify naturally with fully qualified addresses | Addressing is possible, but the procedure tends to scatter | The deeper the hierarchy, the more the declarative approach wins |
| Automation / CI | Drop-in to CI as-is | Requires additional implementation for execution ordering and safety controls | Differences in how easy it is to re-run on failure |
| Learning cost | Stays within the Terraform language | Requires CLI flags and case-by-case explanation | Choose based on the team's proficiency |
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.
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.jsonlPro
問題 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?
正解: 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.
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.
Practice with certification-focused question sets
無料で問題を解いてみる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.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...