How to Use terraform import: Bringing Existing Resources into Code with import Blocks
You have an S3 bucket that was created in a hurry through the console, and now you want to manage it with Terraform. Can you just write a resource block and apply? No. Terraform treats anything not in its state as unknown to it (Terraform Basics #5), so it will try to create another bucket with the same name and fail on the name collision. What you need is a procedure that tells Terraform “that resource which already exists is this code” — and that is import. This post is the hands-on procedure based on the current standard, the import block (Terraform 1.5 and later).
The procedure at a glance #
- Write an import block (target address + import ID)
- Generate a code draft with
terraform plan -generate-config-out=generated.tf - Clean up the draft into a final resource block
- Adjust the code to match reality until plan shows “1 to import, 0 to change”
- Apply, then delete the import block
Step 1: the import block and the ID #
import {
to = aws_s3_bucket.legacy
id = "my-console-made-bucket"
}to is the Terraform address this resource will have; id is the identifier that pins down the real resource. The place people get stuck most often is the format of the id, because it differs per resource type. For an S3 bucket it is the bucket name, for an EC2 instance it is the instance ID (i-…), and some are composite formats like security group rules (sg-.../ingress/...). Do not guess — the right answer is the Import section at the bottom of that resource’s page in the provider documentation. Every resource page documents its own import ID format with an example.
Step 2: generating the code draft #
Before writing the resource block by hand, make Terraform draft it for you.
terraform plan -generate-config-out=generated.tfWith only the import block present and no corresponding resource block, this flag makes Terraform read the real resource and write a draft resource block into generated.tf. The manual labor of copying attribute values from the console by eye disappears.
Step 3: cleaning up the draft #
There are two reasons not to use the generated code as-is. First, it is verbose code with every default value spelled out, which makes it hard to read. Second, it can contain null or empty-value arguments that trip validate if left in place. Keep only the meaningful arguments, tidy it up, and move it into your real code files. This is also the step where you align it with your team’s existing code style (variables, common tags).
Step 4: until “no changes” #
Run plan with the cleaned-up code. The goal is unambiguous.
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.If plan reports changes or a replace instead of 0 to change, it means your code and the real resource disagree. Applying as-is would modify the real resource to match the code the moment it is imported, so unless that change is intentional, read the plan diff and adjust the code to the real values. In particular, applying while a -/+ (replace) is showing is not an import but a re-creation — you must stop. This step is the real substance of the import work, and the moment the mismatch reaches zero, the resource is ready to be adopted.
Step 5: apply and cleanup #
After apply, the resource is registered in state, and from then on it behaves like any other Terraform resource. Delete the import block once it has served its purpose (leaving it is harmless, but the history lives in Git, so keep the code clean).
Things worth knowing #
- Difference from the old approach: the
terraform import <address> <ID>CLI command still works, but it modifies state immediately, which makes review impossible, and it offers no draft generation. The import block lets you preview the result in plan and review it in a PR, which makes it the default for team work. - Importing multiple resources: place several import blocks side by side and they are handled in a single plan and apply. Resources that reference each other (a bucket and its bucket policy, for example) should be imported together — it makes reaching “no changes” much easier.
- Expressions in id: the id can take variables and expressions, so a setup that receives per-environment IDs through variables is possible.
- An audit after adoption: if you imported a lot of resources, it is worth running a security and cost scanner once. Console-era settings often fall short of team standards. Drift detection and the operational context are covered in Terraform Operations #2.
Summary #
- You cannot bring in existing resources with apply. You need a procedure that declares “this resource is this code,” and that is the import block
- The import ID format varies per resource. The Import section of the provider docs is the answer key
- Do not write resource blocks by hand — clean up a
-generate-config-outdraft instead - The completion criterion is “1 to import, 0 to change.” Applying with changes remaining modifies the real resource the moment it is imported
- The import block beats the CLI command because it is reviewable, making it the team default. Delete the block once the import is done