Importing Pre-Existing AWS Resources into Terraform State
Our first prod terraform apply exploded with 'AlreadyExists' errors for AppConfig, GuardDuty, and ECR. We had out-of-band resources that needed to be brought under Terraform management without recreation.
After spending days getting the staging environment right, we ran our first terraform apply for production expecting smooth sailing. Instead, the output was a wall of EntityAlreadyExists errors.
What Happened
We had been iterating on the Terraform module design while staging was being built, and in the process several AWS resources — AppConfig, GuardDuty, and ECR repositories — had been created manually or by previous out-of-band scripts. Terraform had no knowledge of them.
The errors, one by one:
Error: creating AppConfig Application: EntityAlreadyExists:
An application with the name mypie already exists.
Error: creating GuardDuty Detector: ResourceConflictException:
The request failed because a detector already exists for the current account.
Error: creating ECR repository: RepositoryAlreadyExistsException:
The repository with name 'mypie/api-gateway' already exists in the registry
with id '123456789012'
Eight ECR repositories, one AppConfig application, one AppConfig configuration profile, one AppConfig environment, and one GuardDuty detector — all existed in AWS but not in Terraform state.
The Solution: Terraform Import Blocks
Terraform 1.5+ supports import blocks as first-class HCL. Instead of running terraform import as a CLI command (which imports one resource at a time and requires careful sequencing), we created a dedicated imports.tf file in the production environment:
# terraform/environments/prod/imports.tf
# One-shot import file — delete after successful apply
import {
to = module.appconfig.aws_appconfig_application.main
id = "ye5j2ks"
}
import {
to = module.appconfig.aws_appconfig_configuration_profile.feature_flags
id = "nng8s15:ye5j2ks"
}
import {
to = module.appconfig.aws_appconfig_environment.main
id = "ye5j2ks:lczhkon"
}
import {
to = module.guardduty.aws_guardduty_detector.main
id = "a7e6124029e46faa4363db5fc1e9ed9"
}
import { to = module.ecr.aws_ecr_repository.service["api-gateway"]; id = "mypie/api-gateway" }
import { to = module.ecr.aws_ecr_repository.service["pie-service"]; id = "mypie/pie-service" }
import { to = module.ecr.aws_ecr_repository.service["ai-service"]; id = "mypie/ai-service" }
import { to = module.ecr.aws_ecr_repository.service["user-service"]; id = "mypie/user-service" }
import { to = module.ecr.aws_ecr_repository.service["market-data-service"]; id = "mypie/market-data-service" }
import { to = module.ecr.aws_ecr_repository.service["notification-service"]; id = "mypie/notification-service" }
import { to = module.ecr.aws_ecr_repository.service["share-service"]; id = "mypie/share-service" }
import { to = module.ecr.aws_ecr_repository.service["admin-service"]; id = "mypie/admin-service" }
The ID Format Problem
The first attempt at the AppConfig profile import failed immediately:
Error: Invalid import id
The import ID "ye5j2ks/nng8s15" is not valid for this resource type.
expected ConfigurationProfileID:ApplicationID
The AWS Terraform provider sometimes differs from the AWS console URL format. The console URL reads application/ye5j2ks/profile/nng8s15 which made us try ye5j2ks/nng8s15. But the provider wants the resource-first colon-separated format: ConfigurationProfileID:ApplicationID.
You can find the correct import ID format in the Terraform AWS provider documentation for each resource type under the Import section at the bottom. For AppConfig resources:
| Resource | Import ID format |
|---|---|
aws_appconfig_application |
<application_id> |
aws_appconfig_configuration_profile |
<configuration_profile_id>:<application_id> |
aws_appconfig_environment |
<application_id>:<environment_id> |
Getting the actual IDs requires looking in the AWS Console or using the CLI:
# Get AppConfig application IDs
aws appconfig list-applications --query 'Items[*].{Name:Name,Id:Id}'
# Get configuration profiles for an application
aws appconfig list-configuration-profiles \
--application-id <app-id> \
--query 'Items[*].{Name:Name,Id:Id}'
# Get GuardDuty detector ID
aws guardduty list-detectors --query 'DetectorIds[0]'
# ECR repos (the ID is just the repository name)
aws ecr describe-repositories \
--query 'repositories[*].repositoryName'
Running the Import
With import blocks in place, terraform plan generates both import and change plans:
# module.appconfig.aws_appconfig_application.main will be imported
import id = "ye5j2ks"
import to = module.appconfig.aws_appconfig_application.main
# module.guardduty.aws_guardduty_detector.main will be imported
import id = "a7e6124029e46faa4363db5fc1e9ed9"
import to = module.guardduty.aws_guardduty_detector.main
Plan: 12 to import, 2 to add, 1 to change, 0 to destroy.
The 1 to change was a minor tag difference — Terraform wanted to add a ManagedBy=terraform tag to the imported resources. This was expected and safe.
After terraform apply, all resources were in state and subsequent applies would produce No changes.
ECR: Account-Wide Resources
One architectural insight from this exercise: ECR repositories are account-level resources, not environment-level. Our staging Terraform also had an ECR module, which meant staging tried to manage the same repos. The fix was to remove the ECR module from staging/main.tf entirely and manage ECR only in the prod environment:
# staging/main.tf — removed this block
# module "ecr" {
# source = "../../modules/ecr"
# ...
# }
Then clean up the lingering state entries in staging:
cd terraform/environments/staging
terraform state rm 'module.ecr.aws_ecr_repository.service["api-gateway"]'
terraform state rm 'module.ecr.aws_ecr_repository.service["pie-service"]'
# ... and so on for all 8 repos
Key Takeaways
- Create an
imports.tffile rather than scriptingterraform importCLI calls — it’s version-controlled and idempotent. - Check the provider docs for import ID format — it rarely matches the console URL.
- ECR is account-global — do not duplicate it across environments in Terraform.
- After a successful import apply, you can delete
imports.tfsince the resource is now in state. Keep it around or commit it for audit trail if you prefer.