AWS Builder Center

How to do reverse terraforming

Experience about reverse terraforming in korea company

Reverse Terraforming a Production AWS Environment: Lessons from the Trenches

We built our AWS infrastructure by clicking through the console. It worked — until it didn't. After months of organic growth, we had EKS clusters, RDS instances, EC2 fleets, VPCs, Route53 zones, and IAM roles that existed only as console state. No code, no history, no reproducibility.
This is the story of how we reverse-terraformed it all, which tools we tried, what broke, and what we learned.

The Setup: A Read-Only IAM Account

Before touching anything, we created a dedicated IAM user with ReadOnly permissions only. Reverse terraforming tools need to scan your entire AWS account. You do not want to hand them write access.
1
2
3
IAM → Users → Create User
Attach policy: ReadOnlyAccess (AWS managed)
Generate access key → store in AWS CLI profile
This account was used exclusively for all three tools below.

Tool Comparison: Former2, Terracognita, Kopiax

Former2 — Web-based, clean UI, no modularity

Former2 runs in your browser. You install a Chrome extension, point it at your AWS account via the ReadOnly credentials, and it scans your resources interactively. You pick what to export and it generates .tf files on the spot.
Pros: No CLI install. Visual resource picker. Fastest to get started.
Cons: Output is one flat file — no modules, no separation of concerns. Resource names are generic (EC2Instance, EC2Instance2, ...).
Sample output:
1
2
3
4
resource "aws_vpc" "EC2VPC" {
cidr_block = "${aws_vpc.EC2VPC3.cidr_block}" # circular reference — see issues below
...
}

Terracognita — CLI, generates HCL + tfstate simultaneously

1
.\terracognita.exe aws --aws-default-region ap-northeast-2 --hcl .\reverse_infra.tf --tfstate .\reverse.tfstate
Generates both the .tf code and a .tfstate file in one shot. The tfstate means you skip the terraform import step — Terraform already knows what maps to what.
Pros: Produces a state file. Covers more resource types than Former2.
Cons: Older provider version assumptions. Verbose on EKS node groups.

Kopiax — AWS Marketplace SaaS

Kopiax runs as a hosted service. You grant it access via an IAM role (kopiax-read-only-access), and it generates Terraform from a web UI. The output quality is similar to Former2 but with slightly better IAM coverage.
Notable: Kopiax exposed all IAM users including service accounts and their access key metadata as tags — useful for auditing, but you need to scrub this before committing to git.

The Import Step: Connecting Code to Reality

Generated code has resource blocks with logical names. Terraform does not know these names correspond to real AWS resources until you run terraform import.
1
terraform import aws_instance._be__eficar_external_java_backend i-0a71e3eb8b23101ed
This writes the mapping into terraform.tfstate. After importing every resource, running terraform plan should ideally output:
1
No changes. Your infrastructure matches the configuration.
Any drift shown in plan output = something the tool got wrong.

Errors and Edge Cases We Hit

1. Circular References

The biggest issue with AI-assisted or auto-generated code: resources reference each other by logical name, but nothing holds an actual value.
1
2
3
resource "aws_vpc" "EC2VPC" {
cidr_block = "${aws_vpc.EC2VPC3.cidr_block}" # EC2VPC3 references EC2VPC5, which references...
}
Terraform cannot resolve a circular dependency graph. Fix: hardcode one real value to break the cycle.
1
2
3
4
resource "aws_vpc" "EC2VPC3" {
cidr_block = "10.0.0.0/16" # hardcoded — breaks the chain
...
}

2. Redundant CPU Specs on EC2

Specifying instance_type = "t3.large" implicitly defines CPU core count and threads per core. Some tools write these explicitly anyway:
1
2
3
4
cpu_options {
core_count = 2
threads_per_core = 2
}
If the extracted value doesn't exactly match what AWS reports for that instance type, terraform plan shows a diff. Remove these blocks entirely — Terraform derives them from instance_type.

3. Deprecated Terraform Syntax

Both Former2 and Terracognita were built against older provider versions. You will see warnings:
1
2
3
│ Warning: Argument is deprecated
│ inline_policy is deprecated. Use the aws_iam_role_policy resource instead.
│ (and 49 more similar warnings elsewhere)
Also: vpc = true no longer needs to be declared on subnets. EKS Launch Template blocks require an explicit version attribute in newer provider versions. These are not errors — but plan noise that obscures real drift.

4. Meaningless timeouts {} Blocks

Some tools emit empty timeout blocks:
1
2
3
4
resource "aws_instance" "..." {
...
timeouts {}
}
Delete these. They serve no purpose and Terraform treats them as a configuration diff in some provider versions.

5. Load Balancer Stickiness Mismatch

1
2
3
~ stickiness {
~ enabled = false -> true
}
The tool defaulted to false; the actual AWS state is true. This is not a tool bug — it is state drift. Match the code to what AWS actually has.

6. CloudWatch Alarm Description Formatting

Multi-line heredoc descriptions got their newlines collapsed during extraction:
1
2
3
4
- ## EC2의 CPU 사용률이 80%를 초과함
- ---
- 에픽카 홈페이지에서 CPU 사용률이 80%를 초과하고 있습니다.
+ ## EC2의 CPU 사용률이 80%를 초과함---에픽카 홈페이지에서 CPU 사용률이 80%를 초과하고 있습니다.
Restore the line breaks manually in the heredoc. Cosmetic, but terraform plan will show diffs until fixed.

7. user_data as Base64

EKS node group EC2 instances carry large user_data fields. Terraform stores these as Base64 strings internally. The extracted code preserves the encoded form — do not decode it, and do not try to pretty-print it. Leave it as-is.

8. ALB Target Group Attachments

Terraform noticed 8 target group attachments that existed in AWS but were not in the generated code. These were manually wired in the console. Terraform wants to own them — either import them explicitly or accept that it will register them fresh on apply.

terraform plan Results: 8 Adds, 191 Changes

After import, our first plan showed:
  • 8 resources to add — ALB target group attachments and EIP state that tools missed
  • 191 resources to change — mostly the issues above: deprecated syntax, stickiness flags, alarm description formatting, redundant CPU options
Working through the diff systematically, category by category, got this to near-zero.

Test Environment Strategy

You should not run terraform apply against your production account to validate. Three options, in order of safety:
OptionDescriptionTrade-off
Sandbox account (recommended)New AWS account, Seoul region, same AMIs workSafest. No risk to production. AMI IDs are region-specific and still valid.
Tokyo region, same accountReuse the account but deploy to ap-northeast-2 → ap-northeast-1Must replace all 187 AMI IDs with Tokyo equivalents. More churn.
Same account + same regionDeploy side-by-side in productionName collision risk. Live tfstate could accidentally target running services. Do not do this.
Before any test apply:
  1. Back up and isolate terraform.tfstate
  2. Comment out ACM certificates (they cannot be created without DNS validation in place)
  3. Rename S3 buckets in the code — bucket names are globally unique

But in my case, I don't have to test this way. Our software doesn't run in weekend. So I changed it in Friday night, and see how it goes in weekend.

Reaching "No Changes" — The Safe Operational State

No changes. Your infrastructure matches the configuration.
This is the finish line. When terraform plan outputs this, it means your Terraform code is a perfect mirror of what exists in AWS. From this point, your infrastructure is safe to operate under Terraform management — any future apply will only execute what you explicitly wrote in code, nothing more, nothing less.
Here is the systematic path from a noisy first plan to a clean one.

Step 1: Resolve all resource type errors first

Type errors (An argument named "X" is not expected here) prevent plan from running at all. Fix these before looking at any diffs.
Common culprits:
  • timeouts {} blocks → delete
  • cpu_options {} blocks on standard instance types → delete
  • vpc_security_group_ids with duplicate entries → deduplicate
  • inline_policy in aws_iam_role → migrate to aws_iam_role_policy resources

Step 2: Eliminate deprecated warnings

Deprecation warnings do not block plan, but they bury real diffs. Clear them so you can read the actual output.
1
2
3
4
5
6
7
8
9
10
11
12
# Before (deprecated)
resource "aws_iam_role" "example" {
inline_policy { ... }
}

# After
resource "aws_iam_role" "example" { }

resource "aws_iam_role_policy" "example" {
role = aws_iam_role.example.id
policy = jsonencode({ ... })
}

Step 3: Fix diffs category by category

Read the plan output in groups, not line by line.
Diff patternRoot causeFix
~ enabled = false -> true on stickinessTool defaulted wrongSet to true in code
~ alarm_description newline diffHeredoc extraction collapsed newlinesRestore \n in heredoc
~ user_data hash mismatchTool stored decoded; Terraform expects Base64Use base64encode(file(...)) or paste raw Base64
+ aws_lb_target_group_attachmentConsole-attached targets not extractedImport each: terraform import aws_lb_target_group_attachment.x arn:...
~ launch_template { version }EKS node group version not pinnedAdd explicit version = "$Latest" or pin to "1"

Step 4: Import missing resources (the 8 adds)

Resources that show as + resource "..." "..." in plan means Terraform wants to create them. If they already exist in AWS, import them instead of letting Terraform recreate them.
1
2
3
4
5
6
7
# Import ALB target group attachment
terraform import \
aws_lb_target_group_attachment.example \
arn:aws:elasticloadbalancing:ap-northeast-2:850995566165:targetgroup/xxx/yyy_i-0abc123

# Import Elastic IP
terraform import aws_eip.bastion eipalloc-0abc123def456
After each import, run plan again. The + for that resource disappears.

Step 5: Iterate until clean

1
terraform plan → fix diffs → terraform plan → fix diffs → ...
Each pass reduces the diff count. Our path: 199 changes → 47 → 12 → 0.
When plan finally outputs:
1
No changes. Your infrastructure matches the configuration.
Your infrastructure is now fully under Terraform control. Every resource has a corresponding code definition and a state entry. Running terraform apply at this point changes nothing — it is a no-op. This is exactly the safe state you want before making any real infrastructure changes via code going forward.

Takeaways

Reverse terraforming is not a one-command solution. Every tool gets you 80% of the way there. The remaining 20% is manual reconciliation driven by terraform plan output. The tools are a starting point, not a finish line.
The goal: No changes. in terraform plan. Once you reach it, your code and your cloud are in sync — and your team can finally stop fearing terraform apply.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article