A better way to structure AWS CDK projects around Nested Stacks

February 26, 2020

A better way to structure AWS CDK projects around Nested Stacks

When you start creating Infrastructure as Code (IaC) with the CloudFormation the tendency is to create components in NestedStack (Database, Application Load Balancer, Service1, Service2). With this, in case of necessity, rolls back changes automatically if errors are detected! The AWS CDK helps to create these stacks using a programming language (Python, nodeJS, .NET). From the AWS CDK page:

The AWS Cloud Development Kit (AWS CDK) is an open source software development framework to model and provision your cloud application resources using familiar programming languages.

Provisioning cloud applications can be a challenging process that requires you to perform manual actions, write custom scripts, maintain templates, or learn domain-specific languages. AWS CDK uses the familiarity and expressive power of programming languages for modeling your applications. It provides you with high-level components that preconfigure cloud resources with proven defaults, so you can build cloud applications without needing to be an expert. AWS CDK provisions your resources in a safe, repeatable manner through AWS CloudFormation. It also enables you to compose and share your own custom components that incorporate your organization’s requirements, helping you start new projects faster.

But, after some iterations, you may encounter some of the CloudFormation or CDK limitations. For example, in CloudFormation the maximum Resources allowed are 200 Resources, and the maximum number of Stacks is 200 per region, check the CloudFormation Docs for more info.

In this article, the idea is to work around this limitation using NestedStack, and at the same time, keeping the logic of the component!

NOTE: All code snippets will be in Python 3.6.

The Project

First of all, create an application and have a Main Stack, where you can start deploying the components.

Use the App() in the aws_core library to start the application, and create the file app.py, with:

from aws_cdk import core

app = core.App()

The app itself is initially empty. We need a Main Stack where the infrastructure will be deployed. We now create one Main Stack (use the core.Stack() for this) using the us-west-2 region:

...

main_stack = core.Stack(app, 'MainStack', env={'region': 'us-west-2'})

Nested Stack

Create a simple nested stack for the VPC with 2 subnets in it. Call it skeletonStack, and save this in the file skeleton_stack.py.

from aws_cdk import (
    aws_CloudFormation as cfn,
    aws_ec2 as ec2,
    core,
)


class SkeletonStack(cfn.NestedStack):

    def __init__(
        self,
        scope: core.Construct,
        id: str,
        **kwargs
    ) -> None:
        super().__init__(scope, id, **kwargs)

        self.vpc = ec2.Vpc(
            self, 'SkeletonVpc',
            cidr='10.0.0.0/16',
            max_azs=99,  # use all available AZs,
            subnet_configuration=[
                {
                    'cidrMask': 28,
                    'name': 'public',
                    'subnetType': ec2.SubnetType.PUBLIC
                },
                {
                    'cidrMask': 28,
                    'name': 'private',
                    'subnetType': ec2.SubnetType.PRIVATE
                }
            ]
        )

This will create a Nested Stack, where it will have all the information to create the VPC. This Nested Stack is not being part of a Main Stack, include it on the project inside the app.py:

from skeleton_stack import SkeletonStack

...

skeleton = SkeletonStack(
    main_stack,
    'skeleton'
)

Now there is a VPC, and you can start deploying the components inside it. Create a simple Nested Stack with the necessary component (e.g. EC2 Instance).

Starting to deploy the components

The application just requires a simple component with an EC2 instance on it, create a new Nested Stack for the component and create an EC2 instance inside. Call the Component AppStack and save it on app_stack.py.

The EC2 instance needs to be placed in the VPC that was just created on the different stack, so just need to pass the VPC as a parameter to the component.

from aws_cdk import (
    aws_cloudformation as cfn,
    aws_ec2 as ec2,
    core
)

class AppStack (cfn.NestedStack):
    def __init__(
        self,
        scope: core.Construct,
        id: str,
        vpc: ec2.Vpc,
        **kwargs
    ) -> None:
        super().__init__(scope, id, **kwargs)

        ec2.Instance(
            self,
            f'{id}Ec2Instance',
            instance_type=ec2.InstanceType('t2.micro'),
            machine_image=ec2.MachineImage.generic_linux(
                ami_map={
                    'us-west-2': 'ami-04590e7389a6e577c'
                }
            ),
            vpc=vpc,
            vpc_subnets=ec2.SubnetSelection(subnet_name='public')
        )

And on the app.py, where is all the structure, invoke the new Component.

from app_stack import AppStack

...

ec2_app = AppStack(
    main_stack,
    'App',
    vpc=skeleton.vpc
)

The project is ready to be deployed!

Try to include more resources!

More Apps

Based on our AppStack, deploy some more Apps for the application. Now create 2 more instances, from the app.py file. For this invoke the AppStack component 2 more times:

...

ec2_app_1 = AppStack(
    main_stack,
    'App1',
    vpc=skeleton.vpc
)

ec2_app_2 = AppStack(
    main_stack,
    'App2',
    vpc=skeleton.vpc
)

This will create 2 more Apps on AWS!

Merging the Stacks

When you have just a few resources (like EC2 Instances), this process is very simple. But when you need to start deploying more and more components for each part of your application, it starts to get weird. Clouformation has some limitations, and one of these limitations is the 200 Stacks per region. Based on the application, you could only deploy 199 AppStack Components (It’s more than enough, but still).

The idea is to use fewer stacks but without changing the logic (just the minimum necessary)!

Resources in the AWS CDK

Every time you create a new resource within the AWS CDK, you need to pass the scope where you want this resource, looking back to the AppStack it’s possible to see the line:

        ec2.Instance(
            self,
            f'{id}Ec2Instance',

The reserved word self tells AWS CDK the scope where to create this resource, in this case, the AppStack that inherits the cfn.NestedStack class from aws_cloudformation. So, every time that AppStack is invoked, it will create a new NestedStack.

What will happen, if the cfn.NestedStack is created outside of this class, and passed as a parameter to the component?

Reuse the NestedStack

For this to work, it’s necessary to remove the inheritance from the AppStack (change the class name also since it’s not a Stack anymore). First, replace the inheritance from cfn.NestedStack to a simple object, also changing the name to AppLib:

class AppStack (cfn.NestedStack):  => class AppLib (object):

Now, it’s not necessary to invoke the parent class within the constructor, remove the super() line. And since the aws_cloudformation library is not being used anymore, you can remove it, so the class will look like this:

from aws_cdk import (
    aws_ec2 as ec2,
    core
)

class AppLib (object):
    def __init__(
        self,
        scope: core.Construct,
        id: str,
        vpc: ec2.Vpc,
        **kwargs
    ) -> None:
        ec2.Instance(
            self,
            f'{id}Ec2Instance',
        ...

The last thing to fix is the creation of the resources, the AWS CDK doesn’t know what is the scope anymore since the inheritance was removed! For this, pass the scope variable that continues to receive.

        ec2.Instance(
            scope,
            f'{id}Ec2Instance',
        ...

NOTE: Don’t forget to rename your file to app_lib.py

Now you can start to reuse the Stack to create more components inside a single Nested Stack. Going back to the app.py create one Nested Stack so to start reusing it!

import aws_cdk.aws_cloudformation as cfn
from ec2_lib import AppLib

...

stack_1 = cfn.NestedStack(
    main_stack,
    'STACK1'
)

ec2_app = AppLib(
    stack_1,
    'App',
    vpc=skeleton.vpc
)

ec2_app_1 = AppLib(
    stack_1,
    'App1',
    vpc=skeleton.vpc
)

ec2_app_2 = AppLib(
    stack_1,
    'App2',
    vpc=skeleton.vpc
)

Now all 3 Components (App, App1, App2) will be in only one Stack (STACK1), and the logic will stay the same in a cleaner way!

Rafael Zamana Kineippe

Rafael Zamana Kineippe

AWS Consultant at Sentia Consulting