AWS
CloudFormation
sam
lambda
serverless

aws-sam-local 改め aws-sam-cli の新機能 sam init を試す

はじめに

aws-sam-local が バージョンアップされ、aws-sam-cli となりました。
https://aws.amazon.com/about-aws/whats-new/2018/04/aws-sam-cli-releases-new-init-command/

また、新しいコマンド sam init が追加され、
より簡単にサンプルアプリケーションが作れるようになりました。
SAM Localの復習もかねてサンプルアプリケーションの作成からデプロイまで行ってみました。

インストール

前提として Docker のインストールが必要ですが、手順は割愛します。
SAM CLI の古いバージョンがインストールされている場合はアンインストールしておきます。

$ sam --version
A newer version of the AWS SAM CLI is available!
Your version:   0.2.11
Latest version: 0.3.0
See https://github.com/awslabs/aws-sam-local for upgrade instructions

$ npm uninstall -g aws-sam-local

新しい SAM CLI は python で実装されているため、pip でインストールします。
バージョンが 0.3.0 以上であることを確認します。

$ pip install --user aws-sam-cli

$ sam --version
SAM CLI, version 0.3.0

できました!

sam init を試す

サンプルアプリケーションの作成

sam init コマンドで Node.js v8.10 のサンプルアプリケーションを作成してみます。

$ sam init --runtime nodejs
[+] Initializing project structure...
[SUCCESS] - Read sam-app/README.md for further instructions on how to proceed
[*] Project initialization is now complete

$ cd sam-app/
$ ls -l
total 16
drwxrwxr-x 3 ec2-user ec2-user 4096 May  9 16:48 hello_world
-rw-rw-r-- 1 ec2-user ec2-user 4625 May  9 16:48 README.md
-rw-rw-r-- 1 ec2-user ec2-user 1684 May  9 16:48 template.yaml

テストコードを含むサンプルコード と SAM テンプレートが作成されていることがわかります。

hello_wolrd/app.js
const axios = require('axios')
const url = 'http://checkip.amazonaws.com/';
let response;


exports.lambda_handler = async (event, context, callback) => {
    try {
        const ret = await axios(url);
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                message: 'hello world',
                location: ret.data.trim()
            })
        }
    }
    catch (err) {
        console.log(err);
        callback(err, null);
    }

    callback(null, response)
};

hello-world/tests/unit/test_handler.js
'use strict';

const app = require('../../app.js');
const chai = require('chai');
const expect = chai.expect;
var event, context;


describe('Tests index', function () {
    it('verifies successful response', async () => {
        const result = await app.lambda_handler(event, context, (err, result) => {
            expect(result).to.be.an('object');
            expect(result.statusCode).to.equal(200);
            expect(result.body).to.be.an('string');

            let response = JSON.parse(result.body);

            expect(response).to.be.an('object');
            expect(response.message).to.be.equal("hello world");
            expect(response.location).to.be.an("string");
        });
    });
});
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
    sam-app

    Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
    Function:
        Timeout: 3


Resources:

    HelloWorldFunction:
        Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
        Properties:
            CodeUri: hello_world/
            Handler: app.lambda_handler
            Runtime: nodejs8.10
            Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
                Variables:
                    PARAM1: VALUE
            Events:
                HelloWorld:
                    Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
                    Properties:
                        Path: /hello
                        Method: get

Outputs:

    HelloWorldApi:
      Description: "API Gateway endpoint URL for Prod stage for Hello World function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

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

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

ローカルでテストする

まずはユニットテストを実行します。

$ cd hello_wolrd
$ npm install
npm notice created a lockfile as package-lock.json. You should commit this file.
added 34 packages in 1.611s

$ npm test

> hello_world@1.0.0 test /home/ec2-user/sam-app/hello_world
> mocha tests/unit/



  Tests index
    ? verifies successful response (445ms)


  1 passing (453ms)

成功しました。
次にローカルで Lambda を Invoke してみます。
ローカルにdocker lambda のイメージがない場合は最初にダウンロードされるので少し時間がかかります。

$ sam local generate-event api > event_file.json
$ sam local invoke HelloWorldFunction --event event_file.json
2018-05-09 17:16:26 Invoking app.lambda_handler (nodejs8.10)
2018-05-09 17:16:26 Starting new HTTP connection (1): 169.254.169.254
2018-05-09 17:16:26 Starting new HTTP connection (1): 169.254.169.254

Fetching lambci/lambda:nodejs8.10 Docker container image...................................................................................................
2018-05-09 17:16:59 Mounting /home/ec2-user/sam-app/hello_world as /var/task:ro inside runtime container
START RequestId: c6c1315d-7391-1717-5ab6-6130f1c4461b Version: $LATEST
END RequestId: c6c1315d-7391-1717-5ab6-6130f1c4461b
REPORT RequestId: c6c1315d-7391-1717-5ab6-6130f1c4461b  Duration: 372.69 ms     Billed Duration: 400 ms Memory Size: 128 MB     Max Memory Used: 35 MB

{"statusCode":200,"body":"{\"message\":\"hello world\",\"location\":\"xxx.xxx.xxx.xxx\"}"}

続いてローカルAPI Gateway を起動します。

$ sam local start-api
2018-05-09 17:19:44 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
2018-05-09 17:19:44 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-05-09 17:19:44  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

表示されたエンドポイントに curl すると、メッセージとIPアドレスが返ってきます。

$ curl http://127.0.0.1:3000/hello
{"message":"hello world","location":"xxx.xxx.xxx.xxx"}

ローカル API Gateway でも HTTP200 が返っていることを確認できます。

2018-05-09 17:24:21 Invoking app.lambda_handler (nodejs8.10)
2018-05-09 17:24:21 Starting new HTTP connection (1): 169.254.169.254

Fetching lambci/lambda:nodejs8.10 Docker container 
2018-05-09 17:24:57 Mounting /home/ec2-user/sam-app/hello_world as /var/task:ro inside runtime container
START RequestId: 36fdf7c7-5885-1873-ce3f-ff9c4c04f0d7 Version: $LATEST
END RequestId: 36fdf7c7-5885-1873-ce3f-ff9c4c04f0d7
REPORT RequestId: 36fdf7c7-5885-1873-ce3f-ff9c4c04f0d7  Duration: 478.00 ms     Billed Duration: 500 ms Memory Size: 128 MB     Max Memory Used: 35 MB
2018-05-09 17:24:58 No Content-Type given. Defaulting to 'application/json'.
2018-05-09 17:24:58 127.0.0.1 - - [09/May/2018 17:24:58] "GET /hello HTTP/1.1" 200 -

デプロイ

ローカル上で動作確認できたので、実際にCloudFormationでデプロイしてみます。

$ sam validate
2018-05-09 17:45:35 Starting new HTTP connection (1): 169.254.169.254
2018-05-09 17:45:35 Starting new HTTP connection (1): 169.254.169.254
2018-05-09 17:45:35 Starting new HTTPS connection (1): iam.amazonaws.com
/home/ec2-user/sam-app/template.yaml is a valid SAM Template

$ sam package --template-file template.yaml --s3-bucket <bucketname> --output-template-file packaged.yaml

Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /home/ec2-user/sam-app/packaged.yaml --stack-name <YOUR STACK NAME>

$ sam deploy --template-file packaged.yaml --stack-name mystack --capabilities CAPABILITY_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - mystack

デプロイが成功したら、Stack の Outputs に API Gateway の URL が出力されているため確認します。

$ aws cloudformation describe-stacks --stack-name mystack --query 'Stacks[].Outputs[1]'
[
    {
        "Description": "API Gateway endpoint URL for Prod stage for Hello World function",
        "OutputKey": "HelloWorldApi",
        "OutputValue": "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/"
    }
]

動作確認は省略します。
参考になれば幸いです。