Jérôme Decoster

Jérôme Decoster

3x AWS Certified - Architect, Developer, Cloud Practionner

22 Nov 2021

CDK + CDKTF + Lambda + DynamoDB

The Goal
  • Create an API Gateway, a Lambda and a DynamoDB Table via CDK
  • Use curl to add item and list items
  • Create the same test application using CDKTF to compare

    architecture.svg

    Install and setup the CDK project

    Get the code from this github repository :

    # download the code
    $ git clone \
        --depth 1 \
        https://github.com/jeromedecoster/cdk-lambda-dynamodb.git \
        /tmp/cdk
    
    # cd
    $ cd /tmp/cdk
    

    To setup the project, run the following command :

    # install
    $ make setup
    

    This command will :

    To initialize a cdk project we execute this command in an empty folder :

    # possible values [csharp|fsharp|go|java|javascript|python|typescript]
    $ cdk init app --language typescript
    

    Just after installing the basic files necessary to start a project, we can see the disproportionate weight of an empty project : 236 Mo

    # 236M    .
    $ du --summarize --human-readable
    

    The number of files is also colossal :

    # 14353
    $ find . -type f | wc -l
    

    To complete our project we need to install 3 npm packages. This still adds weight and a significant number of files :

    # @aws-cdk/aws-lambda@1.132.0: 3.66 MiB
    $ npm install @aws-cdk/aws-lambda
    
    # @aws-cdk/aws-apigateway@1.132.0: 5.14 MiB
    $ npm install @aws-cdk/aws-apigateway
    
    # @aws-cdk/aws-dynamodb@1.132.0: 4.08 MiB
    $ npm install @aws-cdk/aws-dynamodb
    

    In the end, we have a very simple project which takes on totally disproportionate proportions :

    # project stats
    $ make stats
    
    #  270M crazy size !
    # 15286 crazy files count !
    

    File exploration and infrastructure deployment

    The huge benefit of CDK is that it removes the incredible complexity and illegibility of CloudFormation templates by using easy-to-read code.

    The best example is the creation of the API Gateway via the RestApi class.

    An excerpt from the file apigateway-stack.ts :

    import * as cdk from '@aws-cdk/core'
    import * as apigw from '@aws-cdk/aws-apigateway'
    
    export class ApiGatewayStack extends cdk.Stack {
    
      public readonly api: apigw.LambdaRestApi
    
      constructor(scope: cdk.Construct, id: string, props: apigw.LambdaRestApiProps) {
        super(scope, id, props)
    
        // LambdaRestApiProps : https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_apigateway/LambdaRestApiProps.html
        var apiProps: apigw.LambdaRestApiProps = {
          handler: props.handler,
          restApiName: process.env.PROJECT_NAME,
          defaultCorsPreflightOptions: {
            allowOrigins: ['*'],
            allowCredentials: true,
          },
          endpointTypes: [apigw.EndpointType.REGIONAL],
        }
    
        this.api = new apigw.LambdaRestApi(this, 'ApiGateway', apiProps)
    
        // ...
      }
    }
    

    We deploy the project with ease :

    # destroy all resources
    $ make deploy
    

    This command runs this script :

    $ cdk deploy --all --require-approval never --outputs-file cdk-outputs.json
    # 234 seconds (creation)
    #  23 seconds (no modification, just after creation)
    

    Creating the resources takes more than 3 minutes.

    Executing a new deployment without any modification takes quite a long time : approximately 20 seconds.

    We can test the application with these 2 commands :

    # add item (execute several times)
    $ make curl-add
    
    # list items
    $ make curl-list
    

    Update the Lambda function

    We’ll update the Lambda function and see how long it takes to update the infrastructure.

    Here is an overview of the Lambda function :

    exports.handler = async (event) => {
    
        // return buildResponse(200, event)
    
        var response
        
        if (event.httpMethod == 'GET' && event.path == '/') {
            response = list()
        } else if (event.httpMethod == 'POST' && event.path == '/add') {
            response = add(JSON.parse(event.body).value)
        } else {
            response = buildResponse(200, 'nothing')
        }
    
        return response 
    }
    

    Our modification is simple : we uncomment the first line.

    exports.handler = async (event) => {
    
        return buildResponse(200, event)
    

    We save our changes and deploy our application again :

    #  53 seconds (update lambda, uncomment `return buildResponse(200, event)`)
    $ make deploy
    

    It takes almost 1 minute to update our Lambda

    If we comment and deploy again, this time I have a little over 1 minute :

    #  67 seconds (update lambda,   comment `return buildResponse(200, event)`)
    $ make deploy
    

    The demonstration is over. We can delete our resources with this command :

    # destroy all resources
    $ make destroy
    

    Install and setup the CDKTF project

    architecture.svg

    Get the code from this github repository :

    # download the code
    $ git clone \
        --depth 1 \
        https://github.com/jeromedecoster/cdktf-lambda-dynamodb.git \
        /tmp/cdktf
    
    # cd
    $ cd /tmp/cdktf
    

    To setup the project, run the following command :

    # install
    $ make setup
    

    This command will :

    To initialize a CDKTF project we execute this command in an empty folder :

    # possible values [csharp|go|java|python|python-pip|typescript]
    $ cdktf init --template=typescript --local \
      --project-name cdktf-lambda-dynamodb \
      --project-description cdktf-lambda-dynamodb
    

    Just after installing the basic files necessary to start a project, we can see the disproportionate weight of an empty project : 114 Mo

    # 114M    .
    $ du --summarize --human-readable
    

    The number of files is also colossal :

    # 6809
    $ find . -type f | wc -l
    

    To complete our project we need to install 1 npm package. This still adds weight and a significant number of files :

    # @cdktf/provider-aws@2.0.13: 9.16 MiB
    $ npm install @cdktf/provider-aws
    

    In the end, we have a very simple project which takes on totally disproportionate proportions :

    # project stats
    $ make stats
    
    # 280M crazy size !
    # 7092 crazy files count !
    

    File exploration and deployment

    The code you write using CDKTF is not as concise as with CDK. This is especially true when you need to define the API Gateway instance.

    An excerpt from the apigateway-stack.ts file :

    import { Construct } from 'constructs'
    import { TerraformOutput } from 'cdktf'
    import { APIGateway, LambdaFunction } from '@cdktf/provider-aws'
    
    interface ApiGatewayProps {
      lambda: LambdaFunction.LambdaFunction
    }
    
    export class ApiGatewayStack extends Construct {
        
        public readonly api: APIGateway.ApiGatewayRestApi
    
        constructor(scope: Construct, name: string, props: ApiGatewayProps) {
            super(scope, name)
    
            this.api = new APIGateway.ApiGatewayRestApi(this, 'ApiGateway', {
              name: String(process.env.PROJECT_NAME),
              endpointConfiguration: {
                types: ['REGIONAL']
              }
            })
    
            // path: /{proxy+}
            
            const proxy = new APIGateway.ApiGatewayResource(this, `${process.env.PROJECT_NAME}-proxy`, {
              restApiId: this.api.id,
              parentId: this.api.rootResourceId,
              pathPart: '{proxy+}',
            })
        
            const proxyMethod = new APIGateway.ApiGatewayMethod(this, `${process.env.PROJECT_NAME}-proxy-method`, {
              restApiId: this.api.id,
              resourceId: proxy.id,
              authorization: 'NONE',
              httpMethod: 'ANY'
            })
            
            const proxyIntegration = new APIGateway.ApiGatewayIntegration(this, `${process.env.PROJECT_NAME}-proxy-integration`, {
              httpMethod: proxyMethod.httpMethod,
              resourceId: proxy.id,
              restApiId: this.api.id,
              type: 'AWS_PROXY',
              integrationHttpMethod: 'POST',
              uri: props.lambda.invokeArn
            })
    
            // path: /
        
            const root = new APIGateway.ApiGatewayMethod(this, `${process.env.PROJECT_NAME}-root-method`, {
              restApiId: this.api.id,
              resourceId: this.api.rootResourceId,
              authorization: 'NONE',
              httpMethod: 'ANY'
            })
        
            const rootIntegration = new APIGateway.ApiGatewayIntegration(this, `${process.env.PROJECT_NAME}-root-integration`, {
              restApiId: this.api.id,
              resourceId: root.resourceId,
              httpMethod: root.httpMethod,
              type: 'AWS_PROXY',
              integrationHttpMethod: 'POST',
              uri: props.lambda.invokeArn
            })  
        
            const deployment = new APIGateway.ApiGatewayDeployment(this, `${process.env.PROJECT_NAME}-deployment`, {
              restApiId: this.api.id,
              dependsOn: [
                proxyIntegration, 
                rootIntegration
              ],
              stageName: 'prod',
            })
    
            // ...
        }
    }
    

    With CDK we use LambdaRestApi which is a helper and 33 lines are enough to define our API Gateway.

    With CDKTF we need 87 lines and their writing is particularly laborious !

    We deploy the project with ease :

    # destroy all resources
    $ make deploy
    

    This command runs this script :

    $ cdktf deploy --auto-approve
    # 107 seconds (creation)
    #  66 seconds (no modification, just after creation)
    

    Creating the resources takes a little less than 2 minutes.

    Executing a new deployment without any modification takes quite a long time : approximately 1 minute.

    We can test the application with these 2 commands :

    # add item (execute several times)
    $ make curl-add
    
    # list items
    $ make curl-list
    

    Updating the Lambda

    As before, we comment and uncomment the first line of our Lambda function :

    exports.handler = async (event) => {
    
        // return buildResponse(200, event)
    
        var response
        
        if (event.httpMethod == 'GET' && event.path == '/list') {
            response = list()
        } else if (event.httpMethod == 'POST' && event.path == '/add') {
            response = add(JSON.parse(event.body).value)
        } else {
            response = buildResponse(200, 'nothing')
        }
    
        return response
    }
    

    We get a first time :

    #  60 seconds (update lambda, uncomment `return buildResponse(200, event)`)
    $ make deploy
    

    And we get a second time :

    #  46 seconds (update lambda,   comment `return buildResponse(200, event)`)
    $ make deploy
    

    The demonstration is over. We can delete our resources with this command :

    # destroy all resources
    $ make destroy
    

    Conclusion

    • CDK is a huge advantage over CloudFormation templates. The writing is extremely simplified.
    • CDK has a big drawback : the weight and the number of astronomical files necessary for its use.
    • CDKTF does not provide any particular comfort if you already like to write HCL templates that are already simple and elegant.
    • CDKTF is also not exemplary on the weight and the number of files necessary for its use.