aws/aws-cdk-rfcs

Integrate AWS CloudFormation StackSets for AWS Organizations as part of `cdk deploy`

cynicaljoy opened this issue ยท 5 comments

I'd like to be able to configure Stacks to be part of a StackSet that is deployed to my AWS Organization.

AWS CloudFormation StackSets introduces automatic deployments across accounts and regions through AWS Organizations

Use Case

I want to make use of CloudFormation features provided by the AWS team rather than attempting to roll my own CodePipeline to address the rollout of CloudFormation templates generated from CDK.

Proposed Solution


const stackSet = new cloudformation.StackSet(this, 'MyStackSet', {
   permissionModel: 'SERVICE_MANGED',
   autoDeployment: true,
   retainStacksOnAccountRemoval: true,
   deploymentTargets: {
         organizationalUnitIds: ["ou-rcuk-1x5j1lwo", "ou-rcuk-slr5lh0a"]
   },
  regions: ["eu-west-1"]
});

const stack = new cdk.Stack(app, 'MultipleAccountsStack');

stackSet.add(stack);

This is a ๐Ÿš€ Feature Request

Having this would be a nice feature! I ended up creating a StackSet construct with custom resources. I believe CDK will also have to implement something similar (i.e. consume Cloudformation stack set API directly) to add this feature..

import * as cdk from "@aws-cdk/core";
import { Construct, CfnOutput, Stack } from "@aws-cdk/core";
import * as cloudtrail from "@aws-cdk/aws-cloudtrail";
import * as sns from "@aws-cdk/aws-sns";
import {
  Bucket,
  BucketPolicy,
  BlockPublicAccess,
  BucketAccessControl,
} from "@aws-cdk/aws-s3";

import {
  PolicyStatement,
  Effect,
} from "@aws-cdk/aws-iam";
import { ITopic } from "@aws-cdk/aws-sns";
import {
  AwsCustomResource,
  PhysicalResourceId,
  AwsCustomResourcePolicy,
} from "@aws-cdk/custom-resources";

interface StackSetProps extends cdk.StackProps {
  templateURL: string;
  orgUnitIds: string[];
  regions: string[]
  name: string;
}

export class StackSet extends Construct {
  constructor(scope: Construct, id: string, props: StackSetProps) {
    super(scope, id);

    const rolePolicy = AwsCustomResourcePolicy.fromStatements([
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ["s3:ListBucket"],
        resources: ["arn:aws:s3:::cdktoolkit-stagingbucket*"],
      }),
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ["s3:GetObject"],
        resources: ["arn:aws:s3:::cdktoolkit-stagingbucket*/*"],
      }),
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ["cloudformation:*"],
        resources: ["*"],
      }),
    ]);

    const stackSet = new AwsCustomResource(this, props.name, {
      onCreate: {
        service: "CloudFormation",
        action: "createStackSet",
        physicalResourceId: PhysicalResourceId.of(props.name),
        parameters: {
          StackSetName: props.name,
          TemplateURL: props.templateURL,
          PermissionModel: "SERVICE_MANAGED",
          AutoDeployment: {
            Enabled: true,
            RetainStacksOnAccountRemoval: true,
          },
        },
      },
      onDelete: {
        service: "CloudFormation",
        action: "deleteStackSet",
        parameters: {
          StackSetName: props.name,
        },
      },
      policy: rolePolicy,
    });

    const stackInstancesName = `${props.name}-Instances`;
    const stackInstances = new AwsCustomResource(this, stackInstancesName, {
      onCreate: {
        service: "CloudFormation",
        action: "createStackInstances",
        physicalResourceId: PhysicalResourceId.of(stackInstancesName),
        parameters: {
          Regions: [Stack.of(this).region],
          StackSetName: props.name,
          DeploymentTargets: {
            OrganizationalUnitIds: props.orgUnitIds,
          },
          OperationPreferences: {
            MaxConcurrentCount: 4,
            FailureTolerancePercentage: 100,
          },
        },
      },
      onUpdate: {
        service: "CloudFormation",
        action: "updateStackInstances",
        physicalResourceId: PhysicalResourceId.of(stackInstancesName),
        parameters: {
          Regions: props.regions,
          StackSetName: props.name,
          DeploymentTargets: {
            OrganizationalUnitIds: props.orgUnitIds,
          },
          OperationPreferences: {
            MaxConcurrentCount: 4,
            FailureTolerancePercentage: 100,
          },
        },
      },
      onDelete: {
        service: "CloudFormation",
        action: "deleteStackInstances",
        parameters: {
          Regions: props.regions,
          StackSetName: props.name,
          DeploymentTargets: {
            OrganizationalUnitIds: props.orgUnitIds,
          },
          RetainStacks: false,
        },
      },
      policy: rolePolicy,
    });

    stackInstances.node.addDependency(stackSet);
  }
}

I can see how the above works ๐Ÿ‘. How does it differ to using new cdk.CfnStackSet (plus a couple of hack lines) like here? #66 (comment)

I can see how the above works ๐Ÿ‘. How does it differ to using new cdk.CfnStackSet (plus a couple of hack lines) like here? #66 (comment)

Don't think cdk.CfnStackSet existed at that time. I used above only due to the lack of built-in support at the time.

eladb commented

Duplicate #66

I don't believe this to be a duplicate as it's completely reasonable for the team to build out StackSet Support without building in the support for AWS Organizations.