1. 概要
サーバーレス移行ステーション シリーズの Codelab(自分のペースで進められるハンズオン チュートリアル)と関連動画は、Google Cloud サーバーレス デベロッパーがアプリケーションをモダナイズできるよう、1 つ以上の移行(主にレガシー サービスからの移行)をガイドすることを目的としています。これにより、アプリのポータビリティが向上し、オプションと柔軟性が高まります。また、より幅広い Cloud プロダクトとの統合やアクセスが可能になり、新しい言語リリースへのアップグレードが容易になります。このシリーズは、当初は初期の Cloud ユーザー、主に App Engine(スタンダード環境)デベロッパーに焦点を当てていましたが、Cloud Functions や Cloud Run などの他のサーバーレス プラットフォームや、その他のプラットフォーム(該当する場合)も対象とするようになりました。
このモジュール 15 の Codelab では、モジュール 0 のサンプルアプリに App Engine blobstore の使用を追加する方法について説明します。その後、モジュール 16 でその使用量を Cloud Storage に移行する準備が整います。
GCP コンソールの
- App Engine Blobstore API / ライブラリの使用を追加する
- ユーザーのアップロードを
blobstoreサービスに保存する - Cloud Storage への移行の次のステップを準備する
必要なもの
- 有効な GCP 請求先アカウントを持つ Google Cloud Platform プロジェクト
- 基本的な Python スキル
- 一般的な Linux コマンドに関する実践的な知識
- App Engine アプリの開発とデプロイに関する基本的な知識
- モジュール 0 の App Engine アプリ(リポジトリから取得)
アンケート
このチュートリアルをどのように使用されますか?
Python のご利用経験はどの程度ありますか?
Google Cloud サービスの使用経験はどの程度ありますか?
2. 背景情報
App Engine Blobstore API から移行するには、モジュール 0 の既存のベースライン App Engine ndb アプリにその使用状況を追加します。サンプルアプリは、ユーザーの最近の 10 回の訪問を表示します。アプリを変更して、エンドユーザーに「訪問」に対応するアーティファクト(ファイル)をアップロードするよう促します。ユーザーがこの操作を希望しない場合は、[スキップ] オプションがあります。ユーザーの決定に関係なく、次のページにはモジュール 0(およびこのシリーズの他の多くのモジュール)のアプリと同じ出力が表示されます。この App Engine blobstore 統合を実装したら、次の(モジュール 16)Codelab で Cloud Storage に移行できます。
App Engine では、Django と Jinja2 のテンプレート システムにアクセスできます。この例が異なる点(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 フォルダ(Python 2)
- 完了: モジュール 15 フォルダ(Python 2)
- リポジトリ全体(ZIP ファイルのクローン作成またはダウンロード)
モジュール 0 の開始ファイルのディレクトリは次のようになります。
$ ls README.md index.html app.yaml main.py
3. ベースライン アプリを(再)デプロイする
この段階で実施する必要がある残りの事前作業のステップ:
gcloudコマンドライン ツールを学びなおすgcloud app deployを使用してサンプルアプリを再デプロイする- アプリが App Engine で問題なく動作することを確認する
これらの手順を正常に実行し、ウェブアプリが動作していることを確認したら(以下のような出力が表示されます)、アプリにキャッシュの使用を追加する準備が整いました。

4. 構成ファイルを更新する
app.yaml
アプリケーション構成に重要な変更はありませんが、前述のように、Django テンプレート(デフォルト)から Jinja2 に移行するため、切り替えるには、App Engine サーバーで利用可能な Jinja2 の最新バージョンを指定する必要があります。これを行うには、app.yaml の組み込みサードパーティ ライブラリ セクションに Jinja2 を追加します。
変更前:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
app.yaml ファイルを編集して、次のように新しい libraries セクションを追加します。
変更後:
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 への置き換えが含まれます。変更点は次のとおりです。
osモジュールの目的は、Django テンプレートへのファイル パス名を作成することです。この処理を行う Jinja2 に切り替えるため、osと Django テンプレート レンダラgoogle.appengine.ext.webapp.templateの使用は不要になり、削除されます。- Blobstore API をインポートします。
google.appengine.ext.blobstore - 元の
webappフレームワークにある Blobstore ハンドラをインポートします。これらはwebapp2では使用できません。google.appengine.ext.webapp.blobstore_handlers 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)
これまでに加えられた変更を、図で表すと次のようになります。

ファイルのアップロードをサポートする
機能面での最も重要な変更は、ファイル アップロードのサポートです。これには、ユーザーにファイルのアップロードを促す、スキップ機能をサポートする、訪問に対応するファイルをレンダリングする、といったことが含まれます。これらすべてが全体像の一部です。ファイルのアップロードをサポートするために必要な変更は次のとおりです。
- メインハンドラの
GETリクエストは、表示する最新の訪問を取得しなくなりました。代わりに、ユーザーにアップロードを求めるプロンプトが表示されます。 - エンドユーザーがアップロードするファイルを送信するか、そのプロセスをスキップすると、フォームの
POSTがgoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandlerから派生した新しいUploadHandlerに制御を渡します。 UploadHandlerのPOSTメソッドは、アップロードを実行し、store_visit()を呼び出して訪問を登録し、HTTP 307 リダイレクトをトリガーしてユーザーを「/」に送り返します。- メインハンドラの
POSTメソッドは、fetch_visits()を介して最新の訪問をクエリし、表示します。ユーザーが [スキップ] を選択した場合、ファイルはアップロードされませんが、訪問は登録され、同じリダイレクトが行われます。 - 最近の訪問の表示には、ユーザーに表示される新しいフィールドが含まれます。アップロード ファイルが利用可能な場合はハイパーリンクされた「表示」が表示され、それ以外の場合は「なし」が表示されます。これらの変更は、アップロード フォームの追加とともに HTML テンプレートで実現されます(これについては近日公開予定です)。
- エンドユーザーが、アップロードされた動画を含む訪問の [表示] リンクをクリックすると、
google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandlerから派生した新しいViewBlobHandlerへのGETリクエストが作成されます。画像の場合はファイルがレンダリングされ(ブラウザでサポートされている場合)、画像でない場合はダウンロードを求めるメッセージが表示されます。見つからない場合は HTTP 404 エラーが返されます。 - 新しいハンドラクラスのペアと、それらにトラフィックを送信する新しいルートのペアに加えて、メインハンドラには、前述の 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)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 番目かつ最後の変更セットを、以下に図で示します。

HTML テンプレートを更新する
メイン アプリケーションの更新の一部はアプリのユーザー インターフェース(UI)に影響するため、ウェブ テンプレートにも対応する変更が必要です。実際には、次の 2 つの変更が必要です。
- ファイル アップロード フォームには、ファイル、ファイル アップロード用の送信ボタン、スキップ用の送信ボタンの 3 つの入力要素が必要です。
- 対応するファイルがアップロードされている訪問には「表示」リンクを追加し、それ以外の場合は「なし」を追加して、最新の訪問の出力を更新します。
変更前:
<!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 に必要な更新を示しています。

最後に、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() を介してプログラムでアクセスできます。この値にアクセスするには、新しいインポートが必要です。たとえば、アップロードされたファイルを整理するための接頭辞として使用します。たとえば、ファイル形式で並べ替えるには、次のようにします。

たとえば、画像に対してこのような処理を実装するには、次のようなコードと、ファイル形式をチェックして目的のバケット名を選択するコードを使用します。
ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')
また、Python 標準ライブラリの imghdr モジュールなどのツールを使用して、アップロードされた画像を検証し、画像の種類を確認します。最後に、悪意のあるユーザーに備えて、アップロードのサイズを制限することをおすすめします。
これらがすべて完了したとします。アップロードされたファイルの保存場所を指定できるようにアプリを更新するにはどうすればよいですか?重要なのは、MainHandler.get の blobstore.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.py と main-gcs.py の「差分」を示した図です。

6. まとめ / クリーンアップ
このセクションでは、アプリをデプロイし、想定どおりに動作することと、出力に反映されることを確認して、この Codelab を終了します。アプリの検証後、クリーンアップ手順を実行し、次のステップを検討します。
アプリケーションをデプロイして検証する
gcloud app deploy を使用してアプリを再デプロイし、アプリが想定どおりに動作することを確認します。ユーザー エクスペリエンス(UX)はモジュール 0 のアプリとは異なります。アプリには 2 つの異なる画面が表示されます。1 つ目は、訪問ファイル アップロード フォームのプロンプトです。
エンドユーザーは、ファイルをアップロードして [送信] をクリックするか、[スキップ] をクリックして何もアップロードしないかを選択できます。どちらの場合も、結果は最新の訪問画面になります。この画面には、訪問タイムスタンプと訪問者情報の間に「表示」リンクまたは「なし」が追加されています。

この Codelab を完了し、App Engine Blobstore の使用をモジュール 0 のサンプルアプリに追加しました。コードは、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/imagesconsole.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com- 上記のストレージ リンクは、
PROJECT_IDと *LOC*ation によって異なります。たとえば、アプリが米国でホストされている場合は「us」になります。
一方、このアプリケーションや他の関連する移行 Codelab を続行せず、すべてを完全に削除する場合は、プロジェクトをシャットダウンします。
この Codelab に固有
以下のサービスは、この Codelab に固有のものです。詳細については、各プロダクトのドキュメントをご覧ください。
- App Engine Blobstore サービスは、保存データの割り当てと上限に該当します。以前のバンドル サービスの料金ページもご覧ください。
- App Engine Datastore サービスは、Cloud Datastore(Datastore モードの Cloud Firestore)によって提供されます。これにも無料枠があります。詳細については、料金ページをご覧ください。
次のステップ
次に検討すべき論理的な移行は、モジュール 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
- App Engine Blobstore サービス
- App Engine の保存データの割り当てと上限
- App Engine のドキュメント
- Python 2 App Engine(スタンダード環境)ランタイム
- Python 2 App Engine で App Engine 組み込みライブラリを使用する
- App Engine の料金と割り当てに関する情報
- 第 2 世代 App Engine プラットフォームのリリース(2018 年)
- 第 1 世代と第 2 世代のプラットフォームの比較
- レガシー ランタイムの長期サポート
- ドキュメント移行サンプルのリポジトリ
- コミュニティが提供する移行サンプルのリポジトリ
Google Cloud
- Google Cloud Platform での Python
- Google Cloud Python クライアント ライブラリ
- Google Cloud の「常に無料」枠
- Google Cloud SDK(gcloud コマンドライン ツール)
- Google Cloud に関するすべてのドキュメント
Python
- Django と Jinja2 のテンプレート システム
webapp2ウェブ フレームワークwebapp2ドキュメントwebapp2_extrasリンクwebapp2_extrasJinja2 のドキュメント
動画
ライセンス
この作業はクリエイティブ・コモンズの表示 2.0 汎用ライセンスにより使用許諾されています。