Nested Stack Example in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Sat May 08 20215 min read

banner

Photo by Ana Gabriel

Creating Nested Stacks in AWS CDK #

Nested stacks are stacks we create as part of other stacks. There are 2 main reasons to use nested stacks in AWS CDK:

  1. Separate and reuse resources that are reused in multiple places. For example, if we use the same VPC configuration for all of our applications, it makes sense to extract the logic into a separate, dedicated stack and plug it into all of our applications.

    This makes our nested stack the single source of truth. Should we change our VPC configuration, we don't have to go through every single stack and update it, we only have to update the nested stack and re-deploy the parent stacks.

  2. CDK gets compiled down to CloudFormation, before a deployment, which has a resource limit of 500. Using nested stacks allows us to get around this limitation, because a nested stack counts as only 1 resource, which can contain up to 500 resources, including other nested stacks.

Let's look at an example, where we define a VPC in a nested stack, and reuse it in multiple places.

This pattern allows us to keep down the number of resources our stacks provision, because the VPC nested stack will only count as 1 resource, even though the nested stack itself provisions about 30 resources.

Let's start by defining the nested stack:

The code for this article is available on GitHub
lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as path from 'path'; // ๐Ÿ‘‡ extends NestedStack class VpcNestedStack extends cdk.NestedStack { public readonly vpc: ec2.Vpc; constructor(scope: cdk.Construct, id: string, props?: cdk.NestedStackProps) { super(scope, id, props); this.vpc = new ec2.Vpc(this, 'nested-stack-vpc', { cidr: '10.0.0.0/16', natGateways: 0, maxAzs: 3, subnetConfiguration: [ { name: 'public-subnet-1', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, ], }); } }

In the code snippet we defined a VPC resource and attached it to a property on the class, in order to expose it when instantiating the nested stack.

Note that the nested stack extends NestedStack instead of Stack.

The code for this article is available on GitHub

Next, we'll define our EC2Stack, which launches an EC2 instances in the VPC from the nested stack.

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as path from 'path'; export class EC2Stack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ๐Ÿ‘‡ grab the VPC from the nested stack const {vpc} = new VpcNestedStack(this, 'nested-stack'); const webserverSG = new ec2.SecurityGroup(this, 'webserver-sg', { vpc, }); webserverSG.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'allow HTTP traffic from anywhere', ); const ec2Instance = new ec2.Instance(this, 'ec2-instance', { // ๐Ÿ‘‡ use the VPC from the nested stack vpc, vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC, }, securityGroup: webserverSG, instanceType: ec2.InstanceType.of( ec2.InstanceClass.T2, ec2.InstanceSize.MICRO, ), machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), }); ec2Instance.addUserData( 'sudo su', 'yum install -y httpd', 'systemctl start httpd', 'systemctl enable httpd', 'echo "<h1>It works :)</h1>" > /var/www/html/index.html', ); } }

Let's go over what we did in the code snippet.

  1. we instantiated the nested stack and grabbed the VPC resource

  2. we created a t2.micro EC2 instance, which serves as a webserver and exposed port 80 on the security group. The instance is launched in a public subnet of the VPC from the nested stack.

    We then added a user data script, that installs and starts an apache web server.

Let's execute a deployment:

shell
npx cdk deploy

After the deployment has succeeded we have provisioned the following stacks:

deployed 2 stacks

If we look at the ec2-stack, we see that it only provisions 6 resources. One of them, being the nested VPC stack:

ec2 stack nested stack

The resource of type AWS::CloudFormation::Stack is our nested stack, which provisions 16 resources:

nested stack resources

Nested stacks in CDK get their own CloudFormation templates, however they can't be individually deployed. When using nested stacks in CDK, we interact with the root stack.

If I execute the npx cdk ls command, we would only see 1 stack listed - the ec2-stack:

nested stack cdk ls

If we copy and paste the public IPv4 address from the EC2 instance in the browser, we can see that launching the instance in the nested stack VPC has succeeded:

nested stack ec2 instance response

The code for this article is available on GitHub

If we update the nested stack and re-deploy the parent stack, the changes take effect:

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as path from 'path'; class VpcNestedStack extends cdk.NestedStack { public readonly vpc: ec2.Vpc; constructor(scope: cdk.Construct, id: string, props?: cdk.NestedStackProps) { super(scope, id, props); this.vpc = new ec2.Vpc(this, 'nested-stack-vpc', { cidr: '10.0.0.0/16', natGateways: 0, maxAzs: 3, subnetConfiguration: [ { name: 'public-subnet-1', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, // ๐Ÿ‘‡ added isolated subnet group { name: 'isolated-subnet-1', subnetType: ec2.SubnetType.ISOLATED, cidrMask: 24, }, ], }); } }

In the code snippet, we added an isolated subnet group, which will provision 1 isolated subnet for each of the 3 availability zones of our VPC.

Let's deploy the parent stack:

shell
npx cdk deploy

After the deployment has succeeded, we can see that our nested VPC stack now provisions a total of 25 resources. The route tables, subnets and route table association resources have been added:

updated nested stack

We can instantiate the same nested stack in a parent stack multiple times. For example, let's instantiate the VPC nested stack a second time and provision a lambda function in the VPC.

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as path from 'path'; export class EC2Stack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ...rest // ๐Ÿ‘‡ instantiate the nested stack again const {vpc: vpcLambda} = new VpcNestedStack(this, 'nested-stack-lambda'); const lambdaFunction = new lambda.Function(this, 'lambda-function', { runtime: lambda.Runtime.NODEJS_14_X, // ๐Ÿ‘‡ use the VPC from the second nested stack vpc: vpcLambda, vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC, }, allowPublicSubnet: true, handler: 'index.main', code: lambda.Code.fromAsset(path.join(__dirname, '/../src/my-lambda')), environment: { VPC_CIDR: vpcLambda.vpcCidrBlock, VPC_ID: vpcLambda.vpcId, }, }); } }

In the code snippet we instantiated the VPC nested stack for a second time and provisioned a lambda function. We are basically creating a second VPC with the same configuration, in our parent stack.

Let's add the code for the lambda function at src/my-lambda/index.js:

src/my-lambda/index.js
async function main(event) { try { console.log('VPC CIDR ๐Ÿ‘‰', process.env.VPC_CIDR); console.log('VPC ID ๐Ÿ‘‰', process.env.VPC_ID); return { body: JSON.stringify({ cidr: process.env.VPC_CIDR, id: process.env.VPC_ID, }), statusCode: 200, }; } catch (error) { return {body: JSON.stringify({error})}; } } module.exports = {main};

The lambda simply returns the CIDR and ID of the VPC from the nested stack.

Let's deploy the new resources:

shell
npx cdk deploy

After we've deployed the changes, we can see that we have provisioned 2 nested stacks:

multiple nested stacks

Our parent stack, however, only provisions a total of 10 resources:

parent stack resources

If we were to update our nested stack and re-deploy the parent the changes would apply to both of our nested stacks.

By taking a look at the configuration from the lambda function, we can see that it is associated with the VPC from the nested stack:

lambda vpc nested stack

CDK automatically creates Outputs and Parameters for the interactions between nested and parent stacks, which hides a lot of the complexity of using nested stacks.

Clean up #

To delete the provisioned resources, execute the destroy command:

shell
npx cdk destroy
When the root stack gets deleted, nested stacks also get deleted.

Further Reading #

Add me on LinkedIn

I'm a Web Developer with TypeScript, React.js, Node.js and AWS experience.

Let's connect on LinkedIn

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee