SAM使っての開発の流れを勉強するために簡単なアプリケーション開発しながら、調べたことなど忘れそうなので備忘兼ねてメモしています。順次追記予定です。
例として作ってみたのは、S3バケットにアップロードしたZIPファイルを、他のS3バケットに展開する処理です。
設計
設計というか定義ですが、PPTとかで書いた絵をSAMの定義に落とすのが面倒だったのですが、こんなサービスがあることを、こちらの資料で知り、これを使いました。
このようにSAMの定義をお絵かきできるサービスです。プロパティは最低限IDとDescriptionのみ設定です。定義内容は JSON形式でImport/Exportで保存できます。
今回やりたいことも一目瞭然にわかります。
完了したら build
をすると以下のようなポップアップとともにZIPファイルがダウンロードできます。
ZIPを展開した中身です。
$ tree . ├── template.yaml └── Unzip.js
SAMのYAML定義のひな形が落ちてきます。これだけでも自分的にはかなり助かります。
AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Globals: Function: AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce Resources: ZipFiles: Type: 'AWS::S3::Bucket' Unzip: Type: 'AWS::Serverless::Function' Properties: Handler: Unzip.handler Runtime: nodejs6.10 CodeUri: . Policies: - AmazonS3ReadOnlyAccess - Version: '2012-10-17' Statement: - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' Resource: !Sub ${UnzippedFiles.Arn}/* Description: Get and Unzip and Put Events: BucketZipFiles: Type: S3 Properties: Bucket: !Ref ZipFiles Events: 's3:ObjectCreated:*' UnzippedFiles: Type: 'AWS::S3::Bucket'
nodejsやpythonならひな形のスクリプトも一緒にZIPされています。nodejsの場合は以下のようなスクリプトがダウンロードされてきます。今回はGolang使うのでこのファイルは関係無いです。
'use strict'; console.log('Loading function'); exports.handler = (event, context, callback) => { console.log('Received event:', JSON.stringify(event, null, 2)); callback(null, "Hello World"); //callback('Something went wrong'); };
YAML定義の設定情報
このファイルを確認しながら設定。 https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md
開発
Lambdaならではのところも少しあります、基本的な部分はこちらを確認しながら開発します。
Programming Model for Authoring Lambda Functions in Go - AWS Lambda
実際に簡単ですが、S3からダウンロードしたZIPファイルを展開して、別のS3にアップロードするコード作ってみました。
package main import ( "archive/zip" "context" "crypto/md5" "fmt" "io/ioutil" "log" "os" "github.com/pkg/errors" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" ) type Event map[string]interface{} type S3Object struct { id string region string bucket string key string } func parseEvent(event Event) *S3Object { obj := &S3Object{} for _, er := range event["Records"].([]interface{}) { if region := er.(map[string]interface{})["awsRegion"]; region != nil { obj.region = region.(string) } if s3 := er.(map[string]interface{})["s3"]; s3 != nil { obj.bucket = s3.(map[string]interface{})["bucket"].(map[string]interface{})["name"].(string) obj.key = s3.(map[string]interface{})["object"].(map[string]interface{})["key"].(string) } } obj.id = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s%s%s", obj.region, obj.bucket, obj.key)))) return obj } func wrapError(err error, message string) error { return errors.Wrapf(err, "%s:%s", message, err.Error()) } func logger(id string, step string, message ...string) { log.Printf("[%s]%s %s", id, step, message) } func Handler(ctx context.Context, event Event) error { // SAM定義から設定した環境変数より解凍先のS3情報の取得 step := "Init" s3Endpoint := os.Getenv("S3_ENDPOINT") tgtArn, err := arn.Parse(os.Getenv("TARGET_S3_ARN")) log.Print("S3_ENDPOINT:", s3Endpoint) log.Printf("TARGET_S3_ARN:%#v", tgtArn) // S3イベント情報から必要なものを抽出 step = "ParseEvent" src := parseEvent(event) logger(src.id, step, fmt.Sprintf("Source Object: %#v", *src)) // S3からダウンロードしたファイルを保存するTempファイルを作成 step = "CreateTempfile" tmpfile, err := ioutil.TempFile("/tmp", "srctmp_") if err != nil { return errors.New(err.Error()) } defer os.Remove(tmpfile.Name()) logger(src.id, step, fmt.Sprintf("tmpfilename: %s", tmpfile.Name())) // ダウンロード処理 step = "SetClient" sess := session.Must(session.NewSession(&aws.Config{ S3ForcePathStyle: aws.Bool(true), Region: aws.String(src.region), Endpoint: aws.String(s3Endpoint), })) downloader := s3manager.NewDownloader(sess, func(d *s3manager.Downloader) { // Option指定 d.PartSize = 64 * 1024 * 1024 d.Concurrency = 2 }) logger(src.id, step, "Download Start") n, err := downloader.Download( tmpfile, &s3.GetObjectInput{ Bucket: aws.String(src.bucket), Key: aws.String(src.key), }) if err != nil { } logger(src.id, step, fmt.Sprintf("%d bytes downloaded", n)) step = "UnzipTempFile" // S3からダウロードしてきたTempファイルを解凍 r, err := zip.OpenReader(tmpfile.Name()) if err != nil { return wrapError(err, step) } defer r.Close() logger(src.id, step, tmpfile.Name()) step = "UploadToS3Bucket" // 解凍したファイルを宛先のS3にアップロード uploader := s3manager.NewUploader(sess) for _, f := range r.File { if !f.FileInfo().IsDir() { rc, err := f.Open() if err != nil { return wrapError(err, step) } defer rc.Close() _, err = uploader.Upload(&s3manager.UploadInput{ Bucket: aws.String(tgtArn.Resource), Key: aws.String(f.Name), Body: rc, }) if err != nil { return wrapError(err, step) } logger(src.id, step, fmt.Sprintf("%s uploaded to %s", f.FileInfo().Name(), f.Name)) } } return nil } func main() { lambda.Start(Handler) }
統合テスト
Unit Test以外にはSAM Localとlocalstackを使ってみました。
SAM Local
最新版をインストールしたいのでgo get
で直接取得します。
$ go get github.com/awslabs/aws-sam-local
使い方はこちらに書いた通りです。
AWS SAM ローカルを使用したサーバーレスアプリケーション(Golang版) - YOMON8.NET
イベントの形式を確認
形式や使い方を確認するにはこちらのレポジトリを確認するのが一番です。
aws-lambda-go/events at master · aws/aws-lambda-go · GitHub
コーディングするなかで、実際のイベント例を確認したければ以下のように出力できます。
$ aws-sam-local local generate-event s3 \ --region ap-northeast-1 \ --bucket zipfiles \ --key upload > s3event.json
出力したファイルを確認してイベントのスキーマを確認できます。
{ "Records": [ { "eventVersion": "2.0", "eventTime": "1970-01-01T00:00:00.000Z", "requestParameters": { "sourceIPAddress": "127.0.0.1" }, "s3": { "configurationId": "testConfigRule", "object": { "eTag": "0123456789abcdef0123456789abcdef", "sequencer": "0A1B2C3D4E5F678901", "key": "upload", "size": 1024 }, "bucket": { "arn": "arn:aws:s3:::zipfiles", "name": "zipfiles", "ownerIdentity": { "principalId": "EXAMPLE" } }, "s3SchemaVersion": "1.0" }, "responseElements": { "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH", "x-amz-request-id": "EXAMPLE123456789" }, "awsRegion": "ap-northeast-1", "eventName": "ObjectCreated:Put", "userIdentity": { "principalId": "EXAMPLE" }, "eventSource": "aws:s3" } ] }
あとはこのjsonをSAM Localに渡してGoのハンドラ実行しながらデバックしたりします。UnitTestに組み込んでしまってもよいかもです。
aws-sam-local local invoke unzip -e envet.json --profile default
localstack
localstackですが今回はS3を利用したいので、以下のような docker-compose.yml
用意して実行しています。
version: "3.3" services: localstack: container_name: localstack image: localstack/localstack ports: - "4572:4572" environment: - SERVICES=s3 - DEFAULT_REGION=ap-northeast-1 - DOCKER_HOST=unix:///var/run/docker.sock
aws cliからも endpoint-url
を指定することでアクセスできます。
aws --endpoint-url=http://localhost:4572 s3 mb s3://zipfiles
Golangからアクセスするには上記のコードの以下の部分、特に S3ForcePathStyle
を指定してあげることを注意してください。
sess := session.Must(session.NewSession(&aws.Config{
S3ForcePathStyle: aws.Bool(true),
Region: aws.String(src.region),
Endpoint: aws.String(s3Endpoint),
}))
aws-sam-localとlocalstackも踏まえて以下のようにMakefile作っています。Makefile作っておくと後でCI回すのも便利です。
test-integration
の部分が長いですが、実際にS3にアップロードして、Eventを生成トリガーしてテストしています。
PROJECT_NAME := "lambdagolangsample" STAGING_S3_BUCKET := "otomo-devel" STAGING_S3_PREFIX := "package" STACK_NAME := "my-stack" all: build .PHONY: deps deps: dep ensure .PHONY: build build: deps for d in $$(ls -d ./functions/*);do GOOS=linux GOARCH=amd64 go build $${d};done .PHONY: test-unit test-unit: build go test ./functions/... .PHONY: test-integration test-integration: test-unit docker-compose down zip -r testdata.zip ./test/data docker-compose -p $(PROJECT_NAME) up -d while true;do if [ $$(docker-compose logs | grep "Ready" | wc -l) -gt 0 ];then break;else echo "waiting...";sleep 1;fi;done aws --endpoint-url=http://localhost:4572 s3 mb s3://zipfiles aws --endpoint-url=http://localhost:4572 s3 mb s3://unzippedfiles aws --endpoint-url=http://localhost:4572 s3 cp testdata.zip s3://zipfiles/testdata.zip aws-sam-local local generate-event s3 --region ap-northeast-1 --bucket zipfiles --key testdata.zip > ./test/env/s3event.json aws-sam-local local invoke Unzip -e ./test/env/s3event.json --env-vars ./test/env/sam-local-env.json \ --docker-network $$(docker network ls -q -f name=$(PROJECT_NAME)) aws --endpoint-url=http://localhost:4572 s3 cp s3://unzippedfiles/test/data/data.txt - | cmp test/data/data.txt - docker-compose -p $(PROJECT_NAME) down .PHONY: deploy deploy: build aws-sam-local package --s3-bucket $(STAGING_S3_BUCKET) --s3-prefix $(STAGING_S3_PREFIX) --template-file ./template.yaml --output-template-file ./packaged.yaml aws-sam-local deploy --template-file ./packaged.yaml --stack-name $(STACK_NAME) --capabilities CAPABILITY_IAM
ビルド+デプロイ
ビルド
設計の部分でダウンロードしてきていたSAMのYAML定義ファイルを少し修正します。できたファイルがこちら。
AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Globals: Function: AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce Parameters: S3Endpoint: Type: String Default: "" Resources: ZipFiles: Type: 'AWS::S3::Bucket' Unzip: Type: 'AWS::Serverless::Function' Properties: Handler: unzip Runtime: go1.x CodeUri: . Environment: Variables: TARGET_S3_ARN: !Sub ${UnzippedFiles.Arn} S3_ENDPOINT: !Ref S3Endpoint Policies: - AmazonS3ReadOnlyAccess - Version: '2012-10-17' Statement: - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' Resource: !Sub ${UnzippedFiles.Arn}/* Description: Get and Unzip and Put Events: BucketZipFiles: Type: S3 Properties: Bucket: !Ref ZipFiles Events: 's3:ObjectCreated:*' UnzippedFiles: Type: 'AWS::S3::Bucket'
後は以下のコマンドでMakefileの定義に従いサービスが展開されます。
make deploy
CI
CIにはCodeBuild使ってみます。
CodeBuild用のファイルを準備するのですが、Makefileに諸々書いているので、こちらは比較的すっきりです。
version: 0.2 env: variables: DOCKER_COMPOSE_VERSION: "1.19.0" phases: install: commands: - sudo apt-get update - sudo apt-get install -y zip unzip make - sudo curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose - go get -u github.com/golang/dep/cmd/dep - dep ensure - go get -u github.com/awslabs/aws-sam-local build: commands: - make test-integration post_build: commands: - make deploy
動作確認
動作確認用に以下のようなディレクトリとファイルを準備しました。
$ tree sample/ sample/ ├── dir1 │ ├── file1 │ ├── file2 │ └── file3 ├── dir2 │ ├── file4 │ ├── file5 │ └── file6 └── dir3 ├── file7 ├── file8 └── file9
CloudWatch Logで処理のログが出力されているのがわかります。
対象のS3バケットに無事に解凍されたファイルが存在しています。