【実例】Amazon API Gateway を S3 プロキシにして簡易スタブ API でテスト効率化!
【実例】Amazon API Gateway を S3 プロキシにして簡易スタブ API でテスト効率化!
☘️ スタブ作成の経緯
ある LMS(Learning Management System)と学習データを連携しているシステムで、大量の学習データをテストすることが必要になりました。
データ連携している LMS から提供されている API では、ユーザーは増やせても、学習履歴やテストの成績データなどを捏造・・大量生成することができませんでした。
なるべく簡単に、素早く、開発者が簡単な手順でデータを入れ替えできる必要がありました。
そこで、S3 にレスポンスの JSON ファイルを格納して、それをそのまま返却できるように API Gateway を S3 のプロキシとする方法を採用しました。
これによって、S3 上のレスポンス JSON ファイルを変更すれば簡単にテストデータを返すことができます。
公式な手順は以下になります。
チュートリアル: API Gateway で REST API を Amazon S3 のプロキシとして作成する
👀 Contents
全体構成
PetStore の API を例にして説明します。
API のメソッドが特定のバケット内の対応する result.json を読み込んでレスポンスとして返却します。
GET も POST も S3 に格納された結果をそのまま返すようにします。
手順
主な手順は次のとおりです。
- S3 バケット作成
- IAM ロール作成
- API リソース作成
- 動作確認
1. S3 バケットを作成します
レスポンス結果を格納するためのバケットを作成します。
S3 に配置する API リクエストに対するレスポンス JSON ファイル名は、メソッドタイプを付与した get_result.json
と post_result.json
で固定にします。
PetStore の API を例にすると次のような S3 構成になることを想定しています。
S3
├ [pets]
├ get_result.json
├ [pet]
├ post_result.json
├ put_result.json
├ [{petId}]
├ get_result.json
├ [findByStatus]
├ get_result.json
├ [findByTags]
├ get_result.json
├ [store]
├ [inventory]
├ get_result.json
├ [order]
├ post_result.json
├ [user]
├ [login]
├ get_result.json
├ [logout]
├ get_result.json
2. IAM ロールを作成します
S3 にアクセスできるように IAM ロールを作成します。
信頼されたエンティティタイプでは、「AWS のサービス」を選択し、は API Gateway
を指定します。
AmazonAPIGatewayPushToCloudWatchLogs
となっている状態で、次に進みます。
ロール名を入力して、「ロールを作成」を行います。
作成されたロールを開き、「許可」タブから、「許可を追加>インラインポリシーを作成」を選択します。
インラインポリシーは以下のようにします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::バケット名/*"
}
]
}
ロールの ARN をメモしておくか、ブラウザのタブを閉じずに残したままにします。
3. API リソースを作成します
IAM ロールの ARN はあとで必要になるので、APIGateway を別のタブで開きます。
「API を作成」をクリックし、REST API を「構築」します。
「新しい API」を選択して、API の名前(例:api-stub)を入力し、「API を作成」をクリックします。
3.1. リソースを作成します
リソース名を {folder}
として作成します。
さらにその下の階層に {item}
も作成します。
最終的には次のような構成になります。
3.2. メソッドを作成します
{folder}
と{item}
のそれぞれに、GET
とPOST
のメソッドを作成します。その他、必要に応じて PUT
なども作成します。
- GET メソッド
メソッドタイプにGET
を指定し、HTTP メソッドもGET
とします。
この後、アクションタイプを設定するのですが、GET も POST も共通なので後述します。
- POST メソッド
今回は、POST メソッドも S3 にあるファイルの内容を取得するため、メソッドタイプはPOST
ですが、S3 にアクセスするときの HTTP メソッドはGET
を指定します。
この後、アクションタイプを設定するのですが、GET も POST も共通なので後述します。
3.2.1. アクションタイプ
アクションタイプは、「パスオーバーライドを使用」を選択します。デフォルト選択の アクション名を使用
のままにしていると、後で実施するテストでエラーになります。忘れずに変更してください。
3.2.2. パスオーバーライド
パスオーバーライドには、次のように設定します。
パス | メソッドタイプ | パスオーバーライド |
---|---|---|
/{folder} | GET | S3 バケット名/{folder}/get_result.json |
/{folder} | POST | S3 バケット名/{folder}/post_result.json |
/{folder}/{item} | GET | S3 バケット名/{folder}/{item}/get_result.json |
/{folder}/{item} | POST | S3 バケット名/{folder}/{item}/post_result.json |
- GET メソッド
- POST メソッド
3.2.3. 実行ロール
実行ロールは、「2. IAM ロール」で作成したロールの ARN を貼り付けます。
3.3. 統合リクエストの URL パスパラメータ
メソッドの作成後、作成したメソッドを選択し、「統合リクエスト」タブを開き、「編集」をクリックします。
画面をスクロールし、「URL パスパラメータ」を展開します。
「パスパラメータの追加」をクリックし、先ほどのパスオーバーライドで使用した folder
という部分を URL パスパラメータとマッピングします。
定義は次のようにします。item
についてはパスが /{folder}/{item}
のメソッドの場合のみ追加します。
名前 | マッピング先 | キャッシュ |
---|---|---|
folder | method.request.path.folder | 未選択 |
item | method.request.path.item | 未選択 |
4. 動作確認
テストタブで動作確認を行います。
pets
の API をテストします。
「テスト」をクリックすると、正しく設定できていればレスポンスが返ってきます。
もう一つ、pet/findByStatus
もテストしてみます。
正しくレスポンスが返ってきました。
【おまけ】レスポンスの形式を変更したいとき
統合レスポンスのマッピングテンプレートで編集することができます。
マッピングテンプレートについてついては、ドキュメントのマッピングテンプレートについてを参照してください。
Velocity Template Language (VTL) で表現されるスクリプトです。
よく使うものは次のとおりです。
-
#if #elif #else #end
- 分岐に使用します
#if($foo == $bar)it's true!#{else}it's not!#end
-
#set
- 変数に代入するときに使用します
#set($body = $input.path('$'))
-
#foreach
- 要素を順番に処理する場合に使用します
-
#foreach($item in $body) ~ #end
と書きます -
#if($foreach.hasNext),#end
とすると次が存在するときにカンマを付与することができます -
#break
で抜けることができます -
$foreach.index
で現在処理中要素のインデックスが取得できます - その他
- $foreach.count ⇒1 から始まるカウンタ
- $foreach.first ⇒ 最初だったら true
- $foreach.last ⇒ 最後だったら true
- $foreach.stop()
-
## foo bar
- 単一行コメント
-
#_ ~ _#
- 複数行コメント
-
parseInt
- 文字列を数値に変換する。ただし事前に整数を1つ宣言する必要があります
#set($Integer = 0) #set($result = $Integer.parseInt($NumberString))
入力データ
これ以降、次のような入力データに対して編集する例を説明します。
[
{
"id": 1,
"type": "dog",
"price": 249.99
},
{
"id": 2,
"type": "cat",
"price": 124.99
},
{
"id": 3,
"type": "fish",
"price": 0.99
}
]
例1.必要な情報だけに絞りたい
レスポンスに id 列は不要なので除去します。
#set($body = $input.path('$'))
[
#foreach($item in $body)
{
"type": "$item.type",
"price": $item.price
}#if($foreach.hasNext),#end
#end
]
結果は次のようになります。
[
{
"type": "dog",
"price": 249.99
},
{
"type": "cat",
"price": 124.99
},
{
"type": "fish",
"price": 0.99
}
]
例2.foreach を利用して要素を順番に ①
foreach を利用することで、要素を順番に処理することができます。
#set($body = $input.path('$'))
[
#foreach($item in $body) ## ① 各レコードを順番に処理します
{
#foreach($key in $item.keySet()) ## ② 各レコードの要素を順番に処理します
"$key" : "$item.get($key)"
#if($foreach.hasNext),#end ## ③ 次の要素があったらカンマを付けます
#end
}#if($foreach.hasNext),#end ## ③ 次のレコードがあったらカンマを付けます
#end
]
結果は次のようになります。
この方法だと、一律ダブルクォーテーションでくくるのでもともと数値だった項目も括られます。
[
{
"id": "1",
"type": "dog",
"price": "249.99"
},
{
"id": "2",
"type": "cat",
"price": "124.99"
},
{
"id": "3",
"type": "fish",
"price": "0.99"
}
]
例3.foreach を利用して要素を順番に ②
先ほどの例から、id と price はダブルクォーテーションで括らないようにしたいと思います。
#set($body = $input.path('$'))
[
#foreach($item in $body)
{
#foreach($key in $item.keySet())
"$key" :
#if ($key=="id" or $key=="price") ## ① キーが id か price だったら
$item.get($key)
#else ## ② それ以外は括ります
"$item.get($key)"
#end
#if($foreach.hasNext),#end
#end
}#if($foreach.hasNext),#end
#end
]
結果は次のようになります。
[
{
"id": 1,
"type": "dog",
"price": 249.99
},
{
"id": 2,
"type": "cat",
"price": 124.99
},
{
"id": 3,
"type": "fish",
"price": 0.99
}
]
例4.レイアウトを変更したい
データは、results
にまとめます。
#set($body = $input.path('$'))
{
"results":[
#foreach($item in $body)
{
"id": $item.id,
"type": "$item.type",
"price": $item.price
}#if($foreach.hasNext),#end
#end
]
}
結果は次のようになります。
{
"results": [
{
"id": 1,
"type": "dog",
"price": 249.99
},
{
"id": 2,
"type": "cat",
"price": 124.99
},
{
"id": 3,
"type": "fish",
"price": 0.99
}
]
}
例5.件数などの情報を付加したい
meta
で情報を追加します。
#set($body = $input.path('$'))
#set($results = $body.size())
#set($result_id = $context.requestId)
#set($timestamp = $context.requestTimeEpoch)
{
"results":[
#foreach($item in $body)
{
"id": $item.id,
"type": "$item.type",
"price": $item.price
}#if($foreach.hasNext),#end
#end
],
"meta": {
"total": $results,
"timestamp": $timestamp,
"result_id": "$result_id"
}
}
結果は次のようになります。
{
"results": [
{
"id": 1,
"type": "dog",
"price": 249.99
},
{
"id": 2,
"type": "cat",
"price": 124.99
},
{
"id": 3,
"type": "fish",
"price": 0.99
}
],
"meta": {
"total": 3,
"timestamp": 1706860280008,
"result_id": "9f4093ae-bee2-4c60-a677-1a0b43d1b473"
}
}
例6.レスポンスヘッダーに件数を追加したい
$context.responseOverride.header.count
を設定することで追加できます。
#set($body = $input.path('$'))
#set($results = $body.size())
#set($context.responseOverride.header.count = "$results")
結果は次のようになります。
{
"Content-Type": "application/json",
"X-Amzn-Trace-Id": "Root=1-65bcaa96-e526bc0acbec62008db0b328",
"count": "3"
}
例7.特定の項目だけ書き換えたい
#set($body = $input.path('$'))
#foreach($item in $body)
#set($item.id="ZZZ$item.id")
#end
$input.json('$')
参照渡しになっているので、$input も書き換わっています。
[
{
"id": "ZZZ1",
"type": "dog",
"price": 249.99
},
{
"id": "ZZZ2",
"type": "cat",
"price": 124.99
},
{
"id": "ZZZ3",
"type": "fish",
"price": 0.99
}
]
例8.要素を追加したい
#set($body = $input.path('$'))
#foreach($item in $body)
#set($item.id2="ZZZ$item.id")
#end
$input.json('$')
参照渡しになっているので、$input も書き換わっています。
[
{
"id": 1,
"type": "dog",
"price": 249.99,
"id": "ZZZ1"
},
{
"id": 2,
"type": "cat",
"price": 124.99,
"id": "ZZZ2"
},
{
"id": 3,
"type": "fish",
"price": 0.99,
"id": "ZZZ3"
}
]
例9.整数を計算したい
#set($body = $input.path('$'))
#foreach($item in $body)
#set($item.id=$item.id*2)
#end
$input.json('$')
結果は次のようになります。
id = 1,2,3 が id = 2,4,6 の 2 倍になっています。
[
{
"id": 2,
"type": "dog",
"price": 249.99
},
{
"id": 4,
"type": "cat",
"price": 124.99
},
{
"id": 6,
"type": "fish",
"price": 0.99
}
]
例10.小数を計算したい
整数 × 整数の計算は単純にできるので、小数を計算します。
price を 1.1 倍に変更しましょう。
#set($body = $input.path('$'))
#foreach($item in $body)
#set($item.price=$item.price * 1.1)
#end
$input.json('$')
単純に計算すると、丸め誤差が発生しています。
id=1 の price が 249.99 * 1.1 = 274.989 になっていません。
[
{
"id": 1,
"type": "dog",
"price": 274.98900000000003
},
{
"id": 2,
"type": "cat",
"price": 137.489
},
{
"id": 3,
"type": "fish",
"price": 1.089
}
]
例11.小数を計算したい(丸め誤差対応できる?)
// TODO
こんな感じで出来るのかと思いましたが、だめでした。
#set($body = $input.path('$'))
#foreach($item in $body)
#set($originalPrice = $item.price)
#set($decimalMultiplier = 1.1)
#set($result = $decimalMultiplier.multiply($originalPrice)) ## ※ ここがNG
#set($item.price = $result.doubleValue())
#end
$input.json('$')
上記、”※”のところで計算できていないので、price がなくなっています。
[
{
"id": 1,
"type": "dog",
"price": ""
},
{
"id": 2,
"type": "cat",
"price": ""
},
{
"id": 3,
"type": "fish",
"price": ""
}
]
Discussion