Terraform in Production (Part 4): Declarative State Surgery with moved and import Blocks
State management used to be the most terrifying part of operating Terraform in production.
Historically, if you wanted to reorganize your code, rename a resource, or adopt an existing cloud resource into Terraform, you had to run dangerous imperative commands in your terminal:
# The old manual nightmare (one typo and your state is corrupted)
terraform state mv aws_instance.web module.compute.aws_instance.web
terraform import aws_s3_bucket.data my-production-bucket
This approach had glaring operational flaws:
- It ran with raw admin credentials on local engineer laptops.
- Zero peer review via Pull Requests.
- Zero Git audit trail documenting what changed and why.
- A typo could result in Terraform planning to destroy production resources on the next CI run.
Fortunately, modern Terraform introduced two declarative features that changed the game: moved blocks and import blocks.
1. Declarative Refactoring with moved Blocks (Terraform 1.1+)
When you move a resource into a module, extract it from a module, or convert a count to a for_each, declare the state migration directly in your HCL code:
# In your root configuration:
# Zero downtime, zero manual CLI state manipulation
moved {
from = aws_security_group.allow_tls
to = module.network.aws_security_group.allow_tls
}
moved {
from = aws_route53_record.api[0]
to = aws_route53_record.api["primary"]
}
When your teammates or CI runner runs terraform plan, Terraform detects the migration declaratively:
Plan: 0 to add, 1 to change, 0 to destroy.
~ resource "aws_security_group" "allow_tls" was moved to "module.network.aws_security_group.allow_tls"
Why This is Revolutionary:
- Zero Downtime: Terraform simply updates the internal state pointer without modifying or recreating the cloud resource.
- Git Auditable: The refactoring is committed to Git, reviewed in a standard PR, and applied automatically in CI.
- No State Locks Needed on Laptops: Nobody needs local state write access.
2. Declarative Onboarding with import Blocks (Terraform 1.5+)
What if someone created an S3 bucket or Cloudflare DNS record manually in the web console, and you now need to bring it under Terraform management?
Instead of running imperative CLI terraform import commands, declare an import block in your code:
# In imports.tf
import {
to = aws_s3_bucket.audit_logs
id = "company-audit-logs-2024"
}
Automatic HCL Code Generation
Even better: if you don’t want to write the HCL resource block manually, run:
terraform plan -generate-config-out=generated_resources.tf
Terraform queries the cloud provider API, inspects the live configuration of the bucket, and automatically writes the exact HCL resource block into generated_resources.tf!
# Automatically generated by Terraform!
resource "aws_s3_bucket" "audit_logs" {
bucket = "company-audit-logs-2024"
object_lock_enabled = false
# ...all other live attributes populated
}
You clean up the generated file, open a Pull Request, and review the import plan with your team before merging.
3. Key Takeaways & What's Next
- Never use
terraform state mvmanually again: Declaremovedblocks in HCL and let Git track your state migrations. - Adopt unmanaged resources with
importblocks: Leverage-generate-config-outto eliminate manual HCL writing. - State surgery belongs in CI/CD, reviewed via Pull Requests, not run ad-hoc from personal terminals.
In Part 5 (the series finale), we will cover The Golden Rule of Refactoring: How to rebuild and restructure your code without rebuilding a single piece of live infrastructure.
👈 Part 3: The Module Dilemma — Encapsulation vs. Over-Engineering
👉 Read Part 5: Rebuilding Code Without Rebuilding Infrastructure →