GAE/pyでCloud Endpoints Frameworksを使う

Posted by rhoboro on 2017-05-15

自社プロダクトのバックエンドの開発でCloud Endpoints Frameworksを使い始めました。 3月に出たばかりのものでまだまだ日本語情報が少ないため、備忘録を兼ねて思ったことをまとめておきます。

Cloud Endpoints Frameworksとは

3月に正式リリースされたCloud EndpointsというサービスをApp Engine standard environmentで使うためのフレームワークです。 webapp2の代わりの軽量Web Frameworkとして使いますが、HTTPではなくProtocol Buffersのメッセージとしてリクエスト/レスポンスを扱います。
Cloud Endpoints自体は、GCP上の(Google Maps APIのような)各種サービスのAPIと同じようなAPIキー管理やユーザー認証、モニタリングの仕組みを自分で作成したAPIに簡単に導入できるサービスです。

今のところ、日本語の記事は公式ドキュメントくらいしかないようです。 (しかと書きましたが、ほぼ全訳されているので公式ドキュメントだけでも十分と言えば十分です)

ここでは、ほぼろが実際に使ってみた経験から、下記についてまとめています。

  • defaultモジュール以外で利用する手順
  • api_collectionを利用して複数パッケージを使う際の構成
  • Cloud Endpoints Frameworkで出力するOpen API仕様をSwagger UIで読み込ませてみた結果
  • 注意すべき点
    • HTTPステータスコードについて
    • api_key_required=True を指定したAPIにAPIキーなしでリクエストを行うと、500エラーが返ってくる。
    • レスポンスでmessage.IntegerField()はデフォルトではstringになる

また、サンプルのソースコードはGitHubに置いています。

defaultモジュール以外で利用する手順

公式ドキュメントの必要なAPI管理の追加の補足的な内容です。
公式ドキュメントではdefaultモジュールでの利用方法にしか言及されていませんが、下記のようにすることでdefaultモジュール以外でも利用することが可能でした。

  • 手順2. のOpen API ドキュメントの出力コマンド
    • 下記の--hostnameを modulename-dot-projectid.appspot.com にします
python lib/endpoints/endpointscfg.py get_openapi_spec main.EchoApi --hostname your-service.appspot.com
  • 手順5. のapp.yamlの内容
    • ENDPOINTS_SERVICE_NAME: [YOUR-PROJECT-ID].appspot.com
    • 上記の値を手順2. の--hostnameで指定した値と同じ値にします

api_collectionを利用して複数パッケージを使う際の構成

公式ドキュメントの複数のクラスを使用する API の作成サンプルコードでは、 下記のようにすることで複数クラスを利用するAPIを作成しています。

import endpoints

api_collection = endpoints.api(name='library', version='v1.0')

@api_collection.api_class(resource_name='shelves')
class Shelves(remote.Service):
    // API定義省略

@api_collection.api_class(resource_name='books', path='books')
class Books(remote.Service):
    // API定義省略

api = endpoints.api_server([api_collection])

api_collectionを定義して、それを利用してクラスをデコレートしています。
リソースが少ない場合は上記のまま1ファイルでいいと思いますが、各リソースをパッケージとして切り出す場合は下記のような点が懸念になると思います。

  • 循環参照を避けたい
    • main.pyで定義したapi_collectionをリソース側のパッケージでimportして使うと、mainファイルでリソース側のパッケージをimportした際に循環参照になる
  • import文をコードの途中に書きたくない
    • api_collectionを使うために、変数宣言後にリソース側のパッケージをimportする

ということで、最終的には下記のようにしました。

  • apicollectionパッケージを作成し、api_collection変数を定義する
    • api_collectionをシングルトンの変数として扱う
    • api_collectionを利用する際はすべてこのパッケージをimportして使う
  • mainファイルでリソースパッケージを読み込む
    • クラス定義が欲しいだけでmain内で直接パッケージを利用するわけではない
    • flake8の警告は#noqaコメントで消す
  • コードはこちら

Python的な書き方でもっといい方法があれば教えてください。

ちなみに、複数クラスを使用するAPIの場合のOpen APIドキュメントの生成コマンドですが、下記のようになります(上記ファイルがmain.pyの場合)

python lib/endpoints/endpointscfg.py get_openapi_spec main.api_collection --hostname your-service.appspot.com

Cloud Endpoints Frameworkで出力するOpen API仕様をSwagger UIで読み込ませてみた結果

先ほどから何度かOpen APIという言葉を使用していますが、Open APIとはREST API を定義するための業界標準らしいです。 Cloud Endpoints Frameworkでは、下記のような流れでこのOpen APIを利用しています。

  1. REST APIのリソース、APIに対応するclassとmethodにデコレータで目印をつける
  2. endpointscfg.pyを利用して上記コードからOpen APIドキュメントとしてjsonファイルを生成する
  3. このjsonファイルを元にCloud EndpointsのAPIが管理される

ここで、このOpen APIですが、もともとはSwagger 仕様として定義されていたものです。 Swaggerはオンラインエディタなどで気軽にモックサーバーを作ることができるので、これは!と思い試しにOnline Editorに読み込ませてみました。
結果はこちら。微妙にエラーが出ちゃいました。。。w

swagger-editor

また、この話を知人にしたところ「Swagger UIのほうがゆるいからそっちだったら読み込めるんじゃない?」というアドバイスをもらったのでSwagger UIでも試したところ、こちらも読み込めましたがほぼほぼ同じ結果でした。

swagger-ui

エラーの内容はこちらでした。

{"schemaValidationMessages":[{"level":"error","domain":"validation","keyword":"oneOf","message":"instance failed to match exactly one schema (matched 0 out of 2)","schema":{"loadingURI":"http://swagger.io/v2/schema.json#","pointer":"/definitions/parametersList/items"},"instance":{"pointer":"/paths/~1sample~1v1~1resources~1{entity_id}/put/parameters/0"}}]}

とはいえ、エラーは軽微なので、ここからエラー部分を除去してサンプルレスポンスを追加すれば簡単にモックサーバーは作れそうです!

注意すべき点

その他、実際に使ってみて注意した方がいいなと思った点がこちらの2点。

  • 正常時のHTTPステータスコードについて

    • Cloud Endpointsの仕様として、レスポンスボディがあれば200、なければ204になる
    • 上記以外(例えば201 Createdは返せない) は返せない
    • その他も使えるステータスコードは限られています
  • api_key_required=True を指定したAPIにAPIキーなしでリクエストを行うと、500エラーが返ってくる。

    • これはおそらくフレームワークのバグ
    • APIキーのチェックで401を返そうとした際、InvalidResponseError: status must be a str, got 'unicode'というエラーが発生し、結果として500になっている模様
    • 一応Issueを上げておきました
      • そして初めて(無関係OSSプロジェクトへの)PRを出してみたw
  • レスポンスでmessage.IntegerField()がstringになる

    • 出力されたOpen APIドキュメントをみると"format": "int64", "type": "string"ってなってます
    • message.IntegerField()の引数にvariant=messages.Variant.INT32を指定すれば、"type": "integer"になり、jsonも数値になってくれます

まとめ

なかなか面白いサービスですが、ちょいちょいアラが目立つという印象です。
まだ出てきたばかりのサービスなのでこれからに期待というところでしょうか。
ただ、Cloud Endpoints Frameworkの中でprotorpcパッケージを利用して、Protocol Buffersを触れているのもいい経験になっています。 自分でAPIキーの管理をしたくない人(今回の動機はこちらでした)やProtocol Buffersに気軽に触れてみたい方にはおすすめできるサービスですね。