diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index 693b60209d7a9..4f13a0f63cbf6 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -33,14 +33,18 @@ the actual handler. ```ts import { CustomResource } from '@aws-cdk/core'; import * as logs from '@aws-cdk/aws-logs'; +import * as iam from '@aws-cdk/aws-iam'; import * as cr from '@aws-cdk/custom-resources'; const onEvent = new lambda.Function(this, 'MyHandler', { /* ... */ }); +const myRole = new iam.Role(this, 'MyRole', { /* ... */ }); + const myProvider = new cr.Provider(this, 'MyProvider', { onEventHandler: onEvent, isCompleteHandler: isComplete, // optional async "waiter" - logRetention: logs.RetentionDays.ONE_DAY // default is INFINITE + logRetention: logs.RetentionDays.ONE_DAY, // default is INFINITE + role: myRole, // must be assumable by the `lambda.amazonaws.com` service principal }); new CustomResource(this, 'Resource1', { serviceToken: myProvider.serviceToken }); diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts index fe46a7dd7f0b5..8d99963627a4a 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import { Duration } from '@aws-cdk/core'; @@ -105,6 +106,15 @@ export interface ProviderProps { */ readonly securityGroups?: ec2.ISecurityGroup[]; + /** + * AWS Lambda execution role. + * + * The role that will be assumed by the AWS Lambda. + * Must be assumable by the 'lambda.amazonaws.com' service principal. + * + * @default - A default role will be created. + */ + readonly role?: iam.IRole; } /** @@ -135,6 +145,7 @@ export class Provider extends CoreConstruct implements ICustomResourceProvider { private readonly vpc?: ec2.IVpc; private readonly vpcSubnets?: ec2.SubnetSelection; private readonly securityGroups?: ec2.ISecurityGroup[]; + private readonly role?: iam.IRole; constructor(scope: Construct, id: string, props: ProviderProps) { super(scope, id); @@ -152,6 +163,8 @@ export class Provider extends CoreConstruct implements ICustomResourceProvider { this.vpcSubnets = props.vpcSubnets; this.securityGroups = props.securityGroups; + this.role = props.role; + const onEventFunction = this.createFunction(consts.FRAMEWORK_ON_EVENT_HANDLER_NAME); if (this.isCompleteHandler) { @@ -166,7 +179,6 @@ export class Provider extends CoreConstruct implements ICustomResourceProvider { interval: retry.interval, maxAttempts: retry.maxAttempts, }); - // the on-event entrypoint is going to start the execution of the waiter onEventFunction.addEnvironment(consts.WAITER_STATE_MACHINE_ARN_ENV, waiterStateMachine.stateMachineArn); waiterStateMachine.grantStartExecution(onEventFunction); @@ -197,6 +209,7 @@ export class Provider extends CoreConstruct implements ICustomResourceProvider { vpc: this.vpc, vpcSubnets: this.vpcSubnets, securityGroups: this.securityGroups, + role: this.role, }); fn.addEnvironment(consts.USER_ON_EVENT_FUNCTION_ARN_ENV, this.onEventHandler.functionArn); diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts b/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts index 3b8ba5de5ada9..e9cfa0b5e6867 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import { Duration, Stack } from '@aws-cdk/core'; @@ -342,3 +343,57 @@ describe('log retention', () => { expect(stack).not.toHaveResource('Custom::LogRetention'); }); }); + +describe('role', () => { + it('uses custom role when present', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new cr.Provider(stack, 'MyProvider', { + onEventHandler: new lambda.Function(stack, 'MyHandler', { + code: lambda.Code.fromAsset(path.join(__dirname, './integration-test-fixtures/s3-file-handler')), + handler: 'index.onEvent', + runtime: lambda.Runtime.NODEJS_10_X, + }), + role: new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')], + }), + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Role: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }); + }); + + it('uses default role otherwise', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new cr.Provider(stack, 'MyProvider', { + onEventHandler: new lambda.Function(stack, 'MyHandler', { + code: lambda.Code.fromAsset(path.join(__dirname, './integration-test-fixtures/s3-file-handler')), + handler: 'index.onEvent', + runtime: lambda.Runtime.NODEJS_10_X, + }), + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Role: { + 'Fn::GetAtt': [ + 'MyProviderframeworkonEventServiceRole8761E48D', + 'Arn', + ], + }, + }); + }); +});