Goal
Everything in production is described in code, changes go through pull requests, and the code matches reality. You get there by importing, not rebuilding.
1. Pick a scope
Start with one application or one shared layer (VPC, IAM baseline). List what's there:
aws resourcegroupstaggingapi get-resources --tag-filters Key=app,Values=orders \
--query "ResourceTagMappingList[].ResourceARN" --output text | tr '\t' '\n'
Untagged resources won't show up — use AWS Config's resource inventory or service-specific describe calls.
2. Terraform: import blocks and generated config
import {
to = aws_s3_bucket.invoices
id = "prod-invoices"
}
import {
to = aws_security_group.web
id = "sg-0123456789abcdef0"
}
terraform plan -generate-config-out=generated.tf
terraform apply # imports into state only
terraform plan # goal: "No changes."
Then refactor: move generated resources into modules (use moved blocks so state follows), replace literals with variables and references.
Caution
If a plan shows
must be replacedon a production resource, stop. Your code differs from reality on an immutable attribute; fix the code, never apply it.
3. CloudFormation: IaC generator and resource import
The CloudFormation IaC generator scans the account and builds a template from selected resources, which you can then import into a stack. Alternatively, import into a new or existing stack with a template you write:
aws cloudformation create-change-set --stack-name orders-data --change-set-name import-1 \
--change-set-type IMPORT --template-body file://orders-data.yaml \
--resources-to-import '[{"ResourceType":"AWS::S3::Bucket","LogicalResourceId":"InvoicesBucket","ResourceIdentifier":{"BucketName":"prod-invoices"}}]'
aws cloudformation execute-change-set --stack-name orders-data --change-set-name import-1
Imported resources need DeletionPolicy set in the template — use Retain for data stores.
4. Pipeline and state
- Terraform state in S3 with versioning, encryption and locking (S3 native locking in recent Terraform versions, or DynamoDB).
- Plan on pull request, apply on merge, manual approval for production.
- Pipeline authenticates with OIDC to a deploy role, not access keys.
5. Detect and prevent drift
aws cloudformation detect-stack-drift --stack-name orders-data
aws cloudformation describe-stack-resource-drifts --stack-name orders-data --stack-resource-drift-status-filters MODIFIED DELETED
For Terraform, run terraform plan -detailed-exitcode nightly (exit code 2 = drift) and alert.
Then reduce console write access in production accounts: read-only permission sets for people, changes through the pipeline role.
Checklist
- Scope inventoried, including untagged resources
- Imported with a no-change plan / successful import change set
- Data stores protected (
prevent_destroy,DeletionPolicy: Retain) - Remote state secured; pipeline with OIDC
- Nightly drift detection; console write access reduced