johnpfeiffer
  • Home
  • Engineering (People) Managers
  • John Likes
  • Software Engineer Favorites
  • Categories
  • Tags
  • Archives

Using AWS CDK to configure deploy a Golang Lambda with APIGateway

Contents

  • CDK vocabulary
  • Install the AWS CDK Tool
  • Setup the project directories
  • Install the dependencies for AWS resources that CDK will manage
  • Write the actual Infrastructure Code
    • Output CloudFormation from a CDK Typescript file
  • Deploy resources that CDK has defined
    • Verify the new bucket was created
  • Making updates with CDK
    • Preview changes with diff
    • Apply the new changes
  • APIGateway plus Lambda plus Go
    • A tiny Go Web Request Handler
      • Manually build your go binary for AWS Lambda
    • CDK with a Golang Lambda
    • CDK with an APIGateway integrated with a Go Lambda
  • Verify your new API Gateway and Golang Lambda with CURL

Infrastructure as Code helps guarantee the elusive determinism in infrastructure that we all seek in building applications and services. Often you can defer that complexity by using Heroku, Google App Engine, or other PaaS providers. But when you need to build something really complex (or with specific controls required including managing costs) then using IaaC to tame AWS Serverless reduces some of that pain.

AWS have created their own specific product (domain specific language) "CDK" which competes with the venerable Terraform and nicely focused Serverless.

All of these products use the json syntax of CloudFormation which is the foundational AWS syntax for describing resources

It is straightforward to iteratively setup and use AWS CDK (with the native Typescript syntax).

CDK vocabulary

It all begins with stacks.

From an object oriented perspective, a stack is a definition of components that can be instantiated. An "app" is a collection of related stacks.

Let's say you wanted your persistence (DynamoDB) in one stack, and your more execution oriented components (APIGateway and Lambda) in another stack, and your App might have environment specific parameters it needs to define and pass through to each stack.

This ability to use templates and customize how you structure things, reference other CDK files, etc. makes CDK very modular and re-usable (and yes Typescript is a programming language so you can have linters and tests).

  • https://docs.aws.amazon.com/cdk/latest/guide/stacks.html
  • https://intro-to-cdk.workshop.aws/what-is-cdk.html

Install the AWS CDK Tool

Have to use npm to install the cdk tool...

npm install -g aws-cdk

  • https://docs.aws.amazon.com/cdk/latest/guide/cli.html

Setup the project directories

mkdir cdk-example

cd cdk-example

Create a subdirectory to compartmentalize all the infrastructure code (from the rest of the application, i.e. don't mix the business logic with the infrastructure plumbing)

mkdir infra cd infra

cdk init app --language typescript

creates the default files in this directory for an empty CDK app

cdk ls

   InfraStack

Names are hard but whatever you pick, like "Infra", will then show up in the AWS resources everywhere related to this project

Install the dependencies for AWS resources that CDK will manage

Option 1: manually install dependencies (it will automatically insert this into package.json)

npm install @aws-cdk/aws-s3 @aws-cdk/aws-lambda

Option 2: write the lines into packages.json and run at the command line in the infra directory: npm install

Writing files first (and ensuring they are in version control) is a more IaaC pattern

"dependencies": {
  "@aws-cdk/aws-s3": "*",
  "@aws-cdk/core": "*",
  "source-map-support": "^0.5.16"
}

Write the actual Infrastructure Code

The initial application template creates an empty "class" that represents the "stack", we customize and replace that code with the following...

lib/infra-stack.ts has only 2 changes to the default file, the import of S3 and the new Bucket resource "MyExampleBucket"

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';

export class InfraStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new s3.Bucket(this, 'MyExampleBucket', {
      versioned: true
    });
  }
}

The import renames are important to be consistent in subsequent code, so if something is "cdk" then it is cdk.Construct

More complex applications will use a "main entry point" that can refer to specific files for various stacks. - https://docs.aws.amazon.com/cdk/latest/guide/serverless_example.html

Output CloudFormation from a CDK Typescript file

cdk synth

this command will output to the display the CloudFormation that will be sent/used by AWS

Resources:
  MyExampleBucket8D68EFCA:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Metadata:
      aws:cdk:path: InfraStack/MyExampleBucket/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:

To preview what will occur between changes to the .ts file...

cdk diff

Deploy resources that CDK has defined

cdk deploy

warning, really making a change in AWS (based on your creds)

InfraStack

InfraStack: deploying...
InfraStack: creating CloudFormation changeset...
(3/3)

Stack ARN:
arn:aws:cloudformation:us-east-1:409670809604:stack/InfraStack/b5167030-00fb-11eb-9f36-12f8925a37c4

Verify the new bucket was created

aws s3 ls | grep MyExampleBucket

this assumes you have installed the AWS CLI like sudo apt install awscli

aws cloudformation list-stacks | grep InfraStack

this listing will also show deleted Stacks

Making updates with CDK

A tiny snippet change to allow bucket deletion...

new s3.Bucket(this, 'MyExampleBucket', {
  versioned: true,
  removalPolicy: cdk.RemovalPolicy.DESTROY
});
  • https://docs.aws.amazon.com/cdk/latest/guide/hello_world.html
  • https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-s3.html
  • https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-s3/bucketpolicyprops.html#aws_s3_BucketPolicyProps
  • https://docs.aws.amazon.com/cdk/api/latest/typescript/api/core/removalpolicy.html#core_RemovalPolicy

Preview changes with diff

cdk diff

Stack InfraStack
Resources
[~] AWS::S3::Bucket JohnPBucket JohnPBucket8D68EFCA
 ├─ [~] DeletionPolicy
 │   ├─ [-] Retain
 │   └─ [+] Delete
 └─ [~] UpdateReplacePolicy
     ├─ [-] Retain
     └─ [+] Delete

Apply the new changes

cdk deploy

InfraStack: deploying...
InfraStack: creating CloudFormation changeset...

cdk destroy

aws s3 ls | grep MyExampleBucket

aws cloudformation list-stacks | grep InfraStack

CDK enables "InfrastructureAsCode" and command line (or scripted) resource management, yet you must still understand the intricacies of the domain (i.e. that s3 buckets have policies and do not get destroyed by default)

s3 buckets are designed by default to not delete with a Stack, you must change the removal policy to do so


APIGateway plus Lambda plus Go

The trick to using Golang with AWS Lambdas is that it is a compiled language. Many tutorials for lambdas (even for CDK) use javascript or python since those dynamic languages can just be put in "one more file" without a build step.

A tiny Go Web Request Handler

Put a simple placeholder Golang Lambda in place using the Gin web framework for convenience

mkdir cdk-example/examplefunction/ ; cd cdk-example/examplefunction/

vim main.go

package main

import (
        "context"
        "log"

        "github.com/aws/aws-lambda-go/events"
        "github.com/aws/aws-lambda-go/lambda"
        "github.com/awslabs/aws-lambda-go-api-proxy/gin"
        "github.com/gin-gonic/gin"
)

var ginLambda *ginadapter.GinLambda

// for convenience leverage the Go init startup concept to define a global web server object
func init() {
        // stdout and stderr are sent to AWS CloudWatch Logs
        log.Printf("Gin starting")
        r := gin.Default()
        r.GET("/ping", func(c *gin.Context) {
                c.JSON(200, gin.H{
                        "message": "pong",
                })
        })
        ginLambda = ginadapter.New(r)
}


// Handler is the function that executes for every Request passed into the Lambda
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
        return ginLambda.ProxyWithContext(ctx, req)
}

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

go mod init

Ensure dependencies (like the AWS SDK) are recognized by the Go package manager

go test

This is the simplest way to trigger downloading the dependencies (imported packages), you may have to create a tiny main_test.go in order to force this to work

https://blog.golang.org/using-go-modules

Manually build your go binary for AWS Lambda

This will create an output file named "main"

GOOS=linux GOARCH=amd64 go build -o main main.go

compile for the target arch , AWS Lambda ("firecracker") is Linux =]

  • https://www.usenix.org/system/files/nsdi20-paper-agache.pdf

zip examplefunction.zip main

AWS requires these precompiled binaries to in .zip format

CDK with a Golang Lambda

Focusing on just the "infra" subdirectory in our project:

  • cdk-example/infra/package.json
  • cdk-example/infra/lib/infra-stack.ts

cd cdk-example/infra/

First update package.json

"dependencies": {
  "@aws-cdk/core": "*",
  "@aws-cdk/aws-s3": "*",
  "@aws-cdk/aws-s3-assets": "*",
  "@aws-cdk/aws-lambda": "*",
  "source-map-support": "^0.5.16"
}

Do not forget to npm install

Next update the lib/infra-stack.ts

Layout the resources from the deepest dependency first, so in this case a place for the golang function to be zipped

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as lambda from '@aws-cdk/aws-lambda';
import assets = require("@aws-cdk/aws-s3-assets")
import path = require("path")

export class InfraStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Golang binaries must have a place where they are uploaded to s3 as a .zip
    const asset = new assets.Asset(this, 'ExampleFunctionZip', {
      path: path.join(__dirname, '../../examplefunction.zip'),
    });

    const handler = new lambda.Function(this, "ExampleFunction", {
      runtime: lambda.Runtime.GO_1_X,
      handler: "main",
      code: lambda.Code.fromBucket(
        asset.bucket,
        asset.s3ObjectKey
      ),
    });
  }
}

This crucial glue code indicates that the Lambda will be named "ExampleFunction", that it will get the binary (zipped) from S3, and that the handler expects to have a binary "main"

Note that CDK will handle actually uploading the .zip to an s3 bucket

  • https://docs.aws.amazon.com/cdk/api/latest/docs/aws-s3-assets-readme.html
  • https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-readme.html#handler-code

cdk synth

outputting the CloudFormation is a quick way to valied the syntax and see any warnings

cdk diff

outputting and previewing the changes that will appear in AWS

cdk deploy

InfraStack: deploying...
[0%] start: Publishing 05e95f6b38c932a779e68a7a685e9950eca688e775c77f84787f6fa3e2ade474:current
[100%] success: Published 05e95f6b38c932a779e68a7a685e9950eca688e775c77f84787f6fa3e2ade474:current
InfraStack: creating CloudFormation changeset...
 (4/4)
 InfraStack

Stack ARN:
arn:aws:cloudformation:us-east-1:409670809604:stack/InfraStack/248d2960-0104-11eb-8cc5-0ac853a0932f

Use the AWS Console UI and visually look at the cloud formation stacks

e.g. https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/

When you filter and click on your new stack (e.g. InfraStack) you will be able to see the Resources associated with that stack

The default policy "AWSLambdaBasicExecutionRole" will actually allow the Lambda to output logs to Cloudwatch

CDK with an APIGateway integrated with a Go Lambda

Create the APIG and attach it to the Lambda:

First update infra/package.json

  "dependencies": {
    "@aws-cdk/aws-apigateway": "*",
    "@aws-cdk/aws-lambda": "*",
    "@aws-cdk/aws-s3": "*",
    "@aws-cdk/aws-s3-assets": "*",
    "@aws-cdk/core": "*",
    "source-map-support": "^0.5.16"
  }

npm install

Define the API Gateway in CDK (only adding a few more lines)

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigateway-readme.html#aws-lambda-backed-apis

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as lambda from '@aws-cdk/aws-lambda';
import assets = require("@aws-cdk/aws-s3-assets")
import apigw = require("@aws-cdk/aws-apigateway")
import path = require("path")


export class InfraStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Golang binaries must have a place where they are uploaded to s3 as a .zip
    const asset = new assets.Asset(this, 'ExampleFunctionZip', {
      path: path.join(__dirname, '../../examplefunction'),
    });

    const myhandler = new lambda.Function(this, "ExampleFunction", {
      runtime: lambda.Runtime.GO_1_X,
      handler: "main",
      code: lambda.Code.fromBucket(
        asset.bucket,
        asset.s3ObjectKey
      ),
    });

    // all routes (and REST verbs) will pass through to the lambda
    const api = new apigw.LambdaRestApi(this, 'examplefunction', {handler: myhandler});
  }
}

one gotcha is the ordering of imports, have the "path" one last

cdk synth

now the CloudFormation is very verbose =|

upon inspection you will see the type of integration Lambda is as desired, AWS_PROXY

cdk deploy

There is a warning about security because AssumeRole

InfraStack: deploying...
[0%] start: Publishing 05e95f6b38c932a779e68a7a685e9950eca688e775c77f84787f6fa3e2ade474:current
[100%] success: Published 05e95f6b38c932a779e68a7a685e9950eca688e775c77f84787f6fa3e2ade474:current
InfraStack: creating CloudFormation changeset...
  (14/14)
InfraStack

Outputs:
InfraStack.examplefunctionEndpoint65D9943D = https://abc123.execute-api.us-east-1.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:us-east-1:409670809604:stack/InfraStack/248d2960-0104-11eb-8cc5-0ac853a0932f

Verify your new API Gateway and Golang Lambda with CURL

The very helpful output means you can use CURL directly (as you get to ignore the whole "this apigateway needs to be deployed to a stage")

curl https://abc123.execute-api.us-east-1.amazonaws.com/prod/ping

you should get back either 404 or "pong" =)

To see how much more work it is to do (and understand) the API Gateway concepts...

  • https://blog.john-pfeiffer.com/localstack-apigateway-lambda-and-s3-integration-testing/

  • « Localstack APIGateway Lambda and S3 integration testing
  • Sorting in Golang »

Published

Sep 27, 2020

Category

build-CI-CD-devops

~1539 words

Tags

  • apigateway 3
  • aws 6
  • cdk 1
  • go 16
  • golang 16
  • lambda 3