AWS Lambda上でFalcorを動かす

## はじめに

最近はどちらかというとGraphQLやRelayで遊んでいる時間が長かったので、[Falcor](http://netflix.github.io/falcor/)ネタは久しぶりです。

GraphQL、Falcorの双方とも、Clinet - Server間の通信を効率化するための技術です。したがって、僕のようなフロントの人間が「ちょっとGraphQL/Falcor触ってみたい」と思うと、いちいちGraphQL/FalcorのEndpointとなるServerを用意する必要が出てくる訳です。

GraphQLの場合, [Reindex](https://www.reindex.io/)や[Graph.cool](https://graph.cool/)といったBaaSも登場してきていますし、Serverを用意する敷居が大分下がってきていると思っています。一方、Falcorに目を向けると事情は大分違っていて、[falcor-express](https://github.com/Netflix/falcor-express) をNode.jsのExpress上に載せてエンドポイントサーバを作る以外の手段が無いように見えます。

GraphQLを触りながらも「もう少しFalcorのEndpoint Serverを用意する方法が色々あっても良いのになぁ」というのが頭をよぎり、今回のエントリにいたりました。さすがに自分でBaaSを作るのは難しいですが、「Serverを管理せずにFalcorのEndpointを公開する」を主軸に考えてみました。

今日のテーマは AWS API Gateway + Lambda + Falcor です。

## 作るもののイメージ

ポンチ絵にすると下記のようになります。

![Archetecture.png](https://qiita-image-store.s3.amazonaws.com/0/8808/bc68e385-b221-1021-027a-236a29b39197.png)

AWS API GatewayでHTTP Requestを受け付け、Lambda上に用意したFalcor Routerをキックする構成です。

通常、FalcorのサーバはとRESTful Webサービスと異なり、単一のエンドポイントしか持ちません(図中の`/model.json`)。Falcor RouterがRequestの内容に応じて、返却すべきJSON Graphのレスポンスを決定します。このあたりの話は以前に、[Falcor入門 3日目 Falcor Routerでサーバサイドを実装してみる](http://qiita.com/Quramy/items/def69985cfb05250fd19)に書いたので詳細はこちらを参照してください。


## 出来上がったブツ

上述の構成を実現するために、今回は[Serverless Framework](http://serverless.com/)を選択しました。ServerlessはLambda, API Gatewayの煩わしい設定を纏めて管理出来るツールです。また、自分で作成したServerlessのprojectをNPMにpublishするだけで、projectの雛形が用意できるというのも良い点です。

というわけで、Falcorのstarter kitを作ってみました。

[serverless-falcor-starter](https://github.com/Quramy/serverless-falcor-starter)

事前にServerless CLIが動作する環境を用意してください。といっても、Node.jsとaws CLIが既にinstallされている人であれば下記のコマンドを実行するだけです。

```sh
npm i -g serverless
```

Serverless導入方法の詳細については、 [AWS Lambdaを活用したServerless Frameworkを触ってみる](http://qiita.com/susieyy/items/1c2af0ef7b88b742c37a)のエントリが分かりやすかったです。

さて、Serverless CLIが無事installできたら、次のコマンドで、serverless-falcor-starterの雛形からServerless Projectを作成することができます。

```sh
sls project install serverless-falcor-starter
```

profileやstageについて質問が飛んでくるので適宜入力してください。

続いてLambda 関数, API Gateway Endpointをそれぞれdeployします。

```sh
cd serverless-falcor-starter
sls function deploy
sls endpoint deploy --all
```

無事Endpointのdeployが成功すると、下記のような出力がコンソール上で得られているかと思います:

```text
Serverless: Successfully deployed endpoints in "dev" to the following regions:
Serverless: us-east-1 ------------------------
Serverless:   GET - model - https://xxx.yyy.zzz.amazonaws.com/dev/model
Serverless:   POST - model - https://xxx.yyy.zzz.amazonaws.com/dev/model
Serverless:   OPTIONS - model - https://xxx.yyy.zzz.amazonaws.com/dev/model
```

この段階で、`https://xxx.yyy.zzzz.amazonaws.com/dev/model` のURLにFalcorのEndpointがdeployされています。`model.getValue("greeting")` に相当するGETリクエストを叩ける猛者がいれば、cUrlやPostmanで試してみるのもありでしょう。

今回作成したstarter kitには、Client側のsampleも同梱しています。`client-sample/config.json` をエディタで開き、`"endpoint"` キーの値を、AWS API Gateway Endpoint URL(e.g. `https://xxx.yyy.zzz.amazonaws.com/dev/model`) に書き換えます。

```json:client-sample/config.json
{
  "endpoint": "Replace the endpoint URL of your AWS API Gateway"
}
```

次いで、下記のコマンドを実行してブラウザから`http://localhost:8080`を開いてください。

```sh
cd client-sample/
npm i && npm start
```

下図の画面が立ち上がり、このstarter kitで作成したFalcor Endpointとの通信を確認できます。

![Serverless_Falcor_Starter_Sample_Client.png](https://qiita-image-store.s3.amazonaws.com/0/8808/121876bd-36a5-5ea6-c453-43499d17df76.png)


## 中身の説明

すこし、serverless-falcor-starterの中身の説明を書いておきます。

### LambdaとFalcor DataSource, Router

下記がLambdaに登録する関数です。先述の通り、Falcor ServerがSingle Endpointな構成となることから、LambdaのFunctionはこのhandler唯1つとなります。

```js:falcor/model/handler.js
import dataSourceHanlder from "falcor-lambda";
import MyRouter from "./router";

module.exports.handler = dataSourceHanlder(() => new MyRouter(), {debug: true});
```

`dataSourceHanlder` はFalcorのDataSourceをLambdaのhandlerとして登録するためのhelper関数です。「FalcorのDataSourceからWeb Server用のcontrollerを作成する」という意味において、 falcor-express と同等の役割を担うため、こいつを参考にして [falcor-lambda](https://github.com/Quramy/falcor-lambda) を作成しました。

DataSourceになるのは下記のFalor Routerを継承して作成したClassです。starter kit用のsampleなので軽めにget/setだけ用意しました。

```js:falcor/model/router.js
import Router from "falcor-router";

export default class MyRouter extends Router.createClass([
  {
    route: "greeting",
    get: function(pathset) {
      return {
        path: ["greeting"],
        value: "Hello, world",
      };
    },
    set: function(jsonGraph) {
      return {
        path: ["greeting"],
        value: jsonGraph.greeting,
      };
    }
  },
]) {

}
```

### API GatewayのEndpoint

HTTP Requestを受け付けてLambda Functionを呼びだすのはAPI Gatewayの役割です。今回のようにServerless Framwworkを使った場合、`s-function.json` にその設定が記述されます。

Client SideからFalcor Http Data Sourceを使ってリクエストを飛ばした場合、GET/POSTの両方が飛んでくるため、双方の設定を用意しています。Requestに含まれるパラメータ(`paths`等)はrequestTemplatesを定義してLambdaのeventに変換しています。

```json:falcor/model/s-function.json(一部抜粋)
  "endpoints": [
    {
      "path": "model",
      "method": "GET",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {},
      "requestTemplates": {
        "application/json": {
          "method": "$input.params().querystring.method",
          "paths": "$util.escapeJavaScript($input.params().querystring.paths)"
        }
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {
            "application/json;charset=UTF-8": "Empty"
          },
          "responseTemplates": {
            "application/json;charset=UTF-8": ""
          }
        }
      }
    },
    {
      "path": "model",
      "method": "POST",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {},
      "requestTemplates": {
        "application/json": {
          "method": "$input.json('$.method')",
          "jsonGraph": "$input.json('$.jsonGraph')",
          "callPath": "$input.json('$.callPath')",
          "arguments": "$input.json('$.arguments')",
          "pathSuffixes": "$input.json('$.pathSuffixes')",
          "paths": "$input.json('$.paths')"
        }
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {
            "application/json;charset=UTF-8": "Empty"
          },
          "responseTemplates": {
            "application/json;charset=UTF-8": ""
          }
        }
      }
    }
  ],
```

ところで、API GatewayでPOSTのEndpointを公開する場合、Content-Typeは`application/json`一択なのですが、falcor-http-datasourceは`x-www-form-urlencoded` のPOSTしか対応していませんでした。最初、API Gateway 側で`x-www-form-urlencoded` からパラメータを取り出すtemplateを考えたのですが、[HTMLフォームからAPIGatewayを使ってAWS LambdaにPOSTする](http://qiita.com/satetsu888/items/40fc387735192b794da8) 等を見るとかなり面倒な印象を受けたため挫折。 falcor-http-datasource に [Content-Typeにapplication/json を指定できるようにするPR](https://github.com/Netflix/falcor-http-datasource/pull/34) を送って対応しました。

結果、Client SideでDataSource作成時に下記のように書くだけで、API Gatewayに接続できるようになっています。


```js:client-sample/src/index.js
import { Model } from "falcor";
import HttpDataSource from "falcor-http-datasource";

const url = require("../config.json").endpoint;

const source = new HttpDataSource(url, {

  /* The following options are required for CORS request */
  crossDomain: true, withCredentials: false,

  /* An AWS API Gateway endpoint allows only application/json Content-Type */
  headers: {
    "Content-Type": "application/json",
  },
});

const model = new Model({
  source,
});
```

## おわりに

今回のエントリでは以下を解説してきましたが、如何でしたでしょうか。

* AWS API Gateway + Lambda + Falcor Server 構成の概要
* Serverless Framework, [serverless-falcor-starter](https://github.com/Quramy/serverless-falcor-starter) を利用したProjectのセットアップ方法
* API Gateway, Lambda上でFalcorを扱う場合の諸注意

今回作成したProjectでは、Falcor Routerの先にどのようなデータストアを配置するかは考えていませんが、AWSですので、DynamoDBを使う、なんてのが考えられます。

実際、GraphQL + Serverlessのprojectとして[serverless-graphql-blog](https://github.com/serverless/serverless-graphql-blog) というのがあるのですが、GraphQLのSchema Resolver(FalcorのRouter相当)からDynamoDBを呼びだしてデータの永続化をしていました。

興味がある人は、是非Lambda + Falcor Router + DynamoDB の構成にチャレンジしてみては如何でしょう。

それでは、また。