Using Nested Stacks with AWS CDK
Tags: aws, cdk, typescript, cloudskills
Published: 2021-03-19 12:05:14

## Intro

[CloudFormation Nested Stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html) are great when you want to separate logical resources in your stack and reference those resources in other nested stacks.

An example of this would be creating the base resources like the VPC, Subnets, Security Groups in one nested stack and application resources like EC2 instances and Lambda functions in another. We can then reference the resources like the VPC in the EC2 and Lambda stack.

## Prerequisites

This post assumes a little bit of familiarity with the [CDK](https://aws.amazon.com/cdk/).

Also it's required to install the [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) and creating a `default` profile by running `aws configure`.

To follow with this post, you can clone the repo and also deploy it to see it in action!

{% github ryands17/cdk-nested-stacks no-readme %}

I am skipping the imports, to make the snippets shorter but they can be easily viewed in the file mentioned in the snippet.

## Constructs

### Base Resources

Let's start by creating the stack which will contain the base resources like the VPC and Security Group.

```ts
// lib/nested-stacks.ts

class BaseResources extends cdk.NestedStack {
  vpc: ec2.Vpc
  applicationSg: ec2.SecurityGroup

  constructor(scope: cdk.Construct, id: string, props?: cdk.NestedStackProps) {
    super(scope, id, props)

    this.vpc = new ec2.Vpc(this, 'app-vpc', {
      cidr: '10.0.0.0/20',
      natGateways: 0,
      maxAzs: 2,
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          cidrMask: 22,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 22,
          name: 'private',
          subnetType: ec2.SubnetType.ISOLATED,
        },
      ],
    })

    this.applicationSg = new ec2.SecurityGroup(this, 'application-sg', {
      vpc: this.vpc,
      securityGroupName: 'application-sg',
    })

    this.applicationSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80))
  }
}
```

Here, we have a class named `BaseResources` that extends from `cdk.NestedStack` as this will be the Nested Stack for our main resources. We have created a couple of properties such as `vpc` and `applicationSg` which will be used by the other Nested Stack i.e. our application stack.

- The first resource that we create is the VPC. We have specified the CIDR `10.0.0.0/20` and deployed two Public and two Isolated subnets.

  - The advantage of using this construct is that the public subnets will automatically be configured with a route to the Internet Gateway (IGW) and be associated to the VPC which we would have need to specified if using plain CloudFormation YAML.

- The next resource we create is the Security Group (SG). This SG has Ingress allowed on port **80** as this will be required by our application that created in another nested stack.

### Application

Now, we shall create another nested stack that contains our EC2 instance that displays a webpage via Apache.

```ts
// lib/nested-stacks.ts

interface AppResourcesProps extends cdk.NestedStackProps {
  vpc: ec2.Vpc
  applicationSg: ec2.SecurityGroup
}

class AppResources extends cdk.NestedStack {
  constructor(scope: cdk.Construct, id: string, props: AppResourcesProps) {
    super(scope, id, props)

    // The EC2 instance using Amazon Linux 2
    const instance = new ec2.Instance(this, 'simple-server', {
      vpc: props.vpc,
      instanceName: 'simple-server',
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ec2.MachineImage.latestAmazonLinux({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
      securityGroup: props.applicationSg,
    })

    // Display a simple webpage
    instance.addUserData(
      'yum install -y httpd',
      'systemctl start httpd',
      'systemctl enable httpd',
      'echo "<h1>Hello World from $(hostname -f)</h1>" > /var/www/html/index.html'
    )

    // Add the policy to access EC2 without SSH
    instance.role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')
    )
  }
}
```

We create another class `AppResources` that extends `cdk.NestedStack` and we also specify an interface of the values that we will be receiving from the `BaseResources` stack.

- The first snippet creates an EC2 instance from an Amazon Linux 2 image that it automatically selects. For this instance, we pass the VPC it belongs to and we will see in the final part how we connected these nested stacks.

  - We have then specified it to be a type of `t2.micro` and mentioned the Security Group created in the base stack.
  - Lastly, we make sure that this instance is deployed in a public subnet by specifying the `vpcSubnets` property as we need to view the application we will install on this next.

- The next snippet adds some User Data to this instance. This  installs `httpd` and echoes some text in an `h1` tag that will be displayed when we visit the public IP of this instance.

- The final snippet adds the `AmazonSSMManagedInstanceCore` managed policy that will allow us to SSH into the instance without any KeyPair or allowing inbound SSH access.

### Final resource

We need to combine these nested stacks and for that we need a parent stack. Let's create the final resource needed to make this work.

```ts
// lib/nested-stacks.ts

export class MainApp extends cdk.Stack {
  baseResources: BaseResources
  appResources: AppResources

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    this.baseResources = new BaseResources(this, 'base-resources')
    const { vpc, applicationSg } = this.baseResources

    this.appResources = new AppResources(this, 'app-resources', {
      vpc,
      applicationSg,
    })

    this.appResources.addDependency(this.baseResources)
  }
}
```

Here, we create instances of both the `BaseResources` and `AppResources` nested stack respectively.

- In the `AppResources` stack, we pass the `vpc` and `applicationSg` from the `baseResource` instance to the `appResources` props so in this way, the EC2 instance will be able to access the VPC and Security Group.

- We have also stated that `appResources` has a dependency on `baseResources` as we need the VPC and Security Group from `BaseResources` for the instance to reference.

## Deploying the stack

And we're done! Now let's deploy this stack with the following command to see it in action.

```sh
yarn cdk deploy
```

During deployment, if you open the CloudFormation console, you will see **three stacks**, one would be the main stack and the other two would be the base and application stack respectively.

![Creating of stacks in the CloudFormation UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jiz3900mdhd6nie1dlob.png)

Also note that the `BaseResources` stack will be created first due to the dependency we created via the `AppResources` stack.

After deployment, let's go to our instance and open it via its public IP. We will be greeted with the following text that we added via the User Data.

![Webpage result by viewing the EC2 public IP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tirw36biwy7cadsccmjb.png)

## Conclusion

In this post, we saw how we can use Nested Stacks to separate our resources and how we can pass resources from one nested stack to another. Here's the repo again for anybody who hasn't viewed it yet!

{% github ryands17/cdk-nested-stacks no-readme %}

Do not forget to **destroy this stack** after you're done via:

```sh
yarn cdk destroy
```

Thank you all for reading and let me know your thoughts in the comments :)