概要
Cloud クライアント ライブラリは、アプリケーションから Google Cloud API を呼び出す場合におすすめの方法です。アプリケーションで使用しているプログラミング言語の自然な規則やスタイルを使用します。Cloud クライアント ライブラリは、認証や再試行ロジックなど、サーバーとの下位レベルの通信を処理します。
Firestore は、高速、フルマネージド、かつサーバーレスの NoSQL ドキュメント データベースで、自動スケーリング、高パフォーマンス、アプリケーション開発のしやすさを念頭に置いて設計されています。
Cloud Storage は、世界中のどこからでもデータを提供、分析、アーカイブできる統合型のオブジェクト ストレージです。
このラボでは、書籍のリストを管理する Python アプリケーションを作成します。書籍の追加、編集、削除に加え、著者、タイトル、説明などのデータの収集を行うことができます。初期段階のアプリケーションでは、データがインメモリ Python 辞書に保存されるため、アプリケーションがクラッシュするとすべての書籍が失われます。
すべての書籍データを Firestore に保存するようこのアプリケーションを変更し、書籍の表紙の画像を Cloud Storage に保存する機能を追加します。
学習内容
このラボでは、次のタスクの実行方法を学びます。
- 簡単な Python Flask ウェブ アプリケーションを作成する。
- アプリケーション データを保存する Firestore データベースを作成する。
- アプリケーションで使用する画像を保存するための Cloud Storage バケットを作成する。
設定と要件
各ラボでは、新しい Google Cloud プロジェクトとリソースセットを一定時間無料で利用できます。
-
[ラボを開始] ボタンをクリックします。ラボの料金をお支払いいただく必要がある場合は、表示されるポップアップでお支払い方法を選択してください。
左側の [ラボの詳細] パネルには、以下が表示されます。
- [Google Cloud コンソールを開く] ボタン
- 残り時間
- このラボで使用する必要がある一時的な認証情報
- このラボを行うために必要なその他の情報(ある場合)
-
[Google Cloud コンソールを開く] をクリックします(Chrome ブラウザを使用している場合は、右クリックして [シークレット ウィンドウで開く] を選択します)。
ラボでリソースが起動し、別のタブで [ログイン] ページが表示されます。
ヒント: タブをそれぞれ別のウィンドウで開き、並べて表示しておきましょう。
注: [アカウントの選択] ダイアログが表示されたら、[別のアカウントを使用] をクリックします。
-
必要に応じて、下のユーザー名をコピーして、[ログイン] ダイアログに貼り付けます。
{{{user_0.username | "Username"}}}
[ラボの詳細] パネルでもユーザー名を確認できます。
-
[次へ] をクリックします。
-
以下のパスワードをコピーして、[ようこそ] ダイアログに貼り付けます。
{{{user_0.password | "Password"}}}
[ラボの詳細] パネルでもパスワードを確認できます。
-
[次へ] をクリックします。
重要: ラボで提供された認証情報を使用する必要があります。Google Cloud アカウントの認証情報は使用しないでください。
注: このラボでご自身の Google Cloud アカウントを使用すると、追加料金が発生する場合があります。
-
その後次のように進みます。
- 利用規約に同意してください。
- 一時的なアカウントなので、復元オプションや 2 要素認証プロセスは設定しないでください。
- 無料トライアルには登録しないでください。
その後、このタブで Google Cloud コンソールが開きます。
注: Google Cloud のプロダクトやサービスのリストを含むメニューを表示するには、左上のナビゲーション メニューをクリックするか、[検索] フィールドにサービス名またはプロダクト名を入力します。
Google Cloud Shell の有効化
Google Cloud Shell は、開発ツールと一緒に読み込まれる仮想マシンです。5 GB の永続ホーム ディレクトリが用意されており、Google Cloud で稼働します。
Google Cloud Shell を使用すると、コマンドラインで Google Cloud リソースにアクセスできます。
-
Google Cloud コンソールで、右上のツールバーにある [Cloud Shell をアクティブにする] ボタンをクリックします。

-
[続行] をクリックします。
環境がプロビジョニングされ、接続されるまでしばらく待ちます。接続した時点で認証が完了しており、プロジェクトに各自のプロジェクト ID が設定されます。次に例を示します。

gcloud は Google Cloud のコマンドライン ツールです。このツールは、Cloud Shell にプリインストールされており、タブ補完がサポートされています。
- 次のコマンドを使用すると、有効なアカウント名を一覧表示できます。
gcloud auth list
出力:
Credentialed accounts:
- @.com (active)
出力例:
Credentialed accounts:
- google1623327_student@qwiklabs.net
- 次のコマンドを使用すると、プロジェクト ID を一覧表示できます。
gcloud config list project
出力:
[core]
project =
出力例:
[core]
project = qwiklabs-gcp-44776a13dea667a6
注:
gcloud ドキュメントの全文については、
gcloud CLI の概要ガイド
をご覧ください。
タスク 1. 簡単な Python Flask ウェブ アプリケーションを作成してテストする
このタスクでは、書籍のリストを保存するために使用する Python アプリケーションを作成してテストします。
注: ほとんどの言語では、コードを読みやすくするためにインデントが使用されます。Python ではインデントを使用してコードのブロックを示すため、インデントが正確でなければなりません。インデントに使用するスペースの数は、一貫している必要があります。インデントにスペースとタブを混在させた場合も、問題が発生する可能性があります。このラボでは、Python のインデントに 4 つのスペースを使用します。
Cloud Shell が承認済みであることを確認する
-
Cloud Shell で次のコマンドを実行して、Cloud Shell が承認済みであることを確認します。
gcloud auth list
-
Cloud Shell を承認するように求められたら、[承認] をクリックします。
アプリのディレクトリを作成する
次のコマンドを実行して、アプリのディレクトリを作成します。
mkdir ~/bookshelf
アプリケーション ファイルは ~/bookshelf
ディレクトリ内に作成されます。
要件を指定してインストールする
Python の要件ファイルは、プロジェクトに必要な依存関係のリストを含むシンプルなテキスト ファイルです。まず、この例の要件ファイルに含める必要があるモジュールは 3 つあります。
アプリは、Flask(Python を使用してウェブ アプリケーションを簡単に設計できるウェブ フレームワーク モジュール)で作成されています。アプリケーションは、Gunicorn(Linux で動作する Python HTTP サーバー)を使用して実行します。アプリケーションからの情報は、Cloud Logging を使用してログに記録します。
-
次のコマンドを実行して、要件ファイルを作成します。
cat > ~/bookshelf/requirements.txt <<EOF
Flask==2.3.3
gunicorn==21.2.0
google-cloud-logging==3.6.0
EOF
requirements.txt ファイルには、アプリケーションで使用される Flask、Gunicorn、Google Cloud Logging のバージョンが指定されています。
-
依存関係の選択したバージョンをインストールするには、次のコマンドを実行します。
pip3 install -r ~/bookshelf/requirements.txt --user
pip は Python 用のパッケージ インストーラです。この pip3
コマンドは、Python バージョン 3 で使用するために requirements.txt ファイルで指定されたパッケージをインストールします。
書籍データベースの実装を作成する
-
書籍データベースのコードを作成するには、次のコマンドを実行します。
cat > ~/bookshelf/booksdb.py <<EOF
db = {} # グローバルなインメモリ Python 辞書。キーは常に文字列にする必要があります
next_id = 1 # 使用する次の書籍 ID
def get_next_id():
"""
次の ID を返します。取得時に自動的にインクリメントされます。
"""
global next_id
id = next_id
# 次の ID は 1 大きくなる
next_id = next_id + 1
# ID の文字列バージョンを返す
return str(id)
def read(book_id):
"""
1 冊の書籍の詳細を返します。
"""
# ID を指定してデータベースから書籍を取得する
data = db[str(book_id)]
return data
def create(data):
"""
新しい書籍を作成して、書籍の詳細を返します。
"""
# 書籍の新しい ID を取得する
book_id = get_next_id()
# 書籍データに ID を設定する
data['id'] = book_id
# 書籍をデータベースに保存する
db[book_id] = data
return data
def update(data, book_id):
"""
既存の書籍を更新し、更新された書籍の詳細を返します。
"""
# 書籍 ID は常に文字列にする必要がある
book_id_str = str(book_id)
# 書籍データに ID を追加する
data['id'] = book_id_str
# データベースの書籍を更新する
db[book_id_str] = data
return data
def delete(book_id):
"""
データベースの書籍を削除します。
"""
# データベースから書籍を削除する
del db[str(book_id)]
# 戻り値は不要
def list():
"""
データベース内のすべての書籍のリストを返します。
"""
# 空の書籍リスト
books = []
# データベース内の各アイテムを取得してリストに追加する
for k in db:
books.append(db[k])
# リストを返す
return books
EOF
書籍は、Key-Value ペアを保存するためのデータ構造である Python の辞書に保存されます。キーは一意である必要があるため、get_next_id()
関数は、呼び出されるたびに新しい ID を作成します。
read(book_id)
関数は、指定された book_id に対応するアイテムを取得します。
create(data)
関数は、新しい ID を取得して、その ID を書籍のデータに保存し、データエントリを辞書に保存することにより、書籍をデータベースに追加します。
update(data, book_id)
関数は、指定された ID を書籍のデータに保存して、データエントリを辞書に保存することにより、データベース内の書籍を更新します。
delete(book_id)
関数は、指定された book_id キーを持つデータベース内のエントリを削除します。
list()
関数は、データベース内の各書籍を含む Python リストを返します。このリストは、辞書をループして各アイテムを取得することにより取得されます。各アイテムは、id
キーを使用してその ID を保存します。
HTML テンプレート ファイルを作成する
テンプレートは、静的データと、動的データのプレースホルダの両方を含むファイルです。テンプレートをレンダリングして最終的な HTML ファイルを生成します。
-
ベース テンプレートを作成するには、次のコマンドを実行します。
mkdir ~/bookshelf/templates
cat > ~/bookshelf/templates/base.html <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>ブックシェルフ</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>
<body>
<div class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<div class="navbar-brand">ブックシェルフ</div>
</div>
<ul class="nav navbar-nav">
<li><a href="/">書籍</a></li>
</ul>
</div>
</div>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
EOF
アプリケーションの各ページは、基本的なレイアウトは同じですが、本文は異なります。3 つのメイン テンプレート(リスト、ビュー、フォーム)はそれぞれ、ページの中央に配置するコンテンツを指定することによりこのベース テンプレートを拡張します。
-
リスト テンプレートを作成するには、次のコマンドを実行します。
cat > ~/bookshelf/templates/list.html <<EOF
{% extends "base.html" %}
{% block content %}
<h3>書籍</h3>
<a href="/books/add" class="btn btn-success btn-sm">
<i class="glyphicon glyphicon-plus"></i>
書籍の追加
</a>
{% for book in books %}
<div class="media">
<a href="/books/{{book.id}}">
<div class="media-body">
<h4>{{book.title}}</h4>
<p>{{book.author}}</p>
</div>
</a>
</div>
{% else %}
<p>書籍が見つかりません</p>
{% endfor %}
{% endblock %}
EOF
リスト テンプレートは、テンプレートに送信された書籍のリストをループ処理し、各書籍のタイトル、著者、リンクを表示します。リンクをクリックすると、その書籍のビューページに移動します。
-
ビュー テンプレートを作成するには、次のコマンドを実行します。
cat > ~/bookshelf/templates/view.html <<EOF
{% extends "base.html" %}
{% block content %}
<h3>書籍</h3>
<div class="btn-group">
<a href="/books/{{book.id}}/edit" class="btn btn-primary btn-sm">
<i class="glyphicon glyphicon-edit"></i>
書籍の編集
</a>
<a href="/books/{{book.id}}/delete" class="btn btn-danger btn-sm">
<i class="glyphicon glyphicon-trash"></i>
書籍の削除
</a>
</div>
<div class="media">
<div class="media-body">
<h4 class="book-title">
{{book.title}}
<small>{{book.publishedDate}}</small>
</h4>
<h5 class="book-author">By {{book.author|default('Unknown', True)}}</h5>
<p class="book-description">{{book.description}}</p>
</div>
</div>
{% endblock %}
EOF
ビュー テンプレートには、書籍の詳細(タイトル、著者、出版日、説明)が表示されます。また、書籍を編集するボタン(ユーザーをフォーム テンプレートに送信します)と、書籍を削除するボタン(書籍を削除し、ユーザーを書籍リストに戻します)の 2 つのボタンもあります。
-
フォーム テンプレートを作成するには、次のコマンドを実行します。
cat > ~/bookshelf/templates/form.html <<EOF
{# [START form] #}
{% extends "base.html" %}
{% block content %}
<h3>書籍の {{action}}</h3>
<form method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="title">タイトル</label>
<input type="text" name="title" id="title" value="{{book.title}}" class="form-control"/>
</div>
<div class="form-group">
<label for="author">著者</label>
<input type="text" name="author" id="author" value="{{book.author}}" class="form-control"/>
</div>
<div class="form-group">
<label for="publishedDate">出版日</label>
<input type="text" name="publishedDate" id="publishedDate" value="{{book.publishedDate}}" class="form-control"/>
</div>
<div class="form-group">
<label for="description">説明</label>
<textarea name="description" id="description" class="form-control">{{book.description}}</textarea>
</div>
<button type="submit" class="btn btn-success">保存</button>
</form>
{% endblock %}
{# [END form] #}
EOF
フォーム テンプレートには 2 つの目的があります。書籍を更新すると、テンプレートに現在の書籍の詳細が編集可能なボックスで表示されます。[保存] ボタンをクリックすると、更新されたフォーム フィールドがデータベースに保存されます。
新しい書籍を作成すると、最初は書籍の詳細のボックスが空になっています。[保存] ボタンをクリックすると、書籍のデータが新しい書籍としてデータベースに保存されます。
main コードファイルを作成する
-
main コードファイルを作成するには、次のコマンドを実行します。
cat > ~/bookshelf/main.py <<EOF
from flask import current_app, Flask, redirect, render_template
from flask import request, url_for
import logging
from google.cloud import logging as cloud_logging
import booksdb
app = Flask(__name__)
app.config.update(
SECRET_KEY='secret', # コード内の SECRET_KEY を本番環境アプリに保存しない
MAX_CONTENT_LENGTH=8 * 1024 * 1024,
)
app.debug = True
app.testing = False
# ロギングを構成
if not app.testing:
logging.basicConfig(level=logging.INFO)
# Cloud Logging ハンドラーをルートロガーに添付
client = cloud_logging.Client()
client.setup_logging()
def log_request(req):
"""
ログリクエスト
"""
current_app.logger.info('REQ: {0} {1}'.format(req.method, req.url))
@app.route('/')
def list():
"""
すべての書籍を表示します。
"""
log_request(request)
# 書籍のリストを取得する
books = booksdb.list()
# 書籍のリストをレンダリングする
return render_template('list.html', books=books)
@app.route('/books/<book_id>')
def view(book_id):
"""
指定した書籍の詳細を表示します。
"""
log_request(request)
# 特定の書籍を取得する
book = booksdb.read(book_id)
# 書籍の詳細をレンダリングする
return render_template('view.html', book=book)
@app.route('/books/add', methods=['GET', 'POST'])
def add():
"""
GET の場合は、新しい書籍の詳細を収集するためのフォームを表示します。
POST の場合は、指定されたフォームに基づいて新しい書籍を作成します。
"""
log_request(request)
# フォームが送信された場合は詳細を保存する
if request.method == 'POST':
# フォームから書籍の詳細を取得する
data = request.form.to_dict(flat=True)
# 書籍を追加する
book = booksdb.create(data)
# 書籍の詳細をレンダリングする
return redirect(url_for('.view', book_id=book['id']))
# 書籍を追加するためのフォームをレンダリングする
return render_template('form.html', action='Add', book={})
@app.route('/books/<book_id>/edit', methods=['GET', 'POST'])
def edit(book_id):
"""
GET の場合は、書籍の更新された詳細を収集するためのフォームを表示します。
POST の場合は、指定されたフォームに基づいて書籍を更新します。
"""
log_request(request)
# 既存の書籍の詳細を読み取る
book = booksdb.read(book_id)
# フォームが送信された場合は詳細を保存する
if request.method == 'POST':
# フォームから書籍の詳細を取得する
data = request.form.to_dict(flat=True)
# 書籍を更新する
book = booksdb.update(data, book_id)
# 書籍の詳細をレンダリングする
return redirect(url_for('.view', book_id=book['id']))
# 書籍を更新するためのフォームをレンダリングする
return render_template('form.html', action='Edit', book=book)
@app.route('/books/<book_id>/delete')
def delete(book_id):
"""
指定された書籍を削除し、書籍リストに戻ります。
"""
log_request(request)
# 書籍を削除する
booksdb.delete(book_id)
# 残りの書籍のリストをレンダリングする
return redirect(url_for('.list'))
# ローカルで実行する場合のみ使用
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)
EOF
main.py はアプリケーションのエントリ ポイントです。このファイルによって、ウェブ URL ルーティングの指定、テンプレートのレンダリング、書籍データベースの管理を行う Flask アプリケーションが実装されます。コードにはコメントがたくさん含まれているので、自由にテストしてみてください。
アプリケーションを試す
-
bookshelf ディレクトリの内容を確認するには、次のコマンドを実行します。
cd ~
ls -R bookshelf
次のような 2 つの Python ファイル、1 つの要件ファイル、4 つのテンプレート ファイルを含むリストが表示されます。
bookshelf:
booksdb.py main.py requirements.txt templates
bookshelf/templates:
base.html form.html list.html view.html
-
Gunicorn HTTP サーバーを実行するには、次のコマンドを実行します。
cd ~/bookshelf; ~/.local/bin/gunicorn -b :8080 main:app
ファイルを正常に作成した場合、アプリケーションはポート 8080 でホストされます。
-
ウェブブラウザでアプリケーションを実行するには、[ウェブでプレビュー] をクリックし、[ポート 8080 でプレビュー] を選択します。

ブラウザで新しいタブが開き、アプリケーションが実行されます。これはルート URL であり、既存のすべての書籍のリストが表示されます。まだ書籍はありません。
注: Cloud Shell を承認するよう求められたら、[承認] をクリックします。
-
[アプリケーション] タブで、[+ 書籍の追加] クリックします。
-
(実在または架空の)書籍のタイトル、著者、日付、説明を入力して、[保存] をクリックします。
表示ページに戻ります。そこに、書籍の詳細が表示されています。該当するボタンをクリックして、書籍を編集または削除できます。
-
ページ上部の [書籍] をクリックします。
表示ページに戻ります。追加した書籍がすべてリストに表示されています。
必要に応じて、アプリケーションを操作して書籍を追加、編集、削除できます。
-
Cloud Shell でアプリケーションを終了するには、Ctrl+C
キーを押します。
[進行状況を確認] をクリックして、目標に沿って進んでいることを確認します。
簡単な Python Flask ウェブ アプリケーションを作成してテストする
タスク 2. 書籍データベースに Firestore を使用する
このタスクでは、Firestore データベースを使用して書籍データを保存します。
アプリケーションでは現在、インメモリ Python 辞書が使用されています。アプリを終了したりアプリがクラッシュしたりすると、書籍は失われます。永続的なデータベースとして Firestore を使用する方が安全です。
Firestore データベースを作成する
-
ナビゲーション メニュー(
)で、[すべてのサービスを表示] > [データベース] の順に移動します。
-
[Firestore] の横にある [固定] アイコンをクリックしてナビゲーション メニューに追加し、[Firestore] をクリックします。
-
[Firestore データベースを作成] をクリックします。
-
[構成オプション] で [Firestore ネイティブ] を選択します。
-
[ロケーション タイプ] で [リージョン] をクリックします。
このラボでは、Firestore に [マルチリージョン] を使用しないでください。
-
[リージョン] で を選択してから、[データベースを作成] をクリックします。
注: ロケーションには、正確なリージョン を選択してください。プルダウン リストにこのリージョンが表示されない場合は、[ロケーション タイプ] で [マルチリージョン] ではなく [リージョン] が正しく選択されていることを確認します。
プルダウン リストにリージョンが表示されない場合は、キャンセルして前のページに戻り、データベースの作成プロセスをもう一度試してください。
データベースの作成には数分かかる場合があります。Firestore のコレクションは、ドキュメントをコレクションに追加すると自動的に作成されるため、今すぐ書籍コレクションを作成する必要はありません。
Firestore データベースを作成すると、Firestore API が有効になります。
アプリケーションに変更を加えて Firestore 用に Python クライアントを必須にする
Firestore API の Python クライアントは、アプリケーションから Firestore データにアクセスするために使用します。このクライアントのパッケージ名は google-cloud-firestore で、使用するバージョンは 2.12.0 です。
- アプリケーションに変更を加えて、google-cloud-firestore パッケージのバージョン 2.12.0 を必須にします。
注: nano、vi、Cloud Code エディタなど、任意のファイル エディタを使用できます。
-
更新された依存関係をインストールするには、次のコマンドを実行します。
pip3 install -r ~/bookshelf/requirements.txt --user
書籍データを Firestore に保存するようアプリケーションを変更する
アプリケーションに google-cloud-firestore パッケージが必須になったので、書籍データベースの実装に使用することができます。
Firestore では、書籍データは Firestore データベースに保存されます。一意であることが保証されたフィールドがデータにあれば、そのフィールドを Firestore の ID として使用することもできます。今回の場合は、使用しているデータに一意のフィールドがありません。Firestore では、新しい書籍を作成すると、ID が自動的に作成されます。
Firestore クライアントの使用例を次に示します。
from google.cloud import firestore
# クライアントを取得する
db = firestore.Client()
# 新しいドキュメントを作成する
data = {"name": "Sue", "role": "treasurer"}
member_ref = db.collection("members").document()
member_ref.set(data)
member_id = member_ref.get().id
# ドキュメントを取得する
member_ref = db.collection("members").document(member_id)
member = member_ref.get()
if member.exists:
print(f"ドキュメントのデータ: {member.to_dict()}")
else:
print("メンバーが見つかりません。")
# ドキュメントを更新する
new_data = {"name": "Sue", "role": "president"}
member_ref = db.Collection("members").document(member_id)
member_ref.set(new_data)
# すべてのドキュメントを順番に取得する
members = db.collection("members").order_by("name").stream()
for member in members:
print(f"{member.id} => {member.to_dict()}")
# メンバーを削除する
member_ref = db.Collection("members").document(member_id)
member_ref.delete()
次に、Firestore を使用するよう書籍データベースの実装を変更します。
注: Python のインデントには、4 つのスペースを使用する必要があります。
現在の実装では、db
という名前のグローバル変数が使用されています。これは、インメモリ Python 辞書です。さらに、変数 next_id
と関数 get_next_id()
を使用して、辞書に保存されるアイテムの ID が作成されます。
ID の作成を管理するのは Firestore です。コレクション名は books
にします。作成した ID を、呼び出し元に返す前に、書籍の詳細を含む Python 辞書に追加する必要があります。
注: ヒントでは、変更が必要なコードが隠れています。コードを自分で記述することも、ヒントボタンをクリックしてコードを取得し、追加することもできます。
-
ファイル エディタで、~/bookshelf/booksdb.py
ファイルを開きます。
-
ファイルから次の行を削除します。
db = {} # グローバルなインメモリ Python 辞書。キーは常に文字列にする必要があります
next_id = 1 # 使用する次の書籍 ID
def get_next_id():
"""
次の ID を返します。取得時に自動的にインクリメントされます。
"""
global next_id
id = next_id
# 次の ID は 1 つ大きくなる
next_id = next_id + 1
# ID の文字列バージョンを返す
return str(id)
この実装では、インメモリ データベースと ID 作成機能は必要ありません。
-
Firestore クライアントをインポートするコードを追加します。
- Firestore ドキュメントを辞書に変換する関数を作成します。
書籍を呼び出し元に返すとき、呼び出し元が Firestore ドキュメントを理解する必要がないようにします。書籍データベース関数のインターフェースは変更されないままのため、この実装では引き続き書籍を Python 辞書として返したり受け取ったりします。
書籍の ID は、返される前に辞書に追加する必要があります。
Firestore ドキュメントを入力パラメータとして受け取って辞書を返す、document_to_dict()
という関数を作成します。辞書にはドキュメント内の Key-Value ペアが含まれており、キー id
の値としてドキュメント ID も返されます。ドキュメントが存在しない場合は、None
を返す必要があります。
-
read()
関数を変更して、Firestore コレクション books
から書籍を取得します。更新された関数は、document_to_dict()
関数を呼び出す必要があります。
-
create()
関数を変更して、Firestore コレクション books
に書籍を作成します。
-
update()
関数を変更して、Firestore コレクション books
の書籍を更新します。
-
delete()
関数を変更して、Firestore コレクション books
から書籍を削除します。
-
list()
関数を変更して、Firestore コレクション books
のすべての書籍のリストを、タイトル順に並べ替えて返します。
操作は以上となります。booksdb.py
を更新することで、呼び出しコードを変更せずに、Firestore をデータベースとして使用するようアプリを変更します。
更新されたアプリケーションをテストする
-
Cloud Shell で、次のコマンドを実行します。
cd ~/bookshelf; ~/.local/bin/gunicorn -b :8080 main:app
ファイルを正常に更新した場合、アプリケーションはポート 8080 でホストされます。
注: インポート エラーが発生した場合は、pip3 を使用して、google.cloud.firestore パッケージが含まれる最新の要件をインストールしたことを確認してください。
注: Cloud Shell の承認を求められた場合は、[承認] をクリックします。
-
ウェブブラウザでアプリケーションを実行するには、[ウェブでプレビュー] をクリックし、[ポート 8080 でプレビュー] を選択します。
データベースには書籍がありません。これは、書籍がインメモリ辞書に保存されていたためです。
-
[アプリケーション] タブで、[+ 書籍の追加] クリックします。
-
フォームに次の情報を入力します。
フィールド |
値 |
タイトル |
ハムレット |
著者 |
ウィリアム シェイクスピア |
出版日 |
1603 |
説明 |
王子は生、死、復讐について考えていますが、ほとんどは駄洒落を言っているだけです。 |
-
[保存] をクリックします。
表示ページに戻ります。そこに、書籍の詳細が表示されています。
-
ページ上部の [書籍] をクリックします。
リストページに戻ります。リストに「ハムレット」が表示されています。
注: 必要に応じて他の書籍を追加できますが、「ハムレット」は次のタスクで必要となるため、変更しないでください。
-
Google Cloud コンソールのナビゲーション メニュー(
)で、[Firestore] をクリックします。
注: Firestore コンソール ページをすでに表示している場合でも、データベースを表示するには、そのページに再度移動する必要が生じることがあります。
-
[(デフォルト)] をクリックします。
「ハムレット」のドキュメントが書籍コレクションに作成されていることを確認します。
-
Cloud Shell でアプリケーションを終了するには、Ctrl+C
キーを押します。
タスク 2 のトラブルシューティング
アプリケーションで発生したエラーが現在、Cloud Shell に出力されています。アプリケーションが機能しなかったり、Firestore にデータを保存できなかったりした場合は、エラー情報を使用してデバッグし、問題を修正します。
書籍データベース コードが機能しない場合は、次のヒントのコマンドを実行して、booksdb.py ファイル全体を機能するコードに置き換えます。
[進行状況を確認] をクリックして、目標に沿って進んでいることを確認します。
書籍データベースに Firestore を使用する
タスク 3. 書籍の表紙に Cloud Storage を使用する
このタスクでは、Cloud Storage を使用して書籍の表紙の画像を保存します。
データベースは通常、画像を保存するのに適した場所ではありません。最終的にアプリケーションを別の場所でホストすることになるため、Cloud Shell にファイルを保存することはできません。Cloud Storage は、共有するアセットを保存するのに最適なソリューションです。Cloud Storage は、Google Cloud 向けのメインのオブジェクト ストアです。
Cloud Storage バケットを作成する
Cloud Storage を使用するには、データを保存する基本的なコンテナである Cloud Storage バケットを作成する必要があります。
-
Google Cloud コンソールのナビゲーション メニュー(
)で、[Cloud Storage] > [バケット] をクリックします。
-
[+ 作成] をクリックします。
-
バケット名には、以下を使用します。
{{{ project_0.project_id | project_id }}}-covers
-
[続行] をクリックします。
-
[リージョン] を選択します。
-
を選択します。
-
[続行] をクリックします。
-
ストレージ クラスは変更せず、[続行] をクリックします。
-
[このバケットに対する公開アクセス禁止を適用する] をオフにします。
[アクセス制御] は [均一] のままにします。この設定では、バケットに追加されたすべてのオブジェクトに対してバケットレベルの権限が使用されます。
-
[作成] をクリックします。
アプリケーションで表紙を表示できるようにするには、すべてのユーザーにバケット内のオブジェクトの読み取り権限を付与する必要があります。
-
[権限] タブを選択し、[アクセスを許可] をクリックします。
-
[新しいプリンシパル] に「allUsers」と入力します。
-
ロールとして [Cloud Storage レガシー] > [Storage レガシー オブジェクト読み取り] を選択します。
注: [Cloud Storage] > [Storage オブジェクト閲覧者] ロールには、バケット内のオブジェクトを一覧表示するための権限が含まれていますが、このアプリケーションでは必要ありません。[Cloud Storage レガシー] > [Storage レガシー オブジェクト読み取り] ロールではオブジェクトの取得のみが許可されるため、このユースケースに適しています。
-
[保存] をクリックします。
-
確認を求められたら、[一般公開アクセスを許可] をクリックします。
要件ファイルを更新する
-
Cloud Shell のファイル エディタで ~/bookshelf/requirements.txt
ファイルを開きます。
-
~/bookshelf/requirements.txt
ファイルに次の行を追加します。
google-cloud-storage==2.10.0
requirements.txt ファイルは次のようになります。
Flask==2.3.3
gunicorn==21.2.0
google-cloud-logging==3.6.0
google-cloud-firestore==2.12.0
google-cloud-storage==2.10.0
-
ファイルを保存します。
-
更新された依存関係をインストールするには、Cloud Shell で次のコマンドを実行します。
pip3 install -r ~/bookshelf/requirements.txt --user
画像を Cloud Storage にアップロードするコードを作成する
storage.py
ファイルには、表紙の画像を Cloud Storage にアップロードするコードが含まれています。
-
storage.py
ファイルを作成するには、次のコマンドを実行します。
cat > ~/bookshelf/storage.py <<EOF
from __future__ import absolute_import
import datetime
import os
from flask import current_app
from werkzeug.exceptions import BadRequest
from werkzeug.utils import secure_filename
from google.cloud import storage
def _check_extension(filename, allowed_extensions):
"""
ファイル名の拡張子が許可されているかどうかを検証します。
"""
_, ext = os.path.splitext(filename)
if (ext.replace('.', '') not in allowed_extensions):
raise BadRequest(
'{0} has an invalid name or extension'.format(filename))
def _safe_filename(filename):
"""
Cloud Storage 内の既存のオブジェクトと競合する可能性が低い安全なファイル名を生成します。
filename.ext は filename-YYYY-MM-DD-HHMMSS.ext に変換されます
"""
filename = secure_filename(filename)
date = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H%M%S")
basename, extension = filename.rsplit('.', 1)
return "{0}-{1}.{2}".format(basename, date, extension)
def upload_file(file_stream, filename, content_type):
"""
指定された Cloud Storage バケットにファイルをアップロードし、新しいオブジェクトの公開 URL を返します。
"""
_check_extension(filename, current_app.config['ALLOWED_EXTENSIONS'])
filename = _safe_filename(filename)
# バケットの名前を構築する
bucket_name = os.getenv('GOOGLE_CLOUD_PROJECT') + '-covers'
client = storage.Client()
# バケット オブジェクトを作成する
bucket = client.bucket(bucket_name)
# 指定されたパスのバケットにオブジェクトを作成する
blob = bucket.blob(filename)
# 文字列の内容をオブジェクトにアップロードする
blob.upload_from_string(
file_stream,
content_type=content_type)
}
# オブジェクトの公開 URL を取得する。これは、画像への参照をデータベースに保存し、
# アプリに画像を表示するために使用される
url = blob.public_url
return url
def upload_image(img):
"""
ユーザーがアップロードしたファイルを Cloud Storage にアップロードし、その公開 URL を取得します。
"""
if not img:
return None
public_url = upload_file(
img.read(),
img.filename,
img.content_type
)
return public_url
EOF
upload_file()
関数は、ファイル ストリーム、ファイル名、ファイルのコンテンツ タイプを受け入れます。ファイル名の拡張子はまず、後続の手順で作成される承認済み拡張子リストと照合して検証されます。そのとき、ファイル名に現在の日時が付加されます。したがって、アップロード時に同じファイル名を使用する書籍の画像が競合することはありません。関数の残りの部分は Cloud Storage とやり取りします。
バケット名はまず、プロジェクト ID を使用して構築されます。
bucket_name = os.getenv('GOOGLE_CLOUD_PROJECT') + '-covers'
次に、指定されたバケットとファイル名のオブジェクトへの参照が作成され、画像ファイルの内容がアップロードされます。
client = storage.Client()
# バケット オブジェクトを作成する
bucket = client.bucket(bucket_name)
# 指定されたパスのバケットにオブジェクトを作成する
blob = bucket.blob(filename)
次に、ファイルデータが Cloud Storage にアップロードされ、ウェブアプリに表示できるよう公開されます。
# 文字列の内容をオブジェクトにアップロードする
blob.upload_from_string(
file_stream,
content_type=content_type)
次に、URL が返され、書籍データベースに保存されて画像の表示に使用されます。
書籍の画像を表示するようテンプレートを変更する
テンプレートが特定のデータでレンダリングされ、ウェブページが生成されます。
ベース テンプレートを変更する必要はありませんが、コンテンツ テンプレート(フォーム、リスト、ビュー)を変更して、書籍の表紙を表示およびアップロードする必要があります。
-
ファイル エディタで、~/bookshelf/templates/form.html
ファイルを開きます。
画像ファイルを収集するようフォームを変更する必要があります。
-
フォームの下部付近の [保存] ボタン コントロールの上に、次の行を追加します。
<div class="form-group">
<label for="image">表紙の画像</label>
<input type="file" name="image" id="image" class="form-control"/>
</div>
<div class="form-group hidden">
<label for="imageUrl">表紙の画像の URL</label>
<input type="text" name="imageUrl" id="imageUrl" value="{{book.imageUrl}}" class="form-control"/>
</div>
image 入力では、ユーザーが画像ファイルをアップロードできます。さらに、現在の画像も表示されます。imageUrl 入力は非表示ですが、画像の公開 URL が保存され、それが書籍のデータベース エントリに追加されます。
フォームは次のようになります。
<form method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="title">タイトル</label>
<input type="text" name="title" id="title" value="{{book.title}}" class="form-control"/>
</div>
<div class="form-group">
<label for="author">著者</label>
<input type="text" name="author" id="author" value="{{book.author}}" class="form-control"/>
</div>
<div class="form-group">
<label for="publishedDate">出版日</label>
<input type="text" name="publishedDate" id="publishedDate" value="{{book.publishedDate}}" class="form-control"/>
</div>
<div class="form-group">
<label for="description">説明</label>
<textarea name="description" id="description" class="form-control">{{book.description}}</textarea>
</div>
<div class="form-group">
<label for="image">表紙の画像</label>
<input type="file" name="image" id="image" class="form-control"/>
</div>
<div class="form-group hidden">
<label for="imageUrl">表紙の画像の URL</label>
<input type="text" name="imageUrl" id="imageUrl" value="{{book.imageUrl}}" class="form-control"/>
</div>
<button type="submit" class="btn btn-success">保存</button>
</form>
-
ファイルを保存します。
-
ファイル エディタで、~/bookshelf/templates/view.html
ファイルを開きます。
書籍の画像は、書籍情報の左側に表示されます。
-
<div class="media">
行の後に、次の行を追加します。
<div class="media-left">
{% if book.imageUrl %}
<img class="book-image" src="{{book.imageUrl}}" width="128" height="192" alt="book cover">
{% else %}
<img class="book-image" src="https://storage.googleapis.com/cloud-training/devapps-foundations/no-cover.png" width="128" height="192" alt="no book cover">
{% endif %}
</div>
これにより、書籍の詳細の左側に新しいセクションが追加されます。書籍の画像が存在する場合、このセクションにその画像が表示されます。そうでない場合は、プレースホルダ画像が表示されます。
media
div は次のようになります。
<div class="media">
<div class="media-left">
{% if book.imageUrl %}
<img class="book-image" src="{{book.imageUrl}}" width="128" height="192" alt="book cover">
{% else %}
<img class="book-image" src="https://storage.googleapis.com/cloud-training/devapps-foundations/no-cover.png" width="128" height="192" alt="no book cover">
{% endif %}
</div>
<div class="media-body">
<h4 class="book-title">
{{book.title}}
<small>{{book.publishedDate}}</small>
</h4>
<h5 class="book-author">著者: {{book.author|default('Unknown', True)}}</h5>
<p class="book-description">{{book.description}}</p>
</div>
</div>
-
ファイルを保存します。
-
ファイル エディタで、~/bookshelf/templates/list.html
ファイルを開きます。
リスト内の各書籍の左側に書籍の画像が表示されます。
-
<a href="/books/{{book.id}}">
行の後に、次の行を追加します。
<div class="media-left">
{% if book.imageUrl %}
<img src="{{book.imageUrl}}" width="128" height="192" alt="book cover">
{% else %}
<img src="https://storage.googleapis.com/cloud-training/devapps-foundations/no-cover.png" width="128" height="192" alt="no book cover">
{% endif %}
</div>
ビュー テンプレートに追加したのと同じコードが含まれています。
main.py を変更する
main コードファイルは、フォームが送信されたときに画像を Cloud Storage にアップロードし、画像の URL を書籍データに追加する必要があります。
-
ファイル エディタで、ファイル ~/bookshelf/main.py
を開きます。
-
booksdb
のインポートの後に、次の行を追加します。
import storage
-
インポート行の後に、upload_image_file()
メソッドを追加します。
def upload_image_file(img):
"""
ユーザーがアップロードしたファイルを Cloud Storage にアップロードし、
その一般公開 URL を取得します。
"""
if not img:
return None
public_url = storage.upload_file(
img.read(),
img.filename,
img.content_type
)
current_app.logger.info(
'Uploaded file %s as %s.', img.filename, public_url)
return public_url
この関数は、storage.py
で作成したライブラリ関数を呼び出して、表紙ファイルを Cloud Storage にアップロードします。アップロードしたファイルの公開 URL を返します。
-
app.config.update
セクションに次の行を追加します。
ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif']),
これにより、書籍の表紙をアップロードする際に許可される拡張子が制限されます。構成は次のようになります。
app.config.update(
SECRET_KEY='secret',
MAX_CONTENT_LENGTH=8 * 1024 * 1024,
ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif']),
)
-
add()
関数で、data = request.form.to_dict(flat=True)
行の後に次のコードを追加します。
image_url = upload_image_file(request.files.get('image'))
# 画像がアップロードされた場合は、その画像を参照するようデータを更新します。
if image_url:
data['imageUrl'] = image_url
このコードは、upload_image_file 関数を呼び出して、フォームに追加された画像をアップロードします。さらに、画像の URL を書籍データに追加します。
-
edit()
関数で、data = request.form.to_dict(flat=True)
行の後に次のコードを追加します。
image_url = upload_image_file(request.files.get('image'))
# 画像がアップロードされた場合は、その画像を参照するようデータを更新します。
if image_url:
data['imageUrl'] = image_url
これは、add()
関数に追加したのと同じコードです。
-
ファイルを保存します。
更新されたアプリケーションをテストする
-
Cloud Shell で、次のコマンドを実行します。
cd ~/bookshelf; ~/.local/bin/gunicorn -b :8080 main:app
ファイルを正常に更新した場合、アプリケーションはポート 8080 でホストされます。
注: Cloud Shell を承認するよう求められたら、[承認] をクリックします。
-
ウェブブラウザでアプリケーションを実行するには、[ウェブでプレビュー] をクリックし、[ポート 8080 でプレビュー] を選択します。
アプリケーションが Firestore を使用したときに追加された書籍が表示されます。画像の URL がデータベースに追加されていなかったため、各書籍にプレースホルダの表紙の画像が表示されます。
-
[ハムレット] をクリックし、[書籍の編集] をクリックします。
-
この書籍「ハムレット」の表紙の画像を右クリックし、hamlet.png という名前でコンピュータに保存します。

-
ブックシェルフ アプリで [表紙の画像] の [ファイルを選択] をクリックします。
-
ダウンロードしたファイル(hamlet.png)を選択し、[開く] をクリックします。
-
[保存] をクリックします。
書籍「ハムレット」の画像が表示されます。
-
Google Cloud コンソールのナビゲーション メニュー(
)で、[Cloud Storage] > [バケット] をクリックします。
-
バケット名(-covers)をクリックします。
表紙のが画像が Cloud Storage に保存されます。
[進行状況を確認] をクリックして、目標に沿って進んでいることを確認します。
書籍の表紙に Cloud Storage を使用する
お疲れさまでした
Cloud Shell でアプリケーションをテストできました。Cloud クライアント ライブラリを使用してデータを Firestore に保存し、画像を Cloud Storage に保存するようアプリケーションを変更しました。
次のステップと詳細情報
ラボを終了する
ラボが完了したら、[ラボを終了] をクリックします。ラボで使用したリソースが Google Cloud Skills Boost から削除され、アカウントの情報も消去されます。
ラボの評価を求めるダイアログが表示されたら、星の数を選択してコメントを入力し、[送信] をクリックします。
星の数は、それぞれ次の評価を表します。
- 星 1 つ = 非常に不満
- 星 2 つ = 不満
- 星 3 つ = どちらともいえない
- 星 4 つ = 満足
- 星 5 つ = 非常に満足
フィードバックを送信しない場合は、ダイアログ ボックスを閉じてください。
フィードバックやご提案の送信、修正が必要な箇所をご報告いただく際は、[サポート] タブをご利用ください。
Copyright 2024 Google LLC All rights reserved. Google および Google のロゴは、Google LLC の商標です。その他すべての社名および製品名は、それぞれ該当する企業の商標である可能性があります。