AWS API Gateway クロスドメイン通信

[AWS S3に静的ウェブサイトのホスティング](https://qiita.com/tanj/items/c92481558ffb3f37c241)の記事で、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のドメインが異なるので、クロスドメイン問題が発生します。
![image_arch.png](https://qiita-image-store.s3.amazonaws.com/0/191079/0c0773c6-865d-80b3-a299-56ce6283b335.png)


##クロスドメインとは
簡単に言うと、異なるドメイン間で Ajax の実行を許可しないという制約です。制約といってもブラウザの仕様（デフォルト設定）であり、サーバ側のレスポンスで許可すれば、通信可能になります。
[CORS(Cross-Origin Resource Sharing)によるクロスドメイン通信の傾向と対策](https://dev.classmethod.jp/cloud/cors-cross-origin-resource-sharing-cross-domain/)の説明がわかりやすいです。

##Lambda作成
通信を試すだけなので、GETで指定のTimeZoneを送信し、サーバ時刻を返す簡単なLambda関数**SampleFunction**を作成しておきます。POSTの場合、入力した日時とサーバ時刻の時間差を取得するような処理となります。


```python:SampleFunction
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内にリソースを定義します。
![image11.png](https://qiita-image-store.s3.amazonaws.com/0/191079/9598fa6e-a024-81e9-64de-8582432820e7.png)
sampleというResourceを作成し、リソース名がURLのパスの一部となります。
**Enable API Gateway CORS**チェックすれば、クロスドメイン設定も可能ですが、違いを見るために、ここで設定せずに、Create Resourceを押します。
![image12.png](https://qiita-image-store.s3.amazonaws.com/0/191079/44859b7c-5963-009f-34b1-80f7c15e40ed.png)

###Create Method
最後にリソースにメソッドを定義します。(ActionsのCreate Method)
![image21.png](https://qiita-image-store.s3.amazonaws.com/0/191079/84674234-478c-46ce-5929-ef3b342dd057.png)
メソッドはリソース＋HTTPメソッドで構成され、スタンダードな7つのHTTPメソッドとANYをサポートします。
ここでまずGETとPOSTを定義します。

####GET
![image22.png](https://qiita-image-store.s3.amazonaws.com/0/191079/9cad4bb1-a1e0-a826-2377-3d10edf73bcc.png)
Lambda Functionに作成したSampleFunctionを指定、Saveします。
Add Permission to Lambda Functionポップアップが表示され、API GatewayにLambdaをinvokeする権限を与えます。

```text
You are about to give API Gateway permission to invoke your Lambda function:
arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:SampleFunction
```
![image23.png](https://qiita-image-store.s3.amazonaws.com/0/191079/860c9252-31e4-df82-54e7-3a286db75988.png)

####POST
GETと同じ手順でPOSTも作っておきます。

###デプロイ
リソースとHTTPメソッドを定義したら、Actionsの「Deploy API」でデプロイします。
![image31.png](https://qiita-image-store.s3.amazonaws.com/0/191079/a0edd89e-a0be-d9ba-e0ca-2b31e6bf6314.png)

###ステージ
APIはステージにデプロイされます。ステージはそれぞれの環境を表し、dev、test、beta、prod等で定義します。
![image32.png](https://qiita-image-store.s3.amazonaws.com/0/191079/52c7ddf1-d4c0-d0f0-fa03-338da6bcd89b.png)
Stage nameを**dev**とします。
![image33.png](https://qiita-image-store.s3.amazonaws.com/0/191079/31dc8df2-2991-76c2-0223-b785cc8cc776.png)

デプロイしたら、URLをたたくことでAPIを呼び出し可能です。
https://nsoi8vgl6k.execute-api.ap-northeast-1.amazonaws.com/dev/sample

##Javascript
ただJavascriptからajaxで呼び出すとクロスドメインの制約に引っかかりますので、まず試してみます。
jqueryのajaxを利用して非同期でAPIを呼び出すサンプルhtmlを作成します。

```html:sample.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に静的ウェブサイトのホスティング](https://qiita.com/tanj/items/c92481558ffb3f37c241)の手順通り、S3にホストします。
http://sample-async-invoke.s3-website-ap-northeast-1.amazonaws.com/
このままGetやPost実行するとエラーとなります。
![image41.png](https://qiita-image-store.s3.amazonaws.com/0/191079/31e5740e-4da8-c57c-2cb3-4c35f7556a73.png)

##CORS設定
AWSのAPI Gatewayでのクロスドメイン許可設定は非常に簡単で、Actionsの「Enable CORS」でできます。
![image51.png](https://qiita-image-store.s3.amazonaws.com/0/191079/7be2b31c-8796-a428-b493-79bcd2838b74.png)
![image52.png](https://qiita-image-store.s3.amazonaws.com/0/191079/c7fece5b-c08f-8b32-e9d2-ef8ecc01c9f8.png)
イメージとして、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は使えないということです。）
![image_req_detail.png](https://qiita-image-store.s3.amazonaws.com/0/191079/b341af0a-9d34-3289-20a2-922ddaf7f600.png)
そこで自前にBody Mapping Templatesを作成する必要があります。（自作でもよいですが、実際用意されているデフォルトテンプレートを適用してもよいです）

###GETメソッドBody Mapping Templates
GETメソッドの**Integration Request**画面で、Body Mapping TemplatesのContent-Typeに**application/json**を登録し、Mapping Templateを追加します。

![image_get_body1.png](https://qiita-image-store.s3.amazonaws.com/0/191079/49a102eb-c42f-a16e-58f3-4df2dbf1b75f.png)
Templateは**Method Request passthrough**を利用（説明にあるように、このテンプレートはpath、querystring、header等すべての情報を変換してくれてLambdaにパスします。
![image_get_body2.png](https://qiita-image-store.s3.amazonaws.com/0/191079/08d8a67f-fda6-4f86-cffc-a5b892e844ee.png)

上記を設定すれば、リクエストヘッダとマッピングテンプレートのContent-Typeが一致の場合、mapping templateに従いデータ変換してくれます。

###POSTメソッドBody Mapping Templates
POSTメソッドも同じ考え方です。Content-Typeは**application/x-www-form-urlencoded**になります。
![image_post_body1.png](https://qiita-image-store.s3.amazonaws.com/0/191079/1a1f0b9a-b85f-c4a6-1751-e645b3b5a7da.png)

###GETメソッドのQueryString

```html:sample.html
  <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**に登録しておく必要があります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/191079/32b9cf8f-3184-b1ea-1ae2-264cc3c3b85d.png)

##デプロイ
すべての設定が完了後、デプロイを忘れずに実施します。

##検証
**Get**ボタンを押すと、選択しているNew YorkのTimeZoneのサーバ時刻がテキストボックスに表示されます。
Response Headersに**access-control-allow-origin: \***が追加されています。
![image_ver1.png](https://qiita-image-store.s3.amazonaws.com/0/191079/51055357-b9a4-9a14-4920-2b35813e1403.png)
次に、TimeZoneをTokyoに変更し、PostのテキストボックスにNew Yorkのサーバ時刻を入れて、**Post**ボタンを押します。時間差が問題なく表示されました。
![image_ver2.png](https://qiita-image-store.s3.amazonaws.com/0/191079/5c8b5d64-b3c3-bf1d-410c-97c4b1718619.png)

##参考
* [CORS(Cross-Origin Resource Sharing)によるクロスドメイン通信の傾向と対策](https://dev.classmethod.jp/cloud/cors-cross-origin-resource-sharing-cross-domain/)
* [Amazon API Gateway をクロスオリジンで呼び出す (CORS)](https://dev.classmethod.jp/cloud/aws/amazon-api-gateway-cors/)
* [CORS (Cross-Origin Resource Sharing) ってなに？](https://aloerina01.github.io/blog/2016-10-13-1)
* [HTTPメソッド by とほほHTTP入門](http://www.tohoho-web.com/ex/http.htm#methods)