AWS SAM CLI と Swagger を利用した API 開発環境の作成チュートリアル

API 開発を Lambda と API Gateway の組み合わせで開発することが多くなってきました。
しかし、AWS のサービスはコンソールからポチポチと設定できてしまうので保守・運用する側からは設定情報が Git などに残らずに困ってしまいます。

そこでこれらの環境を Git で管理できるようにすべくソースコードで環境を構築してしまうチュートリアルを用意しました。

# 概要
AWS Lambda と Amazon API Gateway を本番環境で利用するときの Docker 開発環境です。
ここでは API 定義に Swagger を利用しています。

# 目次
[環境構成](#環境構成)
　[・前提条件](#前提条件)
　[・開発環境で作成するファイル](#開発環境で作成するファイル)
　[・AWS 上に作成されるリソース](#aws-上に作成されるリソース)
[開発環境作成手順](#開発環境作成手順)
　[・フォルダ構成](#フォルダ構成)
　[・AWS SAM CLI 用 の Python ベースの dockerfile 作成](#aws-sam-cli-用-の-python-ベースの-dockerfile-作成)
　[・docker-compose.yml の作成](#docker-composeyml-の作成)
　[・swagger.yml の作成](#swaggeryml-の作成)
　[・docker-compose の起動](#docker-compose-の起動)
　[・AWS SAM CLI を使った Lambda 関数の作成](#aws-sam-cli-を使った-lambda-関数の作成)
　　　[・コンテナへのアタッチ](#コンテナへのアタッチ)
　　　[・AWS SAM CLI の実行と初期プロジェクトの作成](#aws-sam-cli-の実行と初期プロジェクトの作成)
　[・template.yaml の実装](#templateyaml-の実装)
　[・ソースコードの実装](#ソースコードの実装)
　[・S3 バケットの作成](#s3-バケットの作成)
　[・swagger.yml を S3 へアップロード](#swaggeryml-を-s3-へアップロード)
[本番デプロイ準備](#本番デプロイ準備)
　[・samconfig.toml の作成](#samconfigtoml-の作成)
　[・ビルド](#ビルド)
[本番デプロイ](#本番デプロイ)
　[・作成されたリソースの確認](#作成されたリソースの確認)
[いざコード開発の開始！！](#いざコード開発の開始)
[削除コマンド](#削除コマンド)
[最後に](#最後に)

# 環境構成
作成するファイルや実際に AWS 上に作成されるリソースです。
## 前提条件
- 著者は Mac 環境で動作検証を行っております。
- Docker を利用するのでインストールしておいてください。
- ホストサーバーに `~/.aws` フォルダと config ファイルにcredential や profile 情報が設定されていることを前提で進めます。
まだ、設定していない方は AWS CLI のインストールと AWS CLI の設定方法を参考にしてください。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html

## 開発環境で作成するファイル
- AWS SAM CLI と node.js のビルド環境を動かす Python用の Dockerfile
- API Gateway と Lambda の環境を構築する template.yaml
- API Gateway の API エンドポイントなどを定義する swagger.yml
- SAM のビルド & デプロイ用の設定ファイル samconfig.toml
- Node.js のパッケージ管理ファイル package.json
- AWS SAM CLI と Swagger Editor のコンテナを起動する docker-compose.yml
- lambda で実行されるソースコード一式

※ 開発環境と言っても、今回ご紹介する手順は最終的に AWS 上へデプロイする形になります。
AWS SAM では lambda をローカルでエミュレートできるみたいですが今回は試していません。

## AWS 上に作成されるリソース
- Swagger の swagger.yml ファイル と SAM の template.yaml を保存する S3 バケット （手動作成）
- API Gateway
- SAM から自動生成される Lambda 用の IAM Role 
- Lambda (nodejs14)

# 開発環境作成手順
では、開発環境を作成していきます。

## フォルダ構成
下記が作成するフォルダ構成です。


```sh
Project
│── Dockerfiles
│   └── python
│       └── Dockerfile
├── README.md
├── docker-compose.yml
├── sam-app
│   ├── README.md
│   ├── events
│   ├── hello-world
│   │   ├── app.js
│   │   ├── node_modules
│   │   ├── package.json
│   │   ├── tests
│   ├── samconfig.toml
│   └── template.yaml
└── swagger.yml
```
Dockerfiles:　利用するコンテナを定義した dockerfile 群
sam-app:　AWS SAM CLI で作成される初期ソースコードのテンプレート
docker-compose.yml:　Dockerfiles で定義したコンテナを管理 & 実行
swagger.yml:　Swagger で定義した OPEN API 設計ファイル

## AWS SAM CLI 用 の Python ベースの dockerfile 作成
AWS SAM CLI を実行する Dockerfile を作成します。ついでに node.js をビルドする npm も Python コンテナにインストールしてしまいます。ここは node コンテナと分けてもらっても構わないですが、特に Web サーバとして動かす必要がないので、 Python コンテナに npm を同居させています。

```dockerfile:Dockerfiles/python/Dockerfile
FROM python:3.9-alpine

ENV NODE_PATH /usr/lib/node_modules/
# install nodejs
RUN apk update \
    && apk add --no-cache \
    nodejs \
    npm \
    gcc \
    libc-dev \
    git

# install aws-cli
RUN pip3 install awscli 

# install aws-sam-cli
RUN pip3 install -U aws-sam-cli

# change work directory
RUN mkdir -p /app
WORKDIR /app
```

## docker-compose.yml の作成
docker-compose.yml です。先ほど作成した AWS SAM CLI 用の Dockerfile と Swagger Editor をローカルで動かすコンテナを実行します。

```yaml:docker-compose.yml
version: "3.5"
services:
  sam:
    build:
      context: ./dockerfiles/python/
      dockerfile: Dockerfile
    tty: true  # 後でコンテナ内にアタッチするので tty と stdin_open を true にする
    stdin_open: true
    image: sam
    working_dir: /app
    volumes:
      - .:/app # ホストのプロジェクトフォルダをコンテナ内の /app にマウント
      - ~/.aws:/root/.aws # aws cli をコンテナから操作できるようにホストの .aws をマウント
    container_name: sam
  swagger:
    ports:
      - "8081:8080"
    image: swaggerapi/swagger-editor
    working_dir: /app
    environment: # コンテナ内の swagger 定義ファイルパス
      - SWAGGER_FILE=/app/swagger.yml
    volumes:
      - .:/app
      - ~/.aws:/root/.aws
    container_name: swagger
```

## swagger.yml の作成
swagger.yml に API 定義を記述していきます。今回はサンプルとして、Swagger 公式が上げている OpenAPI 3.0  の petstore を参考に AWS API Gateway に対応させるように改造していきます。

https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml

今回サンプル用に改造した yaml ファイルが下記になります。

```yaml:swagger.yml
openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:    
              schema:
                $ref: "#/components/schemas/Pets"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-amazon-apigateway-integration:
        uri: # APIからキックするLambda関数のARN
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PetStoreFunction.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: GET
        type: aws_proxy
    post:
      summary: Create a pet
      operationId: createPets
      tags:
        - pets
      responses:
        '201':
          description: Null response
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-amazon-apigateway-integration:
        uri: # APIからキックするLambda関数のARN
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PetStoreFunction.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST
        type: aws_proxy
  ########### 以下省略 ###########
components:
  ########### 以下省略 ###########
```
追加した箇所は、下記2箇所です。

```yaml
      x-amazon-apigateway-integration:
        uri: # APIからキックするLambda関数のARN
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PetStoreFunction.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST     ## POST のエンドポイントは GET を指定する
        type: aws_proxy
```

```yaml
      x-amazon-apigateway-integration:
        uri: # APIからキックするLambda関数のARN
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PetStoreFunction.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: GET     ## GET のエンドポイントは GET を指定する
        type: aws_proxy
```
他はすべて、参考にした github のコードのままになります。
ここでは、**API Gateway がキックする Lambda を定義**しています。 Swagger で定義した **API Gateway のエンドポイントパスを叩くと、ここで定義した lambda が実行される**といった処理になります。

`x-amazon-apigateway-integration` に定義する `httpMethod` は **swagger で定義したエンドポイントパスに対応するメソッド**を指定してください。

また `uri` には後述する **Lambda 関数の ARN** を指定します。したがって `${PetStoreFunction.Arn}` は、**ご自身の環境の** `Lambda 関数名.arn`**を指定**してください

## docker-compose の起動
ここで一度 `docker-compose up` で Docker コンテナを起動してみます。
起動が完了したら `localhost:8081` にアクセスしてみます。

下記のように **Swagger Editor が表示**されたら OK です。
![スクリーンショット 2021-04-13 19.22.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175476/39374b73-9350-5db7-61af-227f6f9e9174.png)

ブラウザの Swagger Editor で修正した yaml ファイルはローカルには自動で反映されないので、必ず**ローカルのIDE または エディタから編集してブラウザで確認**といった形で修正内容を確認しましょう。
もしブラウザで修正したい場合は yaml ファイルをブラウザで修正後ダウンロードもできます。

## AWS SAM CLI を使った Lambda 関数の作成
続いて、立ち上げた **AWS SAM CLI 用のコンテナ（コンテナ名: sam）**にアクセスして **AWS SAM CLI を実行し初期プロジェクトを作成**します。

### コンテナへのアタッチ
▼ コマンドの方は下記コマンドを実行します。

```sh:ホスト
$ docker exec -it sam /bin/sh 
```

▼ VSCodeの場合は Docker Extension からコンテナ内へアタッチすることができます。
![スクリーンショット 2021-04-13 19.38.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175476/c8173a57-4b04-17b8-95f3-3bd7c540ca89.png)

### AWS SAM CLI の実行と初期プロジェクトの作成
アタッチしたコンテナ内で AWS SAM CLI を実行して初期プロジェクトを作成します。
AWS SAM CLI の AWS 公式チュートリアルを参考にしています。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html

▼ 実行した SAM CLI のコマンド（選択した選択肢のみ記載しています）

```sh:samコンテナ
/app $ sam --version
# SAM CLI, version 1.18.1

/app $ sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
Which runtime would you like to use?
        1 - nodejs14.x
AWS quick start application templates:
        1 - Hello World Example
# 出力結果
    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: nodejs14.x
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./sam-app/README.md
``` 
上記のようにテンプレートを作成するための選択肢が出てきます。
今回は nodejs で lambda を開発するので Runtime は `nodejs14.x` を選びました。
デプロイ先は `S３` にしております。
その他の選択肢は上記を参考にしてください。

テンプレートが作成されるとプロジェクトフォルダに `hello-world` フォルダが作成されます。

## template.yaml の実装
ここでは、チュートリアルで作成された `template.yaml` を編集し swagger に対応させるように修正します。
追記する観点は下記２項目です。

- API Gateway の定義を追加
- Lambda function にエンドポイントパスイベントを定義

※ 上記２点の修正箇所に伴って Outputs の値なども修正しております。

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app
  
Globals:
  Function:
    Timeout: 3

Parameters:
  # swagger.yml をアップロードしているバケット名
  BucketName:
    Type: String

Resources:
  # PetStore 用の API Gateway を定義
  PetStoreApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Ref "AWS::StackName"
      StageName: prod
      OpenApiVersion: 3.0.0
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !Sub s3://${BucketName}/swagger.yml
  
  # Lambda 関数の設定を定義 (Hello-world のコードを流用)
  PetStoreFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Events:
        PetsGet:
          Type: Api
          Properties:
            Path: /pets
            Method: get
            RestApiId: !Ref PetStoreApi # API を参照
        PetsPetIdGet:
          Type: Api
          Properties:
            Path: /pets/{petId}
            Method: get
            RestApiId: !Ref PetStoreApi # API を参照
        PetsPost:
          Type: Api
          Properties:
            Path: /pets
            Method: post
            RestApiId: !Ref PetStoreApi # API を参照

Outputs:
  PetStoreApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${PetStoreApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pet/"
  PetStoreFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt PetStoreFunction.Arn
  PetStoreFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt PetStoreFunctionRole.Arn

```

注目するところは下記設定です。

- `PetStoreApi` に `OpenApiVersion: 3.0.0` を指定する。
- `PetStoreApi` の `DefinitionBody` に swagger.yml をアップロードした S3 バケット URL をセットする。

```yaml
      OpenApiVersion: 3.0.0
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !Sub s3://${BucketName}/swagger.yml
```

- `PetStoreFunction` の `Events` に swagger.yml で定義したエンドポイントパスを定義する。

```yaml
      Events:
        PetsGet:
          Type: Api
          Properties:
            Path: /pets
            Method: get
            RestApiId: !Ref PetStoreApi # API を参照
```

## ソースコードの実装
Lambda Function のソースコードを実装します。
ここでは、サンプルのためチュートリアルのコードのままにします。

## S3 バケットの作成
ビルドされた template.yaml やコード、 swagger.yml を保存する S3 バケットを手動で作成します。

```sh:samコンテナ
/app $ aws s3 mb s3://sam-app-bucket-pfr379vgrp0 --profile default
```

## swagger.yml を S3 へアップロード
先ほど作成した S3 バケットに swagger.yml ファイルをアップロードします。

```sh:samコンテナ
/app $ aws s3 cp swagger.yml s3://sam-app-bucket-pfr379vgrp0/swagger.yml --profile default
```

以上が開発環境の作成です。次に AWS 環境へデプロイする準備を行います。

# 本番デプロイ準備
本番へデプロイするための準備を行います。
## samconfig.toml の作成
デプロイするための情報を toml 形式で `sam-app` 直下に保存します。

```toml:sam-app/samconfig.toml
version=0.1
[default.deploy.parameters]
stack_name = "sam-app"
s3_bucket = "sam-app-bucket-pfr379vgrp0"
s3_prefix = "sam-app"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
```
## ビルド
AWS SAM CLI を利用してコードのビルドを行います。

```sh:samコンテナ
/app $ cd sam-app
/app/sam-app $ sam build
```

ビルドが完了し、`.aws-sam` フォルダに `build` フォルダが作成されていればビルドが成功です。
ちなみに自動で `npm install` コマンドが実行され `node_module` をビルドに含めてくれているみたいです。

# 本番デプロイ
最後にデプロイを行います。

```sh:samコンテナ
/app/sam-app $ sam deploy --profile default --config-env default --parameter-overrides BucketName=sam-app-bucket-pfr379vgrp0

### 省略 ###

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2021-04-14 08:23:23 - Waiting for stack create/update to complete
CloudFormation events from changeset
### 省略 ###

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

|オプション  |解説  |
|---|---|
|--profile  | AWS CLI の ~/.aws/config に設定した profile。自身のデプロイ先のAWSアカウントを指定してください。 デフォルト: default| 
|--config-env | samconig.toml の設定情報 deploy.parameters の前の文字列を指定します。 デフォルト: default|
|---parameter-overrides | template.yaml の Parameters に定義したパラメータをここで指定できます。|

Successfully が表示されたらデプロイの完了です。

## 作成されたリソースの確認
▼ API Gateway のリソースとステージ
![スクリーンショット 2021-04-14 17.30.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175476/681be259-1b31-63c5-d8a9-01ae15a8376e.png)

▼ Swagger で定義したモデルの確認
![スクリーンショット 2021-04-14 17.33.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175476/a1be6cac-3ec7-c6e1-22ff-85c621b863a8.png)

# いざコード開発の開始！！
これで環境構築と、デプロイができました！！
あとは、Lambda Function の開発を行い、**コードを修正したら上記手順でまたビルド & デプロイ** すると AWS 上で自身で作成した API が確認できます。

# 削除コマンド
今回作成したリソースは認証のかかっていない API になっています。
なので削除コマンドも載せておきます。

```sh:samコンテナ
/app $ aws cloudformation delete-stack --stack-name sam-app --profile default
```

# 最後に
**Swagger と API Gateway を連携**することで **Swagger の API 定義書と API Gateway の実際のリソースが同期**されるようになりました。
連携しておけば、「**API定義書が古い！！**」といった事象も起こりにくくなると思います。

こういった初期プロジェクトの作成はなかなか全員の開発者が触れるものではないので貴重な体験です。
また、プロジェクトの最初にしか実行しないので、悪戦苦闘することが多いです。
そういった方に見てもらえると幸いですm(_ _)m

今回は手動でデプロイしましたが、これらを自動で開発、ステージ、本番へデプロイしたりテストステージを組み込んだ CI・CD 環境を作ることもできます。

それらは次回ご紹介しようと思いますm(_ _)m