🚀

Lambdaの作成を通じて学ぶGolang(Lambdaのデプロイ)

2021/06/07に公開

普段Lambdaの開発にはPythonを使用していますが、最近Golangを使い始めたのでLambdaの作成を通じてGolangの勉強を進めたいと考えています。
今後APIなどLamdbaを使ったいくつかのパターンをまとめて投稿予定です。

今回は初回ということでCDKを使ってGolangで作成したLambdaをデプロイするところまでをまとめます。
取り扱う内容としては以下になります。(CDKはTypescriptで定義しています)

  • CDKを使ったGolangの定義
  • CDKにおけるGolangのパッケージ化(ビルド)
  • CDKとSAM CLIを組み合わせたローカル稼働
  • Golangを使ったLambdaの基本的な構造

今回作成したソースコードは以下から確認できます。

https://github.com/panyoriokome/go-lambda/tree/c29a8bfa980086be3683d12352eabaa61e473430

前提

  • aws-cdkはインストール済み
  • sam-cliはインストール済み
  • AWSのクレデンシャル情報は設定済み

インストール方法についてはAWSの公式ドキュメントやその他参考資料を適宜参照の上ご対応ください。

AWS SAM CLI のインストール
Getting started with the CDK

Setup

CDKのプロジェクト作成や必要なライブラリのインストールなど、開発を実施するための前準備を行います。

プロジェクト構造の作成

cdkのファイル格納用のディレクトリを作成し、cdk initコマンドでCDKのプロジェクトを作成します。

mkdir cdk
cd cdk
cdk init --language typescript

cdkディレクトリ内の構造は以下の通りになります。

.
├── README.md
├── bin
├── cdk.json
├── jest.config.js
├── lib
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json

CDKで必要なライブラリのインストール

CDKでLambda Functionの定義を行うにあたって必要なライブラリのインストールを行います。
APIを作ったりS3Eventをトリガーにしたりすればそれに応じて必要なライブラリをインストールする必要がありますが、今回はトリガーなしでLambdaを作成しますので必要なのは@aws-cdk/aws-lambdaのみです。

npm install @aws-cdk/aws-lambda

ライブラリの詳細については@aws-cdk/aws-lambda moduleを参照してください。

Go側で必要なライブラリのインストール

libディレクトリ配下にLambdaのソースコードを格納するためのディレクトリとファイルを作成します。

  .
  ├── cdk-stack.ts
+ └── lambda
+    └── api
+        └── main.go

作成したLambdaをGo Modulesとして管理するためgo modコマンドを実行し、GolangのLambdaで必要となるライブラリであるaws-lambda-go/lambdaをインストールします(詳細はLambdaの作成で説明します。)

cd lib/lambda/api
go mod init api

# ライブラリのインストール
go get github.com/aws/aws-lambda-go/lambda

これで依存ライブリラのダウンロードやディレクトリ構造の作成などの準備は完了です。

CDKにおけるLambdaの定義

プロジェクト作成時に自動で作成されるcdk-stack.tsファイルにLambdaの定義を追加します。

GolangのLambda Functionにおける定義のポイントの一つとしてバンドル処理(デプロイ用パッケージの作成)があると思います。

  • Golangにおけるデプロイ用パッケージにはGo executable(go buildで生成される実行用バイナリファイル)を含める必要がある
  • Go executableはソースコードをコンパイルして作成されるものなので、リポジトリの管理対象とはせずにCI/CDパイプラインなどデプロイ等の処理の中で自動的に実行したい

自分でスクリプトを書いたり、ライブラリの機能を利用したり色々な方法があるとは思いますが、今回は先ほどインストールしたaws-lambda-go/lambdaライブラリの機能を利用します。

aws-lambda-go/lambdaにおけるコードの指定方法

まず前提としてCDKにおけるLambdaのソースコードの指定方法を簡単にまとめます。
aws-lambda-go/lambda - Handler Codeに記載してあるとおり、Lambdaのソースコードとして以下4つの指定方法があります。

  • fromBucket
    • S3バケットのオブジェクトを指定
  • fromInline
    • インラインでコードを記載する
  • fromAsset
    • ローカルのディレクトリかZIPファイルを指定
  • fromDockerBuild
    • ビルドしたDocker Imageを指定

以下は公式ドキュメントから転載したものですが、fromAssetを使用してPythonのLambdaを定義する例です。

new lambda.Function(this, 'MyLambda', {
  code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')),
  handler: 'index.main',
  runtime: lambda.Runtime.PYTHON_3_6,
});

前述の通りGoではソースコード→Go Executableへのコンパイルが必要なので、その部分をどうにかして自動化したいです。

bundlingオプションの活用

fromAssetにはbundlingというオプションの指定が可能で、これを使うことで任意のバンドル処理を定義でき、今回はこの方法を利用します。

bundlingオプションを指定することでDockerを使ったビルドを実行することが可能で、設定内容としてビルドに使用するイメージや実施するコマンドなどの指定が可能です。

以下が最終的なコードです。

/lib/cdk-stack.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda'
import * as path from 'path'

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

    // The code that defines your stack goes here
    new lambda.Function(this, 'MyGoFunction', {
      runtime: lambda.Runtime.GO_1_X,
      handler: 'main',
      code: lambda.Code.fromAsset(path.join(__dirname, './lambda/api'), {
        bundling: {
          image: lambda.Runtime.GO_1_X.bundlingImage,
          command: [
            'bash',
            '-c',
            ['go test -v', 'GOOS=linux go build -o /asset-output/main'].join(
              ' && '
            ),
          ],
          user: 'root',
        },
      }),
    })
  }
}

ビルド結果の出力先がasset-ouputディレクトリになっているのはライブラリの仕様としてasset-ouputディレクトリ配下のファイルがZIPされLambdaのコードとして使用されるからです。

詳細についてはBuilding, bundling, and deploying applications with the AWS CDKに非常にわかりやすくまとまっています。
実行環境についてはDockerを使わずにローカルで行うことも可能みたいです。こちらについても上記の記事に載っていますので、よろしければご覧ください。

ビルドの実行

後述するローカル稼働でも説明しますが、cdk synthコマンドを実行することでビルドが行われ、デプロイパッケージが作成されます(今回の例だとgo buildによる依存ライブラリのインストールとGo Executableの作成)。

実際にcdk synthコマンドを実行すると以下の通りcdk.outディレクトリが作成され、CloudFormationのテンプレートとデプロイパッケージが格納されていることが確認できます。

これでデプロイパッケージの作成についてはやり方が確認できましたので、次にLambdaのソースコードを作っていきます。

Lambdaの作成

今回作成するLambdaのコードはGo の AWS Lambda 関数ハンドラーに記載してある内容そのままです。
eventとして受け取った名前を付け加えた文字列を返すだけの簡単なプログラムですね。

package main

import (
	"context"
	"fmt"

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

type MyEvent struct {
	Name string `json:"name"`
}

func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
	return fmt.Sprintf("Hello %s!", name.Name), nil
}

func main() {
	lambda.Start(HandleRequest) // ハンドラー関数を呼び出す必要がある
}

ポイントとしては以下3点になります。

  • github.com/aws/aws-lambda-go/lambda パッケージを含める必要がある
  • main関数の実装が必要になる
  • github.com/aws/aws-lambda-go/lambda パッケージを利用してmain関数からハンドラー関数を呼び出す必要がある

contextやeventの活用についてはAPIの作成等を題材にした記事を執筆予定ですので、その際にまとめようと思います。

ローカルでの稼働

ローカルで稼働させる方法はいくつかあると思いますが、今回はAWS SAMを利用する方法を使います。
手順としては以下の通りです。

  • cdk synthによるtemplateファイルの作成
  • Temlateファイルで稼働対象のLambdaの確認
  • AWS SAMによるLambdaの稼働

ポイントとしてはAWS SAMでLambdaをローカル稼働させたいが、AWS SAMでローカル稼働するためにはCloudFormationのテンプレート(CDKではなく)が必要になるため、CDK→CloudFormationの変換とそれに付随してテンプレートの確認が必要になります。

cdk synthによるtemplateファイルの作成

前述の通りcdk synthコマンドでビルドが行えます。この際、CDKのコードからCloudFormationのコードを出力されますが、出力方法を指定します。

cdk synth --no-staging > template.yaml 

Temlateファイルで稼働対象のLambdaの確認

cdk synthコマンドで生成したtemplate.yamlファイルから稼働させたいLambdaのIDを確認します。
template.yamlファイルには色々なリソースが定義されていますが、Lambdaの定義はType: AWS::Lambda::Functionでされていますので、これをヒントに探すとスムーズだと思います。

以下は今回生成されたテンプレートファイルの中からLambdaの箇所を抜き出したものです。

  ...
  MyGoFunction0AB33E85:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Ref: AssetParam

AWS SAMによるLambdaの稼働

稼働対象のLambdaのIDも特定できたので、後はLambdaを稼働させるだけです。

% sam local invoke MyGoFunction0AB33E85 --no-event
START RequestId: e3159a84-ac33-4bf4-9ac6-dcd7575a3b22 Version: $LATEST
END RequestId: e3159a84-ac33-4bf4-9ac6-dcd7575a3b22
REPORT RequestId: e3159a84-ac33-4bf4-9ac6-dcd7575a3b22  Init Duration: 0.88 ms  Duration: 292.87 ms     Billed Duration: 300 ms        Memory Size: 128 MB     Max Memory Used: 128 MB
"Hello !"%                       

また、-eオプションをつけることでインプットとなるEventの情報を渡すことが可能です。

% sam local invoke MyGoFunction0AB33E85 -e lib/events/test.json 
...
"Hello Test User!"%                                   

eventを渡すとそれが出力結果に反映されることも確認できました。

指定可能なオプション等の詳細はsam local invokeを参照ください。

Deployの実施

ローカル稼働で確認が取れましたので、デプロイを実施していきます。

cdkディレクトリからcdk deployコマンドを実行して、デプロイを実施します。

cdk deploy

コマンドの実行が完了したらマネジメントコンソールからLambdaが作成されていることを確認します。

テストコマンドを実行して、デプロイ後も想定通りに動作していることを確認します。

確認結果にも問題はありませんでしたので、これで今回の対応は完了です。

おまけ(名称等の変更)

デプロイ結果でも分かったと思いますが、Lambdaの名前などが適当なので、該当する部分の設定を変更してみます。

変更対象としては以下2点です。

  • Lambda Functionの名前が適当
  • CloudFormation Stackの名前がデフォルト値(CdkStackのまま)

Lambda Functionの名前が適当

functionNameで、Lambdaの名前を指定することができます

Type: string (optional, default: AWS CloudFormation generates a unique physical ID and uses that ID for the function's name. For more information, see Name Type.)
A name for the function.
@aws-cdk_aws-lambda.Function

以下の通りfunctionNameの指定を加えて再度デプロイを実施します。

/lib/cdk-stack.ts
 new lambda.Function(this, 'MyGoFunction', {
       runtime: lambda.Runtime.GO_1_X,
+      functionName: 'GoSampleLambdaFunction',
       handler: 'main',

Lambdaが指定した名前に変更されていることを確認しました。

CDKstackの名前

ここまで一切いじってきませんでしたが、binディレクトリ直下のcdk.tsファイルでスタック自体の定義が行われているため、この部分の設定を変更します。

/bin/cdk.ts
 #!/usr/bin/env node
 import 'source-map-support/register';
 import * as cdk from '@aws-cdk/core';
 import { CdkStack } from '../lib/cdk-stack';

 const app = new cdk.App();
- new CdkStack(app, 'CdkStack', {
+ new CdkStack(app, 'GoLambdaStack', {

この状態でcdk deployを実施したら、以下のエラーが出てデプロイが落ちてしまいました。

12:23:32 | CREATE_FAILED        | AWS::Lambda::Function | MyGoFunction0AB33E85
GoSampleLambdaFunction already exists in stack arn:aws:cloudformation:ap-northeast-1:111111111111:stac
k/CdkStack/75bc0d30-c5ab-11eb-8bd3-0ea6b7b9f1b7

一度Stackを削除してから再度デプロイを実施すれば問題なくデプロイが完了しました。

Discussion