ぺい

渋谷系アドテクエンジニアの落書き

AWS Serverless Application Model(SAM)を使ったサーバーレスアプリ構築

AWS Serverless Application Model(SAM)とは

f:id:tikasan0804:20180703082833p:plain

github.com

AWS Serverless Application Model (以降AWS SAMとする) は、AWS公式のサーバーレスアプリケーションを構築するためのフレームワークです。

以前の記事のServerlessでAWS Lambdaのデプロイをいい感じにする - ぺいで、Serverlessというつーるを紹介したのですが、SAMは、公式サポートなので、AWS以外のサーバーレス使う予定ない人は、基本的にSAMを使う方向で良さそうな気がします。

SAMとServerless Frameworkの違い

今回使ってみて、感じた違いについてまとめました。

Serverless Frameworkは、全部乗せの万能ツールという感じ。AWSに限らず様々なクラウドで統一の設定が使える。また、デプロイまでが本当サクッと出来る。SAMを使って思ったことですが、裏で暗黙的色々いい感じにやってるんだなと思った。
あと、コミュニティが盛んなこともあり、プラグインとかも色々出ているので、

SAMはAWSに特化していることもあり、AWSしか使わないなら、SAM使っとけばいいやんと使ってみて思った。

SAM使ってみる

どんなもんかをひとまず動かして体感してみる。インストールはpipで出来ます。

❯ python -V
Python 3.6.3
❯ pip install aws-sam-cli
...
❯ sam --version
SAM CLI, version 0.4.0

ちなみに、sam init を実行すると見本となるテンプレートが作られる。

SAM CLI のインストール - AWS Lambda

設定内容を追いかけてみる

template.yaml

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

上記の定義は、定型的に入れろとのことをそのまま鵜呑みにすると、バージョン指定という認識で良さそう。

Globals:
  Function:
    Timeout: 5

関数全体に対する設定。今回はタイムアウトまで5秒となっている。

Resources:
  HelloWorldFunction: # 関数名
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello-world/
      Handler: hello-world # Handler名 今回の場合、go build後のファイル名
      Runtime: go1.x
      Tracing: Active # X-Rayによるトラッキングの有効化
      Events: # どういうイベント(トリガー)を設定するか
        CatchAll:
          Type: Api
          Properties:
            Path: /hello
            Method: GET
      Environment: # 環境変数
        Variables:
          PARAM1: VALUE

関数の定義が書かれている。

Outputs:
  HelloWorldAPI:
    Description: "API Gateway endpoint URL for Prod environment for First Function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

  HelloWorldFunction:
    Description: "First Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn

  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

CloudFormationのOutputsセクションの定義。
出力 - AWS CloudFormation

main.go

package main

import (
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"

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

var (
    // DefaultHTTPGetAddress Default Address
    DefaultHTTPGetAddress = "https://checkip.amazonaws.com"

    // ErrNoIP No IP found in response
    ErrNoIP = errors.New("No IP in HTTP response")

    // ErrNon200Response non 200 status code in response
    ErrNon200Response = errors.New("Non 200 Response found")
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    resp, err := http.Get(DefaultHTTPGetAddress)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    if resp.StatusCode != 200 {
        return events.APIGatewayProxyResponse{}, ErrNon200Response
    }

    ip, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    if len(ip) == 0 {
        return events.APIGatewayProxyResponse{}, ErrNoIP
    }

    return events.APIGatewayProxyResponse{
        Body:       fmt.Sprintf("Hello, %v", string(ip)),
        StatusCode: 200,
    }, nil
}

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

今回の関数。

main_test.go

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"

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

func TestHandler(t *testing.T) {
    t.Run("Unable to get IP", func(t *testing.T) {
        DefaultHTTPGetAddress = "http://127.0.0.1:12345"

        _, err := handler(events.APIGatewayProxyRequest{})
        if err == nil {
            t.Fatal("Error failed to trigger with an invalid request")
        }
    })

    t.Run("Non 200 Response", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(500)
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err != nil && err.Error() != ErrNon200Response.Error() {
            t.Fatalf("Error failed to trigger with an invalid HTTP response: %v", err)
        }
    })

    t.Run("Unable decode IP", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(500)
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err == nil {
            t.Fatal("Error failed to trigger with an invalid HTTP response")
        }
    })

    t.Run("Successful Request", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(200)
            fmt.Fprintf(w, "127.0.0.1")
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err != nil {
            t.Fatal("Everything should be ok")
        }
    })
}

関数に対するテスト。

Makefile

.PHONY: deps clean build

deps:
  go get -u ./...

clean: 
  rm -rf ./hello-world/hello-world
  
build:
  GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world

packageする

次に作成した関数をデプロイ用にパッケージ化します。

❯ aws s3 mb s3://hoge-fuga --region ap-northeast-1

パッケージの置き場所として、S3のバケットが必要なので、任意のバケットを用意するか、既にあるものを使います。作成するときは上のようなコマンドで作成する。

❯ sam package \                          
   --template-file template.yaml \        
   --output-template-file packaged.yaml \ 
   --s3-bucket hoge-fuga     

sam package コマンドで、作成したテンプレートから、パッケージングを実行する。デプロイに必要な情報は、今回の場合だと、outpu-template-fileで指定したpackaged.yamlで作成される。

デプロイ

❯ sam deploy \                   
   --template-file packaged.yaml \
   --stack-name sam-app \         
   --capabilities CAPABILITY_IAM  

sam deploy コマンドで実際にデプロイをします。--template-fileには先程出てきたpackaged.yamlを指定します。--stack-nameはCloudFormationのスタック名で、任意の名前を指定すれば良さそう。--capabilitiesはロールをいい感じにしておいて的なやつ。

f:id:tikasan0804:20180703081118p:plain デプロイ出来た!便利。

ローカル実行

SAMはDockerを使うことで、ローカル実行を可能にしている。

❯ sam local start-api
2018-07-03 08:19:00 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
2018-07-03 08:19:00 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
2018-07-03 08:19:00  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2018-07-03 08:19:02 Invoking hello-world (go1.x)
2018-07-03 08:19:02 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:go1.x Docker container image......
2018-07-03 08:19:05 Mounting /Users/jumpei/go/src/github.com/pei0804/serverless-sample/sam/sam-app/hello-world as /var/task:ro inside runtime container
START RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b Version: $LATEST
END RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b
REPORT RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b  Duration: 1180.19 ms    Billed Duration: 1200 ms        Memory Size: 128 MB     Max Memory Used: 10 MB
2018-07-03 08:19:08 No Content-Type given. Defaulting to 'application/json'.
2018-07-03 08:19:08 127.0.0.1 - - [03/Jul/2018 08:19:08] "GET /hello HTTP/1.1" 200 -
❯ curl 127.0.0.1:3000/hello
Hello, 103.2.249.5

ログはCloudWatchライクなものが出てくる。これは強い。