Skip to content

Commit 77b6fa9

Browse files
authoredMar 6, 2025··
fix(custom-resources): fix circular dependency when a custom role provided to Provider (#33600)
### Issue # (if applicable) Closes #20360 ### Reason for this change When users specify a isCompletehandler and specifies a custom role for the provider framework, the output template is not deployable due to circular dependencies. ### Description of changes The change here is to deprecate the old `role` property because this `role` is shared between the 3 framework lambda functions. The state machine will depends on the sfn default policy. The default policy depends on isCompleteLambda (granting invoke function permission). isCompleteLambda depends on common default role policy. The common role default policy has startExecution permission to SFN. The solution is to deprecate `role` and introduce new roles for the onEvent lambda and isComplete/onTimeout lambda ### Describe any new or updated permissions being added N/A ### Description of how you validated changes New tests ### Checklist - [ ] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7f5bf4e commit 77b6fa9

File tree

16 files changed

+4710
-8
lines changed

16 files changed

+4710
-8
lines changed
 

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/IntegProviderWithWaiterStateMachineCustomRoleDefaultTestDeployAssert1C38BF52.assets.json

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/IntegProviderWithWaiterStateMachineCustomRoleDefaultTestDeployAssert1C38BF52.template.json

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/asset.39472b1c2875cf306d4ba429aeccdd34cb49bcf59dbde81f7e6b6cb9deac23a6/cfn-response.js

+106
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/asset.39472b1c2875cf306d4ba429aeccdd34cb49bcf59dbde81f7e6b6cb9deac23a6/consts.js

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/asset.39472b1c2875cf306d4ba429aeccdd34cb49bcf59dbde81f7e6b6cb9deac23a6/framework.js

+185
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/asset.39472b1c2875cf306d4ba429aeccdd34cb49bcf59dbde81f7e6b6cb9deac23a6/outbound.js

+83
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/asset.39472b1c2875cf306d4ba429aeccdd34cb49bcf59dbde81f7e6b6cb9deac23a6/util.js

+53
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/cdk.out

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/integ-provider-with-waiter-state-machine-custom-role.template.json

+929
Large diffs are not rendered by default.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/manifest.json

+1,022
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/provider-framework/integ.provider-with-waiter-state-machine-custom-role.js.snapshot/tree.json

+1,874
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/// !cdk-integ *
2+
import { App, Stack } from 'aws-cdk-lib';
3+
import * as integ from '@aws-cdk/integ-tests-alpha';
4+
import { Construct } from 'constructs';
5+
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
6+
import { Provider } from 'aws-cdk-lib/custom-resources';
7+
import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
8+
9+
// This test is not deployable prior to this PR fix https://github.com/aws/aws-cdk/pull/32404/files
10+
// due to integ-provider-with-waiter-state-machine-custom-role failed: ValidationError: Circular dependency
11+
// between resources: [MyRoleDefaultPolicyA36BE1DD, MyProviderWithCustomRolewaiterstatemachineRoleDefaultPolicy4808872B,
12+
// MyProviderWithCustomRoleframeworkonEventCE6B50CD, MyProviderWithCustomRoleframeworkonTimeout1A7D4C59,
13+
// MyProviderWithCustomRoleframeworkisComplete10E48A2A, MyProviderWithCustomRolewaiterstatemachineA313C5FC,
14+
// MyProviderWithCustomRolewaiterstatemachineLogGroup836672C3]
15+
16+
class TestStack extends Stack {
17+
constructor(scope: Construct, id: string) {
18+
super(scope, id);
19+
20+
const onEventHandler = new Function(this, 'OnEvent', {
21+
code: Code.fromInline('foo'),
22+
handler: 'index.onEvent',
23+
runtime: Runtime.NODEJS_LATEST,
24+
});
25+
const isCompleteHandler = new Function(this, 'IsComplete', {
26+
code: Code.fromInline('foo'),
27+
handler: 'index.isComplete',
28+
runtime: Runtime.NODEJS_LATEST,
29+
});
30+
31+
new Provider(this, 'MyProviderWithCustomRole', {
32+
frameworkOnEventRole: new Role(this, 'MyRole', {
33+
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
34+
managedPolicies: [
35+
ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
36+
],
37+
}),
38+
frameworkCompleteAndTimeoutRole: new Role(this, 'MyRole2', {
39+
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
40+
managedPolicies: [
41+
ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
42+
],
43+
}),
44+
onEventHandler,
45+
isCompleteHandler,
46+
});
47+
}
48+
}
49+
50+
const app = new App();
51+
const stack = new TestStack(app, 'integ-provider-with-waiter-state-machine-custom-role');
52+
53+
new integ.IntegTest(app, 'IntegProviderWithWaiterStateMachineCustomRole', {
54+
testCases: [stack],
55+
diffAssets: true,
56+
});
57+
58+
app.synth();

‎packages/aws-cdk-lib/custom-resources/lib/provider-framework/provider.ts

+39-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as iam from '../../../aws-iam';
99
import * as kms from '../../../aws-kms';
1010
import * as lambda from '../../../aws-lambda';
1111
import * as logs from '../../../aws-logs';
12-
import { Duration } from '../../../core';
12+
import { Duration, ValidationError } from '../../../core';
1313

1414
const RUNTIME_HANDLER_PATH = path.join(__dirname, 'runtime');
1515
const FRAMEWORK_HANDLER_TIMEOUT = Duration.minutes(15); // keep it simple for now
@@ -117,13 +117,37 @@ export interface ProviderProps {
117117
/**
118118
* AWS Lambda execution role.
119119
*
120-
* The role that will be assumed by the AWS Lambda.
121-
* Must be assumable by the 'lambda.amazonaws.com' service principal.
120+
* The role is shared by provider framework's onEvent, isComplete lambda, and onTimeout Lambda functions.
121+
* This role will be assumed by the AWS Lambda, so it must be assumable by the 'lambda.amazonaws.com'
122+
* service principal.
122123
*
123124
* @default - A default role will be created.
125+
* @deprecated - Use frameworkOnEventLambdaRole, frameworkIsCompleteLambdaRole, frameworkOnTimeoutLambdaRole
124126
*/
125127
readonly role?: iam.IRole;
126128

129+
/**
130+
* Lambda execution role for provider framework's onEvent Lambda function. Note that this role must be assumed
131+
* by the 'lambda.amazonaws.com' service principal.
132+
*
133+
* This property cannot be used with 'role' property
134+
*
135+
* @default - A default role will be created.
136+
*/
137+
readonly frameworkOnEventRole?: iam.IRole;
138+
139+
/**
140+
* Lambda execution role for provider framework's isComplete/onTimeout Lambda function. Note that this role
141+
* must be assumed by the 'lambda.amazonaws.com' service principal. To prevent circular dependency problem
142+
* in the provider framework, please ensure you specify a different IAM Role for 'frameworkCompleteAndTimeoutRole'
143+
* from 'frameworkOnEventRole'.
144+
*
145+
* This property cannot be used with 'role' property
146+
*
147+
* @default - A default role will be created.
148+
*/
149+
readonly frameworkCompleteAndTimeoutRole?: iam.IRole;
150+
127151
/**
128152
* Provider Lambda name.
129153
*
@@ -202,6 +226,13 @@ export class Provider extends Construct implements ICustomResourceProvider {
202226
}
203227
}
204228

229+
if (props.role && (props.frameworkOnEventRole || props.frameworkCompleteAndTimeoutRole)) {
230+
throw new ValidationError('Cannot specify both "role" and any of "frameworkOnEventRole" or "frameworkCompleteAndTimeoutRole".', this);
231+
}
232+
if (!props.isCompleteHandler && props.frameworkCompleteAndTimeoutRole) {
233+
throw new ValidationError('Cannot specify "frameworkCompleteAndTimeoutRole" when "isCompleteHandler" is not specified.', this);
234+
}
235+
205236
this.onEventHandler = props.onEventHandler;
206237
this.isCompleteHandler = props.isCompleteHandler;
207238

@@ -214,11 +245,11 @@ export class Provider extends Construct implements ICustomResourceProvider {
214245
this.role = props.role;
215246
this.providerFunctionEnvEncryption = props.providerFunctionEnvEncryption;
216247

217-
const onEventFunction = this.createFunction(consts.FRAMEWORK_ON_EVENT_HANDLER_NAME, props.providerFunctionName);
248+
const onEventFunction = this.createFunction(consts.FRAMEWORK_ON_EVENT_HANDLER_NAME, props.providerFunctionName, props.frameworkOnEventRole);
218249

219250
if (this.isCompleteHandler) {
220-
const isCompleteFunction = this.createFunction(consts.FRAMEWORK_IS_COMPLETE_HANDLER_NAME);
221-
const timeoutFunction = this.createFunction(consts.FRAMEWORK_ON_TIMEOUT_HANDLER_NAME);
251+
const isCompleteFunction = this.createFunction(consts.FRAMEWORK_IS_COMPLETE_HANDLER_NAME, undefined, props.frameworkCompleteAndTimeoutRole);
252+
const timeoutFunction = this.createFunction(consts.FRAMEWORK_ON_TIMEOUT_HANDLER_NAME, undefined, props.frameworkCompleteAndTimeoutRole);
222253

223254
const retry = calculateRetryPolicy(props);
224255
const waiterStateMachine = new WaiterStateMachine(this, 'waiter-state-machine', {
@@ -263,7 +294,7 @@ export class Provider extends Construct implements ICustomResourceProvider {
263294
}));
264295
}
265296

266-
private createFunction(entrypoint: string, name?: string) {
297+
private createFunction(entrypoint: string, name?: string, role?: iam.IRole) {
267298
const fn = new lambda.Function(this, `framework-${entrypoint}`, {
268299
code: lambda.Code.fromAsset(RUNTIME_HANDLER_PATH, {
269300
exclude: ['*.ts'],
@@ -279,7 +310,7 @@ export class Provider extends Construct implements ICustomResourceProvider {
279310
vpc: this.vpc,
280311
vpcSubnets: this.vpcSubnets,
281312
securityGroups: this.securityGroups,
282-
role: this.role,
313+
role: this.role ?? role,
283314
functionName: name,
284315
environmentEncryption: this.providerFunctionEnvEncryption,
285316
});

‎packages/aws-cdk-lib/custom-resources/test/provider-framework/provider.test.ts

+250
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,256 @@ describe('role', () => {
770770
},
771771
});
772772
});
773+
774+
it('cannot specify both role and framework onEvent roles', () => {
775+
// GIVEN
776+
const stack = new Stack();
777+
778+
// WHEN
779+
expect(() => new cr.Provider(stack, 'MyProvider', {
780+
onEventHandler: new lambda.Function(stack, 'OnEventHandler', {
781+
code: new lambda.InlineCode('foo'),
782+
handler: 'index.onEvent',
783+
runtime: lambda.Runtime.NODEJS_LATEST,
784+
}),
785+
isCompleteHandler: new lambda.Function(stack, 'IsCompleteHandler', {
786+
code: new lambda.InlineCode('foo'),
787+
handler: 'index.onEvent',
788+
runtime: lambda.Runtime.NODEJS_LATEST,
789+
}),
790+
role: new iam.Role(stack, 'MyRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
791+
frameworkOnEventRole: new iam.Role(stack, 'MyRole2', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
792+
})).toThrow('Cannot specify both "role" and any of "frameworkOnEventRole" or "frameworkCompleteAndTimeoutRole"');
793+
});
794+
795+
it('cannot specify both role and framework complete/timeout roles', () => {
796+
// GIVEN
797+
const stack = new Stack();
798+
799+
// WHEN
800+
expect(() => new cr.Provider(stack, 'MyProvider', {
801+
onEventHandler: new lambda.Function(stack, 'OnEventHandler', {
802+
code: new lambda.InlineCode('foo'),
803+
handler: 'index.onEvent',
804+
runtime: lambda.Runtime.NODEJS_LATEST,
805+
}),
806+
isCompleteHandler: new lambda.Function(stack, 'IsCompleteHandler', {
807+
code: new lambda.InlineCode('foo'),
808+
handler: 'index.onEvent',
809+
runtime: lambda.Runtime.NODEJS_LATEST,
810+
}),
811+
role: new iam.Role(stack, 'MyRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
812+
frameworkCompleteAndTimeoutRole: new iam.Role(stack, 'MyRole2', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
813+
})).toThrow('Cannot specify both "role" and any of "frameworkOnEventRole" or "frameworkCompleteAndTimeoutRole"');
814+
});
815+
816+
it('Cannot specify "frameworkCompleteAndTimeoutRole" when "isCompleteHandler" is not specified.', () => {
817+
// GIVEN
818+
const stack = new Stack();
819+
820+
// WHEN
821+
expect(() => new cr.Provider(stack, 'MyProvider', {
822+
onEventHandler: new lambda.Function(stack, 'OnEventHandler', {
823+
code: new lambda.InlineCode('foo'),
824+
handler: 'index.onEvent',
825+
runtime: lambda.Runtime.NODEJS_LATEST,
826+
}),
827+
frameworkCompleteAndTimeoutRole: new iam.Role(stack, 'MyRole2', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
828+
})).toThrow('Cannot specify "frameworkCompleteAndTimeoutRole" when "isCompleteHandler" is not specified.');
829+
});
830+
831+
it('No circular dependency thrown.', () => {
832+
// GIVEN
833+
const app = new App({
834+
context: {
835+
'@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy': false,
836+
},
837+
});
838+
const stack = new Stack(app);
839+
840+
// WHEN
841+
new cr.Provider(stack, 'MyProvider', {
842+
onEventHandler: new lambda.Function(stack, 'OnEventHandler', {
843+
code: new lambda.InlineCode('foo'),
844+
handler: 'index.onEvent',
845+
runtime: lambda.Runtime.NODEJS_LATEST,
846+
}),
847+
isCompleteHandler: new lambda.Function(stack, 'IsCompleteHandler', {
848+
code: new lambda.InlineCode('foo'),
849+
handler: 'index.isComplete',
850+
runtime: lambda.Runtime.NODEJS_LATEST,
851+
}),
852+
frameworkOnEventRole: new iam.Role(stack, 'MyRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
853+
frameworkCompleteAndTimeoutRole: new iam.Role(stack, 'MyRole2', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.como') }),
854+
});
855+
856+
const template = Template.fromStack(stack);
857+
template.hasResourceProperties('AWS::IAM::Policy', {
858+
PolicyDocument: {
859+
Statement: [
860+
{
861+
Action: 'lambda:InvokeFunction',
862+
Effect: 'Allow',
863+
Resource: [
864+
{
865+
'Fn::GetAtt': [
866+
'OnEventHandler42BEBAE0',
867+
'Arn',
868+
],
869+
},
870+
{
871+
'Fn::Join': [
872+
'',
873+
[
874+
{
875+
'Fn::GetAtt': [
876+
'OnEventHandler42BEBAE0',
877+
'Arn',
878+
],
879+
},
880+
':*',
881+
],
882+
],
883+
},
884+
],
885+
},
886+
{
887+
Action: 'lambda:GetFunction',
888+
Effect: 'Allow',
889+
Resource: {
890+
'Fn::GetAtt': [
891+
'OnEventHandler42BEBAE0',
892+
'Arn',
893+
],
894+
},
895+
},
896+
{
897+
Action: 'lambda:InvokeFunction',
898+
Effect: 'Allow',
899+
Resource: [
900+
{
901+
'Fn::GetAtt': [
902+
'IsCompleteHandler7073F4DA',
903+
'Arn',
904+
],
905+
},
906+
{
907+
'Fn::Join': [
908+
'',
909+
[
910+
{
911+
'Fn::GetAtt': [
912+
'IsCompleteHandler7073F4DA',
913+
'Arn',
914+
],
915+
},
916+
':*',
917+
],
918+
],
919+
},
920+
],
921+
},
922+
{
923+
Action: 'lambda:GetFunction',
924+
Effect: 'Allow',
925+
Resource: {
926+
'Fn::GetAtt': [
927+
'IsCompleteHandler7073F4DA',
928+
'Arn',
929+
],
930+
},
931+
},
932+
{
933+
Action: 'states:StartExecution',
934+
Effect: 'Allow',
935+
Resource: {
936+
Ref: 'MyProviderwaiterstatemachineC1FBB9F9',
937+
},
938+
},
939+
],
940+
Version: '2012-10-17',
941+
},
942+
});
943+
template.hasResourceProperties('AWS::IAM::Policy', {
944+
PolicyDocument: {
945+
Statement: [
946+
{
947+
Action: 'lambda:InvokeFunction',
948+
Effect: 'Allow',
949+
Resource: [
950+
{
951+
'Fn::GetAtt': [
952+
'OnEventHandler42BEBAE0',
953+
'Arn',
954+
],
955+
},
956+
{
957+
'Fn::Join': [
958+
'',
959+
[
960+
{
961+
'Fn::GetAtt': [
962+
'OnEventHandler42BEBAE0',
963+
'Arn',
964+
],
965+
},
966+
':*',
967+
],
968+
],
969+
},
970+
],
971+
},
972+
{
973+
Action: 'lambda:GetFunction',
974+
Effect: 'Allow',
975+
Resource: {
976+
'Fn::GetAtt': [
977+
'OnEventHandler42BEBAE0',
978+
'Arn',
979+
],
980+
},
981+
},
982+
{
983+
Action: 'lambda:InvokeFunction',
984+
Effect: 'Allow',
985+
Resource: [
986+
{
987+
'Fn::GetAtt': [
988+
'IsCompleteHandler7073F4DA',
989+
'Arn',
990+
],
991+
},
992+
{
993+
'Fn::Join': [
994+
'',
995+
[
996+
{
997+
'Fn::GetAtt': [
998+
'IsCompleteHandler7073F4DA',
999+
'Arn',
1000+
],
1001+
},
1002+
':*',
1003+
],
1004+
],
1005+
},
1006+
],
1007+
},
1008+
{
1009+
Action: 'lambda:GetFunction',
1010+
Effect: 'Allow',
1011+
Resource: {
1012+
'Fn::GetAtt': [
1013+
'IsCompleteHandler7073F4DA',
1014+
'Arn',
1015+
],
1016+
},
1017+
},
1018+
],
1019+
Version: '2012-10-17',
1020+
},
1021+
});
1022+
});
7731023
});
7741024

7751025
describe('name', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.