SERVERLESSフレームワークを使ってHTTPプロキシを作る【後編】

# はじめに
皆さんご存知、[SERVERLESSフレームワーク](https://serverless.com/)。僕は最近どっぷりこのフレームワークに浸かっております。進化が早くてついていくのが大変です。

SERVERLESSってAPI Gateway作るよね？ってことはHTTPプロキシとしても作れるんじゃね？ってなって試してみました。

- [前編：パスやクエリストリングが決まっている場合のHTTPプロキシの作り方](http://qiita.com/seiya_orz/items/5fbfd81a7a6ec92945fa)
- 後編：流行りの{proxy+}（オールスルー）のHTTPプロキシの作り方

こんな感じの構成で書いています。この記事は後編になります。前編で作ったServiceを書き換えて作っていきたいと思います。

実施環境は以下のとおりです。実施される方は、必要なものをインストールしてください。

- AWSアカウント
- aws-cli v1.10.47
- serverless v1.1.0

また、基本的な使い方は下記記事を参考にしてください。
[SERVERLESSフレームワークv1.0正式リリース版を触ってみた](http://qiita.com/seiya_orz/items/4bd7cc1010fafdbb8fd0)

# オールスルーHTTPプロキシを作る
## この記事のゴール
今回のプロキシ先も、図書館の検索ができるAPI [カーリル](https://calil.jp/doc/api_ref.html)を使わせていただきます。このAPIは、/libraryで図書館検索、/checkでその図書館に指定した本が存在しているかがわかるようになっています。これを、今回作成するAPIGWからパスやクエリストリングをそのままに、カーリルへ投げてくれるようにします。

完成イメージはこんな感じ。今回はCORSの設定までしましょう（本当にCORSが効いているかは未検証です）。
![スクリーンショット 2016-11-08 16.28.49.png](https://qiita-image-store.s3.amazonaws.com/0/74853/a8e0b25d-77da-56e9-db21-c7d5d6bec39a.png)

具体的には、

- https://~~~~.amazonaws.com/library?appkey=hogehoge&pref=東京 のGETアクセスが来た場合
  - https://api.calil.jp/library?appkey=hogehoge&pref=東京 をAPIGWが投げる
- https://~~~~.amazonaws.com/check?appkey=hogehoge&isbn=4834000826&systemid=Aomori_Pref&format=json のGETアクセスが来た場合
  - https://api.calil.jp/check?appkey=hogehoge&isbn=4834000826&systemid=Aomori_Pref&format=json をAPIGWが投げる

という感じです。これを、1つ1つパスやクエリストリングを設定することなく実現します。柔軟に対応してくれるHTTPプロキシって感じですね。

## HTTPプロキシの構築
さて、今回はさっそくメイン部分を触っていきます。完成したserverless.ymlはこんな感じになります（CORSはあとで）。

```yaml:serverless.yml
frameworkVersion: "=1.1.0"
service: httpproxyonly
provider:
    name: aws
    runtime: nodejs4.3
functions:
  hello:
    handler: handler.hello
    events:
        - http:
            path: trash
            method: get

# resourcesはCloudFormationをYAMLで書く感じ。
# ここにAPI GatewayをHTTPプロキシとして作る設定を書きます。
resources:
    Resources:
        CatchAllTestResource: # {proxy+}のリソースID（Methodから参照される）
            Type: AWS::ApiGateway::Resource
            Properties:
                ParentId:
                    Fn::GetAtt:
                        - ApiGatewayRestApi # SERVERLESSフレームワークで作成されるAPI GatewayのID（だと思う）
                        - RootResourceId
                PathPart: "{proxy+}" # プロキシのエンドポイント
                RestApiId:
                    Ref: ApiGatewayRestApi
        CatchAllTestMethod: # {proxy+}のメソッドID
            Type: AWS::ApiGateway::Method
            Properties:
                AuthorizationType: NONE
                ResourceId:
                    Ref: CatchAllTestResource
                RestApiId:
                    Ref: ApiGatewayRestApi
                HttpMethod: ANY # このプロキシに入ってくるときのメソッド
                RequestParameters: # 必須かどうかの判断
                    method.request.path.proxy: false
                MethodResponses: # プロキシの呼び出し元へ返すHTTPステータスコードのうち、あり得るもの
                    - StatusCode: 200
                    - StatusCode: 400
                    - StatusCode: 403
                    - StatusCode: 500
                Integration:
                    IntegrationHttpMethod: ANY # プロキシ先に投げるときのメソッド
                    Type: HTTP_PROXY # HTTP or HTTP_PROXY
                    Uri: https://api.calil.jp/{proxy} # プロキシ先のURL（パラメータ不要）
                    RequestParameters:
                        integration.request.path.proxy: method.request.path.proxy
                    IntegrationResponses: # プロキシ先からのレスポンスHTTPステータスのマッピング
                        - StatusCode: 200
                        -
                            StatusCode: 400
                            SelectionPattern: "400" # レスポンスのHTTPステータスの正規表現
                        -
                            StatusCode: 403
                            SelectionPattern: "403"
                        -
                            StatusCode: 500
                            SelectionPattern: "500"
```

今回、重要なのは以下の点だと思います。

- リソース(CatchAllTestResource)のPathPart
- メソッド(CatchAllTestMethod)のRequestParameters(Integration内のも含む)
- IntegrationのType
- IntegrationのUri

順番に見ていきましょう。

### PathPart
この部分は、特に難しくありません。APIGWのコンソール上で{proxy+}にする場合は2016/11/09現在ではチェックボックスをONにすれば勝手にリソースに{proxy+}となります。対して、CloudFormationではPathPartを{proxy+}と設定すれば勝手にCloudFormationが解釈してくれるようです。便利ですね。

### RequestParameters
この部分は少しハマりました。

これをやり始めたときは、{proxy+}にしていればパスはいい感じにAPIGWがやってくれると思っていて、RequestParametersには何も設定しませんでした。そうしていると、例えば、`http://~~~.amazonawz.com/dev/library?appkey=hogehoge&pref=沖縄`にGETアクセスすると、APIGWは`https://api.calil.jp/{proxy}?appkey=hogehoge&pref=沖縄`というURLにGETリクエストしていました。これは、{proxy}という文字列がURLに入ってしまっていて、libraryが{proxy}にマッピングされていないということになります。

よって、上記(serverless.yml)のようにpathのproxyという変数をマッピングする設定を記述必要があります。

### IntegrationのType
他のWebサーバへのHTTPプロキシを構築する場合、ここの設定はHTTPかHTTP_PROXYの2択になります（と思う）。

前編でも書きましたが、HTTPの場合はクエリストリングのマッピングを明示的に記述する必要があります。言い換えると、予め書いておいたクエリストリングのみマッピングしてくれるため、開発者の意図通りの動きをさせやすいです。

対してHTTP_PROXYの場合は、クエリストリングはすべてそのままのパラメータ名のまま、Webサーバへ渡してくれます。これは非常に便利ですが、例えばプロキシ先のAPIのパラメータ名を秘匿したい場合（こんなことあるのかな？）にはこれは使えないことになりそうです。

### IntegrationのUri
ここには、プロキシ先のURLとして必ず**`{proxy}(proxy+ではない)`**を含めたURLにしなければなりません。APIGWのパスの{proxy+}に当たる部分が{proxy}へとマッピングされるので、それを考慮した設定をしてください。


長くなりましたが、上記serverless.ymlのように書けたら、デプロイしましょう。上に載せた画像のような設定になっているはずです（OPTIONSメソッドはまだ作られていないはず）。


## APIを叩いてみる
- [https://[人による].execute-api.us-east-1.amazonaws.com/dev/library?appkey=[人による]&pref=沖縄](https://[人による].execute-api.us-east-1.amazonaws.com/dev/library?appkey=[人による]&pref=沖縄)
- [https://[人による].execute-api.us-east-1.amazonaws.com/dev/check?appkey=[人による]&isbn=4834000826&systemid=Aomori_Pref&format=json](https://[人による].execute-api.us-east-1.amazonaws.com/dev/library?appkey=[人による]&isbn=4834000826&systemid=Aomori_Pref&format=json)

この2つにブラウザアクセスしてみてください。1つ目はXML形式の図書館一覧、2つ目はJSON形式の書籍貸出情報らしきものが表示されていれば正しく構築できていると思います。

## CORSの設定（未検証）
さて、APIをAjaxを使って叩けるようにするためにはCORSの設定が必要になります。APIGWのコンソールからCORSの設定をするのは非常に簡単で2〜3クリックでできてしまいます。しかし、しかし、しかーし、CloudFormationで構築したAPIにCORSを設定するには面倒な設定をリソースごとに書く必要があります。（Allow-Originの設定を細かくするのが正しいので、仕方ないんですけどね…。SERVERLESSフレームワークでLambdaとつなぐ際のCORSのAllow-Originを*にする場合は、trueと書くだけでいいので余計に面倒に感じてしまいます。）

CORSの設定は下記のように行います。

```yaml:serverless.yml
frameworkVersion: "=1.1.0"
service: httpproxytest
provider:
    name: aws
    runtime: nodejs4.3
functions:
    # 省略

resources:
    Resources:
        CatchAllTestResource: # {proxy+}のリソースID（Methodから参照される）
            # 省略
        CatchAllTestMethod: # {proxy+}のメソッドID
            # 省略
        OptionsMethod:
            Type: AWS::ApiGateway::Method
            Properties:
                AuthorizationType: NONE
                RestApiId:
                    Ref: ApiGatewayRestApi
                ResourceId:
                    Ref: CatchAllTestResource # ここにCORSを適用したいリソースID
                HttpMethod: OPTIONS # CORSの設定はOPTIONSメソッド
                Integration:
                    IntegrationResponses:
                        -
                            StatusCode: 200
                            ResponseParameters: # MethodsやOriginは適宜変更が必要かと
                                method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                                method.response.header.Access-Control-Allow-Methods: "'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT'"
                                method.response.header.Access-Control-Allow-Origin: "'*'"
                            ResponseTemplates:
                                application/json: ''
                    PassthroughBehavior: WHEN_NO_MATCH
                    RequestTemplates:
                        application/json: '{"statusCode": 200}'
                    Type: MOCK
                MethodResponses:
                    -
                        StatusCode: 200
                        ResponseModels:
                            application/json: 'Empty'
                        ResponseParameters:
                            method.response.header.Access-Control-Allow-Headers: false
                            method.response.header.Access-Control-Allow-Methods: false
                            method.response.header.Access-Control-Allow-Origin: false
```

簡単に使いたい方はコピペして使ってみてください。大変申し訳無いですが、Ajaxから叩けるようになっているのかは未検証です、ごめんなさい。どなたか報告をいただけると助かります。

## 番外編：Resourceを外部ファイルに書く
serverless.ymlがやたらと肥大化してしまう現実があります。嬉しいことにSERVERLESSフレームワークでは、設定内容を外部ファイルに書くことができます。書き方は以下のような形です。

```yaml:serverless.yml
frameworkVersion: "=1.1.0"
service: httpproxytest
provider:
    name: aws
    runtime: nodejs4.3
functions:
    # 省略

resources: ${file(./resource.yml)}
```

```yaml:resource.yml
Resources:
    CatchAllTestResource:
        # 省略
    CatchAllTestMethod:
        # 省略
    OptionsMethod:
        # 省略
```

こんな感じで、別ファイルにしてしまえば非常に見やすくなりますね。こうしておくと、resource.ymlファイルをJSON形式に変更してCloudFormationファイルにすることも簡単にできますし、逆にすでにあるCloudFormationファイルをSERVERLESSフレームワークで使いやすくなりますね。

# まとめ

- [前編：パスやクエリストリングが決まっている場合のHTTPプロキシの作り方](http://qiita.com/seiya_orz/items/5fbfd81a7a6ec92945fa)
- 後編：流行りの{proxy+}（オールスルー）のHTTPプロキシの作り方

として書いてきました。AWSやSERVERLESSフレームワークの進化は非常に早くてついていくのが大変ですが、開発者としては非常にワクワクします。非常に便利に早く、そして何よりバックエンド側の構築が簡単にできるおかげで、フロントエンドの開発に注力できることがなによりありがたいです。

この記事が何かの役に立てば幸いです。

おわり