In the lifecycle of a cloud environment, it is common for infrastructure to exist before the adoption of Infrastructure as Code (IaC). Whether a DevOps engineer created a critical server via the AWS Management Console during an emergency, or an organization is migrating from manual provisioning to a structured Terraform workflow, there exists a fundamental gap: the "State Gap." When a resource is created manually, Terraform has no knowledge of its existence, and therefore, it is not tracked in the terraform.tfstate file.
Importing existing resources is the primary mechanism for bridging this gap. Without importing, any attempt to manage the broader environment using Terraform could result in the accidental deletion of these "unmanaged" resources, as Terraform seeks to make the real-world infrastructure match the declared configuration. Adopting Terraform in phases—by importing a few resources at a time—allows teams to start small and migrate their infrastructure systematically without causing downtime or data loss.
The Fundamental Mechanics of Terraform Import
Terraform operates on a state-based logic. The state file acts as a database that maps your configuration code (the .tf files) to the actual resources deployed in the cloud provider. When you create a resource using terraform apply, Terraform creates the resource and simultaneously records its unique ID and attributes in the state file.
When a resource is created manually via the AWS Console, it exists in the cloud, but it does not exist in the state file. The terraform import process is designed to pull the current settings of a cloud resource and write them into the state file. However, a critical distinction must be understood: the legacy terraform import command only updates the state file; it does not automatically write the corresponding HCL (HashiCorp Configuration Language) code into your .tf files. If you import a resource into the state but fail to write the configuration block for it, the next time you run terraform plan, Terraform will see a resource in the state that is missing from the code and will propose to delete that resource.
Pre-requisites for Successful Import
Before beginning the import process, several technical prerequisites must be met to ensure the environment is stable and authorized.
- Terraform Installation: The Terraform CLI must be installed on the local machine.
- AWS Account Access: Valid access to an AWS account with permissions to describe and manage EC2 instances.
- Credential Configuration: AWS credentials must be configured locally. This is typically done via the
~/.aws/credentialsfile or by running theaws configurecommand through the AWS CLI. - Backend Configuration: For production environments, a remote backend (such as S3 with DynamoDB for state locking) is required to prevent state corruption and enable collaboration.
Strategic Approach: Legacy Command vs. Modern Import Block
As of Terraform version 1.5.0, there are two distinct ways to bring existing infrastructure under management.
The Legacy terraform import Command
The traditional method involves a CLI-driven approach. You first create a "shell" or an empty resource block in your configuration, and then you run a command that maps the cloud ID to that specific block.
The Modern import Block
The newer approach introduces the import block directly into the HCL code. This allows the import process to be version-controlled and executed as part of a standard terraform apply workflow, reducing the reliance on manual CLI commands.
| Feature | terraform import (Command) |
import Block (Terraform 1.5+) |
|---|---|---|
| Execution | CLI Command | HCL Configuration |
| State Update | Immediate upon command success | Occurs during terraform apply |
| Code Generation | Manual (User must write HCL) | Integrated into workflow |
| Version Control | Not tracked until HCL is written | Tracked as part of the codebase |
| Primary Use Case | Quick, one-off imports | Structured, collaborative migrations |
Step-by-Step Implementation: Importing an EC2 Instance
The following sections detail the technical execution of importing an EC2 instance, covering both the backend setup and the configuration phase.
Step 1: Establishing the Infrastructure Directory and Backend
To maintain a clean workspace, create a dedicated directory (e.g., import-tutorial/). In a professional DevOps pipeline, you must configure a backend to store the state file securely. A common pattern is using an S3 bucket for state storage and a DynamoDB table for state locking to prevent concurrent modifications.
Example backend.tf configuration:
hcl
terraform {
backend "s3" {
bucket = "tf-backend-lab-123"
key = "import/ec2/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-state-lock"
encrypt = true
}
}
Step 2: Provider Configuration and Initialization
The main.tf file must define the required providers and the AWS region where the EC2 instance resides.
Example Provider Block:
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "eu-central-1"
}
```
Once the configuration is saved, run the initialization command:
bash
terraform init -reconfigure
This command initializes the backend, downloads the necessary provider plugins (e.g., hashicorp/aws), and creates the .terraform.lock.hcl file to ensure provider version consistency across the team.
Step 3: Preparing the Resource Shell
Before the import can take place, Terraform needs a destination in the code. You must create a resource block that matches the type of resource you are importing. If you are using the legacy command, this can be a minimal "empty shell" block.
Example main.tf resource shell:
hcl
resource "aws_instance" "imported" {
# No arguments needed yet for the legacy import command
}
Alternatively, if you have the details of the instance, you can write the full configuration immediately to avoid drift later.
Step 4: Executing the Import
Identify the Instance ID of the EC2 instance from the AWS Console (e.g., i-0b9be609418aa0609). Run the import command by mapping the resource address in your code to the cloud ID.
Command Syntax:
terraform import <resource_address> <cloud_id>
Execution:
bash
terraform import aws_instance.imported i-0f7231d1dbe6f446b
Expected Output:
text
aws_instance.imported: Importing from ID "i-0f7231d1dbe6f446b"...
aws_instance.imported: Import prepared!
Prepared aws_instance for import
aws_instance.imported: Refreshing state.. [id=i-0f7231d1dbe6f446b]
Import successful!
Step 5: Resolving Drift and Finalizing Configuration
After a successful import, the resource exists in the state file, but your .tf code is likely empty or incomplete. If you run terraform plan now, Terraform will detect a massive discrepancy between the state (the actual VM) and the code (the empty block). Terraform will interpret this as a request to destroy the resource and recreate it with no attributes.
To fix this "drift," you must update your HCL code to match the attributes found in the state file. You can inspect the terraform.tfstate file to see the exact attributes Terraform fetched from AWS.
Full corrected configuration example:
```hcl
resource "awsinstance" "appserver" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.large"
tags = {
Name = "app-server-production"
}
lifecycle {
ignore_changes = [ami] # Prevents recreation if AMI is updated manually
}
}
```
Once the code is updated, run terraform plan again. The goal is to reach a state where Terraform reports "No changes. Your infrastructure matches the configuration."
Expanding Imports: S3 and Networking
While EC2 instances are common, the same logic applies to other AWS resources. However, complex resources often require multiple import steps because they are split into several Terraform resources.
S3 Bucket Import
For S3 buckets, you may need to import both the bucket itself and its versioning configuration separately.
HCL Configuration:
```hcl
resource "awss3bucket" "data" {
bucket = "my-company-data-bucket"
}
resource "awss3bucketversioning" "data" {
bucket = awss3bucket.data.id
versioningconfiguration {
status = "Enabled"
}
}
```
Import Blocks (Terraform 1.5+):
```hcl
import {
to = awss3bucket.data
id = "my-company-data-bucket"
}
import {
to = awss3bucket_versioning.data
id = "my-company-data-bucket"
}
```
Networking (VPC and Subnets)
Importing network infrastructure follows the same pattern. You must import the VPC first, as other resources (like subnets and security groups) depend on the VPC ID.
Example VPC Configuration:
```hcl
resource "awsvpc" "main" {
cidrblock = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "awssubnet" "public" {
vpcid = awsvpc.main.id
cidrblock = "10.0.1.0/24"
tags = {
Name = "public-subnet-1"
}
}
```
Summary of Import Workflow Parameters
The following table summarizes the key identifiers required for importing various AWS resources.
| Resource Type | Terraform Resource Name | Import ID Required |
|---|---|---|
| EC2 Instance | aws_instance |
Instance ID (e.g., i-xxxxxx) |
| S3 Bucket | aws_s3_bucket |
Bucket Name |
| VPC | aws_vpc |
VPC ID (e.g., vpc-xxxxxx) |
| Security Group | aws_security_group |
Security Group ID |
| Subnet | aws_subnet |
Subnet ID (e.g., subnet-xxxxxx) |
Conclusion
Importing existing infrastructure into Terraform is a critical skill for any DevOps engineer managing a transitioning cloud environment. The process transforms "shadow IT"—resources created manually without documentation—into managed, versioned, and reproducible infrastructure.
The transition from the legacy terraform import CLI command to the modern import block reflects a shift toward treating the import process as part of the codebase itself. Regardless of the method used, the most dangerous phase of the process is the window between the successful import and the completion of the HCL configuration. In this window, the state knows the resource exists, but the code does not fully describe it. Running terraform apply without accurately matching the configuration to the state will almost certainly result in the accidental deletion or modification of production resources.
By following a systematic approach—establishing a remote backend, creating minimal resource shells, importing the cloud IDs, and iteratively refining the HCL code via terraform plan—engineers can safely bring their entire AWS ecosystem under the governance of Terraform. This not only ensures disaster recovery capabilities but also allows for the implementation of CI/CD pipelines and automated auditing of the infrastructure.
Sources
- spacelift.io/blog/importing-exisiting-infrastructure-into-terraform
- tekanaid.com/posts/terraform-import-example-aws-ec2-instance
- oneuptime.com/blog/post/2026-02-23-how-to-import-existing-aws-resources-into-terraform/view
- dev.to/latchudevops/task-4-terraform-import-importing-an-existing-ec2-instance-into-terraform-57h8