Initializing Enclave...

How to Fix Terraform 'Error: Resource Already Exists' When Importing an aws_iam_role

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: Terraform is trying to CREATE an IAM role that already physically exists in AWS, but has no entry in terraform.tfstate. The plan fails hard.
  • How to fix it: Run terraform import aws_iam_role.<resource_label> <existing-role-name> to pull the live resource into state, then reconcile your HCL to match the live role's attributes.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your failing .tf block and get the corrected import command and HCL diff instantly.

The Incident (What Does the Error Mean?)

Raw error output from a terraform apply:

Error: creating IAM Role (my-app-execution-role): EntityAlreadyExists:
  Role with name my-app-execution-role already exists.
│   with aws_iam_role.app_execution_role,
│   on iam.tf line 4, in resource "aws_iam_role" "app_execution_role":
│    4: resource "aws_iam_role" "app_execution_role" {

Immediate consequence: Terraform's AWS provider attempted a CreateRole API call. IAM rejected it with EntityAlreadyExists. Your entire terraform apply is now aborted — every resource depending on this role (ECS task definitions, Lambda functions, EC2 instance profiles) did not get created or updated. Your pipeline is dead.

This happens in three common scenarios:

  1. The role was created manually in the AWS Console before IaC was adopted.
  2. A previous Terraform run created the role but the state file was lost, corrupted, or the workspace was switched.
  3. Another Terraform root module or stack owns this role, and you have a duplicate resource block.

The Attack Vector / Blast Radius

This is not just a nuisance error. The blast radius is significant:

  • Pipeline deadlock: Every CI/CD run will fail at this step. No infrastructure changes can be applied until resolved.
  • State drift accumulation: The longer the role exists outside of state, the more its live configuration (inline policies, tags, trust relationships) will drift from your HCL. Importing a heavily drifted role and running apply immediately can overwrite production trust policies, breaking service authentication silently.
  • Duplicate ownership risk: If two Terraform stacks both define this role without remote state locking, a concurrent apply from either stack can delete and recreate the role, instantly invalidating all attached instance profiles and assumed-role sessions. Active workloads using sts:AssumeRole will receive AccessDenied with no warning.
  • Audit trail gap: A role living outside of IaC state has no change history. You cannot answer "who modified this role's trust policy and when" from your Terraform logs.

How to Fix It (The Solution)

Step 1: Verify the Role Exists in AWS

aws iam get-role --role-name my-app-execution-role --query 'Role.{Name:RoleName,Arn:Arn,Created:CreateDate}'

Confirm the exact role name. IAM role names are case-sensitive.

Step 2: Basic Fix — Import the Existing Role into State

terraform import aws_iam_role.app_execution_role my-app-execution-role

After import, immediately run terraform plan before any apply. The plan output will show every attribute delta between your HCL and the live role. You must reconcile these before applying.

Step 3: Reconcile HCL to Match Live Resource

The most common drift is in the assume_role_policy JSON (whitespace, key ordering, added/removed principals) and tags. Use terraform plan output to identify deltas.

 resource "aws_iam_role" "app_execution_role" {
   name = "my-app-execution-role"
+  path = "/"
+  description = "ECS task execution role - managed by Terraform"

   assume_role_policy = jsonencode({
     Version = "2012-10-17"
     Statement = [{
       Effect    = "Allow"
-      Principal = { Service = "ecs.amazonaws.com" }
+      Principal = { Service = "ecs-tasks.amazonaws.com" }
       Action    = "sts:AssumeRole"
     }]
   })

+  tags = {
+    ManagedBy   = "terraform"
+    Environment = var.environment
+  }
 }

Enterprise Best Practice — Prevent Orphaned Roles with Remote State and Lifecycle Guards

For roles shared across multiple stacks, use a data source reference instead of a duplicate resource block:

- resource "aws_iam_role" "app_execution_role" {
-   name               = "my-app-execution-role"
-   assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
- }

+ # Role is owned by the platform stack. Reference it here as a data source.
+ data "aws_iam_role" "app_execution_role" {
+   name = "my-app-execution-role"
+ }

For roles that must be owned by this stack, add a lifecycle block to prevent accidental destruction:

 resource "aws_iam_role" "app_execution_role" {
   name               = "my-app-execution-role"
   assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
+
+  lifecycle {
+    prevent_destroy = true
+    # Ignore tag drift from manual emergency changes
+    ignore_changes  = [tags["LastEmergencyModifiedBy"]]
+  }
 }

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Enforce State Existence Before Apply with Checkov

Add Checkov to your pipeline to catch unmanaged IAM resources:

checkov -d . --check CKV_AWS_274 --compact

2. OPA Policy — Block Plans with IAM Role Creation if Role Name Already Exists

package terraform.iam

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_iam_role"
  resource.change.actions[_] == "create"
  # Integrate with aws_iam_role data source pre-check in your pipeline
  msg := sprintf("IAM role '%v' is being created by Terraform. Verify it does not already exist in AWS before applying.", [resource.change.after.name])
}

3. Pre-Apply Shell Guard in CI/CD

Insert this before terraform apply in your pipeline script:

ROLE_NAME=$(terraform show -json | jq -r '.planned_values.root_module.resources[] | select(.type=="aws_iam_role") | .values.name')
if aws iam get-role --role-name "$ROLE_NAME" > /dev/null 2>&1; then
  echo "[WARN] IAM role $ROLE_NAME exists in AWS but may not be in state. Run terraform import before apply."
  exit 1
fi

4. Terragrunt + S3 Remote State Locking

If you are not already using remote state with DynamoDB locking, this class of error will recur. Shared IAM roles must live in a single authoritative state file, referenced by all consumer stacks via terraform_remote_state data source or SSM Parameter Store ARN lookup.

# In consumer stacks, never re-declare shared platform roles
data "terraform_remote_state" "platform_iam" {
  backend = "s3"
  config = {
    bucket = "my-org-terraform-state"
    key    = "platform/iam/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_ecs_task_definition" "app" {
  # ...
  execution_role_arn = data.terraform_remote_state.platform_iam.outputs.app_execution_role_arn
}

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →