CDK Nested Stacks — The “Why” and “How”

We cover nested stacks in the AWS CDK and talk through a full example in TypeScript

Serverless Advocate

Preface

✔️ We talk through the need for nested stacks.
✔️ We talk through how this can be achieved with the AWS CDK.

Introduction 👋🏽

In this quick article, we will talk through AWS CDK Nested Stacks, when we should use them in our services, and how we would achieve this. We are going to cover a fictitious company called ‘Gilmore Cuisine’ to give an example code base to walk through:

💡 Note: the code example is not production-ready and is used to talk through the approach of using nested stacks only.

The code for the article can be found here on GitHub:

For more detailed articles and examples please feel free to use the Serverless Advocate Patterns & Solutions registry:

https://www.serverlessadvocate.com/patterns

How do they work? 💡

There are times when building out our applications that we hit the dreaded 500 resource limit per stack in CloudFormation.

“There are times when building out our applications that we hit the dreaded 500 resource limit per stack in CloudFormation.”

This typically happens because we have perhaps used our own L3 constructs that include many resources within them under the hood (abstracted away), for example, maybe a Lambda construct that includes alarms, progressive deployments, dashboards etc etc — and we then use this construct many times over in a stack which tips us over this limit.

The NestedStack construct in the AWS CDK helps us bypass the AWS CloudFormation 500-resource limit per stack by counting it as a single resource within its parent stack. This means a nested stack can itself contain up to 500 resources, including other nested stacks. Nested stacks can then contain other nested stacks, resulting in a hierarchy of stacks:

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html

A nested stack’s scope (parent essentially) must be either a Stack or NestedStack construct in the AWS CDK, as discussed above. Apart from this, defining constructs in a nested stack is the same as in a regular stack.

When you apply template changes to update a top-level stack, CloudFormation updates the top-level stack and initiates an update to its nested stacks. CloudFormation updates the resources of modified nested stacks, but does not update the resources of unmodified nested stacks.

Furthermore, this stack will not be treated as an independent deployment artefact (won’t be listed in “cdk list” or deployable through “cdk deploy”), but rather only synthesized as a template and uploaded as an asset to S3. — https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.NestedStack.html

During synthesis, the nested stack is converted into its own AWS CloudFormation template and uploaded to the AWS CDK staging bucket. Nested stacks are bound to their parent stack and are not treated as independent deployment artefacts.

💡 Note: They do not appear in the cdk list and cannot be deployed with cdk deploy, which we will see later.

Cross references of resource attributes between the parent stack and the nested stack will automatically be translated to stack parameters and outputs when using the AWS CDK.

Let’s have a look at the code in the next section.

👇 Before we go any further — please connect with me on LinkedIn for future blog posts and Serverless news https://www.linkedin.com/in/lee-james-gilmore/

Talking through the code👨‍💻

Anybody following my work will know that I typically split the application into ‘stateless’ and ‘stateful’ resources. I also have an opinionated convention around clean code and architecture:

To continue with this path and use nested stacks, it would look like the following from a folder structure perspective:

This now makes it easy to see that we have one or more nested stacks under the parent stacks of ‘stateful’ and ‘stateless’, and we can see what those stacks contain at a conceptual level (API resources, Database resources, Event Bus resources and Compute resources).

Now if we expand out the folder structure we can see:

Having separate folders for the nested stacks keeps it clean when we start adding snapshots and unit testing of our nested stacks, otherwise, we end up with just a long list of files which can become confusing and messy.

The nested stacks also have the appended word ‘-nested’ to the files, which also shows in the generated files and what is shown in the console, making it easier to reason about.

If we look at our stateful stack code we will see the following:

import * as cdk from 'aws-cdk-lib';

import { Construct } from 'constructs';
import { DatabaseResources } from './nested/database/database-nested';
import { EventBusResources } from './nested/event-bus/event-bus-nested';

export interface GilmoreCuisineStatefulStackProps extends cdk.StackProps {
stage: string;
}

export class GilmoreCuisineStatefulStack extends cdk.Stack {
public databaseResources: DatabaseResources;
public eventBusResources: EventBusResources;

constructor(
scope: Construct,
id: string,
props: GilmoreCuisineStatefulStackProps
) {
super(scope, id, props);

// we pull in the two stateful nested stacks
this.databaseResources = new DatabaseResources(this, 'DatabaseResources', {
stage: props.stage,
});

this.eventBusResources = new EventBusResources(this, 'EventBusResources', {
stage: props.stage,
});
}
}

As we can see above our parent stack is importing the two nested stacks and instantiating them.

If we then look at the DatabaseResources nested stack we will see the following:

import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

import { Construct } from 'constructs';

interface DatabaseResourcesProps extends cdk.NestedStackProps {
stage: string;
}

export class DatabaseResources extends cdk.NestedStack {
public table: dynamodb.Table;

constructor(scope: Construct, id: string, props: DatabaseResourcesProps) {
super(scope, id, props);

// our database is a static resource
this.table = new dynamodb.Table(this, 'Table', {
tableName: `gilmore-cuisine-table-${props.stage}`,
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING,
},
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}

We can see specifically that this stack is extending the cdk.NestedStack rather than cdk.Stack like its parent does.

If we now jump to our ComputeResources nested stack which uses the stateful resources we will see the following code:

import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as events from 'aws-cdk-lib/aws-events';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as path from 'path';

import { Construct } from 'constructs';

interface ComputeResourcesProps extends cdk.NestedStackProps {
stage: string;
api: apigw.RestApi;
}

export class ComputeResources extends cdk.NestedStack {
private api: apigw.RestApi;
private table: dynamodb.Table;
private bus: events.EventBus;

constructor(scope: Construct, id: string, props: ComputeResourcesProps) {
super(scope, id, props);

this.api = props.api;

// get an instance of the table from the stateful stack
this.table = dynamodb.Table.fromTableName(
this,
'Table',
`gilmore-cuisine-table-${props.stage}`
) as dynamodb.Table;

// get an instance of the bus from the stateful stack
this.bus = events.EventBus.fromEventBusName(
this,
'EventBus',
`gilmore-cuisine-event-bus-${props.stage}`
) as events.EventBus;

// create the lambda powertools config
const lambdaPowerToolsConfig = {
LOG_LEVEL: 'DEBUG',
POWERTOOLS_LOGGER_LOG_EVENT: 'true',
POWERTOOLS_LOGGER_SAMPLE_RATE: '1',
POWERTOOLS_TRACE_ENABLED: 'enabled',
POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS: 'captureHTTPsRequests',
POWERTOOLS_SERVICE_NAME: 'gilmore-cuisine-service',
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'captureResult',
POWERTOOLS_METRICS_NAMESPACE: 'gilmore-cuisine',
};

// create the lambda functions
const listBookingsLambda: nodeLambda.NodejsFunction =
new nodeLambda.NodejsFunction(this, 'ListBookingsLambda', {
functionName: `${props.stage}-gilmore-cuisine-list-bookings`,
runtime: lambda.Runtime.NODEJS_20_X,
entry: path.join(
__dirname,
'../../src/adapters/primary/list-bookings/list-bookings.adapter.ts'
),
memorySize: 1024,
handler: 'handler',
tracing: lambda.Tracing.ACTIVE,
bundling: {
minify: true,
sourceMap: true,
},
environment: {
NODE_OPTIONS: '--enable-source-maps',
...lambdaPowerToolsConfig,
TABLE_NAME: this.table.tableName,
BUS: this.bus.eventBusName,
},
});

const createBookingLambda: nodeLambda.NodejsFunction =
new nodeLambda.NodejsFunction(this, 'CreateBookingLambda', {
functionName: `${props.stage}-gilmore-cuisine-create-booking`,
runtime: lambda.Runtime.NODEJS_20_X,
entry: path.join(
__dirname,
'../../src/adapters/primary/create-booking/create-booking.adapter.ts'
),
memorySize: 1024,
handler: 'handler',
tracing: lambda.Tracing.ACTIVE,
bundling: {
minify: true,
sourceMap: true,
},
environment: {
NODE_OPTIONS: '--enable-source-maps',
...lambdaPowerToolsConfig,
TABLE_NAME: this.table.tableName,
BUS: this.bus.eventBusName,
},
});

// table permissions for the functions
this.table.grantReadData(listBookingsLambda);
this.table.grantWriteData(createBookingLambda);

// allow the function to publish messages
this.bus.grantPutEventsTo(createBookingLambda);

// add the lambda functions to the correct api resources
const orders = this.api.root
.getResource('v1')
?.getResource('bookings') as apigw.Resource;

orders.addMethod(
'GET',
new apigw.LambdaIntegration(listBookingsLambda, {
proxy: true,
})
);

orders.addMethod(
'POST',
new apigw.LambdaIntegration(createBookingLambda, {
proxy: true,
})
);
}
}

From this you can see that we are not actually passing the stateful resources for the DynamoDB table and EventBridge Event Bus as arguments into the stack, but actually gaining a reference to them using the following methods:

...
// get an instance of the table from the stateful stack
this.table = dynamodb.Table.fromTableName(
this,
'Table',
`gilmore-cuisine-table-${props.stage}`
) as dynamodb.Table;

// get an instance of event bus from the stateful stack
this.bus = events.EventBus.fromEventBusName(
this,
'EventBus',
`gilmore-cuisine-event-bus-${props.stage}`
) as events.EventBus;
...

This does mean however that our stateful stack must be deployed before our stateless stack, which we can ensure in our main app file by doing the following with regard to dependencies:

#!/usr/bin/env node

import 'source-map-support/register';

import * as cdk from 'aws-cdk-lib';

import { GilmoreCuisineStatefulStack } from '../stateful/stateful';
import { GilmoreCuisineStatelessStack } from '../stateless/stateless';

const stage = 'prod';

const app = new cdk.App();

const stateful = new GilmoreCuisineStatefulStack(
app,
'GilmoreCuisineStatefulStack',
{
stage,
}
);

const stateless = new GilmoreCuisineStatelessStack(
app,
'GilmoreCuisineStatelessStack',
{
stage,
}
);

// ensure stateful is deployed before stateless
stateless.addDependency(stateful);

In the next section let’s deploy and test the solution.

Deployment and Testing 🧑🏾‍💻

OK, so now let’s deploy the stacks by running the following command from the ‘gilmore-cuisine’ folder:

npm run deploy

If we run a cdk list command from the same folder we would only see the two parent stacks as discussed earlier:

‘cdk list’ command shows the two parent stacks of the application, not the nested stacks

If we look locally at our cdk.out folder which contains the synthesized CloudFormation templates and assets we will see the following files:

We can see that we have the two parent templates, GilmoreCuisineStatefulStack.template.json and GilmoreCuisineStatelessStack.template.json, which have contents similar to this below which point at their nested stacks:

...
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete",
"Metadata": {
"aws:cdk:path": "GilmoreCuisineStatefulStack/EventBusResources.NestedStack/EventBusResources.NestedStackResource",
"aws:asset:path": "GilmoreCuisineStatefulStackEventBusResourcesAC59B19D.nested.template.json",
"aws:asset:property": "TemplateURL"
}
...

If we switch to the console once the deployment is complete and have a look without nested stacks visible in the console it shows:

we can see any CloudFormation that we have two parent stacks

Now if we look at the children nested stacks of the two stacks in the console we can see:

We can see that we have our Stateful and Stateless stacks, as well as our nested stacks beneath them

Now let’s have a look at the contents of each of the stacks in more detail:

✔️ Stateful Stack

We can see the following resources in the stateful stack, which is our nested stacks for DatabaseResources and EventBusResources:

We can see that it points to the two nested stacks

If we look at the two nested stacks in more detail we can see:

DatabaseResources
We can see the nested stack for DatabaseResources which includes our DynamoDB table:

EventBusResources
We can see the nested stack for EventBusResourceswhich contains our event bus, CloudWatch log group, and our event rule target:

Now let’s have a look at the stateless stack equivalents.

✔️ Stateless Stack

We can see the following in the stateless stack which shows our two nested stacks for ApiResources and ComputeResources:

We can see that it points to the two nested stacks

ApiResources
We can see below the nested stack for ApiResources which includes our API Gateway REST API, stages, deployments etc

ComputeResources
We can see below the nested stack for ComputeResources which contains our two Lambda functions:

Testing 🧪

A person making a meal booking online

If we now hit the POST endpoint on /bookings/ using the Postman file we will see the following:

If we now look at our CloudWatch Log catch all events target we can see our event as shown below:

We can see our event has been published successfully
The restaurant owner looking at the bookings to see if they can fit in somebody that wants a table

We can also now hit the list bookings endpoint which will show the following:

Conclusion

Thanks for reading this article, and as a final recap we have covered:

✔️ We talked through the need for nested stacks.
✔️ We talked through how this can be achieved with the AWS CDK.

Wrapping up 👋🏽

I hope you enjoyed this short article, and if you did then please feel free to share and feedback!

Please go and subscribe to my YouTube channel for similar content!

I would love to connect with you also on any of the following:

https://www.linkedin.com/in/lee-james-gilmore/
https://twitter.com/LeeJamesGilmore

If you enjoyed the posts please follow my profile Lee James Gilmore for further posts/series, and don’t forget to connect and say Hi 👋

Please also use the ‘clap’ feature at the bottom of the post if you enjoyed it! (You can clap more than once!!)

About me

Hi, I’m Lee, an AWS Community Builder, Blogger, AWS certified cloud architect, and Global Head of Technology & Architecture based in the UK; currently working for City Electrical Factors (UK) & City Electric Supply (US), having worked primarily in full-stack JavaScript on AWS for the past 6 years.

I consider myself a serverless advocate with a love of all things AWS, innovation, software architecture, and technology.

*** The information provided are my own personal views and I accept no responsibility for the use of the information. ***

You may also be interested in the following:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Written by Serverless Advocate

AWS Serverless Hero empowering others through expert knowledge | AI | Architect | Speaker | Engineering | Cloud Native | AWS x 7 Certified 🚀

No responses yet

Write a response