Skip to content

Commit 65265e1

Browse files
authored
fix(core): crossRegionReferences don't work across multiple regions (#25384)
The first attempt to fix this in #25190 didn't work because it didn't account for the fact that when exporting to multiple regions, we create multiple `ExportWriter`s that all use the same provider (and provider role). This PR fixes that by adding the policy cross region ARNs directly to the custom resource provider (1 per stack) rather than the `ExportWriter` (multiple per stack). I also updated the test case to better account for this scenario. fixes #25377 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a0ad49d commit 65265e1

File tree

8 files changed

+352
-327
lines changed

8 files changed

+352
-327
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-cloudformation/test/integ.core-cross-region-references.js.snapshot/cross-region-producer.assets.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@
2929
}
3030
}
3131
},
32-
"a645bb0b063dc77d60a1af0c1e427999161c49fe9008543153cb48fed642a943": {
32+
"f93ad4a2004b446276ef3fac1ada613b58a45257e6bfd66f18ab8119232f6131": {
3333
"source": {
3434
"path": "cross-region-producer.template.json",
3535
"packaging": "file"
3636
},
3737
"destinations": {
3838
"current_account-us-east-1": {
3939
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-us-east-1",
40-
"objectKey": "a645bb0b063dc77d60a1af0c1e427999161c49fe9008543153cb48fed642a943.json",
40+
"objectKey": "f93ad4a2004b446276ef3fac1ada613b58a45257e6bfd66f18ab8119232f6131.json",
4141
"region": "us-east-1",
4242
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-us-east-1"
4343
}

packages/@aws-cdk-testing/framework-integ/test/aws-cloudformation/test/integ.core-cross-region-references.js.snapshot/cross-region-producer.template.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@
9898
":parameter/cdk/exports/*"
9999
]
100100
]
101+
},
102+
{
103+
"Fn::Join": [
104+
"",
105+
[
106+
"arn:aws:ssm:us-west-2:",
107+
{
108+
"Ref": "AWS::AccountId"
109+
},
110+
":parameter/cdk/exports/*"
111+
]
112+
]
101113
}
102114
],
103115
"Action": [

packages/@aws-cdk-testing/framework-integ/test/aws-cloudformation/test/integ.core-cross-region-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.assets.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
}
1515
}
1616
},
17-
"a916bc3a5655b0f9f8fc2ca727c849da1870288a890fc7a778f18859c60dc3b2": {
17+
"abfdad43ef2795ab9e253a1b3cb3bfd72c1d00fa011b42ce99ffdff17b83c947": {
1818
"source": {
1919
"path": "crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json",
2020
"packaging": "file"
2121
},
2222
"destinations": {
2323
"current_account-current_region": {
2424
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
25-
"objectKey": "a916bc3a5655b0f9f8fc2ca727c849da1870288a890fc7a778f18859c60dc3b2.json",
25+
"objectKey": "abfdad43ef2795ab9e253a1b3cb3bfd72c1d00fa011b42ce99ffdff17b83c947.json",
2626
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
2727
}
2828
}

packages/@aws-cdk-testing/framework-integ/test/aws-cloudformation/test/integ.core-cross-region-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"StackName": "cross-region-producer"
1616
},
1717
"flattenResponse": "false",
18-
"salt": "1682378118042"
18+
"salt": "1682942114901"
1919
},
2020
"UpdateReplacePolicy": "Delete",
2121
"DeletionPolicy": "Delete"
@@ -118,7 +118,7 @@
118118
"StackName": "cross-region-producer"
119119
},
120120
"flattenResponse": "false",
121-
"salt": "1682378118042"
121+
"salt": "1682942114901"
122122
},
123123
"DependsOn": [
124124
"AwsApiCallCloudFormationdeleteStack"

packages/@aws-cdk-testing/framework-integ/test/aws-cloudformation/test/integ.core-cross-region-references.js.snapshot/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"validateOnSynth": false,
1818
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-us-east-1",
1919
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-us-east-1",
20-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-us-east-1/a645bb0b063dc77d60a1af0c1e427999161c49fe9008543153cb48fed642a943.json",
20+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-us-east-1/f93ad4a2004b446276ef3fac1ada613b58a45257e6bfd66f18ab8119232f6131.json",
2121
"requiresBootstrapStackVersion": 6,
2222
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2323
"additionalDependencies": [
@@ -328,7 +328,7 @@
328328
"validateOnSynth": false,
329329
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
330330
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
331-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/a916bc3a5655b0f9f8fc2ca727c849da1870288a890fc7a778f18859c60dc3b2.json",
331+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/abfdad43ef2795ab9e253a1b3cb3bfd72c1d00fa011b42ce99ffdff17b83c947.json",
332332
"requiresBootstrapStackVersion": 6,
333333
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
334334
"additionalDependencies": [

packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-writer-provider.ts

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { Intrinsic } from '../../private/intrinsic';
99
import { makeUniqueId } from '../../private/uniqueid';
1010
import { Reference } from '../../reference';
1111
import { Stack } from '../../stack';
12-
import { builtInCustomResourceProviderNodeRuntime, CustomResourceProvider } from '../custom-resource-provider';
1312
import { Token } from '../../token';
13+
import { builtInCustomResourceProviderNodeRuntime, CustomResourceProvider, CustomResourceProviderProps } from '../custom-resource-provider';
1414

1515
/**
1616
* Properties for an ExportReader
@@ -24,6 +24,43 @@ export interface ExportWriterProps {
2424
readonly region?: string;
2525
}
2626

27+
/**
28+
* Create our own CustomResourceProvider so that we can add a single policy
29+
* with a list of ARNs instead of having to create a separate policy statement per ARN.
30+
*/
31+
class CRProvider extends CustomResourceProvider {
32+
public static getOrCreateProvider(scope: Construct, uniqueid: string, props: CustomResourceProviderProps): CRProvider {
33+
const id = `${uniqueid}CustomResourceProvider`;
34+
const stack = Stack.of(scope);
35+
const provider = stack.node.tryFindChild(id) as CRProvider
36+
?? new CRProvider(stack, id, props);
37+
38+
return provider;
39+
}
40+
41+
private readonly resourceArns = new Set<string>();
42+
constructor(scope: Construct, id: string, props: CustomResourceProviderProps) {
43+
super(scope, id, props);
44+
this.addToRolePolicy({
45+
Effect: 'Allow',
46+
Resource: Lazy.list({ produce: () => Array.from(this.resourceArns) }),
47+
Action: [
48+
'ssm:DeleteParameters',
49+
'ssm:ListTagsForResource',
50+
'ssm:GetParameters',
51+
'ssm:PutParameter',
52+
],
53+
});
54+
}
55+
56+
/**
57+
* Add a resource ARN to the existing policy statement
58+
*/
59+
public addResourceArn(arn: string): void {
60+
this.resourceArns.add(arn);
61+
}
62+
}
63+
2764
/**
2865
* Creates a custom resource that will return a list of stack exports from a given
2966
* AWS region. The export can then be referenced by the export name.
@@ -55,34 +92,20 @@ export class ExportWriter extends Construct {
5592
});
5693
}
5794
private readonly _references: CrossRegionExports = {};
58-
private readonly provider: CustomResourceProvider;
59-
private readonly resourceArns = new Set<string>;
95+
private readonly provider: CRProvider;
6096
constructor(scope: Construct, id: string, props: ExportWriterProps) {
6197
super(scope, id);
6298
const stack = Stack.of(this);
6399
const region = props.region ?? stack.region;
64-
this.addRegionToPolicy(region);
65100

66101
const resourceType = 'Custom::CrossRegionExportWriter';
67-
this.provider = CustomResourceProvider.getOrCreateProvider(this, resourceType, {
102+
this.provider = CRProvider.getOrCreateProvider(this, resourceType, {
68103
codeDirectory: path.join(__dirname, 'cross-region-ssm-writer-handler'),
69104
runtime: builtInCustomResourceProviderNodeRuntime(this),
70-
policyStatements: [{
71-
Effect: 'Allow',
72-
Resource: Lazy.list({
73-
produce: () => [
74-
...Array.from(this.resourceArns),
75-
],
76-
}),
77-
Action: [
78-
'ssm:DeleteParameters',
79-
'ssm:ListTagsForResource',
80-
'ssm:GetParameters',
81-
'ssm:PutParameter',
82-
],
83-
}],
84105
});
85106

107+
this.addRegionToPolicy(region);
108+
86109
const properties: ExportWriterCRProps = {
87110
region: region,
88111
exports: Lazy.any({ produce: () => this._references }),
@@ -126,14 +149,12 @@ export class ExportWriter extends Construct {
126149
*/
127150
private addRegionToPolicy(region: string): void {
128151
if (!Token.isUnresolved(region)) {
129-
this.resourceArns.add(
130-
Stack.of(this).formatArn({
131-
service: 'ssm',
132-
resource: 'parameter',
133-
region,
134-
resourceName: `${SSM_EXPORT_PATH_PREFIX}*`,
135-
}),
136-
);
152+
this.provider.addResourceArn(Stack.of(this).formatArn({
153+
service: 'ssm',
154+
resource: 'parameter',
155+
region,
156+
resourceName: `${SSM_EXPORT_PATH_PREFIX}*`,
157+
}));
137158
}
138159
}
139160

packages/aws-cdk-lib/core/lib/private/refs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// CROSS REFERENCES
33
// ----------------------------------------------------
44

5-
import * as cxapi from '../../../cx-api';
65
import { IConstruct } from 'constructs';
76
import { CfnReference } from './cfn-reference';
87
import { Intrinsic } from './intrinsic';
98
import { findTokens } from './resolve';
109
import { makeUniqueId } from './uniqueid';
10+
import * as cxapi from '../../../cx-api';
1111
import { CfnElement } from '../cfn-element';
1212
import { CfnOutput } from '../cfn-output';
1313
import { CfnParameter } from '../cfn-parameter';
@@ -233,11 +233,11 @@ function createCrossRegionImportValue(reference: Reference, importStack: Stack):
233233

234234
// get or create the export writer
235235
const writerConstructName = makeUniqueId(['ExportsWriter', importStack.region]);
236-
const exportReader = ExportWriter.getOrCreate(exportingStack, writerConstructName, {
236+
const exportWriter = ExportWriter.getOrCreate(exportingStack, writerConstructName, {
237237
region: importStack.region,
238238
});
239239

240-
const exported = exportReader.exportValue(exportName, reference, importStack);
240+
const exported = exportWriter.exportValue(exportName, reference, importStack);
241241
if (importStack.nestedStackParent) {
242242
return createNestedStackParameter(importStack, (exported as CfnReference), exported);
243243
}

0 commit comments

Comments
 (0)