lesson ── terraform-intermediate ── ~15 мин ── 5 шагов
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 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
We emulate "legacy": a bucket created by hand with the AWS CLI.
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.
In /home/student/tf-import/main.tf:
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:
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".id is the bucket name. Each AWS resource has its own ID
format. See the provider documentation.Init:
terraform init
✓ The import block is written. The plan will show what happens.
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.
terraform apply -auto-approve
After the apply:
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.
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.
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.
After the apply the import block has played its part. You can remove it:
# 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.
Ten legacy buckets with the same structure, one block:
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-outWriting a big resource by hand is painful. The command:
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:
Not a finished artifact, but it saves time.
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 importterraform planwith an import block: shows 'will be imported'terraform plan -generate-config-out=generated.tfgenerate a draft resource blockконцепции