はじめに
AWSのAPIGatewayは使えば使うほど奥が深くて色々できて楽しい。
楽しいけど、自由度が高い分動作が謎の部分が多い。
特に、SAMテンプレートは内部動作を隠蔽してくれるので、組み合わせた場合は正しく動作検証をしていく必要があるだろう。
と思い、APIGatewayを記述したSAMテンプレートの更新をした際の動作を確認してみよう。
前提条件
- 使っているアプリケーションが以前の記事の続きなので、記事の内容を把握している
- Amazon APIGatewayをマネージメントコンソールからでもSAMテンプレートでも触ったことがある
SAMテンプレートの修正
とりあえず、今回の目的はデプロイの修正であるため、SAMテンプレートの修正内容は何でも良い(以下の修正も、Lambda関数側では使用していないRequestBodyのチェックを追加したもので意味はない)ので、以下のように修正してみる。
この修正内容自体は本記事の「おまけ」の項で説明する。
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}
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のデプロイを入れ替えているようだ。
新しいデプロイを作成した後に、古いデプロイを削除している。
しかし、これではいきなりAPIが本番化されてしまう。
APIの変更をCanaryでデプロイする
というわけで、それだと困ってしまう場合は、SAMテンプレートのType: AWS::Serverless::Api
のプロパティに
CanarySetting:
PercentTraffic: 50
を追加しておく。こうすることにより、デプロイ後に
といった具合に、Canaryが有効になり、新しいAPI定義に流れるトラフィック量を制御できる。
ただし、CodeDeployのCanaryリリースと異なり、自動ロールバックや時限でのトラフィック100%移行をしてくれないように見える。うーむ、結局マネージメントコンソール経由なりCLIなりでCanaryの昇格をしないといけないのは、IaCでの運用的にどうなのだろうか……。片手落ち……。
Canaryなデプロイの後始末
それでも、CLIで最後まで運用してみよう。
【AWS公式】Canary リリースの昇格
を見ながらやってみる。
まずは、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テンプレートから
CanarySetting:
PercentTraffic: 50
を削除してもう一度aws cloudformation deploy
してやればよいのでは。
と思って試してみたが、どうやら↑の変更だけでは、SAM的には「APIのステージに変更がなかったためデプロイしない」と判断され、Canaryの設定が変更されなかった。うーん、残念。
結論
というわけで、SAMテンプレートを使ってAPI GatewayのCanaryリリースを全自動で実現するのは厳しいという結論となった。今のところ、API Gatewayの更新をしてCanaryリリースをしなければいけないケースはLambda側に寄せるか、手動運用が入ることを念頭に置いた方がよさそうだ。
おまけ
body部のチェックは、APIGatewayのコンソールからは以下の様にモデルを作成し、そのモデルを参照することで実現する。
モデルの定義は、JSONスキーマで行うことができる。
いやいや、SAMテンプレートに加えてOpenAPIだとかSwaggerとかの記法を覚えるのにさらにJSONスキーマとか覚えるのかよ……。な感じになってなかなかカオスである。しかも、SAMテンプレートに書くときには、JSONスキーマをYAMLで書くことができるとか、ますますワケが分からない。
だが、このJSONスキーマは正規表現が使えるので、ちょっとしたチェックであればお手軽に作ることができる。
とは言え、APIGatewayの機能に組み込むことになるので、アプリケーションのCI/CDには組み込めない。さらに、この記事の本筋で書いているように、Canaryデプロイが片手落ちなので、バリデーションチェックなんて危険な機能を修正することはそう簡単にはできない。なんだか色々と中途半端な感じである……。
知っておいて損はないけど、おそらく使うことはあまり無いのだろうなぁ……。
Swagger記法そのものは、Amazon APIGatewayを使うならちゃんと理解しておくべきなのでこの辺を見て勉強しておくのが良いと思う。