サーバーレスでGraphQL（Python + Graphene + DynamoDB + Zappa）

この記事は[Serverless Advent Calendar 2017](https://qiita.com/advent-calendar/2017/serverless)の21日目の記事です。

AppSyncのプレビュー申請が続々と通っているようです。
私も発表直後に申請をしていて、これを書く日までに申請が通っていればAppSyncを使った何かをやってみようかと考えていたのですが、まだ通らないのでやけになってこんなことやってみました。

ぐぐってみるとNode.jsでのサーバーレスGraphQL実装事例はよく出てくるのですがPythonの事例は見つけられなかったので、やってみました。

なおGraphQLの技術については勉強中でまだあやふやなので説明はしないです。ここでは、わーい動いたー！までにとどめます。

# やりたいこと

クライアント
↓  ↑（GraphQL）
API Gateway
↓  ↑
Lambda（Python3）
↓  ↑
DynamoDB

# 環境
```
$ python --version
Python 3.6.1
$ pip --version
pip 9.0.1
```

# 使用するフレームワーク・モジュール
#### [Graphene](http://graphene-python.org/)
GraphQLのフレームワーク。

#### [PynamoDB](http://pynamodb.readthedocs.io/en/latest/)
DynamoDBをPythonからシンプルに使えるようにしてくれるやつ。

#### [graphql-pynamodb](https://github.com/yfilali/graphql-pynamodb)
flask + Graphene + PynamoDBをインテグレーションしてくれる。

#### [Zappa](https://github.com/Miserlou/Zappa)
サーバーレスのフレームワーク。Flaskで書いたアプリケーションをサーバーレスアプリケーションとしてデプロイできる。API GatewayやLambdaをデプロイしてくれる。

# 使用するサンプル
[graphql-pynamodb](https://github.com/yfilali/graphql-pynamodb/tree/master)には[Flaskアプリケーションのサンプル](https://github.com/yfilali/graphql-pynamodb/tree/master/examples/flask_pynamodb)が含まれていて、これを使うとDynamoDBのデータをGraphQLでクエリできるFlaskアプリケーションが構築できます。（少し手直しが必要だったので後で書きます。）

サーバーレスのフレームワークとしてZappaを選択した理由はそのためです。このFlaskアプリをそのままZappaを使ってデプロイすればサーバーレス化できるでしょという単純な理由です。

# 準備
```bash
$ git clone https://github.com/yfilali/graphql-pynamodb.git
$ cd graphql-pynamodb/examples/flask_pynamodb
$ virtualenv env
$ source env/bin/activate
$ pip install -r requirements.txt
```
ここまではREADMEの手順通り。
`requirements.txt`に書いてあるモジュールのバージョンが古く、エラーが出たので最新版をインストールします。

```

$ pip install graphene-pynamodb==1.0.0 graphene==2.0
```

`graphene`と`graphene-pynamodb`のバージョンを変えました。

次に`models.py`を修正します。
ここはDynamoDBのキー名やデータの型などを定義します。このサンプルではローカルのDBを見に行っているようなのでコメントアウトし、リージョンを指定します。すべてのクラス内で以下のように修正します。

```models.py
        # host = "http://localhost:8000"
        region = 'ap-northeast-1'
```

次に`database.py`を修正します。
これは`app.py`でアプリケーションを起動させたときにデータベースを初期化するためのスクリプトが書いてあるのですが、うまくDynamoDBが作成できなかったり、起動するたびにデータが追加されてしまうなど都合が悪かったので修正しました。

```database.py
from uuid import uuid4


def init_db():
    # import all modules here that might define models so that
    # they will be registered properly on the metadata.  Otherwise
    # you will have to import them first before calling init_db()
    from models import Department, Employee, Role

    if not Department.exists():
        Department.create_table(read_capacity_units=1, write_capacity_units=1, wait=True)

        engineering = Department(id=str(uuid4()), name='Engineering')
        engineering.save()

        hr = Department(id=str(uuid4()), name='Human Resources')
        hr.save()

    if not Role.exists():
        Role.create_table(read_capacity_units=1, write_capacity_units=1, wait=True)

        manager = Role(id=str(uuid4()), name='manager')
        manager.save()

        engineer = Role(id=str(uuid4()), name='engineer')
        engineer.save()

    if not Employee.exists():
        Employee.create_table(read_capacity_units=1, write_capacity_units=1, wait=True)

        peter = Employee(id=str(uuid4()), name='Peter', department=engineering, role=engineer)
        peter.save()

        roy = Employee(id=str(uuid4()), name='Roy', department=engineering, role=engineer)
        roy.save()

        tracy = Employee(id=str(uuid4()), name='Tracy', department=hr, role=manager)
        tracy.save()

```
これでアプリケーション起動時にDynamoDBが存在しなければ作成し、サンプルデータが入るようになりました。
`schema.py`と`app.py`はそのままでOKです。

# ローカルで実行
まずはローカルで動くかテストしてみます。

```
$ python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 287-543-857
```

初回起動時にDynamoDBテーブルを作る処理が入るので起動まで少し時間がかかります。

起動したら[http://127.0.0.1:5000/graphql](http://127.0.0.1:5000/graphql)にアクセスしてみます。そうするとGraphiQLというGUIでクエリ操作できるコンソールにアクセスできます。

そこでこのようにクエリをリクエストしてみます。

```
{
  allEmployees {
    edges {
      node {
        id,
        name,
        department {
          id,
          name
        },
        role {
          id,
          name
        }
      }
    }
  }
}
```

<img width="1072" alt="スクリーンショット 2017-12-21 16.57.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/99248/f524017e-ff2a-b151-0ab7-79ba2417bcb0.png">

DynamoDB内のすべてのデータが取得できました。試しに`id`とか`name`とかを消してみるとクエリ文に入っているものだけが取得できると思います。

# サーバーレス化
今はローカルでFlaskサーバが動いているので、これをZappaでデプロイしてサーバーレス化してみます。

```
$ pip install zappa
```

```
$ zappa init
```
色々聞かれますがcredentials profileにdefaultを使うのであれば全部そのままエンターで問題ないかと思います。

### デプロイ
`zappa_settings.json`ができていることを確認したらデプロイします。

```
$ zappa deploy

・・・

Deployment complete!: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
```
出力されたエンドポイント+`/graphql`がGraphQLエンドポイントとなります。


### 確認
確認するためにChromeのエクステンションなどからクエリしてみます。
私は[chromeiql](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)を使用しています。Chromeにダウンロードして開くと、「Set endpoint」のテキストボックスが表示されるのでそこにGraphQLのエンドポイントを入力します。
エンドポイントはこんな感じ。
`https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/graphql`

Set endpointすると先ほどと同様、GraphiQLのコンソール画面が表示されます。

<img width="976" alt="スクリーンショット 2017-12-21 17.58.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/99248/ba3c4956-6c25-70a8-d0b5-809dd56f46da.png">

左側に先ほどローカルで確認したときのクエリを入れて実行してみると、同様の結果が返ってくるかと思います。

### 注意点
ローカルでテストしたときはFlask起動時の処理としてDynamoDBテーブルがない場合は作成をしていましたが、サーバーレス化したあとはその処理はしてくれないので、テーブルがない場合は自分で作る必要があります。今回はローカルで作ったやつをサーバーレスアプリからも使いまわしているので問題ないかと思います。

リソース削除は`$ zappa undeploy`。

# まとめ
Flaskで動いていたものを手直しせずにデプロイしてサーバーレス化できちゃうZappaすごい。

AppSync〜〜〜〜〜！！！