TrademarkTrademark
Features
Documentation
All articles
Comprehensive Guide

Terraform Import: How to Import Existing Resources Into Terraform

Use the terraform import command and import blocks to bring existing AWS, Azure, and GCP resources into Terraform state, with examples, FAQs, and bulk-import patterns.
Sebastian StadilMarch 31, 2026Updated May 14, 2026
Terraform Import: How to Import Existing Resources Into Terraform

Terraform import is the process of bringing existing cloud resources (created manually, by another tool, or before you adopted Terraform) under Terraform management by recording them in your state file. There are two ways to do it: the legacy terraform import command and the modern import {} block introduced in Terraform 1.5.

This guide covers both: when to use each, complete examples for AWS, Azure, and GCP, how to generate Terraform code from existing resources, how to import multiple resources at once, and the common errors that trip people up. Whether you're importing a single S3 bucket or migrating an entire estate of brownfield infrastructure, you'll find a working pattern below.

TL;DR

  • Use import blocks (Terraform 1.5+) instead of terraform import CLI. Import blocks are declarative, plan-previewable, and CI/CD-friendly. Pair them with terraform plan -generate-config-out=generated.tf to generate a draft resource block. The legacy CLI command writes to state directly, without a plan preview, and generates nothing.
  • Always run terraform plan first to see the imported state diff before applying. Mismatches between your HCL and the real resource cause drift on the first apply.
  • Import is state-only. Import updates state but not configuration; you still need to write (or generate) the matching HCL.
  • Bulk imports: for Azure, use aztfexport; for cross-cloud bulk migrations, scripted import-block generation beats manual CLI calls every time.
  • After importing, use moved blocks to refactor resource addresses without destroy/recreate.

What Does Terraform Import Do?

Terraform import takes an existing resource (an EC2 instance, an S3 bucket, an Azure VM, a GCS bucket, or any resource type your provider supports importing) and records it in your Terraform state file. After import, Terraform manages that resource the same as if it had been created by terraform apply from the start. You can then update it, plan changes against it, and destroy it through code.

Two important caveats up front:

  1. Import is state-only. It writes the resource into state but does not generate the corresponding HCL configuration (with one exception: the -generate-config-out flag, covered below). You still have to write the matching resource block by hand, or use code generation.
  2. Import does not modify the cloud resource. It only reads it and adds an entry to state. Nothing about the live resource changes during an import.

When You'll Need Terraform Import

  • Adopting IaC on existing infra: brownfield environments where Terraform shows up after the AWS console has been doing the work for a year
  • Migrating off another tool: CloudFormation, Pulumi, ARM templates, or hand-rolled scripts
  • Console-created hotfixes: a resource someone spun up at 3am during an incident, now needs to live in code
  • State recovery: rebuilding state after a corrupt or lost state file
  • Splitting state: breaking up a monolithic state file across teams or environments

Terraform Import Command vs. Import Block

There are two ways to import existing resources into Terraform:

  1. The terraform import CLI command: available since Terraform 0.7. Imperative, one-off, writes to state directly without a plan preview.
  2. The Terraform import {} block: introduced in v1.5.0. Declarative, plan-previewable, works cleanly in CI/CD, and can be paired with terraform plan -generate-config-out to generate draft HCL.

The short version: use the import block for any new work. The CLI command is still useful for ad-hoc, interactive fixes, and it's worth knowing because most existing tutorials still reference it.

The Terraform Import Command

The terraform import command links a remote, pre-existing resource to a resource block in your Terraform configuration. It is the original Terraform import method and still works in current Terraform versions.

Terraform Import Syntax

terraform import aws_instance.example i-1234567890abcdef0

The syntax is terraform import <resource_address> <remote_id>. The resource address (e.g., aws_instance.example) must already exist in your configuration; the remote ID is the cloud provider's identifier for the live resource.

Common Import Flags

Here are the most useful flags for the terraform import command:

  • -config=path: Specifies the path to the directory containing your Terraform configuration files
  • -input=true/false: Determines whether Terraform should ask for interactive input (set to false for automation)
  • -lock=false: Disables state locking (generally not recommended in collaborative environments)
  • -lock-timeout=0s: Sets a duration to retry acquiring a state lock before failing
  • -no-color: Disables colorized output
  • -var-file=path: Loads variable values from a .tfvars file (useful when your provider config references variables)
  • -parallelism=n: Limits the number of concurrent operations (default is 10)
  • -var 'foo=bar': Sets a variable from the command line

Terraform Import Examples

These are the most common terraform import examples by provider. Each one links a live resource to a resource block already in your configuration. The pattern is always terraform import <RESOURCE_ADDRESS> <RESOURCE_ID>, but the ID format varies by provider.

Terraform Import AWS EC2 Instance

terraform import aws_instance.web_server i-abcd1234

AWS EC2 instance IDs start with i-. You can find them in the EC2 console or via aws ec2 describe-instances.

Terraform Import S3 Bucket

terraform import aws_s3_bucket.data_lake my-data-lake

Note: in AWS provider v4.0+, S3 bucket configuration was split into multiple resources (aws_s3_bucket, aws_s3_bucket_versioning, aws_s3_bucket_acl, etc.). You may need to import related S3 resources separately, depending on which parts of the bucket configuration you want Terraform to manage. Not every bucket needs every split-out resource.

Terraform Import IAM Role

terraform import aws_iam_role.lambda_execution my-lambda-execution-role

Use the role name (not the ARN) as the import ID. Attached policies and inline policies need separate imports. See streamlining AWS IAM role creation with Terraform for the full pattern.

Terraform Import RDS Instance

terraform import aws_db_instance.primary my-primary-db

Terraform Import Azure VM

Azure resource IDs are full ARM paths:

terraform import azurerm_virtual_machine.app_server /subscriptions/xxx-xxx-xxx-xxx-xxx/resourceGroups/myRG/providers/Microsoft.Compute/virtualMachines/myVM

For importing entire Azure resource groups, aztfexport (covered below) is far faster than running individual terraform import commands.

Terraform Import GCP Compute Instance

terraform import google_compute_instance.app_server projects/my-project/zones/us-central1-a/instances/app-server

GCP IDs typically include project and zone. The simpler shorthand my-project/us-central1-a/app-server also works for most resources.

Handling Computed and Default Attributes

When using terraform import, a common challenge arises with computed attributes or default settings. Computed attributes are values determined by the cloud provider after resource creation (like timestamps, default security group IDs, or ARN components).

Solution 1: Explicitly Ignoring Attributes

Use the ignore_changes lifecycle meta-argument to tell Terraform to disregard drift for specific attributes:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
 
  lifecycle {
    ignore_changes = [
      default_security_group_id,
      tags_all,
    ]
  }
}

Use ignore_changes sparingly. It hides drift, so it should be reserved for attributes you intentionally do not want Terraform to manage. Reaching for it on every imported resource turns it into a place where real configuration drift gets buried.

Solution 2: Including Default Values in HCL

For attributes where you can reliably determine the default value, explicitly include that value in your HCL:

resource "aws_db_instance" "example" {
  # ... other imported attributes
  backup_retention_period = 7  # Explicitly set the provider's default
}

The Terraform Import Block

Introduced in Terraform 1.5, the import {} block is a declarative alternative to the terraform import command, integrated directly into the configuration language. You write the import as code, run terraform plan to preview it, and terraform apply to commit. The same workflow as any other Terraform change.

Why Import Blocks Matter

Import blocks make import fit the normal Terraform workflow: write the import in code, plan it, apply it, review it in a PR like anything else. For more on the feature itself, see our Terraform import block deep-dive.

Feature/Aspect Import Block (Modern) terraform import CLI (Legacy)
Preview Yes, see plan before changes No, writes to state directly
HCL Generation Works with terraform plan -generate-config-out to write draft HCL No, write all HCL manually
Safety High, part of plan/apply workflow Low, easy to make mistakes
CI/CD Integration Works well in pipelines Risky and difficult
Version Control Import definitions reviewable in PRs No visibility into imports

Basic Import Block Syntax

import {
  to = aws_s3_bucket.legacy_bucket
  id = "my-legacy-data-bucket"
}

When you run terraform plan and terraform apply with an import block present, Terraform performs the import operation. You can also generate configuration automatically using:

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

Import Block with for_each

Version note: Basic import {} blocks require Terraform 1.5+. for_each inside import blocks was added later, so verify you're on Terraform 1.7+ (or a compatible OpenTofu version) before using this pattern.

For importing multiple resources of the same type:

locals {
  buckets = {
    "staging" = "staging-bucket"
    "uat"     = "uat-bucket"
    "prod"    = "production-bucket"
  }
}
 
import {
  for_each = local.buckets
  to       = aws_s3_bucket.app_data[each.key]
  id       = each.value
}
 
resource "aws_s3_bucket" "app_data" {
  for_each = local.buckets
  bucket   = each.value
}

Import Block into Modules

To import resources into modules:

import {
  to = module.servers.aws_instance.app_server
  id = "i-1234567890abcdef0"
}

Step-by-Step Import Block Workflow

There are two valid import-block workflows. Pick one before you start:

  1. You already have a matching resource block in your config. Add the import {} block, run terraform plan, refine the HCL until the plan is clean, then apply.
  2. You don't have the resource block yet. Write only the import {} block and run terraform plan -generate-config-out=generated.tf to have Terraform produce a draft resource block. Then clean up the generated HCL and apply.

The steps below show workflow #2 (generate-config-out). For workflow #1, skip Step 2 and edit your existing resource block in Step 3 instead.

Step 1: Prepare Import Block

Create an import block in your configuration specifying the resource to import:

import {
  to = aws_s3_bucket.legacy_bucket
  id = "my-legacy-data-bucket"
}

Step 2: Generate Configuration

Run plan with configuration generation enabled:

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

Terraform will create a generated.tf file with configuration based on the live resource.

Step 3: Review and Refine Configuration

Do not blindly trust generated configuration. Review carefully:

  1. Clean it up: Remove computed attributes (like arn, hosted_zone_id) and default values
  2. Correct it: Fix any invalid syntax or errors
  3. Make it yours: Refactor to fit your team's standards and use variables

Example cleanup:

# BEFORE - Generated
resource "aws_s3_bucket" "legacy_bucket" {
  bucket              = "my-legacy-data-bucket"
  bucket_domain_name  = "my-legacy-data-bucket.s3.amazonaws.com"  # Remove
  hosted_zone_id      = "Z3AQBSTGFYJSTF"  # Remove
  region              = "us-east-1"  # Remove
}
 
# AFTER - Cleaned
resource "aws_s3_bucket" "legacy_bucket" {
  bucket = "my-legacy-data-bucket"
 
  tags = {
    Name        = "Legacy Data Bucket"
    Environment = "production"
  }
}

Step 4: Run Validation Plan

terraform plan

Ideally, the output should show 1 to import, 0 to change, 0 to destroy. If Terraform wants changes, adjust your HCL until the plan matches your intent. Generated config almost always needs some cleanup before the plan is clean.

Step 5: Apply the Import

terraform apply

Once approved, the resource is recorded in state. (You can still remove it later with terraform state rm if needed; nothing about the live resource changes.)

Step 6: Verify and Clean Up

Verify the import succeeded:

terraform state show aws_s3_bucket.legacy_bucket

You can now remove the import block from your configuration. It's a one-time operation.

Common Mistakes to Avoid with Import Blocks

  1. Blindly trusting generated code - Always review and clean up generated configuration
  2. Forgetting the destination resource - The destination resource block must either already exist in your configuration or be generated with terraform plan -generate-config-out
  3. Leaving import blocks in configuration - Remove them after successful import
  4. Wrong resource addressing - Missing array indices for count or keys for for_each
  5. Using dynamic values - All import block values must be known at plan time; no data sources or computed values

Generate Terraform Code From Existing Resources

A common ask: "I have a hundred resources running in AWS already. Can Terraform read them and generate the HCL automatically?" The answer is yes, with caveats. None of these tools produces a final artifact. There are three common ways to generate Terraform code from existing resources in 2026:

1. terraform plan -generate-config-out (Built-in, Terraform 1.5+)

Write an import {} block referencing the resource, then run:

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

Terraform reads the live resource, generates a resource block matching its current attributes, and writes it to generated.tf. This is the built-in Terraform approach. It works with providers and resource types that support Terraform import, but generated configuration quality varies. Some cloud services are represented by multiple Terraform resources, so full management may require separate imports, and HashiCorp's docs are explicit that each remote object should be imported to only one resource address.

Limitations: generated HCL is often verbose. It can include provider defaults, hardcoded values, and attributes your team would normally turn into variables or references. Treat the output as a starting point, not a final artifact.

2. aztfexport (Azure-only)

For Azure, Microsoft's aztfexport tool can scan an entire resource group and generate HCL + a state file in one shot. Much faster than per-resource import calls for Azure migrations.

3. Terraformer (multi-cloud, third-party)

Terraformer, originally developed under the GoogleCloudPlatform GitHub organization, is a third-party CLI that walks AWS, GCP, Azure, Kubernetes, and ~20 other providers, generating HCL and state. It's the most automated option for full-estate bulk discovery but the generated code typically needs more cleanup than -generate-config-out, and the project is community-maintained. Verify it still supports your provider versions before relying on it.

# Example: generate Terraform for all EC2 instances in us-west-2
terraformer import aws --resources=ec2_instance --regions=us-west-2

Comparison

Tool Scope Code Quality Best For
terraform plan -generate-config-out Single resource per import block Faithful but verbose Per-resource imports in regular workflow
aztfexport Azure resource group / query Good, needs refactoring Bulk Azure migration
Terraformer Multi-cloud, broad discovery Rough, needs heavy cleanup Initial brownfield audit

Whichever method you use, always review the generated code before committing. Code generation can include provider defaults, hardcoded values that should be variables or references, and produce a main.tf no team would write by hand.

Terraform Import Azure Resources With aztfexport

For Azure environments, Microsoft provides aztfexport (formerly aztfy), a command-line tool that scans existing Azure resources, generates the matching Terraform HCL code, and creates a state file in one pass. It's the fastest way to import existing Azure resources into Terraform at scale. For a hands-on walkthrough, see our guide to getting started with the Azure Terraform Export tool.

What is Aztfexport

Azure Export for Terraform (aztfexport) is an open-source tool from Microsoft that:

  • Scans existing Azure resources
  • Generates corresponding Terraform HCL code
  • Creates a state file mapping to live infrastructure
  • Supports both azurerm and azapi Terraform providers

Under the hood, aztfexport maps Azure resource IDs to Terraform resource types, calls Terraform import on each, and writes the generated HCL and a mapping file. For normal use you don't need to think about that.

Generating Import Blocks (Modern Workflow)

Recent versions of aztfexport (v0.13+) paired with Terraform v1.5+ can generate import {} blocks instead of running imports directly. This lets you review the imports in a PR and apply them through your normal plan/apply pipeline:

aztfexport resource-group --generate-import-block myRG

This produces an import.tf containing import {} blocks and a mapping file. Recommended for any team using Terraform 1.5 or later, since it makes the import auditable in CI/CD.

Installation

Check Microsoft's current installation docs before copying package-manager commands; the examples below show the common paths but Microsoft's repo URLs and supported distros change.

Linux (apt - Debian/Ubuntu):

curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc > /dev/null
sudo apt-add-repository https://packages.microsoft.com/ubuntu/20.04/prod
sudo apt-get update
sudo apt-get install aztfexport

Linux/macOS (Homebrew):

brew install aztfexport

Windows (winget):

winget install aztfexport

Basic Commands

Export a Resource Group:

aztfexport resource-group myRG

Use the -n flag for non-interactive mode with large resource groups.

Export using Azure Resource Graph Query:

aztfexport query "resourceGroup =~ 'myRG' and type =~ 'microsoft.network/virtualnetworks'"

Export a Single Resource:

aztfexport resource /subscriptions/your-sub-id/resourceGroups/myRG/providers/Microsoft.Compute/virtualMachines/myVM

The tool generates .tf files, a terraform.tfstate file, and a JSON mapping file (aztfexportResourceMapping.json).

Primary Use Cases

  • IaC Adoption Acceleration: Automates the initial import of existing resources
  • Migration Support: Provides a snapshot of legacy resources for reference
  • Configuration Auditing: Generated HCL documents point-in-time resource configurations
  • Learning Aid: Practical way to see how Azure resources are defined in HCL

Post-Export Refactoring Requirements

It's important to note that Microsoft states the generated code is not intended to be fully reproducible from scratch. The generated output requires mandatory manual review and refactoring:

  • Rename resources to align with standards
  • Replace hardcoded IDs with dynamic references
  • Introduce variables for parameterization
  • Organize code into logical modules
  • Secure sensitive data that may have been exported
  • Ensure compliance with security and coding standards

Comparison: Aztfexport vs Terraform Import

Feature aztfexport terraform import (CLI) terraform import (Block)
HCL Code Generation Automated Manual Automated
State Import Automated Automated Automated
Resource Discovery Supported (RG, Query, Interactive) Manual Manual identification
Bulk Operations High Low Medium
Manual Effort Medium (refinement) Very High Medium

Handling Import Conflicts

Resource Already Exists in State

If a resource already exists in your state file with a different address:

# Remove from old address
terraform state rm aws_instance.old_name
 
# Then import to new address
terraform import aws_instance.new_name i-1234567890abcdef0

Resolving Import Errors

Error: Cannot import non-existent remote object

  • Verify resource ID format matches provider documentation
  • Check permissions to access the resource
  • Confirm the resource actually exists in the cloud provider

Error: Resource address does not exist in the configuration

  • Create the resource block before importing
  • Ensure the resource block address matches the import statement exactly

Error: Error acquiring the state lock

  • Check for other running Terraform processes
  • Verify state locking mechanism is working correctly
  • Review remote backend configuration

Error: Invalid provider configuration

  • Ensure provider configuration only depends on variables, not data sources
  • Check authentication credentials and permissions
  • Verify provider version compatibility

Handling Dependency Errors

When importing resources with dependencies:

  1. Import dependencies first (VPC, subnets, IAM roles)
  2. If you must work in subsets, use -target only as a temporary recovery tool, and only when you understand which dependencies you're skipping. -target is widely overused; reach for it last, not first.
  3. For circular dependencies, temporarily remove references and reestablish after import

Terraform Import State: How It Works

terraform import and the import {} block both do the same thing under the hood: they write the resource into your Terraform state file. The state file is what Terraform consults on every plan and apply to know what it manages. If a resource isn't in state, Terraform thinks it doesn't exist, even if it's running in your cloud account.

How State Works with Import

Terraform state is central to how import works. For a deeper look at state file structure, see Terraform state file best practices; for setting up the remote storage that holds it, see our guide to remote backends.

  • State-only operation: Import updates the state file but not the configuration
  • One-to-one mapping: Each remote resource should be imported to only one resource address
  • Complex imports: Some resources result in multiple related resources in state
  • State locking: Imports respect state locking to prevent concurrent modifications
  • State backends: Import works with all backends (local, S3, Azure, GCS, etc.)

Remote Backend Considerations

Configure the same backend you use for normal Terraform runs before importing. Imports write to whichever backend is active after terraform init; there's no separate import-only path. For remote backends like Terraform Cloud, Scalr, or S3 with DynamoDB locking, the import inherits the backend's normal locking, RBAC, and audit controls.

State Manipulation After Import

After importing, you might need to reorganize your state:

  • Splitting state: Move resources between state files using terraform state mv with -state and -state-out
  • Renaming resources: Use terraform state mv to rename without destroying
  • Removing from state: Use terraform state rm to remove without destroying
terraform state mv aws_instance.old_name aws_instance.new_name
terraform state mv aws_instance.standalone module.servers.aws_instance.server

How to Import Multiple Resources at Once (Terraform Bulk Import)

When you're migrating dozens or hundreds of existing resources into Terraform, one-at-a-time CLI imports get painful fast. There are three workable patterns for terraform bulk imports.

Bulk Import Strategies

Using Third-Party Tools

Terraformer is a multi-cloud tool for bulk imports:

# Install Terraformer
go install github.com/GoogleCloudPlatform/terraformer@latest
 
# Import all EC2 instances in a region
terraformer import aws --resources=ec2_instance --regions=us-west-2

Using for_each with Import Blocks

For bulk imports in modern Terraform:

locals {
  instances = {
    "web"    = "i-12345678"
    "app"    = "i-23456789"
    "db"     = "i-34567890"
  }
}
 
import {
  for_each = local.instances
  to       = aws_instance.servers[each.key]
  id       = each.value
}
 
resource "aws_instance" "servers" {
  for_each = local.instances
  # configuration...
}

Scripted Bulk Imports

For large-scale imports using the CLI:

#!/bin/bash
for instance_id in i-12345678 i-23456789 i-34567890; do
  terraform import aws_instance.server_${instance_id} ${instance_id}
done

Resource Organization at Scale

  • Logical separation: Group resources by service, application, or team
  • Import order: Import foundation resources first (networking, IAM) before dependent resources
  • State segmentation: Consider splitting resources across multiple state files
  • Batch imports: For large infrastructures, import in stages across multiple planned sessions

Refactoring with Moved Blocks

Terraform's moved block feature (v1.1+) allows you to safely rename or relocate resources without destroying and recreating them. This is particularly useful when organizing imported resources.

Basic Moved Block Syntax

moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}

Practical Refactoring Examples

Renaming a Resource

resource "aws_security_group" "api_security_group" {
  name = "api-security-group"
}
 
moved {
  from = aws_security_group.sg
  to   = aws_security_group.api_security_group
}

Moving a Resource into a Module

# After creating module/storage/main.tf with the resource
module "storage" {
  source      = "./modules/storage"
  bucket_name = "application-logs"
}
 
moved {
  from = aws_s3_bucket.logs
  to   = module.storage.aws_s3_bucket.logs
}

Converting from count to for_each

locals {
  servers = {
    "web" = {}
    "api" = {}
  }
}
 
resource "aws_instance" "server" {
  for_each = local.servers
}
 
moved {
  from = aws_instance.server[0]
  to   = aws_instance.server["web"]
}
 
moved {
  from = aws_instance.server[1]
  to   = aws_instance.server["api"]
}

Moved Blocks Best Practices

  • Document and retain: Keep moved blocks in code indefinitely with explanatory comments
  • Use chained moves: Document the full history when resources move multiple times
  • Plan before applying: Always run terraform plan to verify the moves are correct
  • Refactor incrementally: Move resources in smaller batches rather than all at once
  • Consider module shims: Create shim modules for backward compatibility when breaking modules

For comprehensive details on moved blocks and advanced refactoring scenarios, see the dedicated article: Terraform Moved Blocks: Refactoring Without Pain

Common Pitfalls and Troubleshooting

Frequent Mistakes

  1. Incorrect resource ID format - Always verify the exact format required by your provider
  2. Missing resource block - Create the resource block before importing
  3. Trusting generated code blindly - Always review and clean generated configuration
  4. Mixing old and new import methods - Choose one approach for consistency
  5. Not handling computed attributes - Use ignore_changes or explicit defaults
  6. Forgetting dependencies - Import in the correct order
  7. Concurrent state operations - Ensure only one Terraform operation at a time

Troubleshooting Techniques

Enable debug logging:

export TF_LOG=TRACE
export TF_LOG_PATH=terraform.log

Inspect state:

terraform state list
terraform state show <resource_address>

Validate configuration:

terraform validate
terraform fmt

When Not to Use Import

Consider alternatives when:

  • Resources are easily recreatable: If the resource is simple to recreate from scratch
  • Core infrastructure with high risk: Importing networking or IAM can be risky; consider creating parallel resources
  • Uncertain existing configuration: If you don't fully understand the resource configuration, importing can lead to unexpected changes

Provider-Specific Considerations

Each provider has unique import requirements:

  • AWS Provider 4.0+: S3 buckets split into multiple resources; must import separately
  • Azure: Resources require full ARM IDs with proper escaping in PowerShell
  • GCP: Resources may need project ID in import identifier even if set in provider
  • Kubernetes: Import support depends on the specific Kubernetes provider resource. Check the provider docs for the exact import ID format and whether the resource supports import at all

Running Imports in CI/CD

Import works best as a controlled migration workflow: inventory the resources, write or generate the HCL, review the plan, apply the import, then refactor naming and module structure over time. For CI/CD specifically, prefer import {} blocks over the CLI command. They can be reviewed in a pull request and executed through the same plan/apply pipeline as any other Terraform change, with the same approvals and audit trail. The CLI terraform import command bypasses your pipeline entirely and modifies state out of band, which is usually what you don't want.

Security Considerations for Imported Resources

  • Credential exposure: never commit sensitive values from generated files; use variable files or a secrets manager
  • Sensitive values in state: assume imported state may contain secrets (passwords, connection strings, generated keys). sensitive = true only suppresses values in plan/apply output; it does not redact them from the state file. Restrict state access, encrypt remote state, and avoid committing generated files that expose credentials
  • Access control: limit who can run imports. The operation modifies state and should be gated like any other state-mutating action
  • Audit logging: log import operations through your CI/CD or remote backend so you can answer "who imported what, when"

Terraform Import FAQ

What is Terraform import?

Terraform import is the process of bringing an existing cloud resource (one that was created outside of Terraform) under Terraform management by recording it in your state file. After import, Terraform manages the resource the same way it would any resource it created itself.

What does Terraform import do?

It reads an existing remote resource and writes an entry for it into your Terraform state file. The cloud resource itself is not modified. Terraform import does not generate the corresponding HCL configuration on its own (the legacy CLI command never has); you write that by hand or use terraform plan -generate-config-out with an import block to generate a starting point.

How do I use the Terraform import command?

The basic syntax is terraform import <RESOURCE_ADDRESS> <RESOURCE_ID>. For example, to import an EC2 instance: terraform import aws_instance.web i-1234567890abcdef0. The resource block (aws_instance.web in this example) must already exist in your configuration. After running the command, run terraform plan to see whether your HCL matches the live resource. If not, edit your HCL until plan is clean.

What's the difference between the terraform import command and the import block?

The terraform import CLI command is imperative: you run it and it writes to state directly, without a plan preview. The import {} block is declarative: you add it to your .tf files, run terraform plan to preview what will be imported, then terraform apply to commit. The import block is plan-previewable, CI/CD-friendly, and can be paired with terraform plan -generate-config-out to generate draft HCL. It's the recommended approach in Terraform 1.5 and later.

Can Terraform import multiple resources at once?

Yes. Three options: (1) use import {} blocks with for_each (Terraform 1.7+) to import many resources of the same type in one apply; (2) use aztfexport for bulk Azure imports; (3) use third-party tools like Terraformer for multi-cloud bulk discovery. The for_each-with-import-block pattern is the cleanest for resources you already know about, while Terraformer is best for discovering resources you don't.

Does Terraform import generate code?

Not by itself. The legacy terraform import command only writes state. The modern import {} block can be paired with terraform plan -generate-config-out=generated.tf to generate draft HCL. Treat generated code as a starting point. It's faithful to the live resource but doesn't match your conventions, may include computed attributes you don't want managed, and rarely needs zero cleanup.

How do I import an AWS resource into Terraform?

Write a resource block for the AWS resource you want to manage (e.g., aws_instance, aws_s3_bucket), then either run terraform import <address> <aws_id> or add an import {} block referencing the same address. Run terraform plan to verify the import works and your HCL matches. For AWS S3 specifically, provider v4.0+ split many bucket settings into separate resources (aws_s3_bucket_versioning, aws_s3_bucket_acl, etc.), so you may need to import related resources separately depending on what you want Terraform to manage.

What happens if I import a resource that's already in state?

Terraform will refuse and return an error: "Resource already managed by Terraform." If you need to move a resource to a new address, use terraform state mv instead of importing. If you need to re-import a resource (rare), first remove it from state with terraform state rm, then import to the new address.

Can I remove the import block after importing?

Yes. Once terraform apply completes successfully and the resource is in state, the import {} block is no longer doing anything. You can remove it from your configuration. The resource block stays.

Does Terraform import work with remote backends like Terraform Cloud or Scalr?

Yes. Import works with every Terraform backend: local, S3, GCS, Azure Blob, Terraform Cloud, Scalr, etc. For remote backends, import operations are executed against the remote state with the backend's standard locking and authorization rules. If you're running imports through a CI/CD pipeline, the import {} block approach is preferable since it integrates cleanly with the plan/apply workflow your remote backend already runs.

Wrapping Up

For one-off imports, the terraform import CLI command is fine. For anything you'd want to review, automate, or repeat, use an import {} block. Generate a draft of the HCL with -generate-config-out (or aztfexport --generate-import-block on Azure), clean it up by hand, get a clean plan, and apply.

The piece most people skip and regret: after the import succeeds, refactor. Rename resources to your team's conventions, move them into modules, and use moved blocks so you don't destroy and recreate anything along the way.

About the author
Sebastian StadilCEO at Scalr
Sebastian Stadil is the CEO at Scalr. He has over 15 years of devops experience, and started his career with AWS in 2004. Sebastian was also an early advisor to Microsoft Azure and Google Cloud.