App Engine Blobstore を Cloud Storage に移行する

このガイドでは、App Engine Blobstore から Cloud Storage に移行する方法を説明します。

Cloud Storage では App Engine Blobstore と同様に、Cloud Storage を使用して動画や画像ファイルなどのサイズの大きいデータ オブジェクト(blob)を処理でき、ユーザーがサイズの大きいデータファイルをアップロードできます。App Engine Blobstore には App Engine のレガシー バンドル サービスからのみアクセスできますが、Cloud Storage は Cloud クライアント ライブラリを介してアクセスするスタンドアロンの Google Cloud プロダクトです。Cloud Storage は、最新のオブジェクト ストレージ ソリューションを提供し、後で Cloud Run またはプラットフォームをホストしている他の Google Cloud アプリに柔軟に移行できます。

2016 年 11 月以降に作成された Google Cloud プロジェクトの場合、Blobstore はバックグラウンドで Cloud Storage バケットを使用します。つまり、アプリを Cloud Storage に移行しても、既存のオブジェクトと既存の Cloud Storage バケットの権限はすべて変更されません。Cloud Storage 用の Cloud クライアント ライブラリを使用して、既存のバケットにアクセスすることもできます。

主な違いと類似点

Cloud Storage では、次の Blobstore の依存関係と制限が除外されます。

  • Python 2 用 Blobstore API はウェブアプリに依存します。
  • Python 3 用 Blobstore API では、ユーティリティ クラスを使用して Blobstore ハンドラを使用します。
  • Blobstore の場合、Blobstore にアップロードできるファイルの最大数は 500 です。Cloud Storage バケットに作成できるオブジェクトの数に上限はありません。

Cloud Storage は、以下をサポートしていません。

  • Blobstore ハンドラクラス
  • Blobstore オブジェクト

Cloud Storage と App Engine Blobstore の類似点:

  • ランタイム環境で大規模なデータ オブジェクトを読み書きでき、静的で大規模なデータ オブジェクト(映画、画像、その他の静的コンテンツなど)を保存して提供できます。Cloud Storage のオブジェクト サイズの上限は 5 TiB です。
  • Cloud Storage バケットにオブジェクトを格納できます。
  • 無料枠を用意します。

始める前に

  • Cloud Storage の料金と割り当てを確認、把握する必要があります。
    • Cloud Storage は従量制サービスであり、データのストレージ クラスとバケットのロケーションに基づいてデータ ストレージに独自の料金が設定されています。
    • Cloud Storage の割り当てには App Engine Blobstore の割り当てと上限とのいくつかの違いがあります。これにより、App Engine リクエストの割り当てに影響する場合があります。
  • Blobstore を使用している Python 2 または Python 3 の App Engine アプリがすでにあります。
  • このガイドの例では、Flask フレームワークを使用して Cloud Storage に移行するアプリを示しています。Cloud Storage に移行する際は、webapp2 のままにするなど、任意のウェブ フレームワークを使用できます。

概要

App Engine Blobstore から Cloud Storage への移行プロセスは、大きく分けて次の手順が含まれます。

  1. 構成ファイルを更新する
  2. Python アプリを更新する
    • ウェブ フレームワークを更新する
    • Cloud Storage をインポートして初期化する
    • Blobstore ハンドラを更新する
    • 省略可: Cloud NDB または App Engine NDB を使用する場合はデータモデルを更新する
  3. アプリのテストとデプロイ

構成ファイルを更新する

Blobstore から Cloud Storage に移行するようにアプリケーション コードを変更する前に、Cloud Storage ライブラリを使用するように構成ファイルを更新します。

  1. app.yaml ファイルを更新します。Python のバージョンに応じた手順に沿って操作します。

    Python 2

    Python 2 アプリの場合:

    1. handlers セクションと libraries セクションの不要なウェブアプリ依存関係を削除します。
    2. Cloud クライアント ライブラリを使用する場合は、grpcio ライブラリと setuptools ライブラリの最新バージョンを追加します。
    3. ssl ライブラリは Cloud Storage で必要なため、追加します。

    変更を含む app.yaml ファイルの例を次に示します。

    runtime: python27
    threadsafe: yes
    api_version: 1
    
    handlers:
    - url: /.*
      script: main.app
    
    libraries:
    - name: grpcio
      version: latest
    - name: setuptools
      version: latest
    - name: ssl
      version: latest
    

    Python 3

    Python 3 アプリの場合は、runtime 要素以外のすべての行を削除します。次に例を示します。

    runtime: python310 # or another support version
    

    Python 3 ランタイムでは、ライブラリが自動的にインストールされるため、以前の Python 2 ランタイムの組み込みライブラリを指定する必要はありません。Cloud Storage への移行時に Python 3 アプリが他の以前のバンドル サービスを使用している場合は、app.yaml ファイルをそのままにします。

  2. requirements.txt ファイルを更新します。Python のバージョンに応じた手順に沿って操作します。

    Python 2

    Cloud Storage 用の Cloud クライアント ライブラリを requirements.txt ファイルの依存関係のリストに追加します。

    google-cloud-storage
    

    次に pip install -t lib -r requirements.txt を実行して、アプリで使用可能なライブラリのリストを更新します。

    Python 3

    Cloud Storage 用の Cloud クライアント ライブラリを requirements.txt ファイルの依存関係のリストに追加します。

    google-cloud-storage
    

    Python 3 ランタイムにアプリをデプロイするときに、App Engine によってこれらの依存関係が自動的にインストールされるため、lib フォルダが存在する場合は削除します。

  3. Python 2 アプリで組み込みライブラリまたはコピーされたライブラリを使用している場合、そのパスを appengine_config.py ファイル内に指定する必要があります。

    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set PATH to your libraries folder.
    PATH = 'lib'
    # Add libraries installed in the PATH folder.
    vendor.add(PATH)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(PATH)
    

Python アプリを更新する

構成ファイルを変更したら、Python アプリを更新します。

Python 2 ウェブ フレームワークを更新する

webapp2 フレームワークを使用する Python 2 アプリの場合、古い webapp2 フレームワークから移行することをおすすめします。Python 2 のサポート終了日については、ランタイム サポートのスケジュールをご覧ください。

Flask、Django、WSGI などの別のウェブ フレームワークに移行できます。Cloud Storage では webapp2 への依存関係が除外され、Blobstore ハンドラはサポートされていないため、他のウェブアプリ関連のライブラリを削除または置換できます。

webapp2 を引き続き使用する場合は、このガイド全体を通して例で Cloud Storage と Flask を使用します。

Cloud Storage に加えて Google Cloud サービスを使用する場合や、最新のランタイム バージョンへのアクセス権を取得する場合は、アプリを Python 3 ランタイムにアップグレードすることを検討してください。詳細については、Python 2 から Python 3 への移行の概要をご覧ください。

Cloud Storage をインポートして初期化する

インポートの行と初期化の行を更新してアプリケーション ファイルを変更します。

  1. 次のような Blobstore インポート ステートメントを削除します。

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. 次のように、Cloud Storage と Google Authentication ライブラリのインポート ステートメントを追加します。

    import io
    from flask import (Flask, abort, redirect, render_template,
    request, send_file, url_for)
    from google.cloud import storage
    import google.auth
    

    Google 認証ライブラリは、Cloud Storage 用 Blobstore で使用したのと同じプロジェクト ID を取得するために必要です。アプリに適用できる場合は、Cloud NBD などの他のライブラリをインポートします。

  3. Cloud Storage の新しいクライアントを作成し、Blobstore で使用するバケットを指定します。次に例を示します。

    gcs_client = storage.Client()
    _, PROJECT_ID = google.auth.default()
    BUCKET = '%s.appspot.com' % PROJECT_ID
    

    2016 年 11 月以降の Google Cloud プロジェクトの場合、Blobstore はアプリの URL に合わせて名前を付けた Cloud Storage バケットに書き込み、PROJECT_ID.appspot.com の形式を使用します。Google 認証を使用してプロジェクト ID を取得し、Blobstore への blob の保存に使用する Cloud Storage バケットを指定します。

Blobstore ハンドラを更新する

Cloud Storage では Blobstore のアップロードとダウンロードのハンドラがサポートされていないため、io 標準ライブラリ モジュール、ウェブ フレームワーク、Python ユーティリティを使用して Cloud Storage へのオブジェクト(blob)のアップロードやダウンロードを行う必要があります。

以下では、ウェブ フレームワークの例として Flask を使用して Blobstore ハンドラを更新する方法を示します。

  1. Blobstore のアップロード ハンドラのクラスを Flask のアップロード関数に置き換えます。Python のバージョンに応じた手順に沿って操作します。

    Python 2

    Python 2 の Blobstore ハンドラは、次の Blobstore の例に示すように webapp2 クラスです。

    class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads()
            blob_id = uploads[0].key() if uploads else None
            store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
            self.redirect('/', code=307)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Cloud Storage を使用するには:

    1. ウェブアプリのアップロード クラスを Flask のアップロード関数に置き換えます。
    2. アップロード ハンドラとルーティングを、ルーティングで装飾された Flask POST メソッドに置き換えます。

    更新されたコードサンプル:

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    更新された Cloud Storage コードサンプルでは、オブジェクト アーティファクトが blob_id ではなく、オブジェクト名(fname)で識別されるようになりました。ルーティングはアプリケーション ファイルの下部でも行われます。

    アップロードされたオブジェクトを取得するために、Blobstore の get_uploads() メソッドは Flask の request.files.get() メソッドに置き換えられます。Flask では、secure_filename() メソッドを使用して、ファイルのパス文字(/ など)を含まない名前を取得し、gcs_client.bucket(BUCKET).blob(fname) を使用してバケット名とオブジェクト名を指定します。

    更新された例に示すように、Cloud Storage の upload_from_file() 呼び出しでアップロードを行います。

    Python 3

    Python 3 用 Blobstore のアップロード ハンドラクラスはユーティリティ クラスです。次の Blobstore の例に示すように、入力パラメータとして WSGI environ 辞書を使用する必要があります。

    class UploadHandler(blobstore.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads(request.environ)
            if uploads:
                blob_id = uploads[0].key()
                store_visit(request.remote_addr, request.user_agent, blob_id)
            return redirect('/', code=307)
    ...
    @app.route('/upload', methods=['POST'])
    def upload():
        """Upload handler called by blobstore when a blob is uploaded in the test."""
        return UploadHandler().post()
    

    Cloud Storage を使用するには、Blobstore の get_uploads(request.environ) メソッドを Flask の request.files.get() メソッドに置き換えます。

    更新されたコードサンプル:

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    更新された Cloud Storage コードサンプルでは、オブジェクト アーティファクトが blob_id ではなく、オブジェクト名(fname)で識別されるようになりました。ルーティングはアプリケーション ファイルの下部でも行われます。

    アップロードされたオブジェクトを取得するために、Blobstore の get_uploads() メソッドは Flask の request.files.get() メソッドに置き換えられます。Flask では、secure_filename() メソッドを使用して、ファイルのパス文字(/ など)を含まない名前を取得し、gcs_client.bucket(BUCKET).blob(fname) を使用してバケット名とオブジェクト名を指定します。

    更新された例に示すように、Cloud Storage の upload_from_file() メソッドでアップロードを行います。

  2. Blobstore のダウンロード ハンドラクラスを Flask のダウンロード関数に置き換えます。Python のバージョンに応じた手順に沿って操作します。

    Python 2

    次のダウンロード ハンドラの例では、webapp2 を使用する BlobstoreDownloadHandler クラスを使用しています。

    class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Cloud Storage を使用するには:

    1. Cloud Storage の download_as_bytes() メソッドを使用するように Blobstore の send_blob() メソッドを更新します。
    2. ルーティングを webapp2 から Flask に変更します。

    更新されたコードサンプル:

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    更新された Cloud Storage コードサンプルでは、Flask は Flask 関数でルートを修飾し、'/view/<path:fname>' を使用してオブジェクトを識別します。Cloud Storage では、オブジェクト名とバケット名によって blob オブジェクトを識別し、Blobstore から send_blob メソッドを使用する代わりに、download_as_bytes() メソッドを使用してオブジェクトをバイトとしてダウンロードします。アーティファクトが見つからない場合は、HTTP 404 エラーが返されます。

    Python 3

    アップロード ハンドラと同様に、Python 3 用 Blobstore のダウンロード ハンドラクラスはユーティリティ クラスであり、次の Blobstore の例に示すように WSGI environ 辞書を入力パラメータとして使用する必要があります。

    class ViewBlobHandler(blobstore.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            if not blobstore.get(blob_key):
                return "Photo key not found", 404
            else:
                headers = self.send_blob(request.environ, blob_key)
    
            # Prevent Flask from setting a default content-type.
            # GAE sets it to a guessed type if the header is not set.
            headers['Content-Type'] = None
            return '', headers
    ...
    @app.route('/view/<blob_key>')
    def view_photo(blob_key):
        """View photo given a key."""
        return ViewBlobHandler().get(blob_key)
    

    Cloud Storage を使用するには、Blobstore の send_blob(request.environ, blob_key) を Cloud Storage の blob.download_as_bytes() メソッドに置き換えます。

    更新されたコードサンプル:

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    更新された Cloud Storage コードサンプルでは、blob_keyfname に置き換えられ、Flask は '/view/<path:fname>' URL を使用してオブジェクトを識別します。gcs_client.bucket(BUCKET).blob(fname) メソッドを使用して、ファイル名とバケット名を検索します。Cloud Storage の download_as_bytes() メソッドは、Blobstore から send_blob() メソッドを使用する代わりに、オブジェクトをバイトとしてダウンロードします。

  3. アプリでメインハンドラを使用する場合は、MainHandler クラスを Flask の root() 関数に置き換えます。Python のバージョンに応じた手順に沿って操作します。

    Python 2

    Blobstore の MainHandler クラスを使用した例を次に示します。

    class MainHandler(BaseHandler):
        'main application (GET/POST) handler'
        def get(self):
            self.render_response('index.html',
                    upload_url=blobstore.create_upload_url('/upload'))
    
        def post(self):
            visits = fetch_visits(10)
            self.render_response('index.html', visits=visits)
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Cloud Storage を使用するには:

    1. Flask によってルーティングが処理されるため、MainHandler(BaseHandler) クラスを削除します。
    2. Flask を使用して Blobstore コードを簡素化します。
    3. ウェブアプリ ルーティングを最後に削除します。

    更新されたコードサンプル:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Python 3

    Flask を使用した場合、MainHandler クラスはありませんが、Blobstore を使用する場合は Flask ルート関数を更新する必要があります。次の例では、blobstore.create_upload_url('/upload') 関数を使用しています。

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = blobstore.create_upload_url('/upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Cloud Storage を使用するには、blobstore.create_upload_url('/upload') 関数を Flask の url_for() メソッドに置き換えて、upload() 関数の URL を取得します。

    更新されたコードサンプル:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload') # Updated to use url_for
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

アプリのテストとデプロイ

ローカル開発用サーバーでは、アプリの実行をテストできますが、新しいバージョンをデプロイするまで Cloud Storage をテストできません。これは、すべての Cloud Storage リクエストをインターネット経由で実際の Cloud Storage バケットに送信する必要があるためです。アプリケーションをローカルで実行する方法については、アプリケーションのテストとデプロイをご覧ください。次に、新しいバージョンをデプロイして、アプリが以前と同じように表示されることを確認します。

App Engine NDB または Cloud NDB を使用するアプリ

アプリで App Engine NDB または Cloud NDB を使用して Blobstore 関連のプロパティを含める場合は、Datastore データモデルを更新する必要があります。

データモデルを更新する

NDB の BlobKey プロパティは Cloud Storage ではサポートされていないため、NDB やウェブ フレームワークなどの組み込みサービスを使用するように Blobstore 関連の行を変更する必要があります。

データモデルを更新するには:

  1. 次のように、データモデルで BlobKey を使用する行を見つけます。

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()
    
  2. ndb.BlobKeyProperty()ndb.StringProperty() に置き換えます。

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.StringProperty() # Modified from ndb.BlobKeyProperty()
    
  3. 移行中に App Engine NDB から Cloud NDB にアップグレードする場合も、Python コンテキスト マネージャーを使用するように NDB コードをリファクタリングする方法については、Cloud NDB 移行ガイドをご覧ください。

Datastore データモデルの下位互換性

前のセクションの ndb.BlobKeyPropertyndb.StringProperty に置き換えると、アプリの下位互換性がなくなります。つまり、Blobstore によって作成された古いエントリを処理できなくなります。古いデータを保持する必要がある場合は、ndb.BlobKeyProperty フィールドを更新するのではなく、新しい Cloud Storage エントリ用に追加のフィールドを作成し、データを正規化する関数を作成します。

前のセクションの例から、次の変更を行います。

  1. データモデルを定義するときに、2 つの個別のプロパティ フィールドを作成します。Blobstore によって作成されたオブジェクトを識別するには file_blob プロパティを使用し、Cloud Storage によって作成されたオブジェクトを識別するには file_gcs プロパティを使用します。

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()  # backwards-compatibility
        file_gcs  = ndb.StringProperty()
    
  2. 次のように、新しいアクセスを参照している行を見つけます。

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_blob=upload_key).put()
    
  3. 最近のエントリで file_gcs が使用されるようにコードを変更します。次に例を示します。

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_gcs=upload_key).put() # change file_blob to file_gcs for new requests
    
  4. データを正規化する新しい関数を作成します。次の例では、抽出、変換、低(ETL)を使用してすべてのアクセスをループし、訪問者とタイムスタンプのデータを取得して、file_gcs または file_gcs が存在しているかどうかを確認します。

    def etl_visits(visits):
        return [{
                'visitor': v.visitor,
                'timestamp': v.timestamp,
                'file_blob': v.file_gcs if hasattr(v, 'file_gcs') \
                        and v.file_gcs else v.file_blob
                } for v in visits]
    
  5. fetch_visits() 関数を参照する行を探します。

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    
  6. 次のように、fetch_visits()etl_visits() 関数内でラップします。

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = etl_visits(fetch_visits(10)) # etl_visits wraps around fetch_visits
        return render_template('index.html', **context)
    

次のステップ