「AWS無料相談会」をオンラインで開催中

AWS SAMとGolangでLambda関数の開発環境をつくる

AWS SAMを使う機会があったので、今回は、AWS SAMとGolangでのLambda関数の開発環境を紹介いたします。

AWS SAMテンプレート

まずはAWS SAMのテンプレートファイル(template.yml)から作成してゆきます。

今回は、API Gatewayへのアクセスをイベントとし、シンプルにレスポンスを返すだけの関数を作成しようと思います。テンプレートは以下のようになります(Parametersに関しては後に説明します)。

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Parameters:
  ProjectName:
    Type: String
  Stage:
    Type: String
    AllowedValues:
      - prod
      - stg
    Default: stg

Resources:
  HelloFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Join [ "-", [ !Ref ProjectName, !Ref Stage, HelloFunction ] ]
      AutoPublishAlias: !Ref Stage
      Handler: main
      Runtime: go1.x
      Tracing: Active
      Environment:
        Variables:
          Stage: !Ref Stage
      Events:
        GetEvent:
          Type: Api
          Properties:
            Path: /
            Method: post

ここで定義したHelloFunctionをデプロイできるようにしてゆきます。

コンテナ環境

全体的にDockerによるコンテナ環境を想定しており、docker-compose.ymlは以下のようになります。

version: '3'
services:
  sam-local:
    build: .
    command: ./start-sam.sh
    ports:
      - '3000:3000'
    volumes:
      - .:/var/opt/
      - /var/run/docker.sock:/var/run/docker.sock # AWS SAM Localをdocker-composeと使うときに必要
    depends_on:
      - db
      - go
    environment:
      - VOLUME=$PWD
    env_file:
      - .env # 環境変数をコンテナ内で読み込み

  go:
    command: ./gobuild.sh
    build:
      context: ./
      dockerfile: ./Dockerfile_go
    volumes:
      - .:/go/src/your-project/

  db:
    environment:
      - MYSQL_ROOT_PASSWORD=docker
      - MYSQL_PASSWORD=docker
      - MYSQL_USER=docker
      - MYSQL_DATABASE=reportdb
    build: ./docker/mysql
    ports:
      - "3306:3306"

基本的にはsam-localコンテナを立ち上げてそこで動作確認、goコンテナはビルドまでを責務として行っております。

イメージは以下のようにCLIをインストールするだけのシンプルなものです。

FROM alpine

ENV SAM_CLI_VERSION=0.3.0 
    PYTHONUSERBASE=/usr/local

RUN apk add --no-cache py-pip git bash && 
    pip install --user aws-sam-cli==${SAM_CLI_VERSION} awscli

WORKDIR /var/opt
COPY . /var/opt/

EXPOSE 3000

SAM Localでは、ビルド後の生成物をホスト側からコンテナ側にマウントする仕組みのため、Go側では、ホスト側に生成物を持ってくるようにしています。

FROM golang:1.10.2-alpine

RUN apk add --no-cache git bash && 
    go get -u github.com/golang/dep/cmd/dep && 
    go get -u github.com/golang/lint/golint

WORKDIR /go/src/your-project/
COPY . /go/src/your-project/
RUN dep ensure

CMD CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o ./main ./main.go

gobuild.sh

#!/bin/sh
dep ensure # パッケージ管理はdepを使用
CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o ./main ./main.go

start-sam.sh

#!/bin/sh
set -e

until ls -l /var/opt/main; do
  >&2 echo "go build is not done. - sleeping" # Goのビルドが終わるまで待機
  sleep 2
done

>&2 echo "go build is done - executing command"
env | sort
sam local start-api --docker-volume-basedir "${VOLUME}/" --host 0.0.0.0 --template template.yml # ホスト側のファイル一式をコンテナにマウントしつつ、SAM Local起動

Goアプリケーション

環境ができてきたので、Goで関数の実装を書いてゆきます。レスポンスを返すだけの関数なので、以下のようになります。

package main

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

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

 log.Printf("Processing Lambda request %s
", request.RequestContext.RequestID)

 return events.APIGatewayProxyResponse{
  Body:       "Hello " + request.Body,
  Headers:    map[string]string{ "x-custom-header" : "my custom header value" },
  StatusCode: 200,
 }, nil

}

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

関数の起動

準備ができたので、関数を起動してみます。

docker-compose build
docker-compose up
# (省)
sam-local_1  | 2018-06-06 08:19:51 Mounting HelloFunction at http://0.0.0.0:3000/ [POST]
sam-local_1  | 2018-06-06 08:19:51 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
sam-local_1  | 2018-06-06 08:19:51  * Running on http://0.0.0.0:3000/ (Press CTRL+C to quit)

上記のようにアプリケーションが起動して、SAM Localが動いてくれます。API Gatewayへのイベントをトリガーとしているので、以下を実行すると、Hello Paulとレスポンスが返ってくれます。

curl -H 'Content-Type:application/json' http://localhost:3000 -X POST -d "Paul"

Hello Paul

一方、SAM Localは、SAM LocalのDockerプロセスを立ち上げて、その中でLambdaのイメージ(今回だとlambci/lambda:go1.x)を利用して関数を実行する仕組みですので、コンテナ側では以下のようなログが確認できます。

sam-local_1  | 2018-06-08 08:55:18 Invoking main (go1.x)
sam-local_1  | 2018-06-08 08:55:18 Found credentials in environment variables.
sam-local_1  |
sam-local_1  | Fetching lambci/lambda:go1.x Docker container image......
sam-local_1  | 2018-06-08 08:55:23 Mounting /Users/prop/go/src/your-project as /var/task:ro inside runtime container
sam-local_1  | START RequestId: aa4db6b2-37ef-1628-cadc-3390123c9a16 Version: $LATEST
sam-local_1  | 2018/06/08 08:55:24 Processing Lambda request c6af9ac6-7b61-11e6-9a41-93e8deadbeef
sam-local_1  | END RequestId: aa4db6b2-37ef-1628-cadc-3390123c9a16
sam-local_1  | REPORT RequestId: aa4db6b2-37ef-1628-cadc-3390123c9a16    Duration: 8.44 ms    Billed Duration: 100 ms    Memory Size: 128 MB    Max Memory Used: 5 MB
sam-local_1  | 2018-06-08 08:55:24 No Content-Type given. Defaulting to 'application/json'.
sam-local_1  | 2018-06-08 08:55:24 172.26.0.1 - - [08/Jun/2018 08:55:24] "POST / HTTP/1.1" 200 -

テスト

上記はアプリケーションの動作確認としてのフローですが、テストを書けば、普通にGoのユニットテストも実行できます。

package main

import (
 "testing"
 "github.com/aws/aws-lambda-go/events"
 "github.com/stretchr/testify/assert"
)

func TestHandler(t *testing.T) {
 tests := []struct {
  request events.APIGatewayProxyRequest
  expect  string
  err     error
 }{
   {
    request: events.APIGatewayProxyRequest{Body: "Paul"},
    expect:  "Hello Paul",
    err:     nil,
   },
  }

  for _, test := range tests {
   response, err := Handler(test.request)
   assert.IsType(t, test.err, err)
   assert.Equal(t, test.expect, response.Body)
  }
}
docker-compose run go go test
PASS
ok      your-project    0.016s

環境変数の読み込み

環境変数は、Gitで追わない.envファイルを用意し、それをアプリケーション内で読み込むようにしています。

ただし、デプロイ時に、template.yml内で環境変数を読み込むために、Parametersを使用しています。template.yml内で以下のように書くことで、!Ref Stageと読み込むことができます。

Parameters:
  ProjectName:
    Type: String
  Stage:
    Type: String
    AllowedValues:
      - prod
      - stg
    Default: stg

これでデプロイ時に、--parameter-overridesオプションを使って環境変数を渡してやればOKです。デプロイのコマンドは以下のようになります。

sam deploy 
  --template-file ./packaged.yml 
  --stack-name $StackName 
  --capabilities CAPABILITY_IAM 
  --no-fail-on-empty-changeset 
  --parameter-overrides $(cat .env | tr '
' ' ')

--parameter-overrides$(cat .env | tr '
' ' ')
を指定しているので、環境変数を予めCircleCI(CI/CDはCircleCI想定です…)で設定しておき、そこから.envファイルを動的に生成する必要はあります。

Stageを環境変数として渡せば、本番用/ステージング用のLambda関数を切り分けることもできます。

まとめ

以上、AWS SAMとGolangを使用してLambda関数の開発環境を作ってみました。

以前はあまりできることが少なかった印象ですが、いまはServerlessなどとそこまで大差がなくなってきたのではないかなと思います。むしろCloudFormationで細かい設定ができる分、今後はAWS SAMを使うことが増えそうです。

参考になれば幸いです。

なお、以下のぺージでMMMのAWS Lambdaへの取り組みをご紹介しています。ぜひ合わせてご覧ください。

サーバーレスアーキテクチャ(AWS Lambda)