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/lessons/tf-intermediate-08-import-block

lesson ── terraform-intermediate ── ~15 мин ── 5 шагов

Declarative import: capturing an existing resource

The bucket already exists in the cloud. Someone created it by hand (or it is legacy from 2019). You want to manage it with Terraform. The old way is the terraform import CLI. The new way (TF 1.5+) is a declarative import block right in the HCL: the plan shows what will happen, the apply locks it in.

In this lesson you will create a bucket "by hand" with the AWS CLI, then capture it into the Terraform state through an import block.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Create a bucket outside Terraform

    We emulate "legacy": a bucket created by hand with the AWS CLI.

    bash
    cd /home/student/tf-import
    aws --endpoint-url=http://localstack:4566 s3 mb s3://linuxlab-legacy-data
    aws --endpoint-url=http://localstack:4566 s3api put-bucket-tagging \
      --bucket linuxlab-legacy-data \
      --tagging 'TagSet=[{Key=Origin,Value=manual},{Key=Year,Value=2019}]'
    aws --endpoint-url=http://localstack:4566 s3 ls
    aws --endpoint-url=http://localstack:4566 s3api get-bucket-tagging --bucket linuxlab-legacy-data

    The bucket exists. Terraform knows nothing about it.

    ✓ The bucket is created without Terraform. Now we capture it.

  2. 02

    Describe the resource and the import block

    In /home/student/tf-import/main.tf:

    hcl
    resource "aws_s3_bucket" "legacy" {
      bucket = "linuxlab-legacy-data"
      tags = {
        Origin = "manual"
        Year   = "2019"
      }
    }
    import {
      to = aws_s3_bucket.legacy
      id = "linuxlab-legacy-data"
    }

    Breakdown:

    • The resource block describes the desired state of the resource in HCL. Its attributes have to match the bucket's real values. If they differ, the plan shows a diff after the import.
    • import { to, id } is an instruction to terraform: "take the resource with this cloud ID and bind it to this address in state".
    • For S3 the id is the bucket name. Each AWS resource has its own ID format. See the provider documentation.

    Init:

    bash
    terraform init

    ✓ The import block is written. The plan will show what happens.

  3. 03

    The plan shows the import in advance

    bash
    terraform plan

    In the output:

    Terraform will perform the following actions:
      # aws_s3_bucket.legacy will be imported
        resource "aws_s3_bucket" "legacy" {
            bucket = "linuxlab-legacy-data"
            ...
          }
    Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

    The key part: 1 to import, not "to add". That is the difference between an import block and a plain declaration.

    Without a plan it would be unclear what was about to happen. The old terraform import CLI does it on the spot, with no plan.

    ✓ The plan showed the import. The apply will lock it in.

  4. 04

    Apply: the bucket in state

    bash
    terraform apply -auto-approve

    After the apply:

    bash
    terraform state list
    terraform state show aws_s3_bucket.legacy

    aws_s3_bucket.legacy is in state. state show prints every real attribute of the bucket: name, region, tags, ARN, hosted_zone_id, and so on. Everything AWS returns.

    The main test: the plan after the import has to be clean.

    bash
    terraform plan -detailed-exitcode
    echo "exit: $?"

    Exit 0 means "No changes". The HCL matches reality. If you get 2, there is a diff, which means the attributes in the HCL do not match the real ones exactly. You will have to adjust them.

    ✓ The bucket is captured under Terraform management. The plan is clean.

    The same thing on OpenTofu

    OpenTofu keeps the CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On a first switch, though, back up the state and do a run on a feature branch, the differences cluster in the newer features (variables in backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  5. 05

    Delete the import block (it has done its job)

    After the apply the import block has played its part. You can remove it:

    bash
    # cut out the import block, leave only the resource
    cat > main.tf <<'EOF'
    resource "aws_s3_bucket" "legacy" {
      bucket = "linuxlab-legacy-data"
      tags = {
        Origin = "manual"
        Year   = "2019"
      }
    }
    EOF
    terraform plan

    Plan: No changes. The bucket is under management, the import block is no longer needed.

    Many teams leave import blocks in the code as documentation, a note that this resource was once imported. It is safe to keep them: after the apply Terraform ignores them.

    ✓ The import block is gone, and the bucket is still under management. Capture complete.

    Bulk import with for_each (TF 1.7+)

    Ten legacy buckets with the same structure, one block:

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

    One block of HCL. N imports. This is handy when migrating from another IaC tool (Pulumi, CloudFormation): you have a list of resources, and you capture them in bulk.

    -generate-config-out

    Writing a big resource by hand is painful. The command:

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

    creates a file generated.tf with a draft resource block, every attribute filled in from the cloud. It is a draft, so you need to:

    • Remove computed-only fields.
    • Delete deprecated arguments.
    • Adjust the structure to your own style.

    Not a finished artifact, but it saves time.

    • → Import in full
    • → Refactoring patterns

Что ты узнал

import { to = ADDR, id = "CLOUD_ID" } in HCL ties an existing cloud resource to an address in state. The plan shows "will be imported". The apply locks it in. After that you can delete the block. With TF 1.7+, for_each works inside the import block for bulk operations.

команды

  • aws --endpoint-url=$LS s3 lssee what is in the cloud to import
  • terraform planwith an import block: shows 'will be imported'
  • terraform plan -generate-config-out=generated.tfgenerate a draft resource block

концепции

  • · An import block does NOT write the HCL for you: it links state to the cloud, but you write the resource block yourself
  • · -generate-config-out gives you a draft, but it needs manual cleanup
  • · After apply you can delete the import block from the HCL: it has done its job

← предыдущий

count vs for_each: creating resources in bulk

следующий →

GitHub Actions, the full pipeline through act

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies