Chapter 40: Terraform Real AWS Labs VPC EC2 nginx S3 and IAM Role
40.4 Lab part 4 — IAM role and instance profile
Story. An Infosys client application needed to read from S3, and the old server had an access key in a config file that leaked in a backup. An IAM role gives the instance short-lived credentials that rotate automatically. The trust policy says EC2 may assume the role, the permissions say what it may do, and the instance profile attaches it to the server. Adding the SSM managed policy also lets you open a shell with Session Manager, with no SSH port.
data "aws_iam_policy_document" "ec2_trust" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "web" {
name = "badge-lab-web-role"
assume_role_policy = data.aws_iam_policy_document.ec2_trust.json
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.web.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
data "aws_iam_policy_document" "read_assets" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.assets.arn}/*"]
}
}
resource "aws_iam_role_policy" "read_assets" {
role = aws_iam_role.web.id
policy = data.aws_iam_policy_document.read_assets.json
}
resource "aws_iam_instance_profile" "web" {
name = "badge-lab-web-profile"
role = aws_iam_role.web.name
}
What each line does
data "aws_iam_policy_document" "ec2_trust" {— Builds the trust policy JSON in HCL.actions = ["sts:AssumeRole"]— The action being allowed is assuming the role.identifiers = ["ec2.amazonaws.com"]— Only the EC2 service may assume it.assume_role_policy = ...ec2_trust.json— Attaches the trust policy to the new role.policy_arn = ".../AmazonSSMManagedInstanceCore"— Allows Session Manager shell access with no SSH.actions = ["s3:GetObject"]— Read objects only; no write, no delete.resources = ["${aws_s3_bucket.assets.arn}/*"]— Only objects in the lab bucket from part 5.resource "aws_iam_role_policy" "read_assets" {— Adds that least-privilege policy inline to the role.resource "aws_iam_instance_profile" "web" {— The wrapper EC2 needs in order to use a role.
When it is green.
aws_iam_role.web: Creation complete after 1s [id=badge-lab-web-role]
aws_iam_instance_profile.web: Creation complete after 7s
Plan: 4 to add, 0 to change, 0 to destroy.
When it is red.
Error: creating EC2 Instance: InvalidParameterValue: Value (badge-lab-web-profile) for parameter
iamInstanceProfile.name is invalid. Invalid IAM Instance Profile name
Fix. IAM is eventually consistent; the profile was not visible yet. Run terraform apply again; it usually succeeds on the second run.
What: every term in plain English
- IAM role — An identity with permissions but no password or long-term key. Something trusted assumes it.
- trust policy — The part of a role that says who may assume it. Here: the EC2 service.
- permissions policy — The part that says what the role may do, such as s3:GetObject on one bucket.
- instance profile — A small wrapper that attaches a role to an EC2 instance.
- temporary credentials — Keys from STS that expire in hours and rotate by themselves.
- least privilege — Give only the actions and resources the job needs, nothing more.
- aws_iam_policy_document — A data source that writes policy JSON from HCL, so you never hand-write JSON.
Example 1: See the JSON Terraform builds
Reviewers at a Deloitte client wanted to read the final policy JSON, not HCL. Print it from the plan.
- Run terraform console.
- Print the trust policy JSON.
- Print the read policy JSON.
terraform console
> jsondecode(data.aws_iam_policy_document.ec2_trust.json)
Output
{
"Statement" = [
{
"Action" = "sts:AssumeRole"
"Effect" = "Allow"
"Principal" = {
"Service" = "ec2.amazonaws.com"
}
},
]
"Version" = "2012-10-17"
}
What it shows: Only the EC2 service can assume this role. No user, no other account.
Example 2: Check from inside the server who it is
The first command an SRE at PhonePe runs on a server with a role: get-caller-identity. It proves the role is attached.
- Open Session Manager.
- Run get-caller-identity.
- Look for the role name in the ARN.
aws ssm start-session --target $ID
aws sts get-caller-identity --query Arn --output text
ls ~/.aws 2>/dev/null || echo "no key files"
Output
arn:aws:sts::123456789012:assumed-role/badge-lab-web-role/i-0a1b2c3d4e5f60718
no key files
What it shows: The server has credentials but no files. They come from the metadata service and rotate on their own.
Example 3: Prove least privilege by trying a forbidden action
Good roles fail loudly when used for the wrong job. Try to list buckets, which the role does not allow.
- From the server, read an object you uploaded.
- Try to list all buckets.
- Try to delete the object.
aws s3 cp s3://badge-lab-assets-123456789012/hello.txt -
aws s3 ls
aws s3 rm s3://badge-lab-assets-123456789012/hello.txt
Output
Hello from the badge lab
An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied
delete failed: An error occurred (AccessDenied) when calling the DeleteObject operation
What it shows: Read works, everything else fails. That is exactly the job and no more.
Example 4: Use the IAM policy simulator from the CLI
Before a release at an Amdocs project, the team proved the role could not delete objects, without touching real data.
- Get the role ARN.
- Simulate GetObject and DeleteObject.
ROLE=$(aws iam get-role --role-name badge-lab-web-role --query Role.Arn --output text)
aws iam simulate-principal-policy --policy-source-arn $ROLE \
--action-names s3:GetObject s3:DeleteObject \
--resource-arns arn:aws:s3:::badge-lab-assets-123456789012/hello.txt \
--query "EvaluationResults[].[EvalActionName,EvalDecision]" --output text
Output
s3:GetObject allowed
s3:DeleteObject implicitDeny
What it shows: implicitDeny means no statement allows it. The simulator answers without making real calls.
Command cheat sheet
| Command | What it does | When to use it |
|---|---|---|
aws sts get-caller-identity |
Shows who you are | First check on any server |
aws iam get-role --role-name R |
Shows the role and trust policy | Review trust |
aws iam list-attached-role-policies --role-name R |
Lists managed policies | Check SSM is attached |
aws iam simulate-principal-policy |
Tests actions without calling them | Prove least privilege |
terraform console |
Prints policy JSON | Review before apply |
aws ec2 describe-iam-instance-profile-associations |
Shows profile on instances | Debug attachment |
Verify: did it work?
-
Profile attached
aws ec2 describe-iam-instance-profile-associations --filters Name=instance-id,Values=$ID --query "IamInstanceProfileAssociations[0].State" --output textOutput
associated -
SSM policy present
aws iam list-attached-role-policies --role-name badge-lab-web-role --query "AttachedPolicies[].PolicyName" --output textOutput
AmazonSSMManagedInstanceCore
When it is red 1
Unable to locate credentials. You can configure credentials by running "aws configure".
Cause. No instance profile on the server.
Fix, step by step
- Check iam_instance_profile in aws_instance.
- Apply.
- Wait a minute and try again.
When it is red 2
Error: creating IAM Role: EntityAlreadyExists: Role with name badge-lab-web-role already exists.
Cause. A role from an old lab was left behind.
Fix, step by step
- Import it with an import block, or delete the old role in the console.
- Plan again.
When it is red 3
AccessDenied when calling the GetObject operation
Cause. The resource ARN lacks the /* at the end, so it matches the bucket, not the objects.
Fix, step by step
- Use "${aws_s3_bucket.assets.arn}/*".
- Apply and retry.
Cleanup
terraform destroy -auto-approve
Ravindra Bagale's Tip
If you ever see an access key in a server config file, replace it with a role the same day.
Practice tasks
Easy Who am I
Why: Prove the role.
What to do: Run get-caller-identity on the server.
- Use Session Manager.
Check: assumed-role ARN.
Easy Read JSON
Why: Review policy.
What to do: Print the read policy JSON in console.
- Use jsondecode.
Check: s3:GetObject only.
Medium Forbidden action
Why: Prove least privilege.
What to do: Try aws s3 ls on the server.
- Expect failure.
Check: AccessDenied.
Medium Simulator
Why: Test without risk.
What to do: Simulate GetObject and DeleteObject.
- Use simulate-principal-policy.
Check: allowed and implicitDeny.
Easy Say it in two lines
Why: If you can say it simply, you understand it.
What to do: Write two short lines about this lesson for a fresher.
- Write the problem it solves.
- Write the one command or file you use.
- Read it aloud.
Check: A fresher understands it without a question.
Easy Start a safe lab folder
Why: Practice needs a throwaway place.
What to do: Make a new folder with versions.tf and a random_pet resource, so no AWS cost.
- mkdir tf-lab and cd into it.
- Add the hashicorp/random provider and one random_pet.
- Run terraform init and terraform apply.
Check: Apply complete! with 1 added.
Medium Break it on purpose
Why: You must know the red output by face.
What to do: Cause one error from the red part of this lesson and fix it.
- Follow one red section.
- Copy the exact message to your notes.
- Write the fix in one line.
Check: You fixed it without searching the web.
Medium Run it twice
Why: A second run must be quiet.
What to do: Run the same plan two times with no edits.
- Run terraform plan.
- Run it again.
- Explain any change it still shows.
Check: The second plan says No changes, or you can explain each line.
Medium Write a runbook page
Why: At 2 a.m. nobody remembers flags.
What to do: Write a one-page runbook for this lesson.
- Title, when to use, exact commands, how to check, how to undo.
Check: A teammate can follow it alone.
Medium Run it from a pipeline
Why: Real teams run Terraform from CI, not from laptops.
What to do: Run the main command of this lesson inside a GitHub Actions or Jenkins job.
- Add a job with hashicorp/setup-terraform or a Terraform Docker image.
- Give it AWS access through an IAM role, never a key in the file.
- Run init, then the command from this lesson.
Check: The job log shows the same output you saw on your laptop.
Medium Check the cost
Why: Labs left running cost real money.
What to do: Find what this lesson would cost per day if you forgot to destroy it.
- Open the AWS Pricing Calculator.
- Add each resource from your plan.
- Write the daily cost in your notes.
Check: You know the daily cost and you ran terraform destroy.
Hard Draw the flow top to bottom
Why: A drawing shows the order of work.
What to do: Draw this lesson as boxes from top to bottom.
- Start at your code, go through plan and state, end at AWS.
- Mark where each command reads or writes.
Check: Each box matches a real step you ran.
Hard Explain it like an interview
Why: Interviewers want real stories.
What to do: Record a 60 second answer about this lesson.
- Say the problem, the command, and one error you hit.
Check: Your answer has a real command and a real error message.
Homework: try at home
Try at home
Write a role policy for a server that must write logs to one prefix, logs/, in one bucket, and nothing else.
Recap
- Roles replace keys on servers.
- Trust says who; permissions say what.
- The instance profile attaches the role to EC2.
- Build policy JSON with aws_iam_policy_document.
- Prove least privilege with real failures or the simulator.
FAQ
Why both a role and an instance profile?
EC2 attaches profiles, and a profile holds one role. Terraform makes both.
How long do the credentials last?
A few hours, and they refresh automatically before expiry.
Can the role be used from my laptop?
No. The trust policy allows only EC2.
Why the SSM managed policy?
It lets the SSM agent register so Session Manager works.
Can I change permissions without a restart?
Yes. Policy changes apply within seconds.
Interview questions
-
How do you give an EC2 instance access to S3 without keys?
Role with a trust policy for EC2, a permissions policy, and an instance profile.
-
Trust policy versus permissions policy?
Trust says who may assume the role; permissions say what it may do.
-
Why aws_iam_policy_document?
HCL validation and references instead of hand-written JSON.
-
How do you test a policy safely?
IAM policy simulator or a deliberate forbidden call.
-
What does implicitDeny mean?
No statement allowed the action.
Quick quiz
Q1. Service in the trust policy?
ec2.amazonaws.com
Q2. Object that attaches a role to EC2?
Instance profile
Q3. Command to see the current identity?
aws sts get-caller-identity
Q4. Policy for Session Manager?
AmazonSSMManagedInstanceCore
Q5. Suffix needed for object ARNs?
/*
Samajla ka?
Revision card
Interview card