diff --git a/.changeset/curly-peas-rule.md b/.changeset/curly-peas-rule.md new file mode 100644 index 0000000000..4e4c09f2e6 --- /dev/null +++ b/.changeset/curly-peas-rule.md @@ -0,0 +1,10 @@ +--- +"@guardian/cdk": major +--- + +Removes supports for Stack Sets (added in #977) as it's no longer used, +because of a lack of CD tooling support for deploying Stack Sets. + +Removing unused code means less code to maintain, and reduced complexity. + +Should Stack Sets be needed in future, https://github.com/cdklabs/cdk-stacksets offers an alternative approach to creating them in CDK. diff --git a/src/constructs/core/stack.ts b/src/constructs/core/stack.ts index 3756bf82ce..52bda24e0e 100644 --- a/src/constructs/core/stack.ts +++ b/src/constructs/core/stack.ts @@ -1,6 +1,5 @@ -import type { CfnElement, StackProps } from "aws-cdk-lib"; -import { Annotations, App, Aspects, CfnParameter, LegacyStackSynthesizer, Stack, Tags } from "aws-cdk-lib"; -import { Template } from "aws-cdk-lib/assertions"; +import type { App, CfnElement, StackProps } from "aws-cdk-lib"; +import { Annotations, Aspects, CfnParameter, LegacyStackSynthesizer, Stack, Tags } from "aws-cdk-lib"; import type { IConstruct } from "constructs"; import gitUrlParse from "git-url-parse"; import { AwsBackupTag } from "../../aspects/aws-backup"; @@ -268,19 +267,3 @@ export class GuStack extends Stack implements StackStageIdentity { Annotations.of(construct).addInfo(`Setting logical ID for ${id} to ${logicalId}. Reason: ${reason}`); } } - -/** - * A GuStack but designed for Stack Set instances. - * - * In a stack set application, `GuStackForStackSetInstance` is used to represent the infrastructure to provision in target AWS accounts. - */ -export class GuStackForStackSetInstance extends GuStack { - // eslint-disable-next-line custom-rules/valid-constructors -- GuStackForStackSet should have a unique `App` - constructor(id: string, props: GuStackProps) { - super(new App(), id, props); - } - - get cfnJson(): string { - return JSON.stringify(Template.fromStack(this).toJSON(), null, 2); - } -} diff --git a/src/constructs/stack-set/__snapshots__/stack-set.test.ts.snap b/src/constructs/stack-set/__snapshots__/stack-set.test.ts.snap deleted file mode 100644 index 6af6f22089..0000000000 --- a/src/constructs/stack-set/__snapshots__/stack-set.test.ts.snap +++ /dev/null @@ -1,265 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`The GuStackSet construct should correctly provision a stack with a stack set resource 1`] = ` -{ - "Metadata": { - "gu:cdk:constructs": [ - "GuStack", - "GuStackSet", - ], - "gu:cdk:version": "TEST", - }, - "Resources": { - "StackSet": { - "Properties": { - "AutoDeployment": { - "Enabled": true, - "RetainStacksOnAccountRemoval": false, - }, - "Description": "this stack set provisions some common infrastructure", - "Parameters": [], - "PermissionModel": "SERVICE_MANAGED", - "StackInstancesGroup": [ - { - "DeploymentTargets": { - "OrganizationalUnitIds": [ - "o-12345abcde", - ], - }, - "Regions": [ - { - "Ref": "AWS::Region", - }, - ], - }, - ], - "StackSetName": "my-stack-set", - "Tags": [ - { - "Key": "gu:cdk:version", - "Value": "TEST", - }, - { - "Key": "gu:repo", - "Value": "guardian/cdk", - }, - { - "Key": "Stack", - "Value": "test", - }, - { - "Key": "Stage", - "Value": "TEST", - }, - ], - "TemplateBody": "{ - "Metadata": { - "gu:cdk:constructs": [ - "GuStackForStackSetInstance", - "GuKinesisStream" - ], - "gu:cdk:version": "TEST" - }, - "Resources": { - "accountloggingstreamB8733874": { - "Type": "AWS::Kinesis::Stream", - "Properties": { - "RetentionPeriodHours": 24, - "ShardCount": 1, - "StreamEncryption": { - "Fn::If": [ - "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", - { - "Ref": "AWS::NoValue" - }, - { - "EncryptionType": "KMS", - "KeyId": "alias/aws/kinesis" - } - ] - }, - "Tags": [ - { - "Key": "gu:cdk:version", - "Value": "TEST" - }, - { - "Key": "gu:repo", - "Value": "guardian/cdk" - }, - { - "Key": "Stack", - "Value": "test" - }, - { - "Key": "Stage", - "Value": "TEST" - } - ] - } - } - }, - "Conditions": { - "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { - "Fn::Or": [ - { - "Fn::Equals": [ - { - "Ref": "AWS::Region" - }, - "cn-north-1" - ] - }, - { - "Fn::Equals": [ - { - "Ref": "AWS::Region" - }, - "cn-northwest-1" - ] - } - ] - } - } -}", - }, - "Type": "AWS::CloudFormation::StackSet", - }, - }, -} -`; - -exports[`The GuStackSet construct should support parameters in the stack set instance 1`] = ` -{ - "Metadata": { - "gu:cdk:constructs": [ - "GuStack", - "GuStackSet", - ], - "gu:cdk:version": "TEST", - }, - "Resources": { - "StackSet": { - "Properties": { - "AutoDeployment": { - "Enabled": true, - "RetainStacksOnAccountRemoval": false, - }, - "Description": "this stack set provisions some common infrastructure", - "Parameters": [ - { - "ParameterKey": "CentralSnsTopicArn", - "ParameterValue": { - "Ref": "accountalertsD9982E6F", - }, - }, - ], - "PermissionModel": "SERVICE_MANAGED", - "StackInstancesGroup": [ - { - "DeploymentTargets": { - "OrganizationalUnitIds": [ - "o-12345abcde", - ], - }, - "Regions": [ - { - "Ref": "AWS::Region", - }, - ], - }, - ], - "StackSetName": "my-stack-set", - "Tags": [ - { - "Key": "gu:cdk:version", - "Value": "TEST", - }, - { - "Key": "gu:repo", - "Value": "guardian/cdk", - }, - { - "Key": "Stack", - "Value": "test", - }, - { - "Key": "Stage", - "Value": "TEST", - }, - ], - "TemplateBody": "{ - "Metadata": { - "gu:cdk:constructs": [ - "GuStackForStackSetInstance", - "GuStringParameter" - ], - "gu:cdk:version": "TEST" - }, - "Parameters": { - "CentralSnsTopicArn": { - "Type": "String", - "AllowedPattern": "arn:aws:[a-z0-9]*:[a-z0-9\\\\-]*:[0-9]{12}:.*" - } - } -}", - }, - "Type": "AWS::CloudFormation::StackSet", - }, - "accountalertsD9982E6F": { - "Properties": { - "Tags": [ - { - "Key": "gu:cdk:version", - "Value": "TEST", - }, - { - "Key": "gu:repo", - "Value": "guardian/cdk", - }, - { - "Key": "Stack", - "Value": "test", - }, - { - "Key": "Stage", - "Value": "TEST", - }, - ], - }, - "Type": "AWS::SNS::Topic", - }, - "accountalertsPolicy8E37A8FA": { - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Condition": { - "StringEquals": { - "aws:PrincipalOrgID": "o-12345abcde", - }, - }, - "Effect": "Allow", - "Principal": { - "AWS": "*", - }, - "Resource": { - "Ref": "accountalertsD9982E6F", - }, - "Sid": "0", - }, - ], - "Version": "2012-10-17", - }, - "Topics": [ - { - "Ref": "accountalertsD9982E6F", - }, - ], - }, - "Type": "AWS::SNS::TopicPolicy", - }, - }, -} -`; diff --git a/src/constructs/stack-set/index.ts b/src/constructs/stack-set/index.ts deleted file mode 100644 index e3222464c1..0000000000 --- a/src/constructs/stack-set/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./stack-set"; diff --git a/src/constructs/stack-set/stack-set.test.ts b/src/constructs/stack-set/stack-set.test.ts deleted file mode 100644 index 90610b5a9f..0000000000 --- a/src/constructs/stack-set/stack-set.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Template } from "aws-cdk-lib/assertions"; -import { OrganizationPrincipal } from "aws-cdk-lib/aws-iam"; -import { Topic } from "aws-cdk-lib/aws-sns"; -import { RegexPattern } from "../../constants"; -import { simpleGuStackForTesting } from "../../utils/test"; -import { GuStackForStackSetInstance, GuStringParameter } from "../core"; -import { GuKinesisStream } from "../kinesis"; -import { GuStackSet } from "./stack-set"; - -describe("The GuStackSet construct", () => { - it("should correctly provision a stack with a stack set resource", () => { - const theStackSetInstance = new GuStackForStackSetInstance("the-stack-set", { stack: "test", stage: "TEST" }); - new GuKinesisStream(theStackSetInstance, "account-logging-stream"); - - const parentStack = simpleGuStackForTesting({ stack: "test" }); - - new GuStackSet(parentStack, "StackSet", { - name: "my-stack-set", - description: "this stack set provisions some common infrastructure", - organisationUnitTargets: ["o-12345abcde"], - stackSetInstance: theStackSetInstance, - }); - - expect(Template.fromStack(parentStack).toJSON()).toMatchSnapshot(); - }); - - it("should support parameters in the stack set instance", () => { - const theStackSetInstance = new GuStackForStackSetInstance("the-stack-set", { stack: "test", stage: "TEST" }); - Topic.fromTopicArn( - theStackSetInstance, - "central-topic", - new GuStringParameter(theStackSetInstance, "CentralSnsTopicArn", { allowedPattern: RegexPattern.ARN }) - .valueAsString, - ); - - const awsOrgId = "o-12345abcde"; - const parentStack = simpleGuStackForTesting({ stack: "test" }); - const centralTopic = new Topic(parentStack, "account-alerts"); - centralTopic.grantPublish(new OrganizationPrincipal(awsOrgId)); - - new GuStackSet(parentStack, "StackSet", { - name: "my-stack-set", - description: "this stack set provisions some common infrastructure", - organisationUnitTargets: [awsOrgId], - stackSetInstance: theStackSetInstance, - stackSetInstanceParameters: { - CentralSnsTopicArn: centralTopic.topicArn, - }, - }); - - expect(Template.fromStack(parentStack).toJSON()).toMatchSnapshot(); - }); - - it("should error if the parent stack does not specify all parameters for the stack set instance template", () => { - const theStackSetInstance = new GuStackForStackSetInstance("the-stack-set", { stack: "test", stage: "TEST" }); - new GuStringParameter(theStackSetInstance, "CentralSnsTopic", { allowedPattern: RegexPattern.ARN }); - - const parentStack = simpleGuStackForTesting({ stack: "test" }); - - expect(() => { - new GuStackSet(parentStack, "StackSet", { - name: "my-stack-set", - description: "this stack set provisions some common infrastructure", - organisationUnitTargets: ["o-12345abcde"], - stackSetInstance: theStackSetInstance, - }); - }).toThrow("There are undefined stack set parameters: CentralSnsTopic"); - }); -}); diff --git a/src/constructs/stack-set/stack-set.ts b/src/constructs/stack-set/stack-set.ts deleted file mode 100644 index 0a3b6ffdb7..0000000000 --- a/src/constructs/stack-set/stack-set.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { CfnStackSet } from "aws-cdk-lib"; -import type { GuStack, GuStackForStackSetInstance } from "../core"; - -interface GuStackSetProps { - /** - * The contents for `TemplateBody` within a `AWS::CloudFormation::StackSet`. - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-stackset.html#cfn-cloudformation-stackset-templatebody - */ - stackSetInstance: GuStackForStackSetInstance; - - /** - * The name of the stack set. This will appear in the target account. - */ - name: string; - - /** - * A description of the stack set. This will appear in the target account. - */ - description: string; - - /** - * AWS Organisation Unit IDs to deploy the stack set into. - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-stackset-deploymenttargets.html#cfn-cloudformation-stackset-deploymenttargets-organizationalunitids - */ - organisationUnitTargets: string[]; - - /** - * The contents for`Parameters` within a `AWS::CloudFormation::StackSet`. - * - * Typically, you'll only need parameters if the parent stack creates resources for use in the stack set instances. - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-stackset.html#cfn-cloudformation-stackset-parameters - */ - stackSetInstanceParameters?: Record; - - /** - * Which regions to run the stack set in. - * - * @default The region of the parent stack. - */ - regions?: string[]; -} - -/** - * A construct to create a `AWS::CloudFormation::StackSet`. - * - * The stack set will be configured to automatically deploy into accounts when they join an AWS Organisation Unit. - * It's assumed stack sets are only provisioned for infrastructure, therefore the `STAGE` value will always be `INFRA`. - * - * Usage: - * ```typescript - * // the infrastructure to create in target accounts - * class AccountAlertTopic extends GuStackForStackSetInstance { - * constructor(id: string, props: GuStackProps) { - * super(id, props); - * - * new GuSnsTopic(this, "topic-for-alerts"); - * } - * } - * - * class ParentStack extends GuStackForInfrastructure { - * constructor(scope: App, id: string, props: GuStackProps) { - * super(scope, id, props); - * - * new GuStackSet(this, `${id}StackSet`, { - * stackSetInstance: new AccountAlertTopic("Alerts", { stack: props.stack }), - * name: "centralised-alarms", - * description: "Provisioning of standard account alerting resources", - * organisationUnitTargets: [ "o-abcde12345" ] - * }); - * } - * } - * - * // contents of `bin/cdk.ts` - * new ParentStack(new App(), "AccountAlarmResources", { stack: "alarms" }) - * ``` - * - * This will produce a CloudFormation template like this: - * ```yaml - * Resources: - * AccountAlarmResourcesStackSet: - * Type: AWS::CloudFormation::StackSet - * Properties: - * PermissionModel: SERVICE_MANAGED - * StackSetName: centralised-alarms - * AutoDeployment: - * Enabled: true - * RetainStacksOnAccountRemoval: false - * Description: Provisioning of standard account alerting resources - * Parameters: [] - * StackInstancesGroup: - * - DeploymentTargets: - * OrganizationalUnitIds: - * - o-abcde12345 - * Regions: - * - Ref: AWS::Region - * Tags: - * - Key: gu:cdk:version - * Value: TEST - * - Key: gu:repo - * Value: guardian/cdk - * - Key: Stack - * Value: alarms - * - Key: Stage - * Value: INFRA - * TemplateBody: |- - * { - * "Resources": { - * "topicforalerts57330FBE": { - * "Type": "AWS::SNS::Topic", - * "Properties": { - * "Tags": [ - * { - * "Key": "gu:cdk:version", - * "Value": "TEST" - * }, - * { - * "Key": "gu:repo", - * "Value": "guardian/cdk" - * }, - * { - * "Key": "Stack", - * "Value": "alarms" - * }, - * { - * "Key": "Stage", - * "Value": "INFRA" - * } - * ] - * } - * } - * } - * } - * ``` - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-stackset.html - */ -export class GuStackSet extends CfnStackSet { - constructor(scope: GuStack, id: string, props: GuStackSetProps) { - const stackSetInstanceParameters = props.stackSetInstanceParameters ?? {}; - - const params = Object.keys(stackSetInstanceParameters); - const parameterKeys = Object.keys(props.stackSetInstance.parameters); - - const undefinedStackSetParams = parameterKeys.filter((_) => !params.includes(_)); - - if (undefinedStackSetParams.length !== 0) { - throw new Error(`There are undefined stack set parameters: ${undefinedStackSetParams.join(", ")}`); - } - - super(scope, id, { - stackSetName: props.name, - description: props.description, - permissionModel: "SERVICE_MANAGED", - autoDeployment: { - enabled: true, - retainStacksOnAccountRemoval: false, - }, - stackInstancesGroup: [ - { - regions: props.regions ?? [scope.region], - deploymentTargets: { - organizationalUnitIds: props.organisationUnitTargets, - }, - }, - ], - templateBody: props.stackSetInstance.cfnJson, - parameters: Object.entries(stackSetInstanceParameters).map(([key, value]) => { - return { parameterKey: key, parameterValue: value }; - }), - }); - } -}