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

## 2020/7/23 追記
こちらの記事の内容はかなり古くなってしまったので、以下を参照いただければと思います。

**AWS SAM CLI 再入門 2020.07**
https://qiita.com/hayao_k/items/7827c3778a23c514e196


## はじめに
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の復習もかねてサンプルアプリケーションの作成からデプロイまで行ってみました。

※2018/9/3 
version 0.6.0 のgenerate-events コマンドの構文変更について追記しました。
※2018/11/8
version 0.6.2 で追加された Homebrew/MSIによるインストールについて追記しました。


## インストール
後述するinvokeやstart-apiコマンドを利用してローカルでテストする場合は
前提としてDocker のインストールが必要ですが、手順は割愛します。
SAM CLI の古いバージョンがインストールされている場合はアンインストールしておきます。

```bash
$ 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 以上であることを確認します。

```bash
$ pip install --user aws-sam-cli

$ sam --version
SAM CLI, version 0.3.0
```
できました！

※2018/11/8時点の最新版は 0.6.2 です。 
このバージョンからHomebrewおよびMSIによるインストールが可能になっています。

### masOS
```
$ brew tap aws/tap
$ brew install aws-sam-cli
```
### Windows
※ versionおよびビット数により、URLが変わります。

```
https://github.com/awslabs/aws-sam-cli/releases/download/v0.6.2/AWS_SAM_CLI_64_PY3.msi
```

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

```bash
$ 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
```
### ローカルでテストする
まずはユニットテストを実行します。

```bash
$ 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)
```

成功しました。

generate-event コマンドで 指定したサービスからLambdaに送られる疑似イベントを作成することができます。
複数サービスのペイロードを作成可能ですが、ここではAPI Gatewayのイベントを作成します。
※ verison 0.6.0 から対応サービスが増え、さらにサービスごとに複数のイベントタイプをサポートするようになり、コマンドの構文が変わっています。

```bash
$ cd ../
$ sam local generate-event apigateway aws-proxy > event_file.json
# version 0.5.0 以前
$ sam local generate-event api > event_file.json
```

次にローカルで Lambda を Invoke してみます。
テンプレートファイルを読み取るのでtemplate.yaml があるディレクトリで実行します。
ローカルにdocker lambda のイメージがない場合は最初にダウンロードされるので少し時間がかかります。

``` bash
$ 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 を起動します。 

```bash
$ 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アドレスが返ってきます。

```bash
$ curl http://127.0.0.1:3000/hello
{"message":"hello world","location":"xxx.xxx.xxx.xxx"}
```
ローカル API Gateway でも HTTP200 が返っていることを確認できます。

```bash
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でデプロイしてみます。
validate コマンドで テンプレートがSAMの仕様に沿っているか検証できます。

```bash
$ 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 が出力されているため確認します。

```bash
$ 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/"
    }
]
```
動作確認は省略します。

## IAMポリシー周りの補足(7/19追記)
sam packge や sam deploy では 実体としてはCloudFormationが動いていますので
CloudFormation関係の権限が必要です。
あとは実際に作られるリソースの作成権限などが必要ですので、
今回のサンプルアプリケーション(Lambda/API Gateway/IAMロール)の場合、
一例ですが、こんな感じでしょうか。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:getObject",
                "s3:PutObject",
                "cloudformation:Describe*",
                "cloudformation:EstimateTemplateCost",
                "cloudformation:Get*",
                "cloudformation:List*",
                "cloudformation:ValidateTemplate",
                "cloudformation:CreateChangeSet",
                "cloudformation:ExecuteChangeSet",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:AttachRolePolicy",
                "iam:DetachRolePolicy",
                "iam:GetRole",
                "iam:PassRole",
                "lambda:CreateFunction",
                "lambda:DeleteFunction",
                "lambda:GetFunctionConfiguration",
                "lambda:AddPermission",
                "lambda:RemovePermission",
                "apigateway:POST",
                "apigateway:PATCH",
                "apigateway:DELETE",
                "apigateway:GET"
            ],
            "Resource": "*"
        }
    ]
}
```

リソースの作成権限を直接持たせたくない場合は、サービスロールに切り出して
deploy 実行時に --role-arn を指定するのも良いかと思います。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html

```
sam deploy --template-file packaged.yaml --stack-name mystack --capabilities CAPABILITY_IAM --role-arn arn:aws:iam::0123456789012:role/rolename
```

以上です。
参考になれば幸いです。