Skip to content

Commit 572542e

Browse files
committed
feat: Log when any stateful resource is in stack
A resource is a raw CloudFormation item. A construct is CDK's L1 or L2 abstraction of a resource. A stateful resource can be defined as something that holds state. This could be a database, a bucket, load balancer, message queue etc. This change will, upon stack synthesis, walk the tree of resources and log a warning for all the stateful resources we have identified. This does mean we end up keeping a list of these resources, which is not ideal... The `GuStatefulMigratableConstruct` mixin performs a similar role here, however that only operates against the constructs that exist in the library. Ideally we'd be able to use Stack Policies to protect these resources. However they are not currently supported in CDK. See: - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html - aws/aws-cdk-rfcs#72
1 parent 859ef3e commit 572542e

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* A list of resource types that should be considered stateful
3+
* and care should be taken when updating them to ensure they
4+
* are not accidentally replaced as this could lead to downtime.
5+
*
6+
* For example, if a load balancer is accidentally replaced,
7+
* any CNAME DNS entry for it would now be invalid and downtime
8+
* will be incurred for the TTL of the DNS entry.
9+
*
10+
* Currently, this list is used to generate warnings at synth time.
11+
* Ideally we'd add a stack policy to stop the resource being deleted,
12+
* however this isn't currently supported in CDK.
13+
*
14+
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
15+
* @see https://github.com/aws/aws-cdk-rfcs/issues/72
16+
*/
17+
export const StatefulResourceTypes: string[] = [
18+
"AWS::CertificateManager::Certificate",
19+
"AWS::DynamoDB::Table",
20+
"AWS::ElasticLoadBalancing::LoadBalancer",
21+
"AWS::ElasticLoadBalancingV2::LoadBalancer",
22+
"AWS::S3::Bucket",
23+
];

src/constructs/core/stack.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ import "@aws-cdk/assert/jest";
22

33
import { SynthUtils } from "@aws-cdk/assert";
44
import { Role, ServicePrincipal } from "@aws-cdk/aws-iam";
5+
import { Bucket } from "@aws-cdk/aws-s3";
56
import { App } from "@aws-cdk/core";
67
import { Stage, Stages } from "../../constants";
78
import { TrackingTag } from "../../constants/tracking-tag";
9+
import { Logger } from "../../utils/logger";
810
import { alphabeticalTags, simpleGuStackForTesting } from "../../utils/test";
911
import type { SynthedStack } from "../../utils/test";
1012
import { GuParameter } from "./parameters";
1113
import { GuStack } from "./stack";
1214

1315
describe("The GuStack construct", () => {
16+
const warn = jest.spyOn(Logger, "warn");
17+
18+
afterEach(() => {
19+
warn.mockReset();
20+
});
21+
1422
it("requires passing the stack value as props", function () {
1523
const stack = simpleGuStackForTesting({ stack: "some-stack" });
1624
expect(stack.stack).toEqual("some-stack");
@@ -71,4 +79,19 @@ describe("The GuStack construct", () => {
7179
"Attempting to read parameter i-do-not-exist which does not exist"
7280
);
7381
});
82+
83+
it("During the synthesise process, should advise updating with caution when it contains a stateful resource", () => {
84+
const stack = simpleGuStackForTesting();
85+
const bucket = new Bucket(stack, "MyBucket");
86+
SynthUtils.toCloudFormation(stack);
87+
88+
// `defaultChild can technically be `undefined`.
89+
// We know a `Bucket` has a `defaultChild` so the coalescing is just appeasing the compiler.
90+
const cfnBucketResourcePath = bucket.node.defaultChild?.node.path ?? "";
91+
92+
expect(warn).toHaveBeenCalledTimes(1);
93+
expect(warn).toHaveBeenCalledWith(
94+
`The resource '${cfnBucketResourcePath}' of type AWS::S3::Bucket is considered stateful by @guardian/cdk. Care should be taken when updating this resource to avoid accidental replacement as this could lead to downtime.`
95+
);
96+
});
7497
});

src/constructs/core/stack.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import type { App, StackProps } from "@aws-cdk/core";
2-
import { Stack, Tags } from "@aws-cdk/core";
1+
import type { App, IConstruct, StackProps } from "@aws-cdk/core";
2+
import { CfnResource, Stack, Tags } from "@aws-cdk/core";
33
import { Stage } from "../../constants";
4+
import { StatefulResourceTypes } from "../../constants/stateful-resource-types";
45
import { TrackingTag } from "../../constants/tracking-tag";
6+
import { Logger } from "../../utils/logger";
57
import type { StackStageIdentity } from "./identity";
68
import type { GuStageDependentValue } from "./mappings";
79
import { GuStageMapping } from "./mappings";
@@ -120,4 +122,26 @@ export class GuStack extends Stack implements StackStageIdentity, GuMigratingSta
120122
this.addTag("Stack", this.stack);
121123
this.addTag("Stage", this.stage);
122124
}
125+
126+
protected prepare(): void {
127+
super.prepare();
128+
129+
/*
130+
Log a message whenever a stateful resource is encountered in the stack.
131+
132+
Ideally we'd add a stack policy to stop the resource being deleted,
133+
however this isn't currently supported in CDK.
134+
135+
See:
136+
- https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
137+
- https://github.com/aws/aws-cdk-rfcs/issues/72
138+
*/
139+
this.node.findAll().forEach((construct: IConstruct) => {
140+
if (CfnResource.isCfnResource(construct) && StatefulResourceTypes.includes(construct.cfnResourceType)) {
141+
Logger.warn(
142+
`The resource '${construct.node.path}' of type ${construct.cfnResourceType} is considered stateful by @guardian/cdk. Care should be taken when updating this resource to avoid accidental replacement as this could lead to downtime.`
143+
);
144+
}
145+
});
146+
}
123147
}

0 commit comments

Comments
 (0)