How to easily create nested CDK Stacks with addDependency
Tags: aws, serverless, devops, tutorial
Published: 2020-01-31 08:24:42

At some point in your infrastructure build out with CDK you'll want to group services together into parent and child stacks. 

For instance we have a set of core stacks like networking, database and iAM alongside a group of application services like auth, users and movies.

Taking on from the movies example we often build serverless applications as a group of lambda functions that expose different interfaces.

A classic example is to build a set of lambda functions that allow for CRUD operations on a movie entity over REST. 

We have a create, delete, update and get handler all outlined in our application code but when it comes to creating the infrastructure stack we end with a rather large configuration:

```javascript
const path = require('path')
const apigateway = require('@aws-cdk/aws-apigateway')
const lambda = require('@aws-cdk/aws-lambda')
const cdk = require('@aws-cdk/core')

class MovieService extends cdk.Stack {
    constructor(app, id, { serviceName, stage, config }) {
        super(app, id)

        /** MovieService - Handlers */
        const functionName = `movie-app-${stage}-${serviceName}`
        const createMovieLambda = new lambda.Function(this, 'CreateMovieLambda', {
            functionName,
            code: lambda.Code.fromAsset(
                path.resolve(__dirname, `../../../build/${serviceName}.zip`)
            ),
            handler: `${serviceName}.createMovieHandlerLambda`,
            runtime: lambda.Runtime.NODEJS_10_X,
            memorySize: 512,
            timeout: cdk.Duration.seconds(60),

            environment: {
                NODE_ENV: process.env.NODE_ENV,
                STAGE: stage,
            },
        })

        const functionName = `movie-app-${stage}-${serviceName}`
        const updateMovieLambda = new lambda.Function(this, 'UpdateMovieLambda', {
            functionName,
            code: lambda.Code.fromAsset(
                path.resolve(__dirname, `../../../build/${serviceName}.zip`)
            ),
            handler: `${serviceName}.updateMovieHandlerLambda`,
            runtime: lambda.Runtime.NODEJS_10_X,
            memorySize: 512,
            timeout: cdk.Duration.seconds(60),

            environment: {
                NODE_ENV: process.env.NODE_ENV,
                STAGE: stage,
            },
        })

        const getMovieLambda = new lambda.Function(this, 'GetMovieLambda', {
          functionName,
          code: lambda.Code.fromAsset(
              path.resolve(__dirname, `../../../build/${serviceName}.zip`)
          ),
          handler: `${serviceName}.getMovieHandlerLambda`,
          runtime: lambda.Runtime.NODEJS_10_X,
          memorySize: 512,
          timeout: cdk.Duration.seconds(60),

          environment: {
              NODE_ENV: process.env.NODE_ENV,
              STAGE: stage,
          },
        })

        const deleteMovieLambda = new lambda.Function(this, 'DeleteMovieLambda', {
          functionName,
          code: lambda.Code.fromAsset(
              path.resolve(__dirname, `../../../build/${serviceName}.zip`)
          ),
          handler: `${serviceName}.deleteMovieHandlerLambda`,
          runtime: lambda.Runtime.NODEJS_10_X,
          memorySize: 512,
          timeout: cdk.Duration.seconds(60),

          environment: {
              NODE_ENV: process.env.NODE_ENV,
              STAGE: stage,
          },
        })

        /** MovieService - API Gateway */
        const apiGatewayName = `movie-app-${stage}-${serviceName}-gateway`
        const movieApi = new apigateway.RestApi(this, 'ApiGatewayForMovieService', {
            restApiName: apiGatewayName,
        })

        const createMovieLambdaIntegration = new apigateway.LambdaIntegration(
            createMovieLambda
        )
        movieApi.root.addMethod('POST', createMovieLambdaIntegration)

        const updateeMovieLambdaIntegration = new apigateway.LambdaIntegration(
          updateMovieLambda
        )
        movieApi.root.addMethod('POST', updateMovieLambdaIntegration)

        const getMovieLambdaIntegration = new apigateway.LambdaIntegration(
          getMovieLambda
        )
        movieApi.root.addMethod('GET', getMovieLambdaIntegration)

        const deleteMovieLambdaIntegration = new apigateway.LambdaIntegration(
          deleteMovieLambda
        )
        movieApi.root.addMethod('DELETE', deleteMovieLambdaIntegration)
    }
}

module.exports = { MovieService }


```

If we started adding further dependencies for each handler and more configuration for the Movies API Gateway things quickly start to get out of control.

### Add Dependency

[stack.addDependency(stack)](https://docs.aws.amazon.com/cdk/latest/guide/stacks.html) – Can be used to explicitly define dependency order between two stacks. This order is respected by the cdk deploy command when deploying multiple stacks at once.

Add dependency is a great way to solve this by making it easy to split up the stack configuration into parent and child stacks. 

We can define child stacks for each lambda configuration (and for any configuration you'd like to split out)

```javascript
const path = require('path')
const lambda = require('@aws-cdk/aws-lambda')
const cdk = require('@aws-cdk/core')

class CreateMovieLambda extends cdk.Stack {
    constructor(app, id, { serviceName, stage, config }) {
        super(app, id)

        const functionName = `movie-app-${stage}-${serviceName}`
        const createMovieLambda = new lambda.Function(this, 'CreateMovieLambda', {
            functionName,
            code: lambda.Code.fromAsset(
                path.resolve(__dirname, `../../../build/${serviceName}.zip`)
            ),
            handler: `${serviceName}.createMovieHandlerLambda`,
            runtime: lambda.Runtime.NODEJS_10_X,
            memorySize: 512,
            timeout: cdk.Duration.seconds(60),

            environment: {
                NODE_ENV: process.env.NODE_ENV,
                STAGE: stage,
            },
        })
    }
}

module.exports = { CreateMovieLambda }
```

We can not import this stack and add it as a dependency to the MovieService.

```javascript
const path = require('path')
const apigateway = require('@aws-cdk/aws-apigateway')
const lambda = require('@aws-cdk/aws-lambda')
const cdk = require('@aws-cdk/core')

const { CreateMovieLambda } = require('./create-movie-lambda');

class MovieService extends cdk.Stack {
    constructor(app, id, { serviceName, stage, config }) {
        super(app, id)

        const createMovieLambda = new CreateMovieLambda(app, 'CreateMovieLambda', { config });

        this.addDependency(createMovieLambda);

        const apiGatewayName = `movie-app-${stage}-${serviceName}-gateway`
        const movieApi = new apigateway.RestApi(this, 'ApiGatewayForMovieService', {
            restApiName: apiGatewayName,
        })
        const createMovieLambdaIntegration = new apigateway.LambdaIntegration(
            createMovieLambda
        )
        movieApi.root.addMethod('POST', createMovieLambdaIntegration);

    }
}

module.exports = { MovieService }
```
This is a much cleaner way to organise and create stacks for related services.

CDK will deploy all dependencies in order of assignment so everything will be in sync.