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

1. 概要

Serverless Migration Station の Codelab シリーズ(セルフペース型のハンズオン チュートリアル)と関連動画は、Google Cloud サーバーレス デベロッパーが主にレガシー サービスからの移行を 1 つ以上の移行を通じてガイドし、アプリをモダナイズできるようにすることを目的としています。これにより、アプリのポータビリティが高まり、選択肢と柔軟性が増し、より広範な 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 サービスの使用経験はどの程度ありますか?

<ph type="x-smartling-placeholder"></ph> 初心者 中級 上達 をご覧ください。

2. 背景情報

App Engine Blobstore API から移行するには、モジュール 0 で、その使用を既存のベースライン App Engine ndb アプリに追加します。このサンプルアプリでは、直近 10 件のユーザー訪問を表示します。エンドユーザーが「訪問」に対応するアーティファクト(ファイル)をアップロードするよう促すよう、アプリを変更します。ユーザーがその操作を望まない場合は、スキップします。選択します。ユーザーの判断にかかわらず、次のページにはモジュール 0 のアプリ(およびこのシリーズの他の多くのモジュール)と同じ出力が表示されます。この App Engine blobstore インテグレーションを実装したら、次の Codelab(モジュール 16)で Cloud Storage に移行できます。

App Engine では DjangoJinja2 のテンプレート システムにアクセスできます。この例の違いの 1 つは、Blobstore アクセス権を追加するほかに、モジュール 0 の Django の使用から、モジュール 15 の Jinja2 に切り替えられる点です。App Engine アプリのモダナイゼーションの重要なステップは、ウェブ フレームワークを webapp2 から Flask に移行することです。後者では、デフォルトのテンプレート システムとして Jinja2 を使用しているため、Jinja2 を実装することで Jinja2 を実装し、Blobstore アクセス用に webapp2 を維持します。Flask はデフォルトで Jinja2 を使用するため、モジュール 16 でテンプレートを変更する必要はありません。

3. 設定/事前作業

チュートリアルの本題に入る前に、プロジェクトをセットアップし、コードを入手して、ベースライン アプリをデプロイして、動作するコードから始めます。

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

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

2. ベースラインのサンプルアプリを取得する

この Codelab の前提条件の一つは、機能するモジュール 0 サンプルアプリがあることです。ない場合は、モジュール 0「開始」から取得できます。(下記のリンクを参照)。この 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」)を保存するようにデータモデルを更新し、store_visit() に保存するための参照を追加します。この追加データは、クエリ時に他のすべてのデータとともに返されるため、fetch_visits() は同じままです。

ndb.BlobKeyProperty である file_blob が登場するアップデートの適用前と導入後は次のとおりです。

変更前:

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. エンドユーザーがファイルをアップロードするか、そのプロセスをスキップすると、フォームの POST が、google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler から派生した新しい UploadHandler に制御を渡します。
  3. UploadHandlerPOST メソッドがアップロードを実行し、store_visit() を呼び出して訪問を登録し、ユーザーを「/」に戻すために HTTP 307 リダイレクトをトリガーします。
  4. メインハンドラの POST メソッドは、(fetch_visits() を介して)をクエリし、最新のアクセスを表示します。ユーザーが [スキップ]を選択するとファイルはアップロードされませんが、訪問は登録された後、同じリダイレクトが続きます。
  5. 最新の訪問履歴には、ユーザーに表示される新しいフィールド(ハイパーリンク付きの「ビュー」)が含まれています。アップロード ファイルが使用可能な場合、または「none」できません。これらの変更は、アップロード フォームの追加と併せて HTML テンプレートに実装されます(詳細は近日中に説明します)。
  6. エンドユーザーが [表示] をクリックした場合アップロードされた動画へのアクセスに対するリンクがあると、google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler から派生した新しい ViewBlobHandler に対して GET リクエストが行われます。その際、画像がレンダリングされている場合はブラウザ内でレンダリングし、そうでない場合はダウンロードを促し、見つからなかった場合は HTTP 404 エラーを返します。
  7. ハンドラクラスの新しいペアと、トラフィックを送信するための新しいルートペアに加えて、メインハンドラには上記の 307 リダイレクトを受け取るための新しい POST メソッドが必要です。

これらのアップデートが行われる前は、モジュール 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)「ビュー blob」です。GET メソッドを持つダウンロード ハンドラ、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.getblobstore.create_upload_url への呼び出しがあります。この呼び出しは、POST 形式の URL を生成し、アップロード ハンドラを呼び出してファイルを Blobstore に送信します。
  • UploadHandler.postblobstore_handlers.BlobstoreUploadHandler.get_uploads への呼び出しがあります。これは、ファイルを Blobstore に挿入し、そのファイルの一意で永続的な ID(BlobKey)を返す真の魔法です。
  • ViewBlobHandler.get でファイルの BlobKey を指定して blobstore_handlers.BlobstoreDownloadHandler.send を呼び出すと、ファイルが取得され、エンドユーザーのブラウザに転送されます。

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

da2960525ac1b90d.png

HTML テンプレートを更新

メインアプリの更新の一部はアプリのユーザー インターフェース(UI)に影響するため、対応する変更をウェブ テンプレートで行う必要があります。実際は以下の 2 つの変更を行います。

  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 をそのフォルダ内に移動します。この最後の移動により、モジュール 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 モジュールなどのツールを使用して、アップロードされた画像を検証し、画像タイプを確認します。最後に、不正な行為者に備え、アップロードのサイズを制限することをおすすめします。

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

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

これは、アップロード先を指定するオプションの更新であるため、リポジトリの main.py ファイルの一部ではありません。代わりに、main-gcs.py という名前の代替方法がリポジトリで確認できます。個別のバケットの「フォルダ」を使用するのではなくmain-gcs.py のコードにより、アップロードは「root」に保存されます。バケット(PROJECT_ID.appspot.com)は main.py に似ていますが、このセクションで示すようにサンプルを導出する場合に必要な足場要素を提供します。以下は、「差分」の図です。main.pymain-gcs.py

256e1ea68241a501.png

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

このセクションでは、この Codelab の締めくくりとして、アプリをデプロイし、意図したとおりに動作することを確認し、反映された出力を確認します。アプリの検証後、クリーンアップ手順を実施し、次のステップを検討します。

アプリケーションのデプロイと検証

gcloud app deploy でアプリを再デプロイし、ユーザー エクスペリエンス(UX)がモジュール 0 のアプリと異なり、アプリが広告どおりに動作することを確認します。現在、アプリには 2 つの異なる画面があります。1 つ目は、ファイル アップロード フォームの訪問プロンプトです。

f5b5f9f19d8ae978.pngエンドユーザーは、このページでファイルをアップロードして [送信] をクリックします。または [スキップ]をクリックしますアップロードしないよう設定できますどちらの場合も、最新の訪問画面に「view」を追加して拡張されたものが表示されます。リンクまたは「none」次のような違いがあります。

f5ac6b98ee8a34cb.png

お疲れさまでした。以上で、App Engine Blobstore をモジュール 0 サンプルアプリに追加して、この Codelab は終了です。コードが FINISH(モジュール 15)フォルダの内容と一致しているはずです。このフォルダには代替の main-gcs.py もあります。

クリーンアップ

全般

現時点で完了したら、課金が発生しないように App Engine アプリを無効にすることをおすすめします。さらにテストや実験を行う場合は、App Engine プラットフォームに無料の割り当てが用意されています。この使用量ティアを超えない限り、料金は発生しません。これはコンピューティングに関するものですが、関連する App Engine サービスに対して料金が発生する場合もあります。詳細については、料金ページをご覧ください。この移行に他のクラウド サービスが含まれる場合、それらは別途請求されます。いずれの場合も、該当する場合は「この 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(START)とモジュール 15(FINISH)のリポジトリ フォルダへのリンクを示します。これらは、すべての App Engine Codelab 移行用のリポジトリからもアクセスでき、ZIP ファイルのクローンを作成したりダウンロードしたりできます。

Codelab

Python 2

Python 3

モジュール 0

コード

なし

モジュール 15(この Codelab)

コード

なし

オンライン リソース

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

App Engine

Google Cloud

Python

動画

ライセンス

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