Amazon APIGatewayのSAMテンプレートを更新してcloudformation deployしたときの動作を確認する

# はじめに
AWSのAPIGatewayは使えば使うほど奥が深くて色々できて楽しい。
楽しいけど、自由度が高い分動作が謎の部分が多い。
特に、SAMテンプレートは内部動作を隠蔽してくれるので、組み合わせた場合は正しく動作検証をしていく必要があるだろう。
と思い、APIGatewayを記述したSAMテンプレートの更新をした際の動作を確認してみよう。

# 前提条件
- 使っているアプリケーションが[以前の記事](https://qiita.com/neruneruo/items/fc6685779f155b13601a)の続きなので、記事の内容を把握している
- Amazon APIGatewayをマネージメントコンソールからでもSAMテンプレートでも触ったことがある

# SAMテンプレートの修正
とりあえず、今回の目的はデプロイの修正であるため、SAMテンプレートの修正内容は何でも良い(以下の修正も、Lambda関数側では使用していないRequestBodyのチェックを追加したもので意味はない)ので、以下のように修正してみる。
この修正内容自体は本記事の「おまけ」の項で説明する。

<details><summary>APIGateway.yml</summary><div>

```YAML:APIGateway.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description:
  Create APIGateway template for ApigwTest2
  
Parameters:
  Prefix:
    Description: "Project name prefix"
    Type: "String"
    Default: "ApigwTest2"
  RESTApiNameSuffix:
    Description: "REST API name suffix"
    Type: "String"
    Default: "-RESTAPI"
  RESTApiStageName:
    Description: "REST API stage name suffix"
    Type: "String"
    Default: "default"
  GetLambdaFunctionNameSuffix:
    Description: "Lambda Function name suffix"
    Type: "String"
    Default: "-GetName-LambdaFunction"
  SetLambdaFunctionNameSuffix:
    Description: "Lambda Function name suffix"
    Type: "String"
    Default: "-SetName-LambdaFunction"
  
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Project name prefix"
        Parameters:
          - Prefix
      - Label:
          default: "API Gateway configuration"
        Parameters:
          - RESTApiNameSuffix
          - RESTApiStageName
          - GetLambdaFunctionNameSuffix
          - SetLambdaFunctionNameSuffix

Resources:
  # ------------------------------------------------------------#
  #  API Gateway
  # ------------------------------------------------------------#
  APIGATEWAY:
    Type: AWS::Serverless::Api
    Properties: 
      Name: !Sub ${Prefix}${RESTApiNameSuffix}
      StageName: !Sub ${RESTApiStageName}
      DefinitionBody:
        swagger: "2.0"
        info:
          description: "Created by CloudFormation template"
          version: "1.0.0"
          title: "ApigwTest2-RESTAPI"
        basePath: "/default"
        schemes:
          - "https"
        paths:
          /names:
            get:
              produces:
              - "application/json"
              responses:
                "200":
                  description: "200 response"
                  schema:
                    $ref: "#/definitions/Empty"
              x-amazon-apigateway-integration:
                uri: !Sub
                  - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetLambdaFunctionArn}/invocations
                  - GetLambdaFunctionArn: {'Fn::ImportValue': !Sub '${Prefix}${GetLambdaFunctionNameSuffix}-Arn'}
                passthroughBehavior: when_no_templates
                httpMethod: POST
                type: aws_proxy
          /name:
            put:
              consumes:
              - "application/json"
              produces:
              - "application/json"
              parameters:
              - name: "name"
                in: "query"
                required: false
                type: "string"
              - name: "id"
                in: "query"
                required: true
                type: "string"
              - in: "body"
                name: "Test"
                required: true
                schema:
                  $ref: "#/definitions/Test"
              responses:
                "200":
                  description: "200 response"
                  schema:
                    $ref: "#/definitions/Empty"
              x-amazon-apigateway-request-validator: "本文、クエリ文字列パラメータ、およびヘッダーの検証"
              x-amazon-apigateway-integration:
                uri: !Sub
                  - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SetLambdaFunctionArn}/invocations
                  - SetLambdaFunctionArn: {'Fn::ImportValue': !Sub '${Prefix}${SetLambdaFunctionNameSuffix}-Arn'}
                responses:
                  default:
                    statusCode: "200"
                passthroughBehavior: when_no_match
                httpMethod: POST
                contentHandling: CONVERT_TO_TEXT
                type: aws_proxy
        definitions:
          Empty:
            type: "object"
            title: "Empty Schema"
          Test:
            type: "object"
            required:
            - "id"
            - "name"
            properties:
              id:
                type: "string"
                minLength: 5
                maxLength: 5
                pattern: "^[0-9]*$"
              name:
                type: "string"
                minLength: 1
                maxLength: 20
                pattern: "^[a-zA-Z]*$"
            title: "Test"
        x-amazon-apigateway-request-validators:
          本文、クエリ文字列パラメータ、およびヘッダーの検証:
            validateRequestParameters: true
            validateRequestBody: true
      EndpointConfiguration: REGIONAL
      CanarySetting:
        PercentTraffic: 50
      Tags: 
        Key: Name
        Value: !Sub ${Prefix}
```

</div></details>

```diff:修正内容
79a80,81
>               consumes:
>               - "application/json"
81a84,97
>               parameters:
>               - name: "name"
>                 in: "query"
>                 required: false
>                 type: "string"
>               - name: "id"
>                 in: "query"
>                 required: true
>                 type: "string"
>               - in: "body"
>                 name: "Test"
>                 required: true
>                 schema:
>                   $ref: "#/definitions/Test"
86a103
>               x-amazon-apigateway-request-validator: "本文、クエリ文字列パラメータ、およびヘッダーの検証"
91c108,111
<                 passthroughBehavior: when_no_templates
---
>                 responses:
>                   default:
>                     statusCode: "200"
>                 passthroughBehavior: when_no_match
92a113
>                 contentHandling: CONVERT_TO_TEXT
97a119,139
>           Test:
>             type: "object"
>             required:
>             - "id"
>             - "name"
>             properties:
>               id:
>                 type: "string"
>                 minLength: 5
>                 maxLength: 5
>                 pattern: "^[0-9]*$"
>               name:
>                 type: "string"
>                 minLength: 1
>                 maxLength: 20
>                 pattern: "^[a-zA-Z]*$"
>             title: "Test"
>         x-amazon-apigateway-request-validators:
>           本文、クエリ文字列パラメータ、およびヘッダーの検証:
>             validateRequestParameters: true
>             validateRequestBody: true
```

これで、再度```aws cloudformation deploy```で同じスタックを更新すると、

```
$ curl -XPUT https://${rest_api_id}.execute-api.ap-northeast-1.amazonaws.com/default/name?id=11111\&name=Taro; echo
{"message": "Invalid request body"}
```

となって、それまで通っていた処理がエラーになり、以下のようにちゃんとBodyを設定すると通るようになる。
※```${rest_api_id}```には払い出されたIDをexportしておく。

```
$ curl -XPUT -H 'Content-Type:application/json' -d '{"id": "11111", "name": "Taro"}' https://${domain}.execute-api.ap-northeast-1.amazonaws.com/default/name?id=11111\&name=Taro
```

ちなみに、Bodyに対するバリデーションチェックを入れているので、例えばidの桁数を変えたりするとそれもエラーになる。

CloudFormationの履歴を確認すると、以下のようにAPIのデプロイを入れ替えているようだ。
新しいデプロイを作成した後に、古いデプロイを削除している。

![キャプチャ8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f624bdbf-8e11-aa15-479c-132353bec6c0.png)

しかし、これではいきなりAPIが本番化されてしまう。

# APIの変更をCanaryでデプロイする

というわけで、それだと困ってしまう場合は、SAMテンプレートの```Type: AWS::Serverless::Api```のプロパティに

```YAML
      CanarySetting:
        PercentTraffic: 50
```

を追加しておく。こうすることにより、デプロイ後に

![キャプチャ9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/c9b5beec-b93c-8f5a-6a5d-9130e3f16a19.png)

といった具合に、Canaryが有効になり、新しいAPI定義に流れるトラフィック量を制御できる。

ただし、**CodeDeployのCanaryリリースと異なり、自動ロールバックや時限でのトラフィック100%移行をしてくれない**ように見える。うーむ、結局マネージメントコンソール経由なりCLIなりでCanaryの昇格をしないといけないのは、IaCでの運用的にどうなのだろうか……。片手落ち……。

# Canaryなデプロイの後始末
それでも、CLIで最後まで運用してみよう。

【AWS公式】[Canary リリースの昇格](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/promote-canary-deployment.html)

を見ながらやってみる。

まずは、CLI経由でCanaryリリースの昇格だ。
ここで罠になるのが、↑のドキュメントではマネージメントコンソールで作ったCanaryを昇格する前提としている手順だが、**SAMで作ったCanaryは、Canaryと本来のステージの関係性がなぜか逆になっている**。なので、SAMでCanaryを作ってからマネージメントコンソールでCanaryを昇格したり、↑と同じコマンドを打つと、古い方の動作をしてしまう。これは本当に勘弁してほしい……。

なので、ここでは単純に

```
$ aws apigateway update-stage --rest-api-id ${rest_api_id} --stage-name default \
--patch-operations op=replace,value=0.0,path=/canarySettings/percentTraffic
```

としてやるか、

```
$ aws apigateway update-stage --rest-api-id ${rest_api_id} --stage-name default \
--patch-operations op=remove,path=/canarySettings
```

としてやるかだ。

前者は、万が一全トラフィックを新しいバージョンに向けてエラーが発生したときに、トラフィックコントロールでロールバックができる。後者はデプロイバージョンを戻せばよい。おそらく前者の方が間違いが少ないのではなかろうか。

そして、ここまでやってみて気付いたが、つまりはSAMテンプレートでもこの動作はコントロール可能なのではないか？つまり、一旦Canaryの状態を作った後に、SAMテンプレートから

```YAML
      CanarySetting:
        PercentTraffic: 50
```

を削除してもう一度```aws cloudformation deploy```してやればよいのでは。

と思って試してみたが、どうやら↑の変更だけでは、SAM的には「APIのステージに変更がなかったためデプロイしない」と判断され、Canaryの設定が変更されなかった。うーん、残念。

# 結論
というわけで、SAMテンプレートを使ってAPI GatewayのCanaryリリースを全自動で実現するのは厳しいという結論となった。今のところ、API Gatewayの更新をしてCanaryリリースをしなければいけないケースはLambda側に寄せるか、手動運用が入ることを念頭に置いた方がよさそうだ。

# おまけ
body部のチェックは、APIGatewayのコンソールからは以下の様にモデルを作成し、そのモデルを参照することで実現する。

![キャプチャ11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/81c583e1-1969-f7df-6786-9407308dc5d1.png)

![キャプチャ10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/fc287772-ec72-fe24-e1b6-8b45fc38dc3e.png)

モデルの定義は、[JSONスキーマ](http://json-schema.org/)で行うことができる。
いやいや、SAMテンプレートに加えてOpenAPIだとかSwaggerとかの記法を覚えるのにさらにJSONスキーマとか覚えるのかよ……。な感じになってなかなかカオスである。しかも、SAMテンプレートに書くときには、**JSONスキーマをYAMLで書くことができる**とか、ますますワケが分からない。

だが、このJSONスキーマは[正規表現が使える](http://json-schema.org/understanding-json-schema/reference/string.html#regular-expressions)ので、ちょっとしたチェックであればお手軽に作ることができる。
とは言え、APIGatewayの機能に組み込むことになるので、アプリケーションのCI/CDには組み込めない。さらに、この記事の本筋で書いているように、Canaryデプロイが片手落ちなので、バリデーションチェックなんて危険な機能を修正することはそう簡単にはできない。なんだか色々と中途半端な感じである……。

知っておいて損はないけど、おそらく使うことはあまり無いのだろうなぁ……。
Swagger記法そのものは、Amazon APIGatewayを使うならちゃんと理解しておくべきなので[この辺](https://qiita.com/rllllho/items/53a0023b32f4c0f8eabb)を見て勉強しておくのが良いと思う。