Hello There , here some docs about Automating AWS IAM with Python: Practical Patterns for Safer Access
Practical IAM automation patterns using Boto3, role design, and least-privilege principles.
Managing AWS IAM manually works when you have a few resources and a small team.
Then the environment grows.
A few roles become dozens. Temporary access becomes permanent. Someone creates a policy with * because "it was only for testing." Another person copies an existing role and changes two permissions. Eventually, nobody is completely sure which role exists for which workload.
That is where IAM automation becomes useful.
Python and Boto3 can turn IAM from something we click through in the AWS Console into something we can define, review, reproduce, and automate.
The goal isn't simply to automate IAM API calls.
The goal is to make access predictable and intentionally limited.
Why automate IAM?
IAM is configuration, but it is also security configuration.
That makes manual changes particularly uncomfortable.
Imagine deploying three environments:
development
staging
production
Each environment has applications that need access to different AWS resources.
Without automation, you might end up manually creating:
app-dev-role
app-staging-role
app-production-role
Then attaching policies to each one.
The problem isn't creating the roles.
The problem is keeping them consistent.
Automation gives us a repeatable process:
Python configuration
↓
Validate desired access
↓
Create/update IAM role
↓
Create/update policy
↓
Attach policy
↓
Verify configuration
Now IAM changes can be reviewed like code.
The IAM model we actually need
Before writing Python, it helps to separate two concepts that are often confused.
Trust policy
The trust policy answers:
Who is allowed to assume this role?
For example, an EC2 instance, Lambda function, or another AWS account might be allowed to assume a role.
Permissions policy
The permissions policy answers:
What can the role do after it is assumed?
For example:
s3:GetObject
s3:ListBucket
These are different responsibilities.
A role can have the correct permissions but an overly broad trust policy.
It can also have a tightly restricted trust policy but excessive permissions.
Good IAM design requires both sides to be intentional.
Setting up Boto3
Install Boto3:
pip install boto3
Then create an IAM client:
import boto3
iam = boto3.client("iam")
Boto3 uses the AWS credential/provider chain rather than requiring credentials to be hard-coded into the Python script.
For example, you can work with a configured AWS profile:
import boto3
session = boto3.Session(profile_name="dev")
iam = session.client("iam")
Avoid putting access keys directly into source code.
Pattern 1: Define policies as Python data
Instead of scattering IAM JSON throughout the program, define policies as Python dictionaries.
For example:
backup_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::company-backups"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::company-backups/*"
}
]
}
This gives us something important:
IAM configuration becomes data.
Once permissions are represented as data, we can validate them, test them, generate them, and store them in Git.
Pattern 2: Create the role separately from its permissions
Let's create a role for our application.
First, define the trust relationship:
import json
import boto3
iam = boto3.client("iam")
trust_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
response = iam.create_role(
RoleName="backup-reader",
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description="Role used by the backup application"
)
print(response["Role"]["Arn"])
The important part is:
"Principal": {
"Service": "ec2.amazonaws.com"
}
We are saying that EC2 is allowed to assume the role.
This does not mean the EC2 workload automatically has access to S3.
The role still needs permissions.
Pattern 3: Attach only the permissions the application needs
Now we can create a managed policy.
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::company-backups"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::company-backups/*"
}
]
}
Create the policy:
response = iam.create_policy(
PolicyName="BackupReaderPolicy",
PolicyDocument=json.dumps(policy),
Description="Read-only access to the company backup bucket"
)
policy_arn = response["Policy"]["Arn"]
Then attach it:
iam.attach_role_policy(
RoleName="backup-reader",
PolicyArn=policy_arn
)
A complete small example
Putting the pieces together:
import json
import boto3
ROLE_NAME = "backup-reader"
POLICY_NAME = "BackupReaderPolicy"
iam = boto3.client("iam")
trust_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
permissions_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::company-backups"
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::company-backups/*"
}
]
}
role = iam.create_role(
RoleName=ROLE_NAME,
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description="Read-only backup access"
)
policy = iam.create_policy(
PolicyName=POLICY_NAME,
PolicyDocument=json.dumps(permissions_policy),
Description="Read-only access to backup objects"
)
iam.attach_role_policy(
RoleName=ROLE_NAME,
PolicyArn=policy["Policy"]["Arn"]
)
print(f"Created role: {role['Role']['Arn']}")
print(f"Created policy: {policy['Policy']['Arn']}")
This works as a demonstration, but I would not stop here for production.
Why?
Because running it twice can fail when the role or policy already exists.
That brings us to a more interesting problem.
IAM automation should be idempotent
A good automation script should be safe to run repeatedly.
Consider:
python iam_setup.py
The first run creates everything.
What should happen on the second run?
Ideally:
Role already exists
↓
Check its configuration
↓
Policy already exists
↓
Check/update its version
↓
Verify attachment
↓
Done
Not:
EntityAlreadyExistsException
This is one of the biggest differences between a quick automation script and a useful infrastructure tool.
A simple existence check can be implemented like this:
def role_exists(iam, role_name):
try:
iam.get_role(RoleName=role_name)
return True
except iam.exceptions.NoSuchEntityException:
return False
Then:
if role_exists(iam, ROLE_NAME):
print(f"{ROLE_NAME} already exists")
else:
iam.create_role(
RoleName=ROLE_NAME,
AssumeRolePolicyDocument=json.dumps(trust_policy)
)
This pattern can be extended to policies, attachments, tags, and trust relationships.
Role design: one role, one responsibility
A useful rule for IAM design is:
A role should represent a workload or responsibility, not a collection of unrelated permissions.
For example, avoid creating:
ApplicationEverythingRole
with:
S3
DynamoDB
EC2
RDS
IAM
Lambda
CloudWatch
Secrets Manager
Instead, consider roles based on actual workload responsibilities:
orders-api-role
backup-reader-role
monitoring-role
deployment-role
Then permissions follow the workload.
For example:
orders-api-role
├── DynamoDB access
├── SQS access
└── CloudWatch logging
backup-reader-role
└── S3 read access
monitoring-role
└── CloudWatch read access
This makes access easier to understand and audit.
Don't confuse "working" with "least privilege"
This is probably the most important lesson in IAM automation.
Suppose your application receives:
AccessDenied
The fastest solution is tempting:
"Action": "*",
"Resource": "*"
The application works.
But the security model is now much worse.
Instead, identify the exact API call that failed.
If the application needs:
s3:GetObject
give it:
"Action": "s3:GetObject"
not:
"Action": "s3:*"
And if possible, restrict the resource:
"Resource": "arn:aws:s3:::company-backups/*"
rather than:
"Resource": "*"
Least privilege is not about making policies tiny for the sake of being tiny.
It is about making the permissions match the actual job.
Use conditions when they make sense
Actions and resources are only part of the IAM policy.
Conditions can provide another layer of control.
For example:
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "ap-northeast-2"
}
}
Now the permission can be constrained by context.
Conditions can be useful for things such as:
source VPC endpoints
requested regions
resource tags
principal tags
MFA requirements
request attributes
The exact condition should depend on the threat model and workload.
The important idea is:
Don't automatically treat every permission as unconditional.
Tags make automated IAM easier to manage
If you create IAM resources programmatically, tag them.
For example:
tags = [
{
"Key": "ManagedBy",
"Value": "Python"
},
{
"Key": "Environment",
"Value": "Production"
},
{
"Key": "Application",
"Value": "Backup"
}
]
Then:
iam.create_policy(
PolicyName=POLICY_NAME,
PolicyDocument=json.dumps(permissions_policy),
Description="Read-only backup access",
Tags=tags
)
Now you can answer questions such as:
Which IAM resources are managed automatically?
Which application owns this policy?
Which environment is this role for?
A better architecture: configuration first
For larger projects, I wouldn't hard-code every role inside Python.
Instead, imagine a configuration file:
roles:
backup-reader:
trusted_service: ec2.amazonaws.com
permissions:
- actions:
- s3:GetObject
resources:
- arn:aws:s3:::company-backups/*
- actions:
- s3:ListBucket
resources:
- arn:aws:s3:::company-backups
Python becomes the engine that translates this configuration into AWS resources.
The architecture becomes:
YAML / JSON
↓
Validation
↓
Python
↓
Boto3
↓
AWS IAM
Now changing access doesn't necessarily mean changing application logic.
You change the desired configuration.
Add a "plan" mode before making changes
This is another pattern I strongly recommend.
Before changing IAM, show what the script intends to do.
For example:
IAM PLAN
Role:
backup-reader
Trust:
ec2.amazonaws.com
Permissions:
s3:GetObject
s3:ListBucket
Resources:
arn:aws:s3:::company-backups/*
arn:aws:s3:::company-backups
Then require an explicit flag:
python iam.py --plan
versus:
python iam.py --apply
This gives you a chance to catch mistakes before they become AWS changes.
It also makes the tool much easier to integrate into CI/CD.
Treat IAM changes like code
Once IAM is automated, put the configuration in Git.
For example:
iam-automation/
├── policies/
│ ├── backup-reader.json
│ └── monitoring.json
├── roles/
│ ├── backup-reader.json
│ └── monitoring.json
├── iam.py
├── requirements.txt
└── README.md
Now a permission change can go through:
Developer
↓
Git commit
↓
Pull request
↓
Review
↓
Validation
↓
CI/CD
↓
AWS
That is a major improvement over:
Someone opens AWS Console
↓
Clicks around
↓
Changes a policy
↓
Nobody knows why
Validate before deploying
Automation can make bad changes very quickly.
So automation needs guardrails.
AWS IAM Access Analyzer can validate IAM policies and help identify overly permissive access.
A useful pipeline could therefore look like:
Policy definition
↓
JSON/schema validation
↓
IAM policy validation
↓
Security review
↓
Plan
↓
Apply
↓
Verification
The Python script shouldn't be the only safety mechanism.
Don't create long-lived credentials for automation
Another important distinction:
Automating IAM doesn't mean creating access keys everywhere.
For AWS workloads, prefer temporary credentials and IAM roles where possible.
For example:
EC2
↓
IAM Role
↓
Temporary credentials
↓
AWS API
rather than:
EC2
↓
Hard-coded access key
↓
AWS API
This reduces the number of secrets you have to manage.
What I would automate first
If you're building your first IAM automation project, don't try to automate the entire AWS account on day one.
Start small.
I'd automate these operations first:
1. Create role
2. Configure trust policy
3. Create customer-managed policy
4. Attach policy
5. Add tags
6. Verify configuration
7. Detect drift
8. Produce a plan before changes
Then add:
9. Policy version management
10. Access review
11. Unused permission detection
12. CI/CD integration
13. Multi-account support
This progression keeps the project understandable while giving you room to grow it.
A practical mental model
When designing IAM automation, I use five questions.
1. Who?
Who should be able to assume this role?
EC2?
Lambda?
Another AWS account?
Human identity?
CI/CD system?
2. What?
What actions does the workload actually perform?
s3:GetObject?
dynamodb:GetItem?
logs:PutLogEvents?
3. Where?
Which resources should those actions apply to?
Specific bucket?
Specific table?
Specific log group?
4. Under what conditions?
Can the permission be restricted further?
Region?
Tags?
Network path?
MFA?
Principal attributes?
5. How do we prove it?
Can we validate and monitor the access?
Policy validation
Access Analyzer
CloudTrail
Code review
Automated tests
If you can answer those five questions, your IAM design is usually heading in the right direction.
Final thoughts
IAM automation isn't really about Python.
Python is simply the tool that lets us turn our access model into something repeatable.
The bigger idea is:
Manual IAM
↓
Defined IAM
↓
Automated IAM
↓
Validated IAM
↓
Auditable IAM
Boto3 gives us the API layer. The important engineering work is deciding what access should exist and why.
Start with the workload.
Define the trust relationship.
Define the minimum permissions.
Restrict the resources.
Add conditions where useful.
Put the configuration in Git.
Validate before applying.
And make the automation safe to run more than once.
The best IAM automation isn't the script that creates the most roles.
It's the one that makes it difficult to accidentally create too much access.
Useful References
AWS IAM — Policies and permissions
AWS IAM — Least-privilege permissions
Boto3 IAM documentation
Boto3 IAM examples
Top comments (0)