AWS Serverless Application Model(SAM)を使ったサーバーレスアプリ構築
AWS Serverless Application Model(SAM)とは
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
を実行すると見本となるテンプレートが作られる。
設定内容を追いかけてみる
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") } }) }
関数に対するテスト。
.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はロールをいい感じにしておいて的なやつ。
デプロイ出来た!便利。
ローカル実行
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ライクなものが出てくる。これは強い。