AWS SAMを今さらながら使ってみる

CloudFormationはそれなりに書けるし、Lambda関数も書いたり動かしたり調査したり色々やってきたが、本職のデベロッパーでない悲しさか、AWS SAMは実は一度も触ったことがない（触る必要に迫られたことがない）。
苦手なままでは先々苦労しそうなので、このあたりで一度ウォークスルーしてみることにする。

### やりたいこと
- AWS SAMをインストールする。
- 試し打ちしながら、できることを自分なりに整理してみる。

### インストール
まずはインストール。
詳しくは[公式](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install-mac.html)を参照。
ローカル開発したい場合はDockerも必要らしい。
Dockerはもう入れてあるので、やることは以下。

```
% brew tap aws/tap
% brew install aws-sam-cli
```
入った。

```
% sam --version
SAM CLI, version 0.44.0
```

### 探索
まずはできることをまとめてみる。

|コマンド|内容|
|:----|:---|
|sam init|SAMアプリケーションを初期化する。|
|sam validate|SAMテンプレートを検証するのに使う。|
|sam build|Lambda関数をビルドするのに使う。|
|sam local|サーバレスアプリケーションをローカル実行し、テスト等に使えるらしい。npm run serve的なものか？|
|sam package|SAMアプリケーションをパッケージ？してくれるらしい。パッケージって具体的に何だ？|
|sam deploy|SAMアプリケーションをデプロイしてくれるらしい。Lambda関数をクラウド上にデプロイするということだろう。|
|sam log|Lambda関数のログを取得してくれるようだ。|
|sam publish|AWSサーバーレスアプリケーションレポジトリに公開するために使う、らしい。|

`sam [コマンド] --help`でざっと用法を眺めてみたが、ドキュメント見た方が早そうだったので[公式](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-reference.html)に目を通す。

なるほど。要するに、
1. `sam init`でアプリのひな形を作り、
2. `sam build`でビルドし、
3. `sam deploy`でクラウド側にデプロイする

のだな。あれ、`sam package`は？
と思ったら、[ここ](https://aws.amazon.com/jp/about-aws/whats-new/2019/11/aws-sam-cli-simplifies-deploying-serverless-applications-with-single-command-deploy/)を見る限り今は必須ではないらしい。`sam local`でのローカル実行も今回はパス。

### 試し打ち
だいたいイメージができてきたので、試しにSAMアプリケーションとやらを作ってみる。
Amplifyで見たようなCUIが出てくるので、一番ラクそうなのを選んでいく。

```zsh
% sam init
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Which runtime would you like to use?
	1 - nodejs12.x
	2 - python3.8
	3 - ruby2.7
	4 - go1.x
	5 - java11
	6 - dotnetcore2.1
	7 - nodejs10.x
	8 - python3.7
	9 - python3.6
	10 - python2.7
	11 - ruby2.5
	12 - java8
	13 - dotnetcore2.0
	14 - dotnetcore1.0
Runtime: 2

Project name [sam-app]:

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

AWS quick start application templates:
	1 - Hello World Example
	2 - EventBridge Hello World
	3 - EventBridge App from scratch (100+ Event Schemas)
	4 - Step Functions Sample App (Stock Trader)
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: sam-app
Runtime: python3.8
Dependency Manager: pip
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./sam-app/README.md
```

出来上がったもの。

```zsh
sam-app % ls -F
README.md	events/		hello_world/	template.yaml	tests/

sam-app % ls -lR
total 24
-rw-r--r--  1 tnakatan  staff  7490  6  5 14:51 README.md
drwxr-xr-x  3 tnakatan  staff    96  6  5 14:51 events
drwxr-xr-x  5 tnakatan  staff   160  6  5 14:51 hello_world
-rw-r--r--  1 tnakatan  staff  1623  6  5 14:51 template.yaml
drwxr-xr-x  3 tnakatan  staff    96  6  5 14:51 tests

./events:
total 8
-rw-r--r--  1 tnakatan  staff  1997  6  5 14:51 event.json

./hello_world:
total 16
-rw-r--r--  1 tnakatan  staff     0  6  5 14:51 __init__.py
-rw-r--r--  1 tnakatan  staff  1151  6  5 14:51 app.py
-rw-r--r--  1 tnakatan  staff     8  6  5 14:51 requirements.txt

./tests:
total 0
drwxr-xr-x  4 tnakatan  staff  128  6  5 14:51 unit

./tests/unit:
total 8
-rw-r--r--  1 tnakatan  staff     0  6  5 14:51 __init__.py
-rw-r--r--  1 tnakatan  staff  2579  6  5 14:51 test_handler.py
```

本体らしきapp.pyを見てみる。

```
sam-app % wc -l hello_world/app.py
      42 hello_world/app.py
```

42行ほどのPythonスクリプト。中はほんとにhello worldだった（本筋から外れるので省略）。
単体テストっぽい名前のファイル（'test/unit/test_handler.py'）もあり、こちらはAPI Gatewayイベントを模したテストイベントを生成してくれるものようだ。省略。

次にSAMテンプレートらしきファイルを開く。

```yaml: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: python3.8
      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:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  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
```

なるほど。`app.py`の`lambda.handler()`を呼ぶPyhon3.8ベースのLambda関数を、`AWS::Serverless::Function`というリソースとして作成してくれるようだ。
CloudFormationの書式に則っているが、`AWS::Serverless::Funcion`とか見慣れないリソースがあるので、拡張キットのような感じか。

続いてビルド。

```zsh
% sam build
Building resource 'HelloWorldFunction'
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

何か出た。
ビルドのイメージが掴めてなかったので、安心するために中を見てみる。

```zsh
sam-app % ls -F .aws-sam/build
HelloWorldFunction/	template.yaml
```

`template.yaml`はさっきのやつなので、新しく生成されたのは`HelloWorldFunction`の方だ。

```zsh
sam-app % ls -F .aws-sam/build/HelloWorldFunction
__init__.py			chardet-3.0.4.dist-info/	requirements.txt
app.py				idna/				urllib3/
certifi/			idna-2.9.dist-info/		urllib3-1.25.9.dist-info/
certifi-2020.4.5.1.dist-info/	requests/
chardet/			requests-2.23.0.dist-info/
```

なるほど。Lambda関数のコード本体（`app.py`）と、依存関係のあるライブラリをひとまとめにしてくれているようだ。
これをzipすればそのままLambda関数をデプロイできそうな気配がしてきた。

最後にデプロイしてみる。

```zsh
sam-app % sam deploy --guided

Configuring SAM deploy
======================

	Looking for samconfig.toml :  Not found

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]:
	AWS Region [us-east-1]: ap-northeast-1
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [y/N]:
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]:
	Save arguments to samconfig.toml [Y/n]:

	Looking for resources needed for deployment: Not found.
	Creating the required resources...
	Successfully created!

		Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1b1hnsqvj6mby
		A different default S3 bucket can be set in samconfig.toml

	Saved arguments to config file
	Running 'sam deploy' for future deployments will use the parameters saved above.
	The above parameters can be changed by modifying samconfig.toml
	Learn more about samconfig.toml syntax at
	https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

	Deploying with following values
	===============================
	Stack name                 : sam-app
	Region                     : ap-northeast-1
	Confirm changeset          : False
	Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-1b1hnsqvj6mby
	Capabilities               : ["CAPABILITY_IAM"]
	Parameter overrides        : {}

Initiating deployment
=====================
Uploading to sam-app/64f7e1a020d874114ae09ddedb09fe0e  534640 / 534640.0  (100.00%)
Uploading to sam-app/d4f5b52b0d855ee4384f4c34fea93509.template  1090 / 1090.0  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------
Operation                               LogicalResourceId                       ResourceType
---------------------------------------------------------------------------------------------------------------------
+ Add                                   HelloWorldFunctionHelloWorldPermissio   AWS::Lambda::Permission
                                        nProd
+ Add                                   HelloWorldFunctionRole                  AWS::IAM::Role
+ Add                                   HelloWorldFunction                      AWS::Lambda::Function
+ Add                                   ServerlessRestApiDeployment47fc2d5f9d   AWS::ApiGateway::Deployment
+ Add                                   ServerlessRestApiProdStage              AWS::ApiGateway::Stage
+ Add                                   ServerlessRestApi                       AWS::ApiGateway::RestApi
---------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:016831194521:changeSet/samcli-deploy1591338862/fdb59513-7141-4326-8b8c-5231dbcc785c


2020-06-05 15:34:33 - Waiting for stack create/update to complete

CloudFormation events from changeset
---------------------------------------------------------------------------------------------------------------------
ResourceStatus                ResourceType                  LogicalResourceId             ResourceStatusReason
---------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS            AWS::IAM::Role                HelloWorldFunctionRole        -
CREATE_IN_PROGRESS            AWS::IAM::Role                HelloWorldFunctionRole        Resource creation Initiated
CREATE_COMPLETE               AWS::IAM::Role                HelloWorldFunctionRole        -
CREATE_IN_PROGRESS            AWS::Lambda::Function         HelloWorldFunction            -
CREATE_IN_PROGRESS            AWS::Lambda::Function         HelloWorldFunction            Resource creation Initiated
CREATE_COMPLETE               AWS::Lambda::Function         HelloWorldFunction            -
CREATE_IN_PROGRESS            AWS::ApiGateway::RestApi      ServerlessRestApi             -
CREATE_COMPLETE               AWS::ApiGateway::RestApi      ServerlessRestApi             -
CREATE_IN_PROGRESS            AWS::ApiGateway::RestApi      ServerlessRestApi             Resource creation Initiated
CREATE_IN_PROGRESS            AWS::ApiGateway::Deployment   ServerlessRestApiDeployment   -
                                                            47fc2d5f9d
CREATE_IN_PROGRESS            AWS::ApiGateway::Deployment   ServerlessRestApiDeployment   Resource creation Initiated
                                                            47fc2d5f9d
CREATE_IN_PROGRESS            AWS::Lambda::Permission       HelloWorldFunctionHelloWorl   Resource creation Initiated
                                                            dPermissionProd
CREATE_IN_PROGRESS            AWS::Lambda::Permission       HelloWorldFunctionHelloWorl   -
                                                            dPermissionProd
CREATE_COMPLETE               AWS::ApiGateway::Deployment   ServerlessRestApiDeployment   -
                                                            47fc2d5f9d
CREATE_IN_PROGRESS            AWS::ApiGateway::Stage        ServerlessRestApiProdStage    -
CREATE_IN_PROGRESS            AWS::ApiGateway::Stage        ServerlessRestApiProdStage    Resource creation Initiated
CREATE_COMPLETE               AWS::ApiGateway::Stage        ServerlessRestApiProdStage    -
CREATE_COMPLETE               AWS::Lambda::Permission       HelloWorldFunctionHelloWorl   -
                                                            dPermissionProd
CREATE_COMPLETE               AWS::CloudFormation::Stack    sam-app                       -
---------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole
Description         Implicit IAM Role created for Hello World function
Value               arn:aws:iam::016831194521:role/sam-app-HelloWorldFunctionRole-MSA3RKSU6C23

Key                 HelloWorldApi
Description         API Gateway endpoint URL for Prod stage for Hello World function
Value               https://ixoyzu474a.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/

Key                 HelloWorldFunction
Description         Hello World Lambda Function ARN
Value               arn:aws:lambda:ap-northeast-1:016831194521:function:sam-app-HelloWorldFunction-UY2UF7WMHXI4
---------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - sam-app in ap-northeast-1
```

おお、まじか。ファイルを生成するだけかと思ったら、いきなりCloudFormationのスタックまで作成された。
所要時間は約1分。
マネジメントコンソールでCloudFormationを開いてみる。
![スクリーンショット 2020-06-05 15.39.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/bf158954-ea9f-0110-4e25-c4029ca31196.png)

Rest APIとLambda関数、IAMロールが作られている（Lambdaでよく使う、`AWSLambdaBasicExecutionRole`ポリシーがアタッチされていた）。
出力にAPIのエンドポイントが出ているので、これを叩けばよさそうだ。
![スクリーンショット 2020-06-05 15.52.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/17532fb5-0d0e-8a98-3174-de3aa258491c.png)

クイックにブラウザで開いてみると、
<img width="340" alt="スクリーンショット 2020-06-05 15.40.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/d75c6da5-0045-0e74-d8f3-966ef127f3f5.png">

無事Hello Worldしてくれた。

ちなみに、このLambda関数は「Lambdaアプリケーション」として構成されるようだ。
<img width="1596" alt="スクリーンショット 2020-06-05 16.23.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/116dc959-d902-2110-47ca-1f6c64466299.png">

### おまけ

ついでなので、ローカル実行をやってみた（Dockerはインストール済み）。

```zsh
% sam local start-api
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
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
2020-06-05 16:28:41  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
...（省略）
```

クラウドにデプロイした時と同じくLambdaバックエンドのAPIをローカルのコンテナ上でホストしてくれる模様。
ブラウザで`http://localhost:3000/hello`を叩くと、こちらも無事Hello Worldしてくれた。

### 結論
- できること
    - CloudFormation互換の書式でサーバーレスのテンプレート（SAMテンプレート）を書ける。
    - SAMテンプレートを元に、sam cliで2コマンドでビルドしてデプロイできる。
    - ビルドはコードのみならず依存関係のあるランタイムまで取り纏めて、アーティファクト一式としてbuildフォルダに準備してくれる。
    - デプロイはアーティファクト（buildフォルダ）をクラウド上にデプロイしてくれる。APIやLambda関数のガラなど、必要なAWSリソースはCloudFormationスタックとして作成してくれる。
    - ビルドとデプロイは別々のコマンドなので、パイプラインに応じて、分けて実行できそう。
    - Dockerが入ってればローカルでテスト実行もできる。

以上、今さらながら使ってみたAWS SAMでした。