AWS CDKを使用してLambdaを作る

こんにちは、SREチームの成田です。

前回Serverless Frameworkを使用したLambdaの作成についてご紹介しましたが、今回はCDKを使用したLambda作成について紹介しようと思います。

CDKについて

2019年7月にGAとなった、AWSの構成管理を行うツールです。

TypeScriptやPythonでインフラを定義しプロビジョニングを行えます。 裏側ではTypeScriptやPythonのコードからCloudFormationテンプレートが生成され、CloudFormationを使用してプロビジョニングが行われるのでCloudFormationのラッパーと思うとイメージしやすいでしょうか。

CloudFormationテンプレートを書くよりも圧倒的に少ない記述量でインフラを定義できます。

公式ドキュメントはこちらです。 aws.amazon.com

Lambdaを作ってみる

CDKの準備

それでは、CDKを使ってLambdaを作ってみます。 Lambdaは前回作成したHTTPサーバと同じ機能を持つものを作成します。

まずは、cdkコマンドを使用してテンプレートを作成します。CDKの実装言語は色々選べますが、今回はTypeScriptを使用します。 公式ガイドでは、cdkコマンドはグローバルにインストールしていますが今回はnpxでローカルにインストールして使用していきます。

$ mkdir hello
$ cd hello/
$ npx cdk --version
1.36.1 (build 4df7dac)
$ npx cdk init app --language=typescript

これでテンプレートが作成されました。カレントディレクトリは以下の様になっていると思います。

$ tree
.
├── README.md
├── bin
│   └── hello.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── hello-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── hello.test.ts
└── tsconfig.json

CDKでは、lib以下にStack、bin以下にAppを作成していきます。 Stackは、単一のユニットとしてプロビジョニングされるリソース郡で、CloudFormationのStackと対応します。 Appは、複数のStackの集合で、CDKで使用するデプロイの単位です。

実際には、lib以下に定義したStackをAppで呼び出し、Appはnodeで実行可能な状態として実装します。(なので、それぞれlibやbin以下に作成するという訳ですね。) 詳しくは公式ドキュメントを御覧ください。

Apps - AWS Cloud Development Kit (AWS CDK)

Stacks - AWS Cloud Development Kit (AWS CDK)

Lambda関数を作成

今回は以下のバージョンのGoを使用してLambda関数を作成します。

$ go version
go version go1.14.2 darwin/amd64

まずは以下の様にディレクトリとファイルを作成します。

$ mkdir -p lambda/bin lambda/src

lambda/src/hello.go

package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

// リクエストのJSONの構造体
type RequestBody struct {
    Person string `json:"person"`
}

// レスポンスのJSONの構造体
type ResponseBody struct {
    Text string `json:"text"`
}

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    fmt.Printf("Processing request data for request %s.\n", request.RequestContext.RequestID)

    // リクエストのJSONをパース
    reqBody := &RequestBody{}
    if err := json.Unmarshal([]byte(request.Body), reqBody); err != nil {
        return events.APIGatewayProxyResponse{StatusCode: 500}, err
    }

    // レスポンスBodyとなるJSON文字列を作成
    resBody := &ResponseBody{
        Text: "Hello " + reqBody.Person,
    }
    if bytes, err := json.Marshal(resBody); err != nil {
        return events.APIGatewayProxyResponse{StatusCode: 500}, err
    } else {
        return events.APIGatewayProxyResponse{Body: string(bytes), StatusCode: 200}, nil
    }
}

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

lambda/Makefile

.PHONY: build clean

build:
  GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/hello src/hello.go
clean:
  rm -rf ./bin

ファイルを作成したらビルドを行います。

$ cd lambda
$ go mod init example.com/hoge/hello
go: creating new go.mod: module example.com/hoge/hello
$ make build
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/hello src/hello.go
go: finding module for package github.com/aws/aws-lambda-go/events
go: finding module for package github.com/aws/aws-lambda-go/lambda
go: found github.com/aws/aws-lambda-go/events in github.com/aws/aws-lambda-go v1.16.0

lambda/bin/helloに実行可能ファイルが作成されていればOKです。 このファイルをCDKでAWS上のLambdaへデプロイします。

CDKのコードを書く

Lambda関数ができたので、Lambdaを作成するCDKのコードを書いて行きます。

CDKは、Constructというライブラリ化されたコンポーネントを組み合わせてリソースを定義していきます。Constructについて詳しくは公式ドキュメントを御覧ください。

Constructs - AWS Cloud Development Kit (AWS CDK)

Constructライブラリは、こちらのAPIリファレンスを見ながら使用していきます。

API Reference · AWS CDK

今回はLambdaを作成するので、LambdaのAPIリファレンスを参考にしながら実装していきます。

まずは、LambdaのConstructライブラリをインストールします。

$ npm i @aws-cdk/aws-lambda

インストールできたらlib/hello-stack.tsを以下の様に実装します。

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

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

    const fn = new lambda.Function(this, 'HelloFunction', {
      runtime: lambda.Runtime.GO_1_X,
      handler: 'hello',
      code: lambda.Code.fromAsset('./lambda/bin'),
    });

  }
}

これでLambda自体は作成されますが、HTTPサーバとしてLambdaを作成したいので、API Gatewayの作成とAPI GatewayのイベントからLambdaが起動するように設定する必要があります。

API GatewayのConstructを使用して定義しても良いですが、今回はLambda Event Sourcesを使用してAPI GatewayとLambdaを連携してみます。

@aws-cdk/aws-lambda-event-sourcesをインストールし、ApiEventSourceを使用してLambdaにイベントを登録します。

npm i @aws-cdk/aws-lambda-event-sources

lib/hello-stack.ts

import * as cdk from '@aws-cdk/core';
import lambda = require('@aws-cdk/aws-lambda');
import path = require('path');
import { ApiEventSource } from '@aws-cdk/aws-lambda-event-sources';

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

    const fn = new lambda.Function(this, 'HelloFunction', {
      runtime: lambda.Runtime.GO_1_X,
      handler: 'hello',
      code: lambda.Code.fromAsset('./lambda/bin'),
    });

    fn.addEventSource(new ApiEventSource('POST', '/hello'));
  }
}

これにてHTTPサーバとして動作するLambdaを定義することができました。

デプロイ

最後にAWSにLambdaをデプロイします。

今回はGoでLambda関数を実装したので、まずGoのビルドをしておきます。

$ (cd lambda && make build)

次にTypeScriptで実装したCDKのビルドを行います。

$ npm run build

これでデプロイ出来る状態になりましたが、デプロイの前にdiffコマンドで今回変更されるリソース一覧を見てみます。

$ npx cdk diff

f:id:nrt-krmx:20200501212649p:plain
cdk diff結果

表示されている内容が多くて見ずらいですが、Resourcesを見るとLambdaやApiGatewayが作成されることがわかりますね。

変更されるリソースの確認もできたので、いよいよデプロイを実行します。

$ npx cdk deploy
~省略~

 ✅  HelloStack

Outputs:
HelloStack.HelloStackHelloFunction7E40E13BApiEventSourceA7A86A4FEndpoint0ACF7208 = https://od1wj31ub4.execute-api.ap-northeast-1.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/HelloStack/3ac35710-8ba0-11ea-8cf1-0e3b8595a6ba

デプロイが成功すると、この様にAPI GatewayのエンドポイントのURLを表示してくれます。 試しにリクエスト投げてみると期待したとおりに動作していることがわかります!

$ curl -X POST -H "Content-Type: application/json" -d '{"person": "Narita"}' https://od1wj31ub4.execute-api.ap-northeast-1.amazonaws.com/prod/hello
{"text":"Hello Narita"}

もしデプロイで以下の様なエラーが出た場合は、LambdaのコードをアップロードするS3バケットの準備が必要なので、npx cdk bootstrapを実行してから再度デプロイしましょう。

This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap aws://unknown-account/unknown-region")

最後に

CDKを使用してLambdaを作成しましたが、Serverless Frameworkと比較すると、今回作成したHTTPサーバのような簡単な用途だとServerless Frameworkの方が使い勝手がいいなと感じます。 しかし、LambdaとDynamoDBを連携させるような用途だと、リソースを一元管理できるCDKが勝るかなと思います。

とはいえ、CDKの用途はLambdaに限定されず、比較対象としてはTerraformが適切なところですので、今後両者の比較もしたいところです。

余談ですが、個人的にはGoのサポートが待ち遠しいです。

github.com