Skip to content

Commit 2167289

Browse files
feat(servicecatalogappregistry-alpha): Introduce flag to control application sharing and association behavior for cross-account stacks (#24408)
Problem: * Currently, the ApplicationAssociator construct automatically shares the target Application with any accounts of cross-account stacks. [[code reference](https://github.com/aws/aws-cdk/blob/main/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts#L91-L95)] * If the owner of a cross-account stack is not part of the same AWS Organization as the owner of the ApplicationAssociator stack, or otherwise have not enabled cross-account sharing, during deployment the ApplicationAssociator will fail when attempting to share the application with the stack owner, with a message like below: ``` Principal 123456789012 is not in your AWS organization. You do not have permission to add external AWS accounts to a resource share. (Service: AWSRAM; Status Code: 400; Error Code: OperationNotPermittedException; Request ID: aaa; Proxy: null) ``` Feature: * We want to introduce a mechanism (`associateCrossAccountStacks` field in TargetApplicationOptions) where the user can specify if they want to allow sharing their application to any accounts of cross-account stacks in order to then subsequently associate the stack with the application. * This flag will be `false` by default. This allows customers to have their stack deployments proceed without being blocked on application sharing or cross-account associations. * If set to `false`, ApplicationAssociator will skip the application sharing and association for cross-account stacks. During synthesis, a warning will be displayed to notify that cross-account stacks were detected but sharing and association will be skipped. * If set to `true`, the application will be shared and then associated for cross-account stacks. This relies on the user properly setting up cross-account sharing beforehand. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 274c3d5 commit 2167289

19 files changed

+1051
-17
lines changed

packages/@aws-cdk/aws-servicecatalogappregistry/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,24 @@ const cdkPipeline = new ApplicationPipelineStack(app, 'CDKApplicationPipelineSta
198198
});
199199
```
200200

201+
By default, ApplicationAssociator will not perform cross-account stack associations with the target Application,
202+
to avoid deployment failures for accounts which have not been setup for cross-account associations.
203+
To enable cross-account stack associations, make sure all accounts are in the same organization as the
204+
target Application's account and that resource sharing is enabled within the organization.
205+
If you wish to turn on cross-account sharing and associations, set the `associateCrossAccountStacks` field to `true`,
206+
as shown in the example below:
207+
208+
```ts
209+
const app = new App();
210+
const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', {
211+
applications: [appreg.TargetApplication.createApplicationStack({
212+
associateCrossAccountStacks: true,
213+
applicationName: 'MyAssociatedApplication',
214+
env: { account: '123456789012', region: 'us-east-1' },
215+
})],
216+
});
217+
```
218+
201219
## Attribute Group
202220

203221
An AppRegistry attribute group acts as a container for user-defined attributes for an application.

packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export interface ApplicationAssociatorProps {
2424
* in case of a `Pipeline` stack, stage underneath the pipeline will not automatically be associated and
2525
* needs to be associated separately.
2626
*
27-
* If cross account stack is detected, then this construct will automatically share the application to consumer accounts.
27+
* If cross account stack is detected and `associateCrossAccountStacks` in `TargetApplicationOptions` is `true`,
28+
* then the application will automatically be shared with the consumer accounts to allow associations.
29+
* Otherwise, the application will not be shared.
2830
* Cross account feature will only work for non environment agnostic stacks.
2931
*/
3032
export class ApplicationAssociator extends Construct {
@@ -33,6 +35,7 @@ export class ApplicationAssociator extends Construct {
3335
*/
3436
private readonly application: IApplication;
3537
private readonly associatedStages: Set<cdk.Stage> = new Set();
38+
private readonly associateCrossAccountStacks?: boolean;
3639

3740
constructor(scope: cdk.App, id: string, props: ApplicationAssociatorProps) {
3841
super(scope, id);
@@ -42,8 +45,12 @@ export class ApplicationAssociator extends Construct {
4245
}
4346

4447
const targetApplication = props.applications[0];
45-
this.application = targetApplication.bind(scope).application;
46-
cdk.Aspects.of(scope).add(new CheckedStageStackAssociator(this));
48+
const targetBindResult = targetApplication.bind(scope);
49+
this.application = targetBindResult.application;
50+
this.associateCrossAccountStacks = targetBindResult.associateCrossAccountStacks;
51+
cdk.Aspects.of(scope).add(new CheckedStageStackAssociator(this, {
52+
associateCrossAccountStacks: this.associateCrossAccountStacks,
53+
}));
4754
}
4855

4956
/**
@@ -52,7 +59,9 @@ export class ApplicationAssociator extends Construct {
5259
*/
5360
public associateStage(stage: cdk.Stage): cdk.Stage {
5461
this.associatedStages.add(stage);
55-
cdk.Aspects.of(stage).add(new CheckedStageStackAssociator(this));
62+
cdk.Aspects.of(stage).add(new CheckedStageStackAssociator(this, {
63+
associateCrossAccountStacks: this.associateCrossAccountStacks,
64+
}));
5665
return stage;
5766
}
5867

packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,16 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
172172
}
173173

174174
/**
175-
* Associate all stacks present in construct's aspect with application.
175+
* Associate all stacks present in construct's aspect with application, including cross-account stacks.
176176
*
177177
* NOTE: This method won't automatically register stacks under pipeline stages,
178178
* and requires association of each pipeline stage by calling this method with stage Construct.
179179
*
180180
*/
181181
public associateAllStacksInScope(scope: Construct): void {
182-
cdk.Aspects.of(scope).add(new StageStackAssociator(this));
182+
cdk.Aspects.of(scope).add(new StageStackAssociator(this, {
183+
associateCrossAccountStacks: true,
184+
}));
183185
}
184186

185187
/**

packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts

+37-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import { ApplicationAssociator } from '../application-associator';
55
import { SharePermission } from '../common';
66
import { isRegionUnresolved, isAccountUnresolved } from '../private/utils';
77

8+
export interface StackAssociatorBaseProps {
9+
/**
10+
* Indicates if the target Application should be shared with the cross-account stack owners and then
11+
* associated with the cross-account stacks.
12+
*
13+
* @default - false
14+
*/
15+
readonly associateCrossAccountStacks?: boolean;
16+
}
17+
818
/**
919
* Aspect class, this will visit each node from the provided construct once.
1020
*
@@ -14,9 +24,14 @@ import { isRegionUnresolved, isAccountUnresolved } from '../private/utils';
1424
abstract class StackAssociatorBase implements IAspect {
1525
protected abstract readonly application: IApplication;
1626
protected abstract readonly applicationAssociator?: ApplicationAssociator;
27+
protected readonly associateCrossAccountStacks?: boolean;
1728

1829
protected readonly sharedAccounts: Set<string> = new Set();
1930

31+
constructor(props?: StackAssociatorBaseProps) {
32+
this.associateCrossAccountStacks = props?.associateCrossAccountStacks ?? false;
33+
}
34+
2035
public visit(node: IConstruct): void {
2136
// verify if a stage in a particular stack is associated to Application.
2237
node.node.children.forEach((childNode) => {
@@ -42,6 +57,13 @@ abstract class StackAssociatorBase implements IAspect {
4257
* @param node A Stage stack.
4358
*/
4459
private associate(node: Stack): void {
60+
if (!isRegionUnresolved(this.application.env.region, node.region)
61+
&& node.account != this.application.env.account
62+
&& !this.associateCrossAccountStacks) {
63+
// Skip association when cross-account sharing/association is not enabled.
64+
// A warning will have been displayed as part of `handleCrossAccountStack()`.
65+
return;
66+
}
4567
this.application.associateApplicationWithStack(node);
4668
}
4769

@@ -77,7 +99,7 @@ abstract class StackAssociatorBase implements IAspect {
7799

78100
/**
79101
* Handle cross-account association.
80-
* If any stack is evaluated as cross-account than that of application,
102+
* If any stack is evaluated as cross-account than that of application, and cross-account option is enabled,
81103
* then we will share the application to the stack owning account.
82104
*
83105
* @param node Cfn stack.
@@ -89,12 +111,17 @@ abstract class StackAssociatorBase implements IAspect {
89111
}
90112

91113
if (node.account != this.application.env.account && !this.sharedAccounts.has(node.account)) {
92-
this.application.shareApplication({
93-
accounts: [node.account],
94-
sharePermission: SharePermission.ALLOW_ACCESS,
95-
});
114+
if (this.associateCrossAccountStacks) {
115+
this.application.shareApplication({
116+
accounts: [node.account],
117+
sharePermission: SharePermission.ALLOW_ACCESS,
118+
});
96119

97-
this.sharedAccounts.add(node.account);
120+
this.sharedAccounts.add(node.account);
121+
} else {
122+
this.warning(node, 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled.');
123+
return;
124+
}
98125
}
99126
}
100127
}
@@ -103,8 +130,8 @@ export class CheckedStageStackAssociator extends StackAssociatorBase {
103130
protected readonly application: IApplication;
104131
protected readonly applicationAssociator?: ApplicationAssociator;
105132

106-
constructor(app: ApplicationAssociator) {
107-
super();
133+
constructor(app: ApplicationAssociator, props?: StackAssociatorBaseProps) {
134+
super(props);
108135
this.application = app.appRegistryApplication();
109136
this.applicationAssociator = app;
110137
}
@@ -114,8 +141,8 @@ export class StageStackAssociator extends StackAssociatorBase {
114141
protected readonly application: IApplication;
115142
protected readonly applicationAssociator?: ApplicationAssociator;
116143

117-
constructor(app: IApplication) {
118-
super();
144+
constructor(app: IApplication, props?: StackAssociatorBaseProps) {
145+
super(props);
119146
this.application = app;
120147
}
121148
}

packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export interface TargetApplicationCommonOptions extends cdk.StackProps {
1515
* @deprecated - Use `stackName` instead to control the name and id of the stack
1616
*/
1717
readonly stackId?: string;
18+
19+
/**
20+
* Determines whether any cross-account stacks defined in the CDK app definition should be associated with the
21+
* target application. If set to `true`, the application will first be shared with the accounts that own the stacks.
22+
*
23+
* @default - false
24+
*/
25+
readonly associateCrossAccountStacks?: boolean;
1826
}
1927

2028

@@ -87,6 +95,10 @@ export interface BindTargetApplicationResult {
8795
* Created or imported application.
8896
*/
8997
readonly application: IApplication;
98+
/**
99+
* Enables cross-account associations with the target application.
100+
*/
101+
readonly associateCrossAccountStacks: boolean;
90102
}
91103

92104
/**
@@ -124,6 +136,7 @@ class CreateTargetApplication extends TargetApplication {
124136

125137
return {
126138
application: appRegApplication,
139+
associateCrossAccountStacks: this.applicationOptions.associateCrossAccountStacks ?? false,
127140
};
128141
}
129142
}
@@ -144,6 +157,7 @@ class ExistingTargetApplication extends TargetApplication {
144157
const appRegApplication = Application.fromApplicationArn(applicationStack, 'ExistingApplication', this.applicationOptions.applicationArnValue);
145158
return {
146159
application: appRegApplication,
160+
associateCrossAccountStacks: this.applicationOptions.associateCrossAccountStacks ?? false,
147161
};
148162
}
149163
}

packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,38 @@ describe('Scope based Associations with Application with Cross Region/Account',
179179
});
180180
}),
181181

182-
test('ApplicationAssociator with cross region stacks inside cdkApp throws error', () => {
182+
test('ApplicationAssociator with cross account stacks inside cdkApp gives warning if associateCrossAccountStacks is not provided', () => {
183+
new appreg.ApplicationAssociator(app, 'MyApplication', {
184+
applications: [appreg.TargetApplication.createApplicationStack({
185+
applicationName: 'MyAssociatedApplication',
186+
stackName: 'MyAssociatedApplicationStack',
187+
env: { account: 'account2', region: 'region' },
188+
})],
189+
});
190+
191+
const crossAccountStack = new cdk.Stack(app, 'crossRegionStack', {
192+
env: { account: 'account', region: 'region' },
193+
});
194+
Annotations.fromStack(crossAccountStack).hasWarning('*', 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled.');
195+
});
196+
197+
test('ApplicationAssociator with cross account stacks inside cdkApp does not give warning if associateCrossAccountStacks is set to true', () => {
198+
new appreg.ApplicationAssociator(app, 'MyApplication', {
199+
applications: [appreg.TargetApplication.createApplicationStack({
200+
applicationName: 'MyAssociatedApplication',
201+
stackName: 'MyAssociatedApplicationStack',
202+
associateCrossAccountStacks: true,
203+
env: { account: 'account', region: 'region' },
204+
})],
205+
});
206+
207+
const crossAccountStack = new cdk.Stack(app, 'crossRegionStack', {
208+
env: { account: 'account2', region: 'region' },
209+
});
210+
Annotations.fromStack(crossAccountStack).hasNoWarning('*', 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled.');
211+
});
212+
213+
test('ApplicationAssociator with cross region stacks inside cdkApp gives warning', () => {
183214
new appreg.ApplicationAssociator(app, 'MyApplication', {
184215
applications: [appreg.TargetApplication.createApplicationStack({
185216
applicationName: 'MyAssociatedApplication',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "31.0.0",
3+
"files": {
4+
"19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc": {
5+
"source": {
6+
"path": "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"Resources": {
3+
"AppRegistryAssociation": {
4+
"Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation",
5+
"Properties": {
6+
"Application": "AppRegistryAssociatedApplication",
7+
"Resource": {
8+
"Ref": "AWS::StackId"
9+
},
10+
"ResourceType": "CFN_STACK"
11+
}
12+
}
13+
},
14+
"Parameters": {
15+
"BootstrapVersion": {
16+
"Type": "AWS::SSM::Parameter::Value<String>",
17+
"Default": "/cdk-bootstrap/hnb659fds/version",
18+
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
19+
}
20+
},
21+
"Rules": {
22+
"CheckBootstrapVersion": {
23+
"Assertions": [
24+
{
25+
"Assert": {
26+
"Fn::Not": [
27+
{
28+
"Fn::Contains": [
29+
[
30+
"1",
31+
"2",
32+
"3",
33+
"4",
34+
"5"
35+
],
36+
{
37+
"Ref": "BootstrapVersion"
38+
}
39+
]
40+
}
41+
]
42+
},
43+
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
44+
}
45+
]
46+
}
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "31.0.0",
3+
"files": {
4+
"1db9045d198a45233cf79d4f87770e1d94d5efd69f70a11d40e3a6310ba3b26c": {
5+
"source": {
6+
"path": "TestAppRegistryApplicationStack.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"000000000000-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-000000000000-${AWS::Region}",
12+
"objectKey": "1db9045d198a45233cf79d4f87770e1d94d5efd69f70a11d40e3a6310ba3b26c.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-file-publishing-role-000000000000-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}

0 commit comments

Comments
 (0)