Serverless Framework で AWS Lambda と API Gateway をデプロイする

[Serverless Meetup Tokyo #16 （オンライン開催）](https://serverless.connpass.com/event/165352/) を拝見していて、ちょうど今、AWS への各種リソースのデプロイの自動化が課題だったので、よい機会と思い Serverless Framework を使ってみた。
尚、Serlverless Framwork については知っていたが、offline-start しか使ったことがなかった程度の人間です。

## やったこと

弊社では Webシステムを、

* フロントエンド: SPA(Angular)
* バックエンド:
    * REST っぽいAPI: Lambda + API Gateway
    * DB: PostgreSQL 他

で組むことが増えてきていて、プロジェクトの取っ掛かり時の環境構築を手動でやっているのがダルくなってきた。
今回は、「REST っぽいAPI: Lambda + API Gateway」のところを Serverless Framework で自動化してみた。

## 手順

### 1. 環境構築用 IAM の作成

IAM コンソールで「serverless_deployment」という名前で作成。以下のポリシーをアタッチした。

* AWSLambdaFullAccess
* AmazonS3FullAccess
* AmazonAPIGatewayAdministrator
* AWSCloudFormationFullAccess

さらに、以下のようなインラインポリシーを追加した。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:DeleteRolePolicy",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:PutRolePolicy"
            ],
            "Resource": "*"
        }
    ]
}
```

IAMFullAccess はさすがにヤバいかなと思い必要な権限だけ抽出したものだが、たぶん他の xxxFullAccess も必要なものだけにした方がよいだろう。

作成した IAM のアクセスキーなどを自PCの ``~/.aws/credentials`` に追加した。ちなみに環境は Windows 10 内の WSL(Ubuntu)。

**.aws/credentials**

```
[serverless-deployment]
aws_access_key_id = AKIAxxxxx
aws_secret_access_key = 49s9xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### 2. ツールのインストール

awscli は現在最新の v2 をインストール

* [Installing the AWS CLI version 2 on Linux - AWS Command Line Interface](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html#cliv2-linux-install)

node は 12.14.1

* [Node.js](https://nodejs.org/ja/)

そして Serverless Framework をインストールする。

* [Serverless - The Serverless Application Framework powered by AWS Lambda, API Gateway, and more](https://serverless.com/)

```bash
npm install serverless -g

source ~/.bash_profile
serverless --version

> Framework Core: 1.65.0
> Plugin: 3.4.1
> SDK: 2.3.0
> Components: 2.22.3
```

global じゃなくてもいいけど、パス通すのが面倒なので。

## 3. テンプレートからプロジェクトの作成

```bash
serverless create --template aws-nodejs
```

で、nodejs のテンプレートから Serverless Framework のプロジェクトを作成。

また、``npm init`` で適当に ``package.json`` を作って、

```bash
npm install --save-dev serverless-plugin-custom-binary
```

を実行しておく。これは後に必要になるプラグイン。

``ls`` すると以下のようなファイルとディレクトリがある。

```bash
handler.js  node_modules  package-lock.json  package.json  serverless.yml
```

### 4. serverless.yml を編集する

``serverless.yml`` を開いて次のように編集する（これだと最早テンプレートの意味ないが）。

```yaml
service: my-awesome-service

plugins:
  - serverless-plugin-custom-binary

custom:
  apiGateway:
    binaryMediaTypes:
      - image/jpeg

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, 'dev'}
  region: ap-northeast-1
  apiName: ${self:service}-${self:provider.stage}

functions:
  api:
    handler:  handler.hello
    name: ${self:service}-api-${self:provider.stage}
    events:
      - http:
          path: /{proxy+}
          method: get
          integration: lambda
```

まず ``service: my-awesome-service``、これが AWS に作成されるリソース名の元になるのでちゃんと考えて命名しよう。重複したらどうなちゃうのかは不明。 [**kebab-case**](https://qiita.com/ybiquitous/items/75288bacb596a82a2805) を採用しておくと良いと思われる。例えばサービス名で S3 Bucket を作りたいとき、Bucket 名は CamelCase(大文字) を許可してないため。

次に Plugins と binaryMediaTypes。これを行うために先に serverless-plugin-custom-binary をインストールしておいた。

provider-apiName。これは API Gateway の名前なんだけど、これをしない場合 ``<stage名>-<service名>`` になる。Lambda とかは ``<service名>-<stage名>`` となり逆で気持ち悪いので、他の同じになるように直している。

stage: ${opt:stage, 'dev'}。単純に ``stage: dev`` とするだけだと、 ``--stage prod`` を引数で指定された値が ``${self:provider.stage}`` に代入されないので注意。

functions-api。Labmda に ``<service名>-<stage名>-api`` という名前の関数が作成される。"api" は任意の名称で ok。

functions-api-name。既定だと ``<service名>-<stage名>-api`` になるが、``<service名>-api-<stage名>``にしたい(stage名は最後尾に統一したい) ので ``name: ${self:service}-api-${self:provider.stage}`` とした。

handler:  handler.hello。 ``handles.js`` の hello 関数を呼び出すの意。

path: /{proxy+}。呼び出し URL のパス部分を全てスルーする。``https://hoge.net/dev/fuga/piyo/gege`` とか。

integration: lambda。既定で ON ぽいので要らないかも。

### 5. AWS にデプロイする

```bash
serverless deploy --aws-profile serverless-deployment
```

を実行する。``--aws-profile serverless-deployment`` で AWSプロファイルを指定している事に注意。

```
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service MyAwesomeService.zip file to S3 (1.13 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............................
Serverless: Stack update finished...
Service Information
service: MyAwesomeService
stage: dev
region: ap-northeast-1
stack: MyAwesomeService-dev
resources: 11
api keys:
  None
endpoints:
  GET - https://xxxx.execute-api.ap-northeast-1.amazonaws.com/dev/{proxy+}
functions:
  api: MyAwesomeService-dev-api
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
```

なんやかんや実行されてデプロイされたみたい。

### 6. デプロイされたか確認

#### API Gateway

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/061865db-2644-945c-bd22-30aa39746845.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/320b3303-657a-4d0c-c3f8-59ea5d572482.png)

#### Lambda

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/d0a87c53-babc-05d6-bda4-74f38ae4fa01.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/e3827772-e025-a0d3-545a-cca87071cfba.png)

cURL で呼び出してみる。

```bash
 curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/dev/aaa/bbb/ccc
```

```
{"statusCode":200,"body":"{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!\"...
```

うまくいったみたい。

### 7. 後片付け（削除）

```bash
serverless remove --aws-profile serverless-deployment
```

ですべてのリソースがキレイさっぱり消えます。これはこれで怖いので IAM の権限で制限したほうが良さそう。

## トラブルシューティング

### Q: デプロイが全然終わらない

A: Console から CloudFormation の該当スタックを削除してリトライ

### Q: sls deploy や remove が「S3 のバケットが無い」とかでエラーになる

A: 該当バケット(``my-awesome-service-veri-serverlessdeploymentbucke-rzr9e2jjdrlv`` のようなごちゃごちゃしたやつ) を手動作成するか、Console から CloudFormation の該当スタックを削除してリトライ


## 今後やりたいこと

* Lambda へ VPC の設定
* Lambda タイムアウト値の設定
* スクリプトでビルドとか Webpack した結果を Serverless でデプロイ
* S3 Bucket の作成
* S3 に SPA をデプロイ
* リソース権限周りをもっと深堀り 

## 参考

* [Serverless Framework - AWS Lambda Guide - Introduction](https://serverless.com/framework/docs/providers/aws/guide/intro/)
* [Serverless Variables](https://serverless.com/framework/docs/providers/aws/guide/variables/)
* [Serverless FlameworkでAPI Gatewayのバイナリメディアタイプを設定する方法 - Qiita](https://qiita.com/makoll/items/5f330b2469e310474c4e)
* [Serverless Framework で API Gateway & Lambda を構築する - Qiita](https://qiita.com/maaaashin324/items/9e5d3f02fca94989e9f6)
* [Serverlessで任意のディレクトリ配下に、関数毎にディレクトリを切ってソースを配置する with webpack building - Qiita](https://qiita.com/wara_be/items/f3ec38e5be6bdea7c79d)
* [一時的にPATHを追加する（Linux） - Qiita](https://qiita.com/aical/items/c498c5a69b66b1ea7c3d)
* [ServerlessFrameworkでS3の静的サイトのホスティングをする - マコーの日記](http://hkou.hatenablog.com/entry/2018/05/07/213412)