Deploying CloudFormation StackSets with AWS CDK
Tags: aws, cdk, devops, governance
Published: 2023-07-20 12:55:20

AWS Cloud Development Kit (CDK) is a powerful framework that allows developers to define cloud infrastructure as code using familiar programming languages. With CDK, you can easily provision and manage AWS resources in a consistent and automated manner. In this blog post, we'll walk you through the process of creating a StackSet using AWS CDK.

Before we dive into the details, let's take a quick look at what a StackSet is and how it can help you manage your AWS infrastructure.

StackSets are containers for CloudFormation stacks that enable simultaneous creation, update, and deletion across multiple AWS accounts and regions. With StacksSets, you can ensure that all environments are consistent and compliant with the policies you have in place.

The native support for StackSet in CDK is somewhat rudimentary, due to the fact that it is more common in CDK to use [CDK pipelines](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.pipelines-readme.html) to roll out stacks to multiple accounts and regions.

Therefore, in order to use StackSet in CDK, a few things need to be considered. In this blogpost, we will show steps on how to deploy StackSets via CDK.

Let’s see how this works.

Prerequisites
-------------

To follow this tutorial, make sure you have the following prerequisites

*   [An AWS account with appropriate permissions to create StackSets and associated resources.](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html)  
    
*   [AWS Command Line Interface (CLI)](https://aws.amazon.com/cli/?nc1=h_ls)
*   [AWS Cloud Development Kit (AWS CDK)](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)
*   Basic knowledge of TypeScript

Bootstrap AWS Account
---------------------

Bootstrapping is the process of providing resources for the AWS CDK before you can deploy AWS CDK applications in an AWS environment. Normally you could use the default

`cdk bootstrap aws://ACCOUNT-NUMBER-1/REGION-1` command. However, we need to customise the template to make the assets available to the entire AWS Organization, as we want to use this CDK environment to deploy StackSets. To get the latest version of the CDK bootstrap template, do the following:

`cdk bootstrap --show-template > bootstrap-template.yaml`

ℹ️ The CDK boostrap template contains an S3 bucket for files and an ECR repository for container images. It also creates few IAM roles.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eky00puawwtepe0gwpke.png)

After that, we need to modify two resources in this template. The s3 bucket for Assets and the KMS Key that will be used to encrypt the assets.

Add the following Part to the **Parameters** Section of the template:

      PrincipalOrgID:
        Description: >-
          The identifier of your AWS organization. Used in the KMS key policy and S3 bucket to
          share the key with all accounts under your organization
        Type: String


We will reference this Parameter in the Resource section.

Add this CodeSnippet to the **FileAssetsBucketEncryptionKey** Resource in to the Key Policy Section. This will

            KeyPolicy:
            Statement:
              - Action:
              ....
              - Action:
                  - kms:Decrypt
                  - kms:DescribeKey
                Effect: Allow
                Principal:
                  AWS: "*"
                Resource: "*"
                Condition:
                  StringEquals:
                    kms:ViaService:
                      - Fn::Sub: s3.${AWS::Region}.amazonaws.com
                  ForAnyValue:StringLike:
                    aws:PrincipalOrgID:
                    - !Ref PrincipalOrgID


Extend the PolicyDocument of the **StagingBucketPolicy** with the following CodeSnippet. This will ensure that all Accounts of the Organization get access to the objects in the Asset Bucket.

          PolicyDocument:
            Id: AccessControl
            Version: "2012-10-17"
            Statement:
            ...
              - Sid: ''
                Effect: Allow
                Principal: '*'
                Action:
                - s3:Get*
    
                Resource: !Sub '${StagingBucket.Arn}/*'
                Condition:
                  ForAnyValue:StringLike:
                    aws:PrincipalOrgID:
                    - !Ref PrincipalOrgID
              - Sid: ''
                Effect: Allow
                Principal: '*'
                Action: s3:ListBucket
                Resource: !Sub '${StagingBucket.Arn}'
                Condition:
                  ForAnyValue:StringLike:
                    aws:PrincipalOrgID:
                    - !Ref PrincipalOrgID


After all the adjustments we need to deploy the template.

    aws cloudformation create-stack \
      --stack-name CDKToolkit \
      --template-body file://bootstrap-template.yaml


![Blog Content](https://globaldatanet.com/images/cms/600f8d44147a9ceeb87aabcce6e44b2f7f97e9cd-280x280.webp)

Set Up Your CDK Project
-----------------------

After bootstrapping our Account we are ready to Initialize a new CDK project. We will do that in a new directory. The initialisation creates a new CDK project structure with a `sample lib/stackset-cdk-demo-stack.ts` file, which we will modify to create our StackSet.

    #Create new directory
    mkdir stackset-cdk-demo
    cd stackset-cdk-demo
    
    #Init new CDK Project
    cdk init app --language typescript
    


Define the StackSet
-------------------

Open `lib/stackset-cdk-demo-stack.ts` and remove the example stack definition. We'll define our stackset instead:

    import * as cdk from 'aws-cdk-lib';
    import * as servicecatalog from 'aws-cdk-lib/aws-servicecatalog';
    import { StackSetTemplate } from "./stackSetTemplate";
    import * as s3 from "aws-cdk-lib/aws-s3";
    
    export class StackSetCdkDemoStack extends cdk.Stack {
      constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
    
    
        const stackSetTemplateSandbox = new StackSetTemplate(this, "stacksettemplate", {
          Config: props.stacksetProps,
          assetBucket: s3.Bucket.fromBucketName(this, "assetbucket", "myCDKAssetBucket")
        });
    
        new cdk.CfnStackSet(this, "TESTSTACKSET", {
          permissionModel: "SELF_MANAGED",
          stackSetName: "TEST-STACKSET",
          description:
            "example of StackSet with CDK",
          capabilities: ["CAPABILITY_NAMED_IAM"],
          templateUrl: servicecatalog.CloudFormationTemplate.fromProductStack(stackSetTemplateSandbox).bind(this).httpUrl,
          operationPreferences: {
            failureToleranceCount: 30,
            maxConcurrentCount: 30,
          }
        });
      }
    }
    


⚠️ Ensure to adjust the myCDKAssetBucket to your AWS Account Assets Bucket.

🚨 Using the servicecatalog ProductStack construct we get rid of the PseudoParameter for the assets bucket in lambdas in the template.

    ❌ Without using the servicatalog ProductStack:
    
    !sub cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
    
    ✅ Using the servicatalog ProductStack:
    
    cdk-hnb659fds-assets-123456789012-eu-central-1

Undefined

This workaround will ensure that all AWS accounts can access the assets in our CDK app.

Create StackSet template
------------------------

In the lib directory create a new file called **`stackSetTemplate.ts`** and add the following code to the file:

    import * as cdk from "aws-cdk-lib";
    import { Construct } from "constructs";
    import * as servicecatalog from "aws-cdk-lib/aws-servicecatalog";
    import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
    import * as lambda from "aws-cdk-lib/aws-lambda";
    
    
    //Standardstackdefinition
    export class StackSetTemplate extends servicecatalog.ProductStack {
    // export class CdkStack extends cdk.Stack {
      constructor(scope: Construct, id: string, props: cdk.StackProps) {
        super(scope, id, props);
    
        /**
         * Dummy Node JS Lambda Function
         */
          const lambdaFunction = new NodejsFunction(this, "testFunction", {
            memorySize: 128,
            timeout: cdk.Duration.seconds(60),
            runtime: lambda.Runtime.NODEJS_18_X,
            handler: "handler",
            entry: path.join(__dirname, "lambda/index.ts"),
            bundling: {
              minify: true,
              externalModules: ["aws-sdk"]
            }
          });
          lambdaFunction.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
        }
      }
    }
    


### Example Lambda Code

Create a new directory **`lambda`** in the **`lib`** directory. In the new lambda directory create a new file called **`index.ts`** and add the following code:

    import { Handler } from 'aws-lambda';
    
    export const handler: Handler = async (event, context) => {
        console.log('EVENT: \n' + JSON.stringify(event, null, 2));
        return context.logStreamName;
    };


Deploy the StackSet
-------------------

Run `cdk deploy`  
in a terminal to deploy the StackSet and associated CloudFormation stacks.

ℹ️ CDK will ask you to confirm that you want to deploy the changes. Type **y** and press Enter to continue.

Conclusion
----------

In this blog post, we've explored how to create a StackSet using AWS CDK. We also learned how to share the CDK Assets to the whole AWS Organization. StackSets are an essential tool for managing infrastructure at scale across multiple AWS accounts and regions. Using CDK, you can easily define and deploy complex cloud infrastructure as code, and leverage the full power of AWS CloudFormation to ensure consistency, compliance, and efficiency across your organisation's cloud resources.