AWS Lambda
AWS CDK
Custom Role
Cloud Development Kit
IAM Roles

Specifying a custom role for lambda with the AWS CDK

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When you create a Lambda function with the AWS CDK, the construct can generate an execution role for you automatically. That is convenient, but sometimes you need to provide a custom IAM role instead so you can enforce least privilege, reuse an existing policy design, or integrate the function into a broader security model.

Creating a custom role in CDK

In the CDK, the main requirement is that the role must trust the Lambda service. In TypeScript, that looks like this:

typescript
1import * as cdk from "aws-cdk-lib";
2import * as iam from "aws-cdk-lib/aws-iam";
3import * as lambda from "aws-cdk-lib/aws-lambda";
4import { Construct } from "constructs";
5
6export class DemoStack extends cdk.Stack {
7  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
8    super(scope, id, props);
9
10    const role = new iam.Role(this, "CustomLambdaRole", {
11      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
12    });
13
14    role.addManagedPolicy(
15      iam.ManagedPolicy.fromAwsManagedPolicyName(
16        "service-role/AWSLambdaBasicExecutionRole"
17      )
18    );
19
20    new lambda.Function(this, "MyFunction", {
21      runtime: lambda.Runtime.NODEJS_20_X,
22      handler: "index.handler",
23      code: lambda.Code.fromInline(
24        'exports.handler = async () => ({ statusCode: 200, body: "ok" });'
25      ),
26      role,
27    });
28  }
29}

The critical part is the role property on the function. Once you pass it, Lambda uses that role instead of creating a new default execution role.

Add only the permissions you actually need

Every Lambda function needs some baseline logging permissions if it writes to CloudWatch Logs. The AWS managed policy AWSLambdaBasicExecutionRole is a common starting point, but many functions also need service-specific access.

For example, if the function reads from S3:

typescript
1role.addToPolicy(
2  new iam.PolicyStatement({
3    actions: ["s3:GetObject"],
4    resources: ["arn:aws:s3:::my-bucket/*"],
5  })
6);

This is usually better than attaching broad administrator-style permissions, because it keeps the function’s blast radius smaller.

Reusing an existing role

If the role already exists, import it instead of creating a new one:

typescript
1const role = iam.Role.fromRoleArn(
2  this,
3  "ImportedLambdaRole",
4  "arn:aws:iam::123456789012:role/MyExistingLambdaRole"
5);
6
7new lambda.Function(this, "MyFunction", {
8  runtime: lambda.Runtime.PYTHON_3_12,
9  handler: "index.handler",
10  code: lambda.Code.fromInline("def handler(event, context): return 'ok'"),
11  role,
12});

This is common in organizations where IAM roles are managed centrally and infrastructure stacks are not allowed to create their own execution roles freely.

Why a custom role is useful

A custom role helps when:

  • security policy requires explicit IAM review
  • several functions should share the same permission set
  • you want stable role names and policies across deployments
  • you need to separate infrastructure generation from IAM governance

It also makes the role visible in code as a first-class design decision instead of an implicit side effect of the Lambda construct. That usually leads to clearer reviews and better least-privilege decisions.

Grant helpers and imported-role caveats

The CDK also provides higher-level helpers such as bucket.grantRead(role) or table.grantReadWriteData(role). Those are often preferable to handwritten policy statements because they stay aligned with the resource definition and reduce policy mistakes.

If you import an existing role, remember that some imported roles are effectively immutable from the current stack. In that case, attaching new policies may not behave the way a newly created role would.

Common Pitfalls

  • Forgetting the Lambda trust relationship by using the wrong assumedBy principal.
  • Passing a custom role but forgetting to include basic CloudWatch Logs permissions.
  • Granting permissions too broadly instead of tailoring them to the function’s real actions.
  • Importing an existing role without checking whether that role is mutable from the current stack.

Summary

  • In the CDK, pass a role through the Lambda role property to use a custom execution role.
  • The role must trust lambda.amazonaws.com.
  • Add only the permissions the function actually needs, including basic logging permissions.
  • Import existing roles when IAM is managed outside the stack.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.