CDK Lambda Deployment takes about a minute - how about sub second Function Code Deployment?



CDK Lambda Deployment takes about a minute - how about sub second Function Code Deployment?

Creation of Lambda infrastructure with the CDK is really powerful. Updating the Function code is really slow. Here is a fix for that to get to a sub-second Lambda function deployment time.

Setup

Let`s assume we have a simple lambda function we want to test in the cloud by invoking it from the AWS console. We deploy the code to Lambda often, so the deployment should be as fast as possible.

We start with using a cool CDK Lambda Infrastructure Construct, assuming we have a Lambda function called “warmkalt” - the german word for weather in April, “warm and cold”:

  const image = DockerImage.fromRegistry("golang:latest")
  const backend=new Function(this, 'warmkalt', {
    runtime: Runtime.GO_1_X,
    handler: 'main',
    code: Code.fromAsset(join(__dirname, '../code'),
      {
        bundling: {
          user: "root",
          //included GO version to old
          //image: Runtime.GO_1_X.bundlingDockerImage,
          image,
          command: [
            'bash','-c', [
              'go build -mod=mod -o /asset-output/main main/main.go' ,
            ].join(' && '),
          ],
          environment: environment,
        }

      }),
    memorySize: 1024,
  });

As we like the newest (1.16 at this time) GO version, we use a custom image with the latest GO version. The bundle version with the CDK is older.

We have the following Lambda code in code/main.go. For your own deployment you can use any code and language, you just have to have a build script.

Function Code for Function “warmkalt”

package main

import (
        "fmt"
        "github.com/aws/aws-lambda-go/lambda"
        "os"
)

func hello() (string, error) {

        _, err := os.Stat("/tmp/test.txt")
        if os.IsNotExist(err) {
                fmt.Println(err)
                fmt.Println("File does not exist, creating file.")
                f, _ := os.Create("/tmp/test.txt")
                defer f.Close()
                _, err := f.WriteString("Hello World")
                if err != nil {
                        fmt.Println(err)
                }

        } else {
                fmt.Println("File exists.")
        }

        return "Hello ƛ", nil
}

func main() {
        lambda.Start(hello)
}

The code shows that a file created during the Lambda cold start is still there with a warm start.

We deploy it the first time, the resources are created, everything fine.

Update Code in a long Minute

We now change only a single line and deploy again with the cdk:

cdk deploy --force --require-approval "never"  

This takes about a minute.

The time output:

 5,86s user 0,78s system 10% cpu 1:04,33 total

That takes too long!

Next step - using the AWS cli.

Update code in 4 seconds

Now we build the code separately, so that CDK does not have to build it.

GOOS=linux go build -o dist/main main.go

This is valid for a real life development cycle, because we only deploy pre-build function code. If the build does not work, we would not deploy it. If you work inside a CI/CD pipeline, just stick with the CDK..

Now we can deploy the code build into the static binary directly with the update-function-call:

zip function.zip main
aws lambda update-function-code --function-name "warmkalt" --zip-file fileb://function.zip

The median from 5 iterations gives a 3.61 seconds deploy time, which is about 18 times faster.

7307f7b7.png

(This is a german excel, so “1,01” means “1.01”)

Update Code in 3 seconds

How to speed this up even more?

We could zip the build (or build the zip) in the background after each build cycle!

With Task and other build systems it is possible to watch for file changes, here with --watchas a parameter.

  build-zip:
    desc: Builds lambda
    cmds:
      - clear && date
      - GOOS=linux go build -o dist/main main.go
      - cd dist && zip function.zip main
    sources:
      - main.go    
    generates:
      - dist/main
      - dist/function.zip
    silent: true
    ignore_error: true

When we start task with:

task build-zip --watch

It will update the dist/funktion.zip each time the main.go is changed.

So we change the deploy code (also in Taskfile.yml) as follows to deploy the pre-build zip:

  deploy2:
    dir: dist
    deps: [ build-zip ]  
    desc: Deploy from local disc
    cmds:
      - aws lambda update-function-code --function-name "warmkalt" --zip-file fileb://function.zip

85361adf.png

Update Code in 2 seconds

Updating from a S3 Bucket should be even faster. So we upload the zip file to s3 also in the background:

  build-zip-s3:
    desc: Builds lambda
    cmds:
      - clear && date
      - GOOS=linux go build -o dist/main main.go
      - cd dist && zip function.zip main
      - cd dist && aws s3 cp function.zip s3://cdktoolkit-stagingbucket/functions/warmkalt.zip --quiet && echo "Upload complete"
    sources:
      - main.go    
    generates:
      - dist/main
      - dist/function.zip
    silent: true
    ignore_error: true

and run

task build-zip-s3 --watch

To deploy we use:

  deploy3:
    dir: dist
    deps: [ build-zip-s3 ]  
    desc: Deploy from s3
    cmds:
      - aws lambda update-function-code --function-name "warmkalt" --s3-bucket cdktoolkit-stagingbucket --s3-key functions/warmkalt.zip 

And now we are down to 1,56 seconds, about 40 times faster.

Sub 1 second

The remove the last slow step, we replace the aws-cli with a less than 30 lines GO programm:

package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/lambda"
)
func main() {
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		panic(err)
	}
	client := lambda.NewFromConfig(cfg)
	params := &lambda.UpdateFunctionCodeInput{
	  FunctionName: aws.String("warmkalt"),
	  S3Bucket: aws.String("cdktoolkit-stagingbucket"),
	  S3Key: aws.String("functions/warmkalt.zip"),
	}
	response, err := client.UpdateFunctionCode(context.TODO(), params);
	if err != nil {
		fmt.Println("Error: ", err)
	}else{
		fmt.Println("Deploy:",response.LastUpdateStatus)
	}
}

This just calls the UpdateFunctionCode Lambda api with the parameters, where to find the zip on the bucket.

We compile it in the binary tools/deploylf with go build -o deploylf main.go

The task changes to:

  deploy4:
    deps: [ build-zip]
    desc: Deploy with go programm
    cmds:
      - tools/deploylf

Now we are down to 0.6 seconds for a Lambda Function deployment, wich is about 100times faster than the CDK deployment.

4bec5636.png

Fast Edit setup

So when i save the code:

Edit

The build is started, zipped & uploaded in the background in the build&upload window.

Upload

Now i can deploy the Lambda function code in less than one second:

667d62d6.png

If the update process is not running we “slow back” to 6 seconds:

3bf21538.png

Taskfile as whole, try yourself!

# https://taskfile.dev

version: '3'

tasks:
  build:
    desc: Builds lambda
    cmds:
      - clear && date
      - GOOS=linux go build -o dist/main main.go
    sources:
      - main.go    
    generates:
      - dist/main
    silent: true
    ignore_error: true


  deploy1:
    dir: dist
    deps: [ build ]  
    desc: Deploy from local disc
    cmds:
      - zip function.zip main
      - aws lambda update-function-code --function-name "warmkalt" --zip-file fileb://function.zip

  build-zip:
    desc: Builds lambda
    cmds:
      - clear && date
      - GOOS=linux go build -o dist/main main.go
      - cd dist && zip function.zip main
    sources:
      - main.go    
    generates:
      - dist/main
      - dist/function.zip
    silent: true
    ignore_error: true

  build-zip-s3:
    desc: Builds lambda
    cmds:
      - clear && date
      - GOOS=linux go build -o dist/main main.go
      - cd dist && zip function.zip main
      - cd dist && aws s3 cp function.zip s3://cdktoolkit-stagingbucket/functions/warmkalt.zip --quiet && echo "Upload complete"
    sources:
      - main.go    
    generates:
      - dist/main
      - dist/function.zip
    silent: false
    ignore_error: true



  deploy2:
    dir: dist
    deps: [ build-zip ]  
    desc: Deploy from local disc
    cmds:
      - aws lambda update-function-code --function-name "warmkalt" --zip-file fileb://function.zip

  deploy3:
    dir: dist
    deps: [ build-zip ]  
    desc: Deploy from s3
    cmds:
      - aws lambda update-function-code --function-name "warmkalt" --s3-bucket cdktoolkit-stagingbucket --s3-key functions/warmkalt.zip 

  deploy4:
    deps: [ build-zip-s3]
    desc: Deploy with go programm
    cmds:
      - tools/deploylf

Thanks to

Photo by Mélody P on Unsplash

Similar Posts You Might Enjoy

Using CloudFormation Modules for Serverless Standard Architecture

Serverless - a Use Case for CloudFormation Modules? Let´s agree to “infrastructure as code” is a good thing. The next question is: What framework do you use? To compare the frameworks, we have the tRick-benchmark repository, where we model infrastructure with different frameworks. Here is a walk through how to use CloudFormation Modules. This should help you to compare the different frameworks. - by Gernot Glawe

Test driven development with AWS and golang

Why Go? Go(lang) is a fast strongly typed language, which is a good fit for AWS lambda and other backend purposes. I am going to highlight some nice go features. Usually this leads to heated discussions about the “best” programming language… - by Gernot Glawe

Working with lists in DynamoDB

DynamoDB supports complex data types like lists. In this post we take a look at different ways to interact with lists. We will use Python to write code that may be used in a data access layer to manipulate items with list attributes. - by Maurice Borgmeier