linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/State/tf-state-import

kb/state ── State ── intermediate

Import: bringing an existing resource under Terraform management

Import means: this resource already exists in the cloud; start managing it. The old way: `terraform import <address> <cloud_id>`, and you write the HCL yourself. The new way (TF 1.5+): an `import` block directly in HCL. Plan shows what will happen; apply commits it. Import does not write HCL for you. That is your job.

view as markdownaka: terraform-import, terraform-state-import

When you need import

A Terraform project rarely starts from scratch. Common situations:

  • Legacy infrastructure. Buckets, IAM roles, and VPCs were created by hand or with another tool. You want to bring them under Terraform. You need to "adopt" them.
  • Duplicate-resource conflict. One project ran apply; another project lost its state. Now both try to create the same resource, and the cloud rejects it. Either remove the old resource with state rm and import it into the new project, or decide which project is the owner.
  • State recovery. The state file is gone. The resources are still in the cloud. Import each one. There is no other way.

Import does not write HCL for you. It only links "this resource block in HCL" to "this ID in the cloud." You must write the resource block yourself.

The old way: terraform import

A CLI command that works with all Terraform versions.

bash
# 1. Describe the resource in HCL, without cloud attributes for now
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "legacy" {
  bucket = "my-legacy-bucket-2018"
}
EOF
# 2. Run the import
terraform import aws_s3_bucket.legacy my-legacy-bucket-2018

After this:

  • State now contains aws_s3_bucket.legacy with the real attributes from the cloud.
  • terraform plan compares HCL against state. Any difference appears as a diff.

The ID format depends on the resource type

Each AWS resource has its own import ID format:

ResourceID format
aws_s3_bucketbucket name: my-bucket
aws_iam_rolerole name: my-role
aws_security_groupsg-id: sg-1234abcd
aws_route_table_associationcomposite: subnet_id/rtb_id
aws_vpcvpc-id: vpc-1234abcd
aws_instanceinstance-id: i-1234abcd

The Registry page for each resource has an "Import" section at the bottom. If you do not know the ID, use aws s3 ls, aws iam list-roles, or aws ec2 describe-*.

The new way: import block (TF 1.5+)

Declarative, leaves a trace in HCL, and is visible in plan.

hcl
# main.tf
import {
  to = aws_s3_bucket.legacy
  id = "my-legacy-bucket-2018"
}
resource "aws_s3_bucket" "legacy" {
  bucket = "my-legacy-bucket-2018"
}

Run:

bash
terraform plan

Output:

Terraform will perform the following actions:
  # aws_s3_bucket.legacy will be imported
      id        = "my-legacy-bucket-2018"
      ...
bash
terraform apply

After apply:

  • State contains the resource.
  • HCL contains the import block (you can delete it; it is no longer needed) and the resource block.

Advantages over the CLI command:

  • Visible in a diff. A PR with an import block makes it clear: "we are adopting this resource."
  • Plan before apply. The CLI terraform import acts immediately. The import block does not: plan shows what will happen first, then apply commits it.
  • Catches HCL mistakes. If you mistyped the bucket name in HCL, plan shows the conflict before anything changes.
  • Supports for_each. An import block with for_each (TF 1.7+) adopts N resources with one block.

import block with for_each

hcl
variable "legacy_buckets" {
  type = set(string)
  default = ["bucket-a", "bucket-b", "bucket-c"]
}
import {
  for_each = var.legacy_buckets
  to       = aws_s3_bucket.legacy[each.key]
  id       = each.value
}
resource "aws_s3_bucket" "legacy" {
  for_each = var.legacy_buckets
  bucket   = each.value
}

One block, N imports. Especially useful when migrating from another IaC system (Pulumi, CloudFormation) where the list of resources is already known.

HCL generation: -generate-config-out

Writing HCL by hand for a large resource (say, aws_iam_policy with dozens of statements) is tedious. With TF 1.5+ you can ask Terraform to generate a draft:

hcl
# main.tf - no resource block, only the import block
import {
  to = aws_iam_policy.legacy
  id = "arn:aws:iam::123456789012:policy/legacy"
}
bash
terraform plan -generate-config-out=generated.tf

generated.tf will contain a generated resource "aws_iam_policy" "legacy" { ... } with all attributes. Read it carefully. It may include:

  • Computed-only fields (the provider sets these automatically; you can remove them from HCL).
  • Lifecycle blocks you did not intend.
  • Arguments that no longer exist in newer versions of the AWS provider.

The generated HCL is a draft, not a final version. You must review and clean it up.

What to do after import

  1. Run terraform plan. It must be clean (No changes).
  2. If it shows a diff, your HCL attributes differ from the real resource. Adjust HCL to match the cloud. If you intentionally want to change something, apply will reconcile the cloud to your HCL, but that is risky in production.
  3. You can delete import blocks after a successful apply. Many teams keep them in HCL as documentation of what was adopted and when.

Pitfalls

  • Import does not write HCL attributes. It only links state to the cloud. Writing HCL is your job. Without a matching HCL block, apply will try to destroy the resource (it is in state but absent from HCL).

  • Module resources use their full address. terraform import module.app.aws_s3_bucket.this <ID>. With an import block inside a module, you can declare the block within the module and write to = aws_s3_bucket.this; Terraform resolves the address from context.

  • count/for_each resources require an index. Use aws_iam_user.user["alice"], not aws_iam_user.user. A wrong address imports into the wrong slot, and plan will show a destroy+create pair.

  • Not every resource supports import. The AWS provider covers most resources, but exceptions exist. Some aws_lambda_alias and aws_ssm_parameter_* resources may lack an import handler. Check the "Import" section at the bottom of each resource's documentation page, or look for "This resource cannot be imported."

  • -generate-config-out places the file in the current directory. Terraform writes generated.tf next to your other files and does not integrate it further. Reviewing, splitting across files, and removing unneeded parts is your responsibility.

  • Secret data ends up in state after import. If you import an aws_db_instance, the database password appears in state in plain text. This is how import works, not a bug. See tf-state for guidance on protecting state.

  • Import does not carry over tags, lifecycle rules, or similar metadata. If your HCL has lifecycle { prevent_destroy = true }, that setting is a Terraform-side metadata instruction and is not pushed to the cloud resource.

§ команды

bash
terraform import aws_s3_bucket.legacy my-bucket-2018

The old CLI approach. Writes directly to state with no plan preview.

bash
terraform plan

With an import block in HCL: shows what will be imported before apply.

bash
terraform plan -generate-config-out=generated.tf

TF 1.5+: generate a draft resource block. Review and edit before using.

bash
terraform state show <ADDRESS>

After import, inspect which real attributes were captured.

§ см. также

  • tf-stateState: Terraform's memory of what it createdState is the JSON file `terraform.tfstate` where Terraform records what it created in the cloud. Without it, Terraform would have no way to tell which bucket is "its own" and which belongs to something else. The file holds resource IDs, all attributes, and often secrets. It is the most sensitive part of any project.
  • tf-state-manipulationstate mv, state rm, state pull/push: manual operations`terraform state mv` renames a resource address in state without destroy/recreate. `terraform state rm` removes a resource from state but not from the cloud. `terraform state pull/push` downloads or uploads state as a file. All four are sharp operations; do them with a backup and a clear reason. For declarative alternatives, see [[tf-moved-block]] and [[tf-removed-block]].
  • tf-moved-blockThe moved block: rename without destroy`moved { from = ..., to = ... }` in HCL declaratively tells Terraform: "this resource used to live at one address and now lives at another, the cloud object is the same." The plan shows a "move", not a "destroy + create". It arrived in TF 1.1. It replaces the manual `terraform state mv`, leaves a trace in git, repeats for everyone on the team, and shows up in the diff.
  • tf-removed-blockremoved block: drop a resource from state, keep it in the cloud`removed { from = ..., lifecycle { destroy = false } }` tells Terraform declaratively: remove this resource from management, but do not touch it in the cloud. The block was introduced in TF 1.7 and replaces the manual `terraform state rm` command. With `destroy = true` it behaves like an ordinary resource deletion from HCL.
  • aws-providerAWS provider: configuration and where Terraform finds your keysThe AWS provider looks for credentials in several places in order: env variables, ~/.aws/credentials, the instance IAM role. Usually `aws configure` locally or a role on EC2 is enough, and you configure nothing else.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies