Skip to content

Commit 1593500

Browse files
authored
feat(core): configure Stack SNS notification ARNs on the Stack construct (#31107)
### Issue # (if applicable) #8581. ### Reason for this change It is easier and clearer to specify the SNS Topic ARNs on the stack construct itself instead of passing it as a command line argument. ### Description of changes Added a new optional stack prop, `notificationArns`, that is written to the CloudAssembly and concatenated with the CLI option `--notification-arns`. Don't forget to select stacks by hierarchical ID (currently display name, in our tests) when writing certain test code. Otherwise, the tests may not select the stack you expect. Depends on: cdklabs/cdk-assets#87 and cdklabs/cloud-assembly-schema#58. ### Description of how you validated changes Unit tests + CLI integ test. Framework integ tests not included because they would require an externally-created SNS Topic, which is not what we want in integ tests; besides, the case is covered by the CLI integ test. ### Checklist - [x] 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 b49032b commit 1593500

File tree

20 files changed

+527
-165
lines changed

20 files changed

+527
-165
lines changed

Diff for: packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

+10
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,12 @@ class BuiltinLambdaStack extends cdk.Stack {
639639
}
640640
}
641641

642+
class NotificationArnPropStack extends cdk.Stack {
643+
constructor(parent, id, props) {
644+
super(parent, id, props);
645+
new sns.Topic(this, 'topic');
646+
}
647+
}
642648
class AppSyncHotswapStack extends cdk.Stack {
643649
constructor(parent, id, props) {
644650
super(parent, id, props);
@@ -708,6 +714,10 @@ switch (stackSet) {
708714
new DockerStack(app, `${stackPrefix}-docker`);
709715
new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`);
710716

717+
new NotificationArnPropStack(app, `${stackPrefix}-notification-arn-prop`, {
718+
notificationArns: [`arn:aws:sns:${defaultEnv.region}:${defaultEnv.account}:${stackPrefix}-test-topic-prop`],
719+
});
720+
711721
// SSO stacks
712722
new SsoInstanceAccessControlConfig(app, `${stackPrefix}-sso-access-control`);
713723
new SsoAssignment(app, `${stackPrefix}-sso-assignment`);

Diff for: packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

+33-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
withCDKMigrateFixture,
3434
withExtendedTimeoutFixture,
3535
randomString,
36+
withoutBootstrap,
3637
} from '../../lib';
3738

3839
jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime
@@ -276,9 +277,12 @@ integTest(
276277
}),
277278
);
278279

280+
// bootstrapping also performs synthesis. As it turns out, bootstrap-stage synthesis still causes the lookups to be cached, meaning that the lookup never
281+
// happens when we actually call `cdk synth --no-lookups`. This results in the error never being thrown, because it never tries to lookup anything.
282+
// Fix this by not trying to bootstrap; there's no need to bootstrap anyway, since the test never tries to deploy anything.
279283
integTest(
280284
'context in stage propagates to top',
281-
withDefaultFixture(async (fixture) => {
285+
withoutBootstrap(async (fixture) => {
282286
await expect(
283287
fixture.cdkSynth({
284288
// This will make it error to prove that the context bubbles up, and also that we can fail on command
@@ -613,12 +617,13 @@ integTest(
613617
);
614618

615619
integTest(
616-
'deploy with notification ARN',
620+
'deploy with notification ARN as flag',
617621
withDefaultFixture(async (fixture) => {
618-
const topicName = `${fixture.stackNamePrefix}-test-topic`;
622+
const topicName = `${fixture.stackNamePrefix}-test-topic-flag`;
619623

620624
const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName }));
621625
const topicArn = response.TopicArn!;
626+
622627
try {
623628
await fixture.cdkDeploy('test-2', {
624629
options: ['--notification-arns', topicArn],
@@ -641,6 +646,31 @@ integTest(
641646
}),
642647
);
643648

649+
integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixture) => {
650+
const topicName = `${fixture.stackNamePrefix}-test-topic-prop`;
651+
652+
const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName }));
653+
const topicArn = response.TopicArn!;
654+
655+
try {
656+
await fixture.cdkDeploy('notification-arn-prop');
657+
658+
// verify that the stack we deployed has our notification ARN
659+
const describeResponse = await fixture.aws.cloudFormation.send(
660+
new DescribeStacksCommand({
661+
StackName: fixture.fullStackName('notification-arn-prop'),
662+
}),
663+
);
664+
expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]);
665+
} finally {
666+
await fixture.aws.sns.send(
667+
new DeleteTopicCommand({
668+
TopicArn: topicArn,
669+
}),
670+
);
671+
}
672+
}));
673+
644674
// NOTE: this doesn't currently work with modern-style synthesis, as the bootstrap
645675
// role by default will not have permission to iam:PassRole the created role.
646676
integTest(

Diff for: packages/@aws-cdk/cx-api/FEATURE_FLAGS.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Flags come in three types:
7373
| [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) |
7474
| [@aws-cdk/aws-s3:keepNotificationInImportedBucket](#aws-cdkaws-s3keepnotificationinimportedbucket) | When enabled, Adding notifications to a bucket in the current stack will not remove notification from imported stack. | 2.155.0 | (fix) |
7575
| [@aws-cdk/aws-stepfunctions-tasks:useNewS3UriParametersForBedrockInvokeModelTask](#aws-cdkaws-stepfunctions-tasksusenews3uriparametersforbedrockinvokemodeltask) | When enabled, use new props for S3 URI field in task definition of state machine for bedrock invoke model. | 2.156.0 | (fix) |
76+
| [@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions](#aws-cdkaws-ecsreduceec2fargatecloudwatchpermissions) | When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration | 2.159.0 | (fix) |
7677

7778
<!-- END table -->
7879

@@ -134,7 +135,8 @@ The following json shows the current recommended set of flags, as `cdk init` wou
134135
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
135136
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
136137
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
137-
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false
138+
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
139+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true
138140
}
139141
}
140142
```
@@ -1378,4 +1380,22 @@ When this feature flag is enabled, specify newly introduced props 's3InputUri' a
13781380
**Compatibility with old behavior:** Disable the feature flag to use input and output path fields for s3 URI
13791381

13801382

1383+
### @aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions
1384+
1385+
*When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration* (fix)
1386+
1387+
Currently, we automatically add a number of cloudwatch permissions to the task role when no cloudwatch log group is
1388+
specified as logConfiguration and it will grant 'Resources': ['*'] to the task role.
1389+
1390+
When this feature flag is enabled, we will only grant the necessary permissions when users specify cloudwatch log group.
1391+
1392+
1393+
| Since | Default | Recommended |
1394+
| ----- | ----- | ----- |
1395+
| (not in v1) | | |
1396+
| 2.159.0 | `false` | `true` |
1397+
1398+
**Compatibility with old behavior:** Disable the feature flag to continue grant permissions to log group when no log group is specified
1399+
1400+
13811401
<!-- END details -->

Diff for: packages/@aws-cdk/cx-api/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@
8282
"semver": "^7.6.3"
8383
},
8484
"peerDependencies": {
85-
"@aws-cdk/cloud-assembly-schema": "^36.0.5"
85+
"@aws-cdk/cloud-assembly-schema": "^38.0.0"
8686
},
8787
"license": "Apache-2.0",
8888
"devDependencies": {
8989
"@aws-cdk/cdk-build-tools": "0.0.0",
90-
"@aws-cdk/cloud-assembly-schema": "^36.0.24",
90+
"@aws-cdk/cloud-assembly-schema": "^38.0.0",
9191
"@aws-cdk/pkglint": "0.0.0",
9292
"@types/jest": "^29.5.12",
9393
"@types/mock-fs": "^4.13.4",

Diff for: packages/@aws-cdk/integ-runner/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@
7171
},
7272
"dependencies": {
7373
"chokidar": "^3.6.0",
74-
"@aws-cdk/cloud-assembly-schema": "^36.0.24",
74+
"@aws-cdk/cloud-assembly-schema": "^38.0.0",
7575
"@aws-cdk/cloudformation-diff": "0.0.0",
7676
"@aws-cdk/cx-api": "0.0.0",
77+
"cdk-assets": "^2.154.0",
7778
"@aws-cdk/aws-service-spec": "^0.1.24",
78-
"cdk-assets": "^2.151.29",
79+
7980
"@aws-cdk/cdk-cli-wrapper": "0.0.0",
8081
"aws-cdk": "0.0.0",
8182
"chalk": "^4",

Diff for: packages/aws-cdk-lib/core/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,18 @@ const stack = new Stack(app, 'StackName', {
12421242
});
12431243
```
12441244

1245+
### Receiving CloudFormation Stack Events
1246+
1247+
You can add one or more SNS Topic ARNs to any Stack:
1248+
1249+
```ts
1250+
const stack = new Stack(app, 'StackName', {
1251+
notificationArns: ['arn:aws:sns:us-east-1:23456789012:Topic'],
1252+
});
1253+
```
1254+
1255+
Stack events will be sent to any SNS Topics in this list.
1256+
12451257
### CfnJson
12461258

12471259
`CfnJson` allows you to postpone the resolution of a JSON blob from

Diff for: packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function addStackArtifactToAssembly(
4848
terminationProtection: stack.terminationProtection,
4949
tags: nonEmptyDict(stack.tags.tagValues()),
5050
validateOnSynth: session.validateOnSynth,
51+
notificationArns: stack._notificationArns,
5152
...stackProps,
5253
...stackNameProperty,
5354
};

Diff for: packages/aws-cdk-lib/core/lib/stack.ts

+22
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ export interface StackProps {
127127
*/
128128
readonly tags?: { [key: string]: string };
129129

130+
/**
131+
* SNS Topic ARNs that will receive stack events.
132+
*
133+
* @default - no notfication arns.
134+
*/
135+
readonly notificationArns?: string[];
136+
130137
/**
131138
* Synthesis method to use while deploying this stack
132139
*
@@ -364,6 +371,13 @@ export class Stack extends Construct implements ITaggable {
364371
*/
365372
public readonly _crossRegionReferences: boolean;
366373

374+
/**
375+
* SNS Notification ARNs to receive stack events.
376+
*
377+
* @internal
378+
*/
379+
public readonly _notificationArns: string[];
380+
367381
/**
368382
* Logical ID generation strategy
369383
*/
@@ -451,6 +465,14 @@ export class Stack extends Construct implements ITaggable {
451465
}
452466
this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags);
453467

468+
for (const notificationArn of props.notificationArns ?? []) {
469+
if (Token.isUnresolved(notificationArn)) {
470+
throw new Error(`Stack '${id}' includes one or more tokens in its notification ARNs: ${props.notificationArns}`);
471+
}
472+
}
473+
474+
this._notificationArns = props.notificationArns ?? [];
475+
454476
if (!VALID_STACK_NAME_REGEX.test(this.stackName)) {
455477
throw new Error(`Stack name must match the regular expression: ${VALID_STACK_NAME_REGEX.toString()}, got '${this.stackName}'`);
456478
}

Diff for: packages/aws-cdk-lib/core/test/stack.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -2075,6 +2075,32 @@ describe('stack', () => {
20752075
expect(asm.getStackArtifact(stack2.artifactId).tags).toEqual(expected);
20762076
});
20772077

2078+
test('stack notification arns are reflected in the stack artifact properties', () => {
2079+
// GIVEN
2080+
const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'];
2081+
const app = new App({ stackTraces: false });
2082+
const stack1 = new Stack(app, 'stack1', {
2083+
notificationArns: NOTIFICATION_ARNS,
2084+
});
2085+
2086+
// WHEN
2087+
const asm = app.synth();
2088+
2089+
// THEN
2090+
expect(asm.getStackArtifact(stack1.artifactId).notificationArns).toEqual(NOTIFICATION_ARNS);
2091+
});
2092+
2093+
test('throws if stack notification arns contain tokens', () => {
2094+
// GIVEN
2095+
const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'];
2096+
const app = new App({ stackTraces: false });
2097+
2098+
// THEN
2099+
expect(() => new Stack(app, 'stack1', {
2100+
notificationArns: [...NOTIFICATION_ARNS, Aws.URL_SUFFIX],
2101+
})).toThrow('includes one or more tokens in its notification ARNs');
2102+
});
2103+
20782104
test('Termination Protection is reflected in Cloud Assembly artifact', () => {
20792105
// if the root is an app, invoke "synth" to avoid double synthesis
20802106
const app = new App();

Diff for: packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export class CloudFormationStackArtifact extends CloudArtifact {
5454
*/
5555
public readonly tags: { [id: string]: string };
5656

57+
/**
58+
* SNS Topics that will receive stack events.
59+
*/
60+
public readonly notificationArns: string[];
61+
5762
/**
5863
* The physical name of this stack.
5964
*/
@@ -158,6 +163,7 @@ export class CloudFormationStackArtifact extends CloudArtifact {
158163
// We get the tags from 'properties' if available (cloud assembly format >= 6.0.0), otherwise
159164
// from the stack metadata
160165
this.tags = properties.tags ?? this.tagsFromMetadata();
166+
this.notificationArns = properties.notificationArns ?? [];
161167
this.assumeRoleArn = properties.assumeRoleArn;
162168
this.assumeRoleExternalId = properties.assumeRoleExternalId;
163169
this.cloudFormationExecutionRoleArn = properties.cloudFormationExecutionRoleArn;

Diff for: packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ afterEach(() => {
2121
rimraf(builder.outdir);
2222
});
2323

24+
test('read notification arns from artifact properties', () => {
25+
// GIVEN
26+
const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'];
27+
builder.addArtifact('Stack', {
28+
...stackBase,
29+
properties: {
30+
...stackBase.properties,
31+
notificationArns: NOTIFICATION_ARNS,
32+
},
33+
});
34+
35+
// WHEN
36+
const assembly = builder.buildAssembly();
37+
38+
// THEN
39+
expect(assembly.getStackByName('Stack').notificationArns).toEqual(NOTIFICATION_ARNS);
40+
});
41+
2442
test('read tags from artifact properties', () => {
2543
// GIVEN
2644
builder.addArtifact('Stack', {

Diff for: packages/aws-cdk-lib/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
"@aws-cdk/asset-awscli-v1": "^2.2.202",
123123
"@aws-cdk/asset-kubectl-v20": "^2.1.2",
124124
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0",
125-
"@aws-cdk/cloud-assembly-schema": "^36.0.24",
125+
"@aws-cdk/cloud-assembly-schema": "^38.0.0",
126126
"@balena/dockerignore": "^1.0.2",
127127
"case": "1.6.3",
128128
"fs-extra": "^11.2.0",

Diff for: packages/aws-cdk/lib/api/deploy-stack.ts

+10
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,12 @@ async function canSkipDeploy(
644644
return false;
645645
}
646646

647+
// Notification arns have changed
648+
if (!arrayEquals(cloudFormationStack.notificationArns, deployStackOptions.notificationArns ?? [])) {
649+
debug(`${deployName}: notification arns have changed`);
650+
return false;
651+
}
652+
647653
// Termination protection has been updated
648654
if (!!deployStackOptions.stack.terminationProtection !== !!cloudFormationStack.terminationProtection) {
649655
debug(`${deployName}: termination protection has been updated`);
@@ -694,3 +700,7 @@ function suffixWithErrors(msg: string, errors?: string[]) {
694700
? `${msg}: ${errors.join(', ')}`
695701
: msg;
696702
}
703+
704+
function arrayEquals(a: any[], b: any[]): boolean {
705+
return a.every(item => b.includes(item)) && b.every(item => a.includes(item));
706+
}

Diff for: packages/aws-cdk/lib/api/util/cloudformation.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,21 @@ export class CloudFormationStack {
138138
/**
139139
* The stack's current tags
140140
*
141-
* Empty list of the stack does not exist
141+
* Empty list if the stack does not exist
142142
*/
143143
public get tags(): CloudFormation.Tags {
144144
return this.stack?.Tags || [];
145145
}
146146

147+
/**
148+
* SNS Topic ARNs that will receive stack events.
149+
*
150+
* Empty list if the stack does not exist
151+
*/
152+
public get notificationArns(): CloudFormation.NotificationARNs {
153+
return this.stack?.NotificationARNs ?? [];
154+
}
155+
147156
/**
148157
* Return the names of all current parameters to the stack
149158
*

0 commit comments

Comments
 (0)