Right, so you’ve created this beautifully scoped IAM Role with just the right permissions. It’s a work of art. But it’s just sitting there in IAM, useless, like a car with no keys. An EC2 instance can’t just wear a role. It’s not a piece of clothing. It needs a very specific set of keys and a permission slip, and that, my friend, is what we call an Instance Profile.

Think of it this way: the IAM Role is the set of permissions (the “what”). The Instance Profile is the container that allows an EC2 instance to assume that role (the “how”). It’s the name tag we stick on the EC2 instance that says, “Hey, I’m allowed to carry this Role.” You can’t attach a role directly to an instance; you must wrap it in an instance profile first. For the longest time, the AWS CLI and APIs forced you to understand this distinction, which was frankly a bit pedantic. These days, the console and most modern tools (aws ec2 run-instances --iam-instance-profile) will silently create the profile for you if you just specify a role name, which is a welcome bit of sanity. But under the hood, the machinery is still there, and you need to know about it for those times when the automation magic fails.

The Nuts and Bolts of Creation

You can do this the old-school way, which reveals the plumbing, or the new, cleaner way. Let’s look at both because you’ll see the old way in a terrifying amount of legacy scripts.

The “I See How the Sausage is Made” Method (using AWS CLI): First, create the role. Then, create the instance profile. Then, add the role to the profile. It’s a three-step process.

# 1. Create the role (assuming you have a trust-policy.json file)
aws iam create-role --role-name MyEC2Role --assume-role-policy-document file://trust-policy.json

# 2. Create the instance profile
aws iam create-instance-profile --instance-profile-name MyEC2InstanceProfile

# 3. Add the role to the profile
aws iam add-role-to-instance-profile --role-name MyEC2Role --instance-profile-name MyEC2InstanceProfile

The critical part here is the trust policy in trust-policy.json. It must allow the EC2 service to assume this role. This is the “permission slip” I mentioned.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

The “Just Get On With It” Method (Modern CLI): The aws iam create-role command now has a --create-instance-profile flag that does steps 2 and 3 for you. Thank goodness.

aws iam create-role --role-name MyEC2Role --assume-role-policy-document file://trust-policy.json --create-instance-profile

Attaching the Profile to Your Instance

This is the payoff. You can attach the instance profile at launch time (highly recommended) or later (a decent backup plan). Never, ever bake access keys and secrets into a custom AMI or user data. That’s how you end up on a headline.

At Launch (via CLI): You use the --iam-instance-profile parameter. Notice you specify the instance profile name, not the role name.

aws ec2 run-instances \
    --image-id ami-0abcdef1234567890 \
    --instance-type t3.micro \
    --iam-instance-profile Name=MyEC2InstanceProfile \
    --key-name my-key-pair

After Launch (Attaching on the fly): Yes, you can do this! It’s incredibly useful for remediating a misconfigured instance without a full reboot. You use the associate-iam-instance-profile command.

aws ec2 associate-iam-instance-profile \
    --instance-id i-1234567890abcdef0 \
    --iam-instance-profile Name=MyEC2InstanceProfile

An instance can only have one instance profile associated at a time, but you can swap it for another one. This is far cleaner than the old mess of trying to manage multiple instance profiles.

How Your Application Actually Uses It

Here’s the beautiful part: your application code doesn’t need to do a thing. No keys, no configuration. The AWS SDKs and CLI running on the instance automatically know to look for the attached role. They do this by querying the Instance Metadata Service (IMDS) at the magic URL http://169.254.169.254. The SDKs handle the entire process of fetching temporary credentials, refreshing them, and using them to sign your API calls. It’s completely transparent. You just write code like you have access.

# This boto3 client will automatically use the instance's role credentials.
# No configuration needed. It Just Works™.
import boto3

s3 = boto3.client('s3')
response = s3.list_buckets()
print(response['Buckets'])

The Gotchas and Rough Edges

  1. The Name Tag Mismatch: The most common facepalm moment is trying to attach a role by its name instead of the instance profile’s name. The error messages aren’t always clear about this. Remember: you attach the instance profile.
  2. The Trust Policy: If you manually create the role and forget to set the principal to ec2.amazonaws.com, nothing will work. The instance will try to assume the role and get a big, fat “denied” from the security token service (STS).
  3. IMDSv2: The Instance Metadata Service has a v2 that is more secure (it’s session-oriented). Your application code might need updates if you enforce it. Most modern SDKs handle it, but older custom scripts might break. Always enforce IMDSv2 in your launch templates.
  4. Propagation Delay: While attaching a profile to a running instance is near-instantaneous, there can be a brief second or two before the credentials are available via the metadata service. Your application should have retry logic for initial boot.
  5. The Dirty Secret: An instance profile is literally just a container for a single role. You can’t attach multiple roles to one EC2 instance. If you need more permissions, you add them to the one role it can assume. This is usually the right choice for simplicity, but it’s a constraint people often go looking for.