Skip to content
Mulgamulga

IAM Roles and Instance Profiles

Create IAM roles with trust policies, wrap them in instance profiles, and grant EC2 instances credentials without static keys.

iamrolesinstance profilestrust policiescredentialssecurity

Overview

A role is an IAM identity with permissions but no long-lived credentials. Instead of an access key pair, a role has a trust policy declaring who may assume it; whoever assumes the role receives short-lived, auto-rotating STS credentials.

An instance profile is the container that binds a role to an EC2 instance. An instance launched with a profile gets the role's credentials delivered through IMDS — no static keys baked into the image, no ~/.aws/credentials inside the guest.

The full arc is: create a role → attach permissions → wrap it in an instance profile → launch an instance with the profile → the guest picks up credentials automatically. Each instance profile holds exactly one role (matching AWS).

Like all IAM resources, roles and instance profiles are scoped to the account that creates them.


Prerequisites

bash
export AWS_PROFILE=spinifex

Instructions

Roles

Create a Role

Every role needs a trust policy. For a role that EC2 instances will use, trust the ec2.amazonaws.com service principal:

bash
cat > /tmp/ec2-trust.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws iam create-role --role-name app-server \
  --assume-role-policy-document file:///tmp/ec2-trust.json \
  --description "Role for app servers"
json
{
  "Role": {
    "Path": "/",
    "RoleName": "app-server",
    "RoleId": "AROA1A2B3C4D5E6F7890",
    "Arn": "arn:aws:iam::000000000001:role/app-server",
    "CreateDate": "2026-07-03T10:00:00Z",
    "AssumeRolePolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": { "Service": "ec2.amazonaws.com" },
          "Action": "sts:AssumeRole"
        }
      ]
    },
    "Description": "Role for app servers",
    "MaxSessionDuration": 3600
  }
}

To let a user (rather than EC2) assume the role, trust an AWS principal instead:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::000000000001:user/admin" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Trust policy validation: Spinifex rejects NotPrincipal, NotAction, empty Principal blocks, and empty-string Action elements at write time with MalformedPolicyDocument. Condition blocks are rejected except the StringEquals form used for web identity federation.

Get, List, and Update Roles

bash
aws iam get-role --role-name app-server
aws iam list-roles
aws iam list-roles --path-prefix /services/

Update the description or the maximum STS session duration (3600 seconds by default):

bash
aws iam update-role --role-name app-server \
  --description "Updated" --max-session-duration 7200

Replace the trust policy on an existing role:

bash
aws iam update-assume-role-policy --role-name app-server \
  --policy-document file:///tmp/ec2-trust.json

Granting Permissions to a Role

A freshly created role can do nothing. Grant permissions the same two ways as users: attach managed policies, or embed inline policies.

Attach a Managed Policy

bash
aws iam attach-role-policy --role-name app-server \
  --policy-arn arn:aws:iam::000000000001:policy/S3ReadOnly

aws iam list-attached-role-policies --role-name app-server
aws iam detach-role-policy --role-name app-server \
  --policy-arn arn:aws:iam::000000000001:policy/S3ReadOnly

See IAM Users and Policies for creating managed policies and the policy document format.

Inline Role Policies

Inline policies live inside the role and are deleted with it:

bash
aws iam put-role-policy --role-name app-server \
  --policy-name ec2-describe \
  --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ec2:DescribeInstances"],"Resource":"*"}]}'

aws iam list-role-policies --role-name app-server
aws iam get-role-policy --role-name app-server --policy-name ec2-describe
aws iam delete-role-policy --role-name app-server --policy-name ec2-describe

Instance Profiles

Create a profile and add the role to it:

bash
aws iam create-instance-profile --instance-profile-name app-server-profile
aws iam add-role-to-instance-profile \
  --instance-profile-name app-server-profile --role-name app-server

Inspect it — the Roles array shows the bound role:

bash
aws iam get-instance-profile --instance-profile-name app-server-profile
aws iam list-instance-profiles
aws iam list-instance-profiles-for-role --role-name app-server

A profile holds at most one role; adding a second returns LimitExceeded. To swap roles, remove the current one first:

bash
aws iam remove-role-from-instance-profile \
  --instance-profile-name app-server-profile --role-name app-server

Launching an Instance with a Role

Pass the profile by name (or ARN) at launch:

bash
aws ec2 run-instances \
  --image-id ami-0dd52c90440ff4150 \
  --instance-type t3.micro \
  --subnet-id subnet-a0e5fc381376d82a1 \
  --iam-instance-profile Name=app-server-profile

The instance description shows the association:

bash
aws ec2 describe-instances --instance-ids i-d2e09ff7de71b6341 \
  --query 'Reservations[0].Instances[0].IamInstanceProfile'
json
{
  "Arn": "arn:aws:iam::000000000001:instance-profile/app-server-profile",
  "Id": "AIPA1A2B3C4D5E6F7890"
}

Inside the guest, the AWS CLI and SDKs pick up the role's credentials from IMDS with no configuration — see IMDS for fetching them manually, rotation timing, and limits.

Managing Profile Associations at Runtime

Profiles can be attached to, removed from, or swapped on a running instance — no relaunch needed.

List associations:

bash
aws ec2 describe-iam-instance-profile-associations \
  --filters Name=instance-id,Values=i-d2e09ff7de71b6341
json
{
  "IamInstanceProfileAssociations": [
    {
      "AssociationId": "iip-assoc-9d7a617c605e337cd",
      "InstanceId": "i-d2e09ff7de71b6341",
      "IamInstanceProfile": {
        "Arn": "arn:aws:iam::000000000001:instance-profile/app-server-profile"
      },
      "State": "associated"
    }
  ]
}

Attach a profile to an instance launched without one:

bash
aws ec2 associate-iam-instance-profile \
  --instance-id i-d2e09ff7de71b6341 \
  --iam-instance-profile Name=app-server-profile

Swap to a different profile (returns a new association ID):

bash
aws ec2 replace-iam-instance-profile-association \
  --association-id iip-assoc-9d7a617c605e337cd \
  --iam-instance-profile Name=other-profile

Remove the profile — the guest's iam/ metadata subtree starts returning 404:

bash
aws ec2 disassociate-iam-instance-profile \
  --association-id iip-assoc-9d7a617c605e337cd

Assuming a Role

Users and services can assume a role directly with STS, provided the trust policy allows their principal:

bash
aws sts assume-role \
  --role-arn arn:aws:iam::000000000001:role/deploy \
  --role-session-name release-42
json
{
  "Credentials": {
    "AccessKeyId": "ASIA1A2B3C4D5E6F7890",
    "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "SessionToken": "IQoJb3JpZ2luX2VjE...",
    "Expiration": "2026-07-03T11:00:00Z"
  },
  "AssumedRoleUser": {
    "AssumedRoleId": "AROA1A2B3C4D5E6F7890:release-42",
    "Arn": "arn:aws:sts::000000000001:assumed-role/deploy/release-42"
  }
}

The returned ASIA-prefixed credentials are temporary, valid up to the role's MaxSessionDuration. See STS and Temporary Credentials for session durations, using the credentials, and the other STS flows.

Cleaning Up

Deletion order matters — a role must be empty and unreferenced before it can be deleted:

bash
# 1. Remove the role from any instance profiles
aws iam remove-role-from-instance-profile \
  --instance-profile-name app-server-profile --role-name app-server
aws iam delete-instance-profile --instance-profile-name app-server-profile

# 2. Delete inline policies and detach managed policies
aws iam delete-role-policy --role-name app-server --policy-name ec2-describe
aws iam detach-role-policy --role-name app-server \
  --policy-arn arn:aws:iam::000000000001:policy/S3ReadOnly

# 3. Delete the role
aws iam delete-role --role-name app-server

Troubleshooting

MalformedPolicyDocument When Creating a Role

The trust policy is invalid. In addition to the JSON checks that apply to all policies, trust policies must not use NotPrincipal, NotAction, an empty Principal block, or an empty-string Action. Condition blocks are rejected except StringEquals conditions used with web identity federation.

DeleteConflict When Deleting a Role

The role still has attached managed policies, inline policies, or is bound to an instance profile. Remove all three first:

bash
aws iam list-attached-role-policies --role-name app-server
aws iam list-role-policies --role-name app-server
aws iam list-instance-profiles-for-role --role-name app-server

DeleteConflict When Deleting an Instance Profile

The profile still contains a role:

bash
aws iam remove-role-from-instance-profile \
  --instance-profile-name app-server-profile --role-name app-server
aws iam delete-instance-profile --instance-profile-name app-server-profile

LimitExceeded When Adding a Role to a Profile

The profile already holds a role — the limit is one per profile. Remove the existing role first, or create a second profile.

InvalidIamInstanceProfile.NotFound at Launch

The profile name or ARN passed to run-instances does not exist in your account. Check the spelling:

bash
aws iam list-instance-profiles

Instance Has No Credentials in IMDS

If the iam/ metadata subtree returns 404, the instance has no associated profile. Attach one without relaunching:

bash
aws ec2 associate-iam-instance-profile \
  --instance-id i-d2e09ff7de71b6341 \
  --iam-instance-profile Name=app-server-profile

If the profile is associated but contains no role, the security-credentials/ listing is empty; add the role and credentials appear on the next request.