How to easily create nested CDK Stacks with addDependency

cjjenkinson

Cameron Jenkinson

Posted on January 31, 2020

How to easily create nested CDK Stacks with addDependency

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:

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 }


Enter fullscreen mode Exit fullscreen mode

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) – 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)

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 }
Enter fullscreen mode Exit fullscreen mode

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

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 }
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
cjjenkinson
Cameron Jenkinson

Posted on January 31, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related