App Engine blobstore の使用方法(モジュール 15)

1. 概要

サーバーレス移行ステーション シリーズの Codelab(ご自分のペースで進められる実践型のチュートリアル)と関連動画は、主にレガシー サービスからの移行を 1 つ以上行うことで、Google Cloud サーバーレスのデベロッパーがアプリケーションをモダナイズできるよう支援することを目的としています。これにより、アプリの移植性が向上し、オプションと柔軟性が高まるため、幅広い Cloud プロダクトとの統合やアクセスが可能になり、新しい言語リリースへのアップグレードが容易になります。このシリーズは、当初は初期の Cloud ユーザー、主に App Engine(標準環境)のデベロッパーを対象としていましたが、Cloud FunctionsCloud Run などの他のサーバーレス プラットフォームや、該当する場合は他のプラットフォームも対象とするほど幅広くなっています。

このモジュール 15 の Codelab では、モジュール 0 のサンプルアプリApp Engine blobstore の使用状況を追加する方法について説明します。これで、次のモジュール 16 で その使用状況を Cloud Storage に移行する準備が整いました。

GCP コンソールの

  • App Engine Blobstore API/ライブラリの使用を追加
  • ユーザーのアップロードを blobstore サービスに保存する
  • Cloud Storage への移行の次のステップを準備する

必要なもの

アンケート

このチュートリアルをどのように使用されますか?

通読のみ 通読して演習を行う

Python のご利用経験はどの程度ありますか?

初心者 中級者 上級者

Google Cloud サービスの使用経験はどの程度ありますか?

初心者 中級者 上級者

2. 背景情報

App Engine Blobstore API から移行するには、その使用状況をモジュール 0 の既存のベースライン App Engine ndb アプリに追加します。サンプルアプリには、ユーザーが最近アクセスした 10 件のサイトが表示されます。アプリを修正し、エンドユーザーに「訪問」に対応するアーティファクト(ファイル)のアップロードを求めるようにします。お客様がご希望されない場合は、[スキップ] オプションがあります。ユーザーの決定に関係なく、次のページではモジュール 0 のアプリ(およびこのシリーズの他の多くのモジュール)と同じ出力がレンダリングされます。この App Engine blobstore 統合を実装したので、次の Codelab(モジュール 16)で Cloud Storage に移行できます。

App Engine は DjangoJinja2 のテンプレート システムへのアクセスを提供します。この例が異なるのは(Blobstore アクセスの追加に加えて)、モジュール 0 で Django を使用していたのが、モジュール 15 では Jinja2 に切り替わっている点です。App Engine アプリをモダナイズするうえで重要な手順は、ウェブ フレームワークを webapp2 から Flask に移行することです。後者は Jinja2 をデフォルトのテンプレート システムとして使用するため、Blobstore アクセス用の webapp2 を維持しながら Jinja2 を実装することで、その方向に移行を開始します。Flask はデフォルトで Jinja2 を使用するため、モジュール 16 でテンプレートを変更する必要はありません。

3. セットアップ/事前作業

チュートリアルの主要部分に進む前に、まずプロジェクトをセットアップし、コードを取得して、ベースライン アプリをデプロイすることで、作業コードの作成を開始します。

1. プロジェクトのセットアップ

モジュール 0 アプリをすでにデプロイしている場合は、同じプロジェクト(とコード)を再利用することをおすすめします。または、新しいプロジェクトを作成するか、別の既存のプロジェクトを再利用することもできます。プロジェクトにアクティブな請求先アカウントがあり、App Engine が有効になっていることを確認します。

2. ベースライン サンプルアプリを入手する

この Codelab の前提条件の 1 つは、機能するモジュール 0 のサンプルアプリを用意することです。まだお持ちでない場合は、モジュール 0 の「START」フォルダ(下のリンク)から入手できます。この Codelab では、各ステップを順を追って説明します。最後に、モジュール 15 の「FINISH」フォルダにあるコードに似たコードが完成します。

モジュール 0 の開始ファイルのディレクトリは次のようになります。

$ ls
README.md               index.html
app.yaml                main.py

3. ベースライン アプリを(再)デプロイする

この段階で実施する必要がある残りの事前作業のステップ:

  1. gcloud コマンドライン ツールを学びなおす
  2. gcloud app deploy を使用してサンプルアプリを再デプロイする
  3. アプリが App Engine で問題なく実行されることを確認する

これらの手順を正常に実行し、ウェブアプリが動作することを確認したら(次のような出力が表示されます)、アプリにキャッシュの使用を追加する準備が整います。

a7a9d2b80d706a2b.png

4. 構成ファイルを更新する

app.yaml

アプリケーション構成に大きな変更はありませんが、前述のように、Django テンプレート(デフォルト)から Jinja2 に移行するため、切り替えるには、App Engine サーバーで利用可能な最新バージョンの Jinja2 を指定する必要があります。これを行うには、app.yaml の組み込みのサードパーティ ライブラリ セクションに追加します。

変更前:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

次のように新しい libraries セクションを追加して、app.yaml ファイルを編集します。

変更後:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: jinja2
  version: latest

他の構成ファイルを更新する必要はないので、アプリケーション ファイルに進みましょう。

5. アプリケーション ファイルを変更する

インポートと Jinja2 のサポート

main.py の最初の変更セットには、Blobstore API の使用の追加と、Django テンプレートの Jinja2 への置き換えが含まれます。変更内容は次のとおりです。

  1. os モジュールの目的は、Django テンプレートへのファイル パス名を作成することです。Jinja2 に切り替えることでこの処理が不要になるため、os と Django テンプレート レンダラ google.appengine.ext.webapp.template の使用は不要になり、削除されます。
  2. Blobstore API をインポートします。google.appengine.ext.blobstore
  3. 元の webapp フレームワークにある Blobstore ハンドラをインポートします。これらは webapp2 では使用できません。google.appengine.ext.webapp.blobstore_handlers
  4. webapp2_extras パッケージから Jinja2 サポートをインポートする

変更前:

import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template

上記のリストの変更を実装するには、main.py の現在のインポート セクションを次のコード スニペットに置き換えます。

変更後:

import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers

インポートの後、webapp2_extras ドキュメントで定義されている Jinja2 の使用をサポートするボイラープレート コードを追加します。次のコード スニペットは、標準の webapp2 リクエスト ハンドラ クラスを Jinja2 機能でラップします。このコードブロックを main.py のインポートの直後に追加します。

class BaseHandler(webapp2.RequestHandler):
    'Derived request handler mixing-in Jinja2 support'
    @webapp2.cached_property
    def jinja2(self):
        return jinja2.get_jinja2(app=self.app)

    def render_response(self, _template, **context):
        self.response.write(self.jinja2.render_template(_template, **context))

Blobstore のサポートを追加する

このシリーズの他の移行では、UX を大きく変更することなく、サンプルアプリの機能や出力を同じ(またはほぼ同じ)に保ちますが、この例では、標準から大きく逸脱しています。新しい訪問をすぐに登録して最新の 10 件を表示するのではなく、アプリを更新して、訪問を登録するためのファイル アーティファクトをユーザーに求めるようにします。エンドユーザーは、対応するファイルをアップロードするか、[スキップ] を選択して何もアップロードしないかを選択できます。この手順が完了すると、[最近のアクセス] ページが表示されます。

この変更により、アプリは Blobstore サービスを使用して、その画像やその他のファイルタイプを最近アクセスしたページに保存(および後でレンダリング)できるようになります。

データモデルを更新して使用を実装する

より多くのデータを保存します。具体的には、Blobstore にアップロードされたファイルの ID(「BlobKey」)を保存するようにデータモデルを更新し、その ID を store_visit() に保存するための参照を追加します。この追加データはクエリ時に他のすべてのデータとともに返されるため、fetch_visits() は変わりません。

これらのアップデートを適用した file_blobndb.BlobKeyProperty)の変更前と変更後の状態は次のとおりです。

変更前:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

変更後:

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()

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent),
            file_blob=upload_key).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

これまでの変更を図で表すと次のようになります。

2270783776759f7f.png

ファイル アップロードをサポートする

機能面で最も大きな変更は、ファイルのアップロードのサポートです。ユーザーにファイルの入力を求める場合、スキップ機能をサポートする場合、訪問に対応するファイルを表示する場合などがあります。すべてが写真の一部です。ファイル アップロードをサポートするために必要な変更は次のとおりです。

  1. メイン ハンドラ GET リクエストは、表示用の最新のアクセス履歴を取得しなくなりました。代わりに、ユーザーにアップロードを求めるメッセージが表示されます。
  2. エンドユーザーがアップロードするファイルを送信するか、そのプロセスをスキップすると、フォームの POSTgoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler から派生した新しい UploadHandler に制御を渡します。
  3. UploadHandlerPOST メソッドはアップロードを実行し、store_visit() を呼び出してアクセスを登録し、HTTP 307 リダイレクトをトリガーしてユーザーを「/」に戻します。ここで...
  4. メイン ハンドラの POST メソッドは、fetch_visits() を介して最新のアクセスをクエリして表示します。ユーザーが [スキップ] を選択した場合、ファイルはアップロードされませんが、アクセスは登録され、同じリダイレクトが実行されます。
  5. 最新のアクセス状況の表示に、ユーザーに表示される新しいフィールドが追加されました。アップロード ファイルがある場合はハイパーリンクされた [表示]、それ以外の場合は [なし] が表示されます。これらの変更は、HTML テンプレートで実現され、アップロード フォームが追加されます(詳細については近日公開予定です)。
  6. エンドユーザーがアップロードされた動画を含む任意の訪問の [表示] リンクをクリックすると、新しい ViewBlobHandlergoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler から派生)に GET リクエストが送信されます。画像の場合はファイルがレンダリングされ(ブラウザでサポートされている場合)、そうでない場合はダウンロードを求めるプロンプトが表示されます。ファイルが見つからない場合は HTTP 404 エラーが返されます。
  7. 新しいハンドラ クラスのペアと、それらにトラフィックを送信する新しいルートのペアに加えて、メインハンドラには、上記の 307 リダイレクトを受信する新しい POST メソッドが必要です。

これらの更新前、Module 0 アプリには GET メソッドと単一のルートを持つメイン ハンドラのみが含まれていました。

変更前:

class MainHandler(webapp2.RequestHandler):
    'main application (GET) handler'
    def get(self):
        store_visit(self.request.remote_addr, self.request.user_agent)
        visits = fetch_visits(10)
        tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
        self.response.out.write(template.render(tmpl, {'visits': visits}))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
], debug=True)

これらの更新を実装すると、ハンドラは 3 つになります。1)POST メソッドを含むアップロード ハンドラ、2)GET メソッドを含む「BLOB を表示」ダウンロード ハンドラ、3)GET メソッドと POST メソッドを含むメイン ハンドラです。これらの変更を行うと、アプリの残りの部分は次のようになります。

変更後:

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)

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)

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)

追加したコードには、いくつかの重要な呼び出しがあります。

  • MainHandler.get で、blobstore.create_upload_url の呼び出しがあります。この呼び出しにより、フォームが POST する URL が生成され、アップロード ハンドラが呼び出されてファイルが Blobstore に送信されます。
  • UploadHandler.post で、blobstore_handlers.BlobstoreUploadHandler.get_uploads の呼び出しがあります。これが、ファイルを Blobstore に配置し、そのファイルの一意の永続 ID(BlobKey)を返す実際の処理です。
  • ViewBlobHandler.get で、ファイルの BlobKey を使用して blobstore_handlers.BlobstoreDownloadHandler.send を呼び出すと、ファイルが取得され、エンドユーザーのブラウザに転送されます。

これらの呼び出しは、アプリに追加された機能へのアクセスを大部分で表しています。次に、main.py の 2 回目(最後)の変更の図を示します。

da2960525ac1b90d.png

HTML テンプレートを更新する

メイン アプリケーションの更新の一部はアプリのユーザー インターフェース(UI)に影響するため、ウェブ テンプレートで対応する変更が必要です。実際には 2 つの変更が必要です。

  1. ファイル アップロード フォームには、ファイルと、ファイル アップロードとスキップ用の 1 組の送信ボタンの 3 つの入力要素が必要です。
  2. 対応するファイルのアップロードがある場合は「表示」リンクを、それ以外の場合は「なし」を追加して、最新のアクセス履歴の出力を更新します。

変更前:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

</body>
</html>

上記のリストの変更を実装して、更新されたテンプレートを構成します。

変更後:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
{% if upload_url %}

<h3>Welcome... upload a file? (optional)</h3>
<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
    <input type="file" name="file"><p></p>
    <input type="submit"> <input type="submit" value="Skip">
</form>

{% else %}

<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }}
    <i><code>
    {% if visit.file_blob %}
        (<a href="/view/{{ visit.file_blob }}" target="_blank">view</a>)
    {% else %}
        (none)
    {% endif %}
    </code></i>
    from {{ visit.visitor }}
</li>
{% endfor %}
</ul>

{% endif %}

</body>
</html>

この図は、index.html に必要な更新を示しています。

8583e975f25aa9e7.png

最後の変更として、Jinja2 はテンプレートを templates フォルダに配置することを推奨しているため、そのフォルダを作成して index.html をその中に移動します。これで、Module 0 サンプルアプリに Blobstore の使用を追加するために必要な変更がすべて完了しました。

(省略可)Cloud Storage の「拡張機能」

Blobstore ストレージは最終的に Cloud Storage に進化しました。つまり、Blobstore のアップロードは Cloud コンソール(特に Cloud Storage ブラウザ)で確認できます。問題は、どこで発生するかです。答えは、App Engine アプリのデフォルトの Cloud Storage バケットです。名前は、App Engine アプリの完全なドメイン名 PROJECT_ID.appspot.com です。すべてのプロジェクト ID は一意なので、とても便利です。

サンプル アプリケーションに加えた更新により、アップロードされたファイルがそのバケットにドロップされますが、デベロッパーはより具体的な場所を選択することもできます。デフォルト バケットには google.appengine.api.app_identity.get_default_gcs_bucket_name() を介してプログラムでアクセスできます。この値にアクセスして、アップロードされたファイルを整理するための接頭辞として使用する場合は、新しいインポートが必要です。たとえば、ファイル形式で並べ替えるには:

f61f7a23a1518705.png

たとえば、画像に対してこのような処理を実装するには、次のようなコードと、ファイルタイプをチェックして目的のバケット名を選択するコードが必要になります。

ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')

また、Python 標準ライブラリの imghdr モジュールなどのツールを使用して、アップロードされた画像を検証し、画像タイプを確認します。最後に、悪意のあるユーザーに備えて、アップロードのサイズを制限することをおすすめします。

すべてが完了したとします。アップロードされたファイルの保存場所を指定できるようにアプリを更新するにはどうすればよいですか?重要なのは、MainHandler.getblobstore.create_upload_url の呼び出しを調整して、次のように gs_bucket_name パラメータを追加することで、アップロード先の Cloud Storage の場所を指定することです。

blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))

アップロード先を指定する場合のオプションの更新であるため、リポジトリの main.py ファイルには含まれていません。代わりに、main-gcs.py という代替手段がリポジトリで確認できます。main-gcs.py のコードは、別のバケット「フォルダ」を使用するのではなく、main.py と同様に「ルート」バケット(PROJECT_ID.appspot.com)にアップロードを保存しますが、このセクションで説明したように、サンプルをより複雑なものに派生させる場合に必要なスキャフォールディングを提供します。以下は、main.pymain-gcs.py の「差分」の図です。

256e1ea68241a501.png

6. 概要/クリーンアップ

このセクションでは、アプリをデプロイし、意図したとおりに動作することと、出力に反映されることを確認して、この Codelab を終了します。アプリの検証が完了したら、クリーンアップの手順を実行し、次の手順を検討します。

アプリケーションをデプロイして検証する

gcloud app deploy を使用してアプリを再デプロイし、アプリが宣伝どおりに動作し、Module 0 アプリとはユーザー エクスペリエンス(UX)が異なることを確認します。アプリには 2 つの異なる画面が表示されるようになります。1 つ目は訪問ファイル アップロード フォームのプロンプトです。

f5b5f9f19d8ae978.pngエンドユーザーは、ファイルをアップロードして [送信] をクリックするか、[スキップ] をクリックして何もアップロードしないかを選択します。いずれの場合も、結果は最新のアクセス画面になります。アクセス タイムスタンプと訪問者情報の間に「表示」リンクまたは「なし」が追加されています。

f5ac6b98ee8a34cb.png

お疲れさまでした。これで、App Engine Blobstore の使用をモジュール 0 のサンプルアプリに追加する Codelab は完了です。コードは FINISH(モジュール 15)フォルダのコードと一致するようになりました。代替の main-gcs.py もそのフォルダにあります。

クリーンアップ

全般

現時点で完了した場合は、課金が発生しないように App Engine アプリを無効にすることをおすすめします。ただし、さらにテストや実験を行う場合は、App Engine プラットフォームに無料割り当てがあります。この使用量枠を超えない限り、課金されることはありません。これはコンピューティングの料金ですが、関連する App Engine サービスの料金も発生する可能性があります。詳細については、料金ページをご覧ください。この移行に他の Cloud サービスが含まれる場合は、それらのサービスは別途請求されます。どちらの場合も、該当する場合は、以下の「この Codelab に固有」のセクションをご覧ください。

完全に開示すると、App Engine などの Google Cloud サーバーレス コンピューティング プラットフォームにデプロイすると、ビルドとストレージの費用がわずかに発生しますCloud Build には独自の無料割り当てがあり、Cloud Storage にも独自の無料割り当てがあります。そのイメージの保存には、その割り当ての一部が使用されます。ただし、お住まいの地域でこのような無料枠が提供されていない可能性もあります。そのため、保存容量の使用状況を把握して、費用を最小限に抑えるようにしてください。確認する必要がある特定の Cloud Storage の「フォルダ」は次のとおりです。

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • 上記のストレージ リンクは、PROJECT_ID と *LOC*ーションによって異なります。たとえば、アプリが米国でホストされている場合は「us」になります。

ただし、このアプリケーションや関連する移行 Codelab を続行せず、すべてを完全に削除する場合は、プロジェクトをシャットダウンしてください。

この Codelab に固有

以下のサービスは、この Codelab 固有のものです。詳細については、各プロダクトのドキュメントをご覧ください。

次のステップ

次に検討すべき論理移行については、モジュール 16 で説明します。ここでは、App Engine Blobstore サービスから Cloud Storage クライアント ライブラリの使用に移行する方法をデベロッパーに示します。アップグレードのメリットとしては、より多くの Cloud Storage 機能にアクセスできること、App Engine 以外のアプリ(Google Cloud、他のクラウド、オンプレミス)で動作するクライアント ライブラリに慣れることなどがあります。Cloud Storage のすべての機能が必要ない場合や、費用への影響が気になる場合は、App Engine Blobstore を引き続きご利用いただけます。

モジュール 16 以降には、Cloud NDB や Cloud Datastore、Cloud Tasks、Cloud Memorystore など、他にも多くの移行の可能性があります。Cloud Run と Cloud Functions へのプロダクト間の移行もあります。移行リポジトリには、すべてのコードサンプルが用意されています。また、利用可能なすべての Codelab と動画へのリンクも用意されています。さらに、検討すべき移行と、関連する移行の「順序」に関するガイダンスも提供されています。

7. 参考情報

Codelab に関する問題/フィードバック

この Codelab に問題が見つかった場合は、提出する前にまず問題を検索してください。新しい問題の検索と登録を行うためのリンク:

移行に関するリソース

モジュール 0(開始)とモジュール 15(終了)のリポジトリ フォルダへのリンクを次の表に示します。これらのフォルダには、すべての App Engine Codelab 移行のリポジトリからアクセスすることもでき、クローンを作成する、または ZIP ファイルをダウンロードすることができます。

Codelab

Python 2

Python 3

モジュール 0

コード

なし

モジュール 15(この Codelab)

コード

なし

オンライン リソース

このチュートリアルに関連する可能性のあるオンライン リソースは次のとおりです。

App Engine

Google Cloud

Python

動画

ライセンス

この作業はクリエイティブ・コモンズの表示 2.0 汎用ライセンスにより使用許諾されています。