【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第1回(全3回)

# はじめに
こんにちは！
最近、仕事で初めてサーバーレスアプリを作る機会がありました。その際Serverless Frameworkを使ったのですが、結構お手軽にアプリを作ることができたので、初めてサーバーレスをする方にお勧めだと思いました。今回は自分の整理も兼ねてその手順を記事として残したいと思います。
今回はSwaggerをAPI Gatewayのテンプレートの一部として組み込む方法もご紹介したいと思います。
全部で記事が三つと長いですが、サーバーレスが初めての方でもAPIを構築できるように書きましたので、最後までご覧いただけたら嬉しいです。

第二回、第三回の記事はこちら

* [【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第2回 WAF適用編(全3回)](https://qiita.com/nekoze-climber/items/92916700b9375690c1ef)
* [【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第3回 Dynamo編(全3回)](https://qiita.com/nekoze-climber/items/03f201d4adfe4b754059)

# 今回作るもの
今回は以下のアーキテクト図のようなWeb APIバックエンドを作っていきます。
API GatewayでクライアントからのAPIリクエストを受信し、該当するLambda関数を呼び出し、必要に応じてDynamoDBからのデータ読み出しおよび書き込みを行います。さらに、WAFを適用することでセキュアにします。

![slsTestAppArchitect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/484ad570-257d-1e01-de43-13376c137266.png)

APIとしては、ID・名前・身長・体重・年齢の情報を持つPersonモデルを登録・取得・更新・削除するAPIを作りたいと思います。

- GET dev/slsTestApp/v1/api/person/{personId}
- POST dev/slsTestApp/v1/api/person
- PUT dev/slsTestApp/v1/api/person/{personId}
- DELETE dev/slsTestApp/v1/api/person/{personId}

第1回目の本記事では、具体的なAPIロジックは実装せず、簡単なメッセージを返すだけのLambda関数をバックエンドとするAPI Gatewayを作ります。WAFは第2回、DynamoDBを交えたロジック実装は第3回を予定しています。

# Serverless FrameworkとSwagger
Serverless Frameworkとはサーバーレスアプリケーションの構成管理およびデプロイをするためのツールです。コマンド一つでデプロイできるため、とても使いやすいです。

SwaggerとはWeb APIの仕様を記述しドキュメント化するツールです。

本記事はSwaggerでAPI仕様を記載し、それをそのままAPI Gatewayのテンプレートとして取り込み、さらにそれをServerless Frameworkを使ってデプロイする一連の流れを解説したものです。Swaggerを使うことでAPIドキュメントがそのままインフラコードとして流用できるため、作業を効率化できます。

#事前準備

今回はAWSにデプロイしますので、IAMユーザーの作成をお願いします。アカウントIDを後ほど使います。

# 環境構築

それではまずはクライアント側の環境構築を行っていきます。
使用する環境・ツールは以下の通りです。

- 環境
    - Windows10

- ツール
    - Visual Studio Code v1.47.0
    - Node.js v12.18.2
    - AWS CLI v2
    - Serverless framework v1.74.1
    - Swagger Viewer v3.0.1
    
## VSCode(Visual Studio Code)のインストール
今回はエディターとしてVisual Studio Codeを使用します。以下のリンクからインストールします。
[Visual Studio Codeダウンロード](https://code.visualstudio.com/)

##Node.jsのインストール
Serverless Frameworkはnpmパッケージとして公開されていますので、インストールするためにはNode.jsが必要となります。以下のリンク先から皆さんの環境にあうインストーラーをダウンロードし実行してください。

[Node.jsのインストール](https://code.visualstudio.com/)

##AWS CLIのインストール
以下からインストールします。

[AWS CLIのインストール](https://aws.amazon.com/jp/cli/)

インストール後、どこでもよいので適当な場所でコマンドプロンプトを起動し、以下のコマンドを実行して皆さんがお持ちのIAMユーザーの情報を登録してください。

```shell:
> aws configure
AWS Access Key ID [None]: [自身のアクセスキーID]
AWS Secret Access Key [None]: [自身のアクセスキー]
Default region name [None]: ap-northeast-1
Default output format [None]:
```

## Serverless Frameworkのインストール

どこでもよいので適当な場所でコマンドプロンプトを開き、以下のコマンドを実行してインストールします。
ちなみに、以下のコマンドの中で -g オプションを指定しているのは、グローバルに(どこでも使えるように)インストールするためです。

```shell:
> npm install -g serverless
```

インストールが完了したら、以下のコマンドを実行します。

```shell:
> serverless
```

すると、アプリを作る言語を何にするか聞かれます。今回はNode.jsを選択します。

```shell:
> Serverless: What do you want to make? AWS Node.js
```

そのあと、作成するプロジェクトフォルダ名を何にするか聞かれますので、お好きな名前を設定してください。

```shell:
> What do you want to call this project? test
```

最後に「serverless frameworkのアカウントを作るか？」と聞かれます。今回はアカウントを作らないのでNoを選択します。

```shell:
You can monitor, troubleshoot, and test your new service with a free Serverless account.
Serverless: Would you like to enable this? No
```

これでServerless frameworkのインストールは完了です。

##Swagger Viewerのインストール
最後に、Swagger Viewerをインストールします。こちらはVSCodeのプラグインとして用意されています。
まず、VSCodeを起動し、Ctrl+Shift+Xでプラグインの検索窓を起動します。「Swagger　Viewwer」と入力するとSwagger Viewerが見つかると思いますので、インストールボタンをクリックします。

<img width="951" alt="Swagger_Viewer.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/cc83b0c9-80d2-a558-3ef5-0db50a31ed1b.png">

これで、必要な環境は準備できました！

# Serverless Frameworkの仕組み
本題に入る前にServerless Frameworkのデプロイの仕組みを簡単に説明します。
Serverless Frameworkはyml形式のテンプレートファイルに記載した内容をCloudFormationテンプレートに変換し、CloudFormationを実行することでAWSへサービスをデプロイします。
デプロイコマンドを実行すると、ローカルにある必要なファイルをCloudFormationテンプレートに変換後、zip化しS3のバケットに配置します。それをソースとしてCloudFormationを実行します。この時、AWS CLIに設定されたシークレット情報を参照しデプロイを行います。これがあるため、事前準備でAWS CLIをインストールしました。

# Lambda関数をデプロイしてみる
では早速本題に入っていきます！
まずはHTTPリクエストなどは考えない簡単なLambda関数を実装しデプロイするところまでやってみます。
Serverlessアプリインストール時に指定したディレクトリに移動してみると、以下のファイルが作成されていると思います。

- .gitignore　→　Gitの管理対象としないディレクトリ・ファイルを指定
- handler.js　→　Lambda関数の記述したファイル
- serverless.yml　→　Serverless Frameworkの定義ファイル

.gitignoreは今はそのままで結構です。ここではhandler.jsとserverless.ymlを修正していきます。

## handler.js
今は以下のようになっていると思います。

```javascript:handler.js(変更前)
'use strict';

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};

```

上記はHttpリクエストを受け取ることを前提したコードになっています。しかし前述の通りまずはHTTPリクエストなどは考えないため、既存の部分はコメントアウトして以下のように受け取ったeventとともにシンプルにメッセージをリターンするだけの実装に変えます。

```javascript:handler.js(変更後)
"use strict";

module.exports.hello = async (event) => {
  return {
    message: "Go Serverless v1.0! Your function executed successfully!",
    event,
  };
  /*
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };*/

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
```
## serverless.yml

今は以下のようになっていると思います。

```yml:serverless.yml(変更前)

# Welcome to Serverless!
# 
# 中略
# Happy Coding!

service: slsTestApp

# 中略

provider:
  name: aws
  runtime: nodejs12.x

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# 中略

functions:
  hello:
    handler: handler.hello
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#中略
```
主要な個所について説明していきます。

まず```service:```の部分ですが、ここにデプロイするスタック名を定義します。今は```serverless```コマンドを実行したときに指定したプロジェクト名がそのまま入っていると思います。

```yml:serverless(抜粋)
service: slsTestApp
```

最終的なスタック名は```servie:```にデプロイ対象のstageがハイフンで連結されたものになります。なので上記の例だとdev stageにデプロイした場合はスタック名は```slsTestApp-dev```となります。

次に```provider:```ですが、こちらはデプロイする対象のクラウドプラットフォーム名とLambda関数を実装する言語の設定が記載されています。今回の場合はAWSとnode.jsなので、そのようになっていますね。

```yml:serverless.yml(抜粋)
provider:
  name: aws
  runtime: nodejs12.x

  # you can overwrite defaults here
  # stage: dev
  # region: ap-northeast-1
```

最後に```functions:```の部分です。ここで実装したLambda関数の定義を記載しています。
一つインデントを下げて任意の属性名を記載し、```handler:```にはLambda関数名を記載します。今回はhander.js内のhello関数を呼び出すので、```hander.hello```と記載さています。

```yml:serverless.yml(抜粋)
functions:
  hello:
    handler: handler.hello
```

では、serverless.ymlを少し修正していきます。
初めに、Serverless Frameworkでデプロイするstage名とリージョンのデフォルト値を設定します。```provider:```下の```# you can overwrite defaults here```直下二行のコメントアウトを外し、以下のように記載します。これで、デフォルトでap-northeast-1リージョンにdevステージとしてデプロイされるようになります。

```yml:serverless.yml(抜粋、修正後)
provider:
  name: aws
  runtime: nodejs12.x

  # you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
```

次にhello関数の定義に少し追加します。```name:```と```description:```を追加し、hello関数の名前と説明を記載してみてください。

```yml:serverless.yml(抜粋、修正後)
functions:
  hello:
    handler: handler.hello
    name: "slsTestApp-hello-${self:provider.stage}"
    description: "This function is test app."
```
ここで、```name:```に```${self:provider.stage}```という文字列がありますが、これは変数指定の書き方です。先ほどの```provider:```の```stage:```に指定した値を変数として使用する書き方になります。こうすることで、stageごとにLambda関数を出しわけることができます。

では、試しにデプロイしてみます。serverless.ymlと同じ階層で以下のコマンドを実行してみてください。```-v```オプションをつけることで途中経過がコンソールに表示されるようになります。

```shell
sls deploy -v
```

以下のように```Stack Outputs```が最後に表示されればデプロイ成功です！

```shell:
functions:
  hello: slsTestApp-hello-dev
layers:
  None

Stack Outputs
HelloLambdaFunctionQualifiedArn: …
ServerlessDeploymentBucketName: …
```

では、デプロイされたLambda関数を見てみましょう。

AWSマネージメントコンソールにログインし、Lambdaのページの「関数」メニューを選択します。すると、serverless.ymlで設定したLambda関数が表示されていると思います。

<img width="906" alt="Lamda_testFunc.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/6556561a-b4d6-8c90-4a3e-1a8311f9e323.png">

関数をクリックし、Lambda関数の詳細画面を表示します。この画面でLambda関数のテストをすることができますので、やってみます。
右上の「テストイベントの選択」をクリックし、「テストイベントの設定」をクリックします。
<img width="846" alt="set_test_event.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/ee9fb520-5b78-fb39-1d2e-1ab02a6aa9d0.png">

すると、テストで流し込むテストデータの設定画面が表示されます。
任意のイベント名を入力し、それ以外はデフォルトで保存します。

<img width="647" alt="test_event.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/83363b29-d0e5-5ed3-8387-b96137a1b10a.png">

保存したら、「テスト」ボタンをクリックし実行します。
以下のような結果になれば成功です！

<img width="846" alt="test_result.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/103f604e-f8de-b58d-52ad-dde812013ccf.png">

# Swaggerテンプレートを作る
Lambda関数をServerless Frameworkを使ってデプロイするところまでできました。
次はAPIの実装を行っていくのですが、まずはSwaggerでAPIの仕様を書いてみましょう。
まずは、名前はなんでもよいのでAPI定義を記載するyamlファイルを作成してください。
私はtemplatesというフォルダを作りその下に「swagger.yaml」として作りました。こちらに定義を書いていきます。
swaggerの書き方を解説しようと思いましたが、すでにわかりやすい記事がありましたので、こちらをご参照ください。

[Swagger記法まとめ](https://qiita.com/rllllho/items/53a0023b32f4c0f8eabb)

[今回つくるもの](#今回作るもの)で記載したAPIのうち、GET APIの定義を書いたものが以下になります。

```yaml:swagger.yaml
swagger: "2.0"
info:
  title: slsTestApp API
  description: API description of slsTestApp.
  version: 1.0.0
host: api.example.com
basePath: /dev
schemes:
  - https
tags:
  - name: person
    description: About person API

produces:
  - application/json

consumes:
  - application/json

paths:
  /slsTestApp/v1/api/person/{personId}:
    get:
      summary: Returns a person which matches id in path.
      description: Returns a person which matches id in path.
      tags:
        - person
      parameters:
        - name: personId
          in: path
          description: person id which you want to get.
          required: true
          type: "string"
      responses:
        200:
          description: OK
          schema:
            $ref: "#/definitions/producePersonModel"
        404:
          description: Not Found
          schema:
            $ref: "#/definitions/error"

definitions:
  consumePersonModel:
    type: object
    properties:
      name:
        type: "string"
      weight:
        type: "string"
      age:
        type: "string"
  producePersonModel:
    type: object
    properties:
      personId:
        type: "string"
      name:
        type: "string"
      weight:
        type: "string"
      age:
        type: "string"
  error:
    type: object
    properties:
      errorCode:
        type: "string"
      errorMessage:
        type: "string"
```
これをプレビューするには、VSCode上でF1キーを押し、```Preview Swagger```と入力しEnterを押します。以下のような画面が表示されると思います。
![Swagger_Get.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/cb8e983a-c8c7-975e-019d-0fc12da3d757.png)



# Swagger定義を取り込んだAPI Gatewayのデプロイ
先ほど作ったSwaggerをAPI Gatewayのテンプレートとして取り込みデプロイするかを解説していきます。
やることは大きく以下の四つです。

- hello関数の修正
- swagger.yamlにAPI Gateway統合の設定を追記する
- API Gatewayのテンプレートファイルを作成する(Bodyにswaggerファイルを指定)
- serverless.ymlにテンプレートファイルへのパスを記載する

## hello関数の修正
先ほどデプロイしたhello関数をHttpリクエストを受け付けられるように変更していきます。
まずは```src/lamda/getPerson```フォルダを作成し、そこに```handler.js```を移動します。さらに、```handler.js```から```index.js```に変更し、関数名も```hello```から```handler```に変えます。個人的にわかりやすい構成に変えているだけですので、必ずしもこれに倣う必要はありません。
さらに実装を以下のように変更します。これでindex関数が呼ばれると```callback```内のstatusCodeとbodyが返却されるようになります。

```javascript:src/lambda/getPerson/index.js
"use strict";

module.exports.handler = async (event, context, callback) => {
  callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: "Go Serverless v1.0! Your function executed successfully!",
    }),
  });
};
```
ファイルパスと関数名を変更したので、serverless.ymlも修正していきましょう。私と同じファイルパス、関数名にした場合は以下のようになります。違う名前にした場合はそちらに変更してください。

```yml:serverless.yml(抜粋)
functions:
  getPerson:
    handler: src/lambda/getPerson/index.handler
    name: "slsTestApp-getPerson-${self:provider.stage}"
    description: "This function is test app."
```
ここまでできたら、一度デプロイしておきましょう。

```shell
sls deploy -v
```


## swagger.yamlにAPI Gateway統合の設定を追記する
swagger.yamlにAPI Gateway統合の設定を記載していきます。今回は最低限の設定のみ記載することとします。

以下のようにメソッド定義の下に```x-amazon-apigateway-integration:```を記載し、必要なプロパティを設定していきます。

```yaml:swagger.yaml(抜粋)
/slsTestApp/v1/api/person/{personId}:
    get:
      summary: Returns a person which matches id in path.
#中略
      x-amazon-apigateway-integration:
        type: aws_proxy
        responses:
          default:
            statusCode: "200"
        uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:{Your Account ID}:function:{Lambda function name}/invocations"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
```
簡単に解説します。

|属性名|説明|
|:----|:----|
|type|API Gatewayとの統合タイプを指定します。今回はAPI Gatewayのリクエスト処理をするのがLambda関数なので、```aws_proxy```を設定します|
|response|APIレスポンスのオブジェクトを指定します。今回は細かく設定しないので、上記のコード例のようにしています|
|uri|APIのバックエンドのエンドポイントを指定します。今回はLambda関数のuriを指定しています。上記のコード例にAWS Account IDとLambda関数名を入れてください。|
|httpMethod|呼び出されるHttpメソッドを指定しますが、Lambda関数をバックエンドとして使う場合は常に```POST```を指定します|
|contentHandling|リクエストのペイロードのエンコードタイプを指定します。今回はテキストとして扱いたいので```CONVERT_TO_TEXT```を指定します|

これでswagger.yamlの修正は完了です。```uri:```のAWS Account IDと関数名はみなさんが設定したものを入力してくださいね。

## API Gatewayのテンプレートファイルを作成する
API GatewayのCloudFormationのテンプレートファイルを作っていきます。
API Gatewayのデプロイには二段階あります。一つはリソース(API定義)のデプロイ、もう一つはデプロイしたリソースをステージにデプロイすることです。ステージにデプロイすることでAPIがweb経由で利用できるようになります。
今回のテンプレートではリソースのデプロイとdev ステージへのリソースデプロイを同時に行うテンプレートを作成します。
```templates/```に以下のテンプレートファイルを作成します。

```yml:api-gateway.yml

Resources:
  ApiGatewayRestApi:
    Type: "AWS::ApiGateway::RestApi"
    Properties:
      Body: ${file(./templates/swagger.yaml)}
  GetPersonApiPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      FunctionName: "slsTestApp-getPerson-${self:provider.stage}"
      Action: "lambda:InvokeFunction"
      Principal: "apigateway.amazonaws.com"
  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: ApiGatewayRestApi
      StageName: ${self:provider.stage}
```

|リソース名|説明|
|:----|:----|
|ApiGatewayRestApi|API Gateway自体のリソース定義です。Bodyには先ほどのswagger.yamlへのパスを記載します|
|GetPersonApiPermission|API GatewayからLambda関数を実行するための権限です。```FunctionName```をserverless.ymlの関数名と一致させます|
|ApiGatewayDeployment|API GatewayのAPI定義をステージへデプロイするための設定です|
これでAPI Gatewayのテンプレート作成が完了しました。swagger定義を取り込んだAPI Gatewayが作成されるようになります。

## serverless.ymlにテンプレートファイルへのパスを記載する

serverless.ymlに先ほど作成したAPI Gatewayのテンプレートファイルへのパスを記載していきます。
以下のように```resources```に配列形式でテンプレートファイルへのパスを記載します。

```yml:serverless.yml(抜粋)
# you can add CloudFormation resource templates here
resources:
  - ${file(./templates/api-gateway.yml)}
```
これで準備は整いました！ではデプロイしてみましょう。

```shell
sls deploy -v
```
## 動作確認
これでGET APIが実装されているはずですので、実際にAPIを実行してみたいと思います。
まずは、API GatewayのURLをAWSマネージメントコンソールから調べます。
API Gatewayのメニューを開くと先ほどデプロイしたAPI Gatewayがあると思いますので、そちらをクリックします。
![api-gateway-list.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/94c53df0-bc5d-6b58-a7ea-357ddd29316b.png)

左のメニューから「ステージ」を選択し、中央に表示される「dev」をクリックすると、右にURLが表示されます。これにswaggerで定義したURIを連結したものが、最終的なAPIのURIとなります。
![api-gateway-stg.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/fe49469c-a3a2-6d33-4ebe-dbf772d88319.png)

このURLをコピーしておきます。

では、動作確認をしてみましょう。今回はcurlコマンドを使います(別途インストールが必要です)。

以下のコマンドを実行します。

```shell
curl GET https://{Your Api gateway Id}.execute-api.ap-northeast-1.amazonaws.com/dev/slsTestApp/v1/api/person/aaa
```
以下のようなレスポンスが返ってくれば成功です。

```shell
{"message":"Go Serverless v1.0! Your function executed successfully!"}
```

# Lambdaのデプロイパッケージを削減する
AWSマネージメントコンソールでLambda関数を見てみると、関数の欄が以下のようになっていると思います。
<img width="859" alt="Lambda_funcCode.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/521fff6d-30ab-cb07-d980-c6b62be99a0e.png">

これはLambda関数の実行には本来不要なほかのコードも含まれてしまっているからです。

これを防ぐには、serverless.ymlにexclude設定を追加し、不要なファイルがパッケージされないようにします。

```yml:serverless.yml(抜粋)
# you can add packaging information here
package:
  exclude:
    - docs/**
    - node_modules/**
    - templates/**
    - .eslintrc.json
    - LICENSE
    - README.md
    - package-lock.json
```

もう一回デプロイしてみると、以下のように必要なものだけパッケージされてるのが分かります。

<img width="877" alt="Lambda_funcCode_modified.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/68b6e083-4966-7eb4-803d-a7b453849ba9.png">

# デプロイしたリソースの削除
デプロイしたAPI GatewayにはまだWAFを適用していないので、だれでもAPIにアクセスできる状態です。使っていないときはデプロイしたリソースを削除しておきましょう。

削除するときは以下のコマンドを実行します。

```shell
sls remove
```
# 再デプロイ時の注意点
再デプロイすると、デプロイに失敗すると思います。
これは、Lambda関数がデプロイされる前にAPI Gatewayがデプロイされるため、API Gatewayで参照されているLambda関数がないためエラーになっています。
ちゃんとした解決策はあると思いますが、ひとまず今は最初にserverless.yml内でapi-gateway.ymlを読み込んでいる箇所をコメントアウトして一度デプロイし、そのあとにコメントアウトを外してデプロイしてみてください。

# おわりに
今回はごく簡単なメッセージを返すだけのLambda関数を実行するAPIを作りました。
バックエンドの大枠はできつつあるので、次回はWAFを適用し、特定のIPからしかAPIにアクセスできないようにします。

※WAF構築はこちら↓
[第2回 WAF構築編](https://qiita.com/nekoze-climber/items/92916700b9375690c1ef)