AWS S3に静的ウェブサイトのホスティングの記事で、S3上静的Webサイトをホストするやり方を紹介しましたが、静的ページに動的コンテンツを加えることもできます。
JavascriptのSDKやAPI Gateway + Lambdaを活用した2TierアーキテクトはS3の定番ユースケースです。
API Gateway + Lambdaを利用すると、JavascriptによるAjax通信がよく使われます。その場合API Gatewayの設定もクロスドメインを許可する必要があります。
ここで、API Gatewayのクロスドメイン設定をして、GetとPostによるAjax通信を検証してみました。
##概要アーキテクチャ図
静的なHtml等をS3にホストし、動的コンテンツをAPI Gateway+Lambdaから取得するアーキテクチャを以下で実現します。
もちろん、HtmlをホストしているS3のドメインと、動的コンテンツを取得するAPI Gatewayのドメインが異なるので、クロスドメイン問題が発生します。
##クロスドメインとは
簡単に言うと、異なるドメイン間で Ajax の実行を許可しないという制約です。制約といってもブラウザの仕様(デフォルト設定)であり、サーバ側のレスポンスで許可すれば、通信可能になります。
CORS(Cross-Origin Resource Sharing)によるクロスドメイン通信の傾向と対策の説明がわかりやすいです。
##Lambda作成
通信を試すだけなので、GETで指定のTimeZoneを送信し、サーバ時刻を返す簡単なLambda関数SampleFunctionを作成しておきます。POSTの場合、入力した日時とサーバ時刻の時間差を取得するような処理となります。
from datetime import datetime, timedelta
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info(event)
dt = datetime.now()
result = ""
try:
httpmethod = event["context"]["http-method"]
if "GET" == httpmethod:
timezone = event["params"]["querystring"]["timezone"]
dt = dt + timedelta(hours=int(timezone))
result = dt.strftime("%Y/%m/%d %H:%M:%S")
elif "POST" == httpmethod:
postservertime = event["body-json"]["postservertime"]
postedtime = datetime.strptime(postservertime, "%Y/%m/%d %H:%M:%S")
timezone = event["body-json"]["timezone"]
dt = dt + timedelta(hours=int(timezone))
delta = dt - postedtime
result = str(round(delta.total_seconds() / 3600, 2)) + " hours difference."
else:
logger.error("Unexpected http method : " + httpmethod)
return {"statusCode" : 200,
"body" : result,
"headers" : {"Content-Type" : "application/json"}}
except Exception as e:
logger.error("type : %s", type(e))
logger.error(e)
return {"statusCode" : 200,
"body" : e,
"headers" : {"Content-Type" : "application/json"}}
##API Gateway作成
早速作ってみます。
###Create API
[Create API]でSampleAPIを新規作成します。
###Create Resource
API作成したら次API内にリソースを定義します。
sampleというResourceを作成し、リソース名がURLのパスの一部となります。
Enable API Gateway CORSチェックすれば、クロスドメイン設定も可能ですが、違いを見るために、ここで設定せずに、Create Resourceを押します。
###Create Method
最後にリソースにメソッドを定義します。(ActionsのCreate Method)
メソッドはリソース+HTTPメソッドで構成され、スタンダードな7つのHTTPメソッドとANYをサポートします。
ここでまずGETとPOSTを定義します。
####GET
Lambda Functionに作成したSampleFunctionを指定、Saveします。
Add Permission to Lambda Functionポップアップが表示され、API GatewayにLambdaをinvokeする権限を与えます。
You are about to give API Gateway permission to invoke your Lambda function:
arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:SampleFunction
####POST
GETと同じ手順でPOSTも作っておきます。
###デプロイ
リソースとHTTPメソッドを定義したら、Actionsの「Deploy API」でデプロイします。
###ステージ
APIはステージにデプロイされます。ステージはそれぞれの環境を表し、dev、test、beta、prod等で定義します。
Stage nameをdevとします。
デプロイしたら、URLをたたくことでAPIを呼び出し可能です。
https://nsoi8vgl6k.execute-api.ap-northeast-1.amazonaws.com/dev/sample
##Javascript
ただJavascriptからajaxで呼び出すとクロスドメインの制約に引っかかりますので、まず試してみます。
jqueryのajaxを利用して非同期でAPIを呼び出すサンプルhtmlを作成します。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
<head>
<title>Sample API Async Invoke</title>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
function startget() {
timezone = document.getElementById("timezone").value;
geturl = "https://nsoi8vgl6k.execute-api.ap-northeast-1.amazonaws.com/dev/sample?timezone=" + timezone;
$.ajax({
type : "GET",
url : geturl,
success: function(resp, status) {
document.getElementById("getservertime").value = resp["body"];
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
document.getElementById("getservertime").value =
"XMLHttpRequest: " + XMLHttpRequest.status +
"\r\ntextStatus: " + textStatus +
"\r\nerrorThrown: " + errorThrown.message;
}
});
}
function startpost() {
postservertime = document.getElementById("postservertime").value;
timezone = document.getElementById("timezone").value;
posturl = "https://nsoi8vgl6k.execute-api.ap-northeast-1.amazonaws.com/dev/sample";
var jsondata = {"postservertime" : postservertime, "timezone" : timezone};
$.ajax({
dataType : "json",
data : JSON.stringify(jsondata),
type : "POST",
url : posturl,
success: function(resp, status) {
document.getElementById("postservertime").value = resp["body"];
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
document.getElementById("postservertime").value =
"XMLHttpRequest: " + XMLHttpRequest.status +
"\r\ntextStatus: " + textStatus +
"\r\nerrorThrown: " + errorThrown.message;
}
});
}
</script>
</head>
<body>
<div>
<div>
<p>Time Zone<br>
<select id="timezone">
<option value="-5">America/New_York</option>
<option value="9">Asia/Tokyo</option>
<option value="10">Australia/Sydney</option>
<option value="0">Europe/London</option>
</select></p>
<textarea id="getservertime" cols="30" rows="5"></textarea>
</div>
<div>
<button type="button" id="btn-get" onclick="startget()">Get</button>
<div>
</div>
<br>
<div>
<div>
<textarea id="postservertime" cols="30" rows="5"></textarea>
</div>
<div>
<button type="button" id="btn-post" onclick="startpost()">Post</button>
</div>
</div>
</body>
</html>
AWS S3に静的ウェブサイトのホスティングの手順通り、S3にホストします。
http://sample-async-invoke.s3-website-ap-northeast-1.amazonaws.com/
このままGetやPost実行するとエラーとなります。
##CORS設定
AWSのAPI Gatewayでのクロスドメイン許可設定は非常に簡単で、Actionsの「Enable CORS」でできます。
イメージとして、OPTIONSメソッドを追加してくれて、Response headerをつけてくれるような感じですね。
Response header | 設定値 | 説明 |
---|---|---|
Access-Control-Allow-Headers | 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token' | 次に来るCORSリクエストに含めてよいヘッダ |
Access-Control-Allow-Methods | 'POST,GET,OPTIONS' | 次にCORSリクエスト可能なHTTPメソッド |
Access-Control-Allow-Origin | '*' | 次にCORSリクエスト可能なオリジン(リクエスト元のドメイン) |
##Request Detail情報の有効化
CORS設定完了後、Ajax通信が可能になりますが、Lambdaのevent引数からまだRequest Detail情報を受け取れません。なぜなら、Use Lambda Proxy integrationにチェックが外れている状態です。(チェックを入れいると今度Ajax通信が失敗するので、Lambda Proxy integrationは使えないということです。)
そこで自前にBody Mapping Templatesを作成する必要があります。(自作でもよいですが、実際用意されているデフォルトテンプレートを適用してもよいです)
###GETメソッドBody Mapping Templates
GETメソッドのIntegration Request画面で、Body Mapping TemplatesのContent-Typeにapplication/jsonを登録し、Mapping Templateを追加します。
TemplateはMethod Request passthroughを利用(説明にあるように、このテンプレートはpath、querystring、header等すべての情報を変換してくれてLambdaにパスします。
上記を設定すれば、リクエストヘッダとマッピングテンプレートのContent-Typeが一致の場合、mapping templateに従いデータ変換してくれます。
###POSTメソッドBody Mapping Templates
POSTメソッドも同じ考え方です。Content-Typeはapplication/x-www-form-urlencodedになります。
###GETメソッドのQueryString
<script type="text/javascript">
function startget() {
timezone = document.getElementById("timezone").value;
geturl = "https://nsoi8vgl6k.execute-api.ap-northeast-1.amazonaws.com/dev/sample?timezone=" + timezone;
$.ajax({
type : "GET",
url : geturl,
success: function(resp, status) {
document.getElementById("getservertime").value = resp["body"];
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
document.getElementById("getservertime").value =
"XMLHttpRequest: " + XMLHttpRequest.status +
"\r\ntextStatus: " + textStatus +
"\r\nerrorThrown: " + errorThrown.message;
}
});
}
最後に、GETの場合、timezoneをQueryStringとしてくっつけているので、timezoneのvalueをLambdaに渡すには、Method Request画面でURL Query String Parametersに登録しておく必要があります。
##デプロイ
すべての設定が完了後、デプロイを忘れずに実施します。
##検証
Getボタンを押すと、選択しているNew YorkのTimeZoneのサーバ時刻がテキストボックスに表示されます。
Response Headersに**access-control-allow-origin: ***が追加されています。
次に、TimeZoneをTokyoに変更し、PostのテキストボックスにNew Yorkのサーバ時刻を入れて、Postボタンを押します。時間差が問題なく表示されました。
##参考