GAE/Python の 2.7(gen1) から 3.7(gen2) への移行作業メモ

趣味で作った GAE/Python Python 2.7 のサービスを Python 3.7 に移行したときにやったことのまとめです。

サービスの概要

  • cronで定期的にクローリングしてDatastoreに格納
    • cronから呼び出されるエンドポイントは /_ah/login: adminの場所に設置
  • DatastoreからRSSを動的生成

cron.yaml → Cloud Scheduler

  • App Engine HTTPで/_ah/のエンドポイントを指定することですぐに移行できた。
  • gcloud コマンドでは、べき等にできないため、デプロイコマンドが作りづらい。

Cloud ndb 移行

ドキュメント Migrating to Cloud NDB

  • そもそも db ライブラリだったので、ndb に移行するところからスタート
  • requirements.txt に googleapis_common_protos と google-cloud-ndb を追加
  • from google.appengine.ext import dbfrom google.cloud import ndb に置き換え
  • ndb.Client() を作って、with client.context() のブロックに入れる必要あり。webapp2 では BaseHandler の dispatch で対応する。
client = ndb.Client()


class BaseHandler(webapp2.RequestHandler):
    def dispatch(self):
        with client.context():
            super(BaseHandler, self).dispatch()

urlfetch を urllib2 へ移行

  • urllib2 が使えるようになっているのでそちらに移行する
  • 以下のようなラッパー関数を作った
def fetch(url, headers=None):
    if headers is not None:
        req = urllib2.Request(url, headers=headers)
        result = urllib2.urlopen(req)
        return result.read()
    else:
        result = urllib2.urlopen(url)
        return result.read()

今回のアプリはこの時点でApp Engine Serviceの参照が切れた


Python 3.7 ランタイムへ移行

Python レベルでの変更

  • urllib2 を標準 urllib ライブラリへ移行
    • import urllib2import urllib.error import urllib.request に分ける必要がある
    • urllib2.Requesturllib.request.Request
    • urllib2.urlopenurllib.request.urlopen
    • urllib2.URLErrorurllib.error.URLError
    • urlopen の引数に context = ssl._create_unverified_context() を与えないと SSL 証明書のルートがうまくいってないと通らない
  • 正規表現ur'...'r'...' に変更

App Engine の変更

  • requirements.txt に以下の変更
    • webapp2==3.0.0b1 に変更
    • gunicorn 追加
    • googleapis_common_protos 削除
  • app.yaml に以下の変更
    • rumtime: python37 に変更
    • entrypoint: gunicorn -b :$PORT main:app 追加
    • handlers の script: は auto に変更
    • login: admin は一旦消すしかなさそう
  • appengine_config.py 削除

構造変更

  • 以下の形にパッケージ構成を変更 (外部APIコール、RSS生成も別パッケージに分割している)
    • model: ndb 含めてモデルクラス
    • datastore: Cloud Datastore 関連処理
    • web: App Engine 関連処理 (__init__.py に app を記載)
      • handlers: Request Handler ごとにファイル分けて分割。admin 用のものは削除した。
  • app.yaml の entrypoint は gunicorn -b :$PORT web:app に変更

Cloud Functions に移行

  • admin にあったのは cron のエントリポイントだけだったので、Functionsへの移行が楽ではないかと考えて移設
  • functions パッケージに関数ごとにファイルを作って、実装はそちらに記載
  • main.py に Cloud Functions から呼び出される関数を記載。with client.context(): でラッパーして functions パッケージの実装を呼ぶ
    • def store_update(event, context) の2引数の関数を記載
  • Cloud PubSub にトピックを作成
  • gcloud functions deploy [Cloud Functions名] --project [プロジェクト名] --trigger-topic [main.py内の関数名] --runtime python37 でデプロイ
  • Cloud Scheduler をApp Engine HTTPからPubSubに向ける

この時点で準備完了したので、App Engineをメインドメインへデプロイした。


その他

  • GAEのコードとCloud Functionsのコードはこの方法を使うことで共有可能
  • 画像等、Cloud Functionsにデプロイしなくて良いファイルが多い場合は gcloud コマンドの引数に --ignore-file.gcloudignore 以外のファイル名の ignore ファイルを指定可能
    • 逆に App Engine 側は skip_files の指定ができなくなっているので、直下の .gcloudignore は App Engine 用にして、Cloud Functions 用は --ignore-file にしたほうが良いかと。
  • 移行完了までGAEのデプロイは --no-promote にして、アクセスには影響がでないようにした。
  • cronのPubSubは1本にして、 Schedulerからどの関数を呼ぶかペイロードに指定して、main.py でペイロード見て振り分けでも良さそう。
  • wepapp2はメンテ止まってそうなので、本当はそれも移行した方が良さそう。