Terraform Import Without Downtime: A Production Migration Guide
Almost every high-growth startup begins with "ClickOps." In the early days, speed is everything: engineers spin up AWS VPCs, RDS Postgres databases, and Application Load Balancers directly in the AWS Console. But as your team scales to 20+ developers, that manual infrastructure turns into an un-auditable liability.
When engineering leadership finally mandates moving to Infrastructure as Code (Terraform), panic usually sets in. How do you bring live production databases, DNS records, and VPC networks into Terraform state without tearing down active resources or causing user-facing downtime?
In this guide, we share the exact zero-downtime migration methodology we use at Velocity Stream during our Cloud Architecture Assessments and platform engagements.
The Golden Rule of IaC Migrations
Never execute terraform apply on a production migration until terraform plan shows:
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
If Terraform plans to change or replace (-/+) a single attribute on a production resource—such as an RDS instance, security group rule, or ALB listener—you do not apply. You adjust your HCL configuration until the planned state is a bit-for-bit match with the live cloud reality.
Legacy CLI vs. Modern Declarative Import Blocks
Historically, importing resources required running the imperative CLI command:
# Legacy approach (Terraform < 1.5) terraform import aws_db_instance.main db-xyz123abc
This old approach had three massive flaws:
- It immediately modified the remote state file without code review.
- You had to manually reverse-engineer and write the HCL resource block by guessing every attribute.
- Team members running CI/CD would see drift or collisions during concurrent merges.
Since Terraform 1.5+, HashiCorp introduced native declarative import blocks with automated code generation. This turns infrastructure migration into a version-controlled, fully reviewable Git pull request.
Step-by-Step Production Migration Blueprint
Step 1: Resource Scoping and ID Gathering
Never try to import an entire AWS account in one run. Break down your migration into isolated domains:
- Networking: VPC, Subnets, Internet Gateways, Route Tables.
- Security: Security Groups, IAM Roles, KMS Keys.
- Data Layer: RDS clusters, ElastiCache, S3 Buckets.
- Compute & Ingress: ALBs, Target Groups, ECS services, EKS node groups.
Step 2: Write the Declarative Import Block
Create an imports.tf file in your environment repository:
# imports.tf
import {
to = aws_db_instance.production_postgres
id = "prod-aurora-cluster-node-1"
}
import {
to = aws_security_group.db_ingress
id = "sg-01a2b3c4d5e6f7g8h"
}Step 3: Generate the HCL Configuration Automatically
Run Terraform with the -generate-config-out flag. Terraform inspects the live AWS API and synthesizes the exact corresponding HCL block:
terraform plan -generate-config-out=generated_resources.tf
Terraform will create generated_resources.tf containing the full attribute definition for aws_db_instance.production_postgres.
Step 4: Clean Up "Computed" and Read-Only Attributes
The generated code often includes read-only attributes returned by the AWS API that will trigger drift if kept. Examples include:
arn,id,latest_restorable_time- Default timeouts or AWS-managed tags
- Auto-assigned IPv6 parameters
Strip out read-only fields and move the cleaned resource blocks into your standardized modular architecture (such as modules/rds/main.tf).
Step 5: The Crucial Zero-Drift Verification
Run standard terraform plan. Inspect the output meticulously:
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: tfplan
If you see any ~ update in-place or -/+ replacement, do not proceed! Check your local attributes against the AWS console until you achieve 0 to change, 0 to destroy.
Step 6: Execute Apply and Clean Up
Execute the plan with complete confidence:
terraform apply "tfplan"
Once applied, the resource is officially governed by your Terraform state file. You can safely remove the temporary import block from imports.tf or leave it as documentation.
The 4 Most Dangerous Traps During AWS Imports
1. RDS Master Passwords
When importing an existing RDS instance, AWS never returns the master password over the API. If your Terraform code defines password = var.db_password and it doesn't match, Terraform will attempt to reset the master password on apply, breaking all live application connections.
The Fix: Use manage_master_user_password = true with AWS Secrets Manager, or use lifecycle { ignore_changes = [password] } during the initial migration window.
2. Security Group Ingress Rule Collisions
Mixing inline ingress blocks inside aws_security_group with standalone aws_security_group_rule resources will cause Terraform to continually delete and recreate rules. Always standardize exclusively on standalone aws_vpc_security_group_ingress_rule resources.
3. S3 Bucket Policy Cycles
Importing S3 buckets alongside bucket policies often triggers cyclic dependency errors. Import the bucket first, verify state, and import the policy as a decoupled aws_s3_bucket_policy resource.
4. Default VPCs and Security Groups
Every AWS account comes with a default VPC and default security group. Never attempt to manage default security groups with standard resource blocks—use aws_default_security_group to prevent unexpected state conflicts.
Next Steps for Your Infrastructure
Transitioning legacy AWS infrastructure into version-controlled Terraform eliminates configuration drift and allows your team to deploy environments reproducibly.
Planning a Production Terraform Migration?
Velocity Stream architects modular, drift-free Infrastructure as Code for scaling engineering teams. We ensure zero downtime during cutover.

