10.5 Execution Role: Granting Lambda Permission to Call AWS Services
Right, so you’ve written a function. It’s beautiful. It’s perfect. It’s going to take a string, reverse it, and save it to an S3 bucket. You deploy it, you test it, and… AccessDenied. It blew up the moment it tried to even look at S3. Why? Because your Lambda function is a digital amnesiac. It has no idea who it is or what it’s allowed to do. It’s running in a vacuum, utterly powerless.
This is where the Execution Role comes in. Think of it as your function’s ID badge and set of keys. It’s an AWS Identity and Access Management (IAM) role that you attach to the function. This role defines exactly what AWS services and resources your function is permitted to interact with. AWS doesn’t ask your function for a username and password; it checks this role. No role, no permissions. Bad role, AccessDenied errors. It’s that simple.
The Absolute Non-Negotiable Minimum
You cannot run a Lambda function without an execution role. The AWS console will literally not let you finish creating a function without selecting one. This is one of those brilliant “make the right thing the easy thing” design choices from AWS. They force you to think about permissions from the very start, which saves countless hours of debugging later. The role must also include a trust policy that allows the Lambda service to AssumeRole. Luckily, the console usually handles this part for you when you create a role via the Lambda workflow.
Here’s what that trust policy looks like. It’s basically Lambda’s way of saying, “Hey IAM, is it cool if I wear this role?” and IAM replying, “Yeah, sure, if you’re actually the Lambda service.”
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Crafting a Useful Policy (Beyond the Basics)
The default basic execution role is… well, basic. It grants permissions just to spit logs into CloudWatch Logs. That’s it. For almost any real workload, you’ll need to add permissions. Let’s build a role for that function I mentioned, the one that saves to S3.
You need two main things:
- Permission to write to a specific bucket.
- Permission to write logs (so you can see if it worked!).
The AWS-managed policy AWSLambdaBasicExecutionRole handles point #2. For point #1, we craft a custom policy. This is where most people mess up. They grant s3:* on * (a.k.a., “do anything to any bucket in the account”) because it’s easy. Don’t be that person. Follow the Principle of Least Privilege. Be specific.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::my-super-specific-bucket/*"
}
]
}
This policy is beautiful. It doesn’t allow DeleteObject, ListBucket, or anything else. It definitely doesn’t allow access to any other bucket. It’s a surgical instrument, not a grenade. You attach this custom policy and the AWSLambdaBasicExecutionRole to your execution role, and now your function has the precise keys it needs and nothing more.
The Subtle, Maddening Pitfalls
Here are the things that will make you question your life choices, so pay attention.
- The Confusing Console Dropdown: When creating a function in the console, the role dropdown is… a choice. It will show you every role in your account. You must choose one that is specifically designed for Lambda (i.e., has the correct trust policy). The best practice is to hit “Create a new role” and let AWS build the right foundation for you.
- The Deploy-Time vs. Run-Time Confusion: Your function’s permissions are fixed at deploy time. You change the role’s policies, and the function picks up the new permissions immediately. You do not need to redeploy the function. This is a huge “aha!” moment for many. IAM policy changes are effective almost instantly for existing function executions.
- The Cold Start Nuance: There’s a tiny delay when Lambda assumes your role to get temporary credentials. This happens during a cold start. If you have a truly enormous number of policies attached to your role (like, thousands), it can very slightly prolong the cold start. It’s rarely an issue, but it’s a fun fact to know.
- The Wildcard That Isn’t Wild Enough: Look at the S3 policy above. The
Resourceisarn:aws:s3:::my-bucket-name/*. See the/*at the end? That grants permission to objects inside the bucket. If you just usearn:aws:s3:::my-bucket-name, you’re only granting permissions for bucket-level operations (ListBucket,GetBucketLocation), which is probably not what you want forPutObject. This tripped me up for a solid 20 minutes once. I’m not proud of it.
The “Why” Behind the Design
Why does Lambda do it this way? Why not just let us stick an API key in an environment variable like some kind of animal? Security and agility.
The execution role, with its temporary credentials, means you never, ever have to hardcode a secret key into your function code. Those credentials are injected into the execution environment by Lambda itself and are rotated automatically. This is a monumental win. It also means you can change what your function is allowed to do without ever touching the code. You just update the IAM policy. The code and the permissions are cleanly separated, which is how all good modern cloud infrastructure should work.
So, the next time your function gets an AccessDenied, your first question shouldn’t be “What’s wrong with my code?” It should be “What isn’t my role allowed to do?” Trust me, it’s a much faster debugging path.