Python
AWS
DynamoDB
GraphQL

サーバーレスでGraphQL(Python + Graphene + DynamoDB + Zappa)

この記事はServerless Advent Calendar 2017の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

GraphQLのフレームワーク。

PynamoDB

DynamoDBをPythonからシンプルに使えるようにしてくれるやつ。

graphql-pynamodb

flask + Graphene + PynamoDBをインテグレーションしてくれる。

Zappa

サーバーレスのフレームワーク。Flaskで書いたアプリケーションをサーバーレスアプリケーションとしてデプロイできる。API GatewayやLambdaをデプロイしてくれる。

使用するサンプル

graphql-pynamodbにはFlaskアプリケーションのサンプルが含まれていて、これを使うとDynamoDBのデータをGraphQLでクエリできるFlaskアプリケーションが構築できます。(少し手直しが必要だったので後で書きます。)

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

準備

$ 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

graphenegraphene-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.pyapp.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にアクセスしてみます。そうするとGraphiQLというGUIでクエリ操作できるコンソールにアクセスできます。

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

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

スクリーンショット 2017-12-21 16.57.59.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を使用しています。Chromeにダウンロードして開くと、「Set endpoint」のテキストボックスが表示されるのでそこにGraphQLのエンドポイントを入力します。
エンドポイントはこんな感じ。
https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/graphql

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

スクリーンショット 2017-12-21 17.58.56.png

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

注意点

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

リソース削除は$ zappa undeploy

まとめ

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

AppSync〜〜〜〜〜!!!