2014/10/03
Google Compute Engineが10%OFFに
嬉しいニュースですね。6ヶ月前の発表どおり、ムーアの法則に従った結果の値下げのようです。
ハードの値段が下がった結果とはいえ、自社でコストカットできた結果を、こんなに早いタイミングでユーザーに還元する企業ってそんなに無いのではないでしょうか。
この調子だと、来年の春にはまた嬉しいニュースが聞けるかもしれませんね。その時はGoogle App Engineも値下げてくれると嬉しいなぁ。
あとは、良いサービス作るだけだ。。。(2回目)
2014/09/25
HTTPロードバランサが公開プレビューに
限定プレビュー版だったGoogle Cloud Platform のロードバランサが誰でも使える公開プレビュー版になったようです。
このロードバランサの凄いところは、Google検索やGmail、Youtube等の主要Googleサービスと同じインフラに乗っかっているので、圧倒的なパフォーマンスを低価格(約1800円/月)で提供できているという事。また、ロードバランサに対しての監視や設定もRESTful APIが用意されているので、アプリケーション側から動的&柔軟な対応もできる。
リリースされれば、今後Google Cloud Platformの目玉のひとつになるかもしれない。
それにしても、インフラのコストはどんどん安くなるなぁ・・・あとはこのロードバランサに不満を持つほどのWebサービスを作るだけだ。
以下、関連記事まとめ。
Googleの中の人による解説: GoogleのHTTPロードバランサーの破壊力があり過ぎる #gcpja
国内メディア記事: Google Cloud PlatformのHTTPロードバランサーがオープンプレビューに。ウォームアップなしで100万リクエスト/秒に対応する性能、1つのグローバルIPで複数リージョンに分配
2014/09/24
Google Custom Search を使って評価の低い(高い)レビューを検索する
Google Custom Searchの公式ドキュメントによると、構造化データの情報を指定して、さらにフィルタリングができるみたい。
To filter by attribute, add a more:pagemap:TYPE-NAME:VALUE operator to a search query.
とあるので、検索キーワードに、more:pagemap:属性名:属性値を追加すればOK。
APIの戻り値を見てみると、価格.comのレビューの評価数の属性値は、review-ratingstarsでとれるので、例えば、評価数3のレビューのみ取得したい場合は、検索キーワードmore:pagemap:review-ratingstars:3.0とすることで、フィルタリングすることができる。また、値をカンマで区切る事でOR条件も指定できる。例えば、評価4と5も含めたい場合はmore:pagemap:review-ratingstars:3.0,4.0,5.0と書ける。前回のフォームに、評価数を指定するプルダウンを設置してみた。ソースはこんな感じ。
入力フォーム
<form class="form-horizontal" role="form" action="search" method="post">
<div class="form-group">
<label class="col-sm-2 control-label" for="keyword">キーワード</label>
<div class="col-sm-10">
<input type="text" id="keyword" name="keyword" class="form-control" >
</div>
</div>
<div class="form-group">
<label for="stars" class="col-sm-2 control-label">評価</label>
<div class="col-sm-10">
<select class="form-control" id="stars" name="stars">
<option value="">指定しない</option>
<option value="1.0">★☆☆☆☆</option>
<option value="2.0">★★☆☆☆</option>
<option value="3.0">★★★☆☆</option>
<option value="4.0">★★★★☆</option>
<option value="5.0">★★★★★</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary">検索</button>
</form>
リクエストハンドラ
#!-*- coding:utf-8 -*-
#!/usr/bin/env python
import json
import logging
import urllib
from google.appengine.api import urlfetch
from lib.controller import *
class Top(Controller):
def get(self):
self.draw_template('front/cse/top.html')
class Search(Controller):
def post(self):
# キーワードを得る
keyword = self.request.get('keyword')
# 評価が指定されていればキーワードに追加
stars = self.request.get('stars')
if stars:
keyword += ' ' + 'more:pagemap:review-ratingstars:%s' % stars
# urlエンコード
keyword = urllib.quote(keyword.encode('utf-8'))
# リクエストパラメータ組み立て
url = 'https://www.googleapis.com/customsearch/v1'
url += '?key=%s' % 'YOUR_API_KEY'
url += '&cx=%s' % '000122034385005128488:etmrnaufuww'
url += '&q=%s' % keyword
# 検索
result = urlfetch.fetch(url)
logging.info(url)
items = list()
if not 200 <= result.status_code <= 299:
# エラー
logging.error('google custom search error: %s' % str(result.status_code))
logging.error(result.content)
else:
# 結果を得る
content_dict = json.loads(result.content)
items = content_dict.get('items', list())
self.set_template_value('items', items)
self.draw_template('front/cse/search.html')
url_map = [
('/cse/top', Top),
('/cse/search', Search),
]
application = webapp.WSGIApplication(url_map, debug=True)
ちなみにAPI越しじゃなくても、Custom Search の追加キーワードでも指定できるみたい。ただ、この属性名がreview-ratingstarsとなっているのが、Google独自の命名なのか、構造化ルールでの命名なのかが不明。。。このあたりはもう少し調べてみます。
Google App Engine SDK Version 1.9.12 リリース
Google App Engine SDK のバージョン 1.9.12 がダウンロード可能になっています。
が、、、リリースノートは更新されていない様子。更新されたら追記します。
【追記】
リリースノート更新されてました。pythonは以下の2点が更新されたようです。
- libxsltライブラリのバージョンがv1.1.22からv1.1.28に。
- NDBでクエリを実行した時に「AugmentedQuery’ object has no attribute ‘filter_predicate’」というエラーが発生する問題を対応。
Python版については、ここしばらくマイナーアップデートが続いていますね。そろそろアジアリージョン来て欲しい!
2014/09/16
Google App Engine Version 1.9.11 リリース(Python)
4日前ですが、Google App Engine SDK Version 1.9.11 がリリースされています。今後はリリース情報も掲載していく予定です。とりあえずPythonのみ。
Python版では、Search API の日付順の結果が正しく返却されるようになったようです。
Google Compute Engine入門を読んで
Google Cloud Platform(GCP)関連の情報をWeb検索すると、ほとんどと言っていいほどヒットする会社のサイトがある。吉積情報株式会社さんの関連サイトだ。Google関連の事業に力を入れられているようで、このたび、代表の方がGoogle Compute Engineの入門書「Google Compute Engine入門」を出版されたとの事で早速読んでみた。
内容はざっくりとこんな感じ。
- Google Compute Engineの概要と導入手順
- コマンドリファレンス
- AWSとの比較
- その他
コマンドリファレンスまでが本書のページ数の8割強を占める。GCPはガッツリ使っていないので、リファレンス本としてはまだ出番は無さそうかな。
個人的には、機能、価格、性能(ベンチ)までとったAWSとの比較はとても参考になった。仕事でも「AWSでいいじゃん」とよく言われる事があるので、このAWSとの比較記事だけでも本書の値段分の価値はあると思っている。
先日Googleが、日本でも本格的にGCPを展開すると明らかにしていたけれど、まだまだWebや書籍の情報量が少ない。特に日本語の。だ。国内で本格的に展開するには、日本語のドキュメントが充実しているかどうかにかかっていると個人的には考えている。あとは国内の導入事例やサポート体制の情報が欲しい。
それにしてもGoogle関連の技術書の陳腐化は早い(iOSもだけど)。しょうがないことだけど、この本も来年には「こないだまではそうだったね」という部分がでてくるだろう。その時はぜひ第2版を!
2014/09/11
無料でGoogle検索を自動化する
プログラムから自動でGoogle検索して、結果を収集したい時がたまにある。
今回は、Google App Engine と、Google Custom Search APIを使って、Google検索を自動化する方法をメモ。サンプルでは、価格.comのレビューのみを取得するフォームを作ってみた。
Google Custom Search API は、Google Custom Search サービスを外部から操作できるAPI。100リクエスト/日まで無料だ。それ以上リクエストしたい場合は、1000リクエスト毎に5ドル支払えば検索可能となる。1日100件ぐらいであれば、Google Appe Engine側も無料範囲で十分まかなえる量だ。
Custom Search APIを有効にする
まずは、Custom Search APIが使えるように、有効化とAPIキーを取得する。
Google Developers Console にログインして、該当のプロジェクトを選択し、「APIと認証」から「API」メニューを選択する。
APIの一覧からCustom Search APIを探し、右のボタンを押す。
ちなみに、100リクエスト/日以上検索したい場合は、API名を選択後に表示される画面で割り当てを選択し・・・
ここのSet billable limitsから、1日のリクエスト可能数を引き上げる事ができる。(※プロジェクトの課金を有効にしておく必要がある。)
続いてAPIにアクセスするためのキーを取得する。「APIと認証」から「認証情報」を選択する。
次に表示される画面で、「新しいキーを作成」から「サーバーキー」を選択する。OAuthは、例えばユーザー情報を取得するようなAPIを使用する時に選択する。今回は検索のみを使用するので、サーバーキーでOKだ。
許可IPアドレスの設定は今回空欄とした。特定のIPアドレスのサーバーからのみキーを有効としたい場合は設定する。
これでキーが作成できた。このキーはプログラムからAPIへ接続する際に使用する。
Google Custom Searchを作成する
APIキーを取得したら次は Google Custom Search で検索エンジンを作成する。Custom Search APIはここで作成した検索エンジンを操作するAPIだ。
Google Custom Search へアクセスし、新しい検索エンジンを作る。
検索対象のサイトドメインを登録する。かならず1つのドメインを指定する必要がある。今回はとりあえず kakaku.com(あとで変更する) を入力し、検索エンジンの名前を指定。
これで検索エンジンが完成。コントロールパネルへアクセスする。
ここから検索エンジンのIDを取得しておく、IDもAPIキー同様にプログラムからAPIへアクセスする場合に必要。
今回は、価格.comのレビューのみを取得する事が目的なので、その他のページがヒットしないよう、レビューページのURLのみをターゲットにする。対象サイトを選択して・・・
レビューページのURLを入力する。商品IDなど動的に変わる部分は*
でワイルドカード指定ができる。
これで、価格.comのレビューページだけがヒットする検索エンジンが完成した。あとはこいつをプログラムから呼び出す処理を実装する。
検索処理を実装する
まずはキーワードを入力するフォーム。
<form role="form" method="post" action="search">
<div class="form-group">
<label for="keyword">検索キーワード</label>
<input type="text" id="keyword" name="keyword">
</div>
<button type="submit" class="btn btn-default">検索</button>
</form>
次にリクエストハンドラ。キーワードを取得して、しかるべきパラメータでAPIのURLへ送信。戻り値はJSON。※APIの詳細は、Google公式ドキュメントを参照。
import json
import logging
import urllib
from google.appengine.api import urlfetch
from lib.controller import *
class Top(Controller):
def get(self):
self.draw_template('front/cse/top.html')
class Search(Controller):
def post(self):
# キーワードを得る(URLエンコードしておく)
keyword = self.request.get('keyword')
keyword = urllib.quote(keyword.encode('utf-8'))
# リクエストパラメータ組み立て
url = 'https://www.googleapis.com/customsearch/v1'
url += '?key=%s' % '生成したAPIキー'
url += '&cx=%s' % '作成した検索エンジンID'
url += '&q=%s' % keyword
# 検索
result = urlfetch.fetch(url)
logging.info(url)
items = list()
if not 200 <= result.status_code <= 299:
# エラー
logging.error('google custom search error: $s' % str(result.status_code))
logging.error(result.content)
else:
# 結果を得る
content_dict = json.loads(result.content)
items = content_dict.get('items', list())
self.set_template_value('items', items)
self.draw_template('front/cse/search.html')
url_map = [
('/cse/top', Top),
('/cse/search', Search),
]
application = webapp.WSGIApplication(url_map, debug=True)
items
に検索結果のリストが格納される。
Googleは、商品情報や、レビュー、レシピなど、特定の情報については、ルールに従ってHTML(構造化データ)を組んでおけば、意味のある情報としてインデックスしてくれる。たとえば、レビューのタイトルや評価の星の数などだ。
価格.comやヨドバシなどそれなりのショッピングサイトは、この構造化データに準じてサイト構築してくれており、これらのサイトに対して検索した Custom Search API の戻り値も構造化されていて参照しやすい。
つまり、戻されたJSONデータの中には、レビューのタイトル、本文(一部)、評価数(星の数)が、(ほぼ)おなじキー名で格納されているので取り出しやすいのだ。
ちょっと話は脱線したけど、ここまでできれば後はcronで自動化したりと自由自在だ。
実はこの構造化データの値も条件としてフィルタリングできるので、「価格.comの星1つのレビューのみ抽出」といった事も可能となる。この方法はまた次の機会に。。。
2014/09/10
Google App Engineでファイルを保存する方法3つ
Google App Engineでは、通常のWebアプリケーションのように、サーバー上のファイルシステムへアクセスすることができない。別の方法でファイルの読み書きをする事になるけど、方法はいくつかあるので、それぞれの方法と長所短所をまとめてみた。サンプルと全てのソースコードはこちら。(サンプル/ソースコード)
方法1:BlobPropertyを使う
ndb(db)のBlobProperty(https://developers.google.com/appengine/docs/python/ndb/properties)ではバイナリデータをそのまま格納できる。まずは保存するModelの定義を実装する。
from google.appengine.ext import ndb
class UserFileModel(ndb.Model):
"""
ファイル格納モデル
"""
file_data = ndb.BlobProperty() # ファイルデータ
続いて、アップロードフォーム。
<form>
<div class="form-group">
<input id="exampleInputFile" name="file_data" type="file" />
</div>
<button class="btn btn-default" type="submit">保存</button>
</form>
アップロードハンドラを実装。リクエストされたデータをそのまま突っ込むだけ。
def post(self):
file_data = self.request.get('file_data')
user_file = UserFileModel()
user_file.file_data = file_data
user_file.put()
一番お手軽な方法だけど、1エンティティの上限が1MBまでという制限があるので注意が必要。それより大きいファイルを保存しようとするとこんなエラーが出る。
RequestTooLargeError: The request to API call datastore_v3.Put() was too large.
BlobPropertyではzip圧縮オプション(compressed=True)が使えるので多少容量を抑えることができる。ただ、圧縮しない時よりデータ読み書き時にCPUを消費するのでそこはトレードオフだ。
方法2:Blobstoreを使う
Blobstoreとは、Googleが提供するキー・バリュー型のデータストアサービス。上のBlobPropertyと名前が似てるけど、厳密にはGoogle App Engineとは別領域のサービスだ。まずはアップロード先URLの生成する。Blobstoreでは、最初にアップロード先のURLを生成しておく必要がある。生成にはgoogle.appengine.ext.blobstoreライブラリの create_upload_url を使う。
upload_url = blobstore.create_upload_url('/file_save/upload2')
引数の /file_save/upload2 は、アップロード後にリダイレクトされるパスを指定する。
続いて、アップロードフォームを実装する。上で生成したアップロードURLがPOST先となるようにする。
<form action="{{ upload_url }}" enctype="multipart/form-data" method="post" role="form">
<div class="form-group">
<input id="exampleInputFile" name="file_data" type="file" />
</div>
<button class="btn btn-default" type="submit">保存</button>
</form>
生成したアップロードURLはこんな感じになる。
http://gcp-memo.appspot.com/_ah/upload/AMmfu6aKr-VcNuFlEljprrh7rkbKnBA7WndhyciPTGolHiRkxVq670JENWihVEWViTxRue0K5Y10fOFpmnFzF4hn7qBMIZSn8vhmDT55aO5jNMPqEeQL46R6iWQvEps64_i8WQJsXAHl/ALBNUaYAAAAAVA_q4rzSwstBg1S6i5l3FpfHv4N0EdAR/
/_ah/upload はGoogle App Engineで決まっているシステム用のパスだ。ここでアップロードが行われた後、create_upload_urlで指定したパスに302リダイレクトされる。
アップロード後にリダイレクトされる処理を実装する。ここでBlobstoreに保存したファイルの情報が取れるので、キーを格納したりできる。このハンドラは、google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandlerクラス を継承しておく必要がある。
class Upload2(blobstore_handlers.BlobstoreUploadHandler):
def post(self):
upload_files = self.get_uploads('file_data')
blob_info = upload_files[0]
self.redirect('/file_save/upload2_done?key=%s' % blob_info.key())
先にアップロードするURLを生成する必要があったり、アップロード後にリダイレクトされたりとちょっと扱いにくい印象。保存したファイルを参照する方法はまたの機会に。
方法3:Google Cloud Storageを使う
Google Cloud Storageは、Google Cloud Platformで提供されているストレージサービスだ。Google App Engineからも簡単にアクセスすることができる。アップロードフォーム。
<form action="upload3" enctype="multipart/form-data" method="post" role="form">
<div class="form-group">
<input id="exampleInputFile" name="file_data" type="file" />
</div>
<button class="btn btn-default" type="submit">保存</button>
</form>
アプロードハンドラを実装。通常のPythonアプリケーションの様に、ストレージ上のファイルの読み書きができる。ファイル操作には、Googleから提供されている Google Cloud Storage Client Libraryを使用する。(ダウンロードしてGAEアプリケーションに組み込んでおく必要がある)。
また、blobstore.create_gs_key 関数を使用すると、blobstoreライブラリで利用できるキーを生成することができる。blobstoreライブラリを使うと大きめのファイルのダウンロード処理が簡単にできるようになるので覚えておきたい。
関連ライブラリがごちゃごちゃしている感があるけど、通常のファイル操作と同じように扱えるため利用しやすい。
import lib.cloudstorage as gcs
# (中略)
class Upload3(Controller):
def post(self):
# ファイル名とファイルデータを得る
file_name = self.request.POST['file_data'].filename
file_data = self.request.get('file_data')
# このアプリケーションのデフォルトGCSバケット名を得る
bucket_name = app_identity.get_default_gcs_bucket_name()
# 保存パスを作成
filepath = '/' + bucket_name + '/file_save/' + file_name
# ファイル作成
gcs_file = gcs.open(filepath, 'w')
gcs_file.write(file_data)
gcs_file.close()
gcs_key = blobstore.create_gs_key('/gs' + filepath)
self.set_template_value('message', gcs_key)
self.draw_template('front/file_save/done.html')
まとめ
総合的に見るとGoogle Cloud Storageが一番良い。1MB未満のデータで無料枠で収まるようなアプリならBlobPropertyがお手軽でいいかも。Blobstoreを使うメリットは無さそうかな。方法 | 扱いやすさ | 容量制限 | ストレージ価格(GB/月) |
---|---|---|---|
BlobProperty | ◎ | 1MB | $0.18 |
Blobstore | × | 2GB | $0.026 |
Google Cloud Storage | ◯ | ほぼ無制限 | $0.026 |
2014/05/13
Managed Virtual Machine に望むこと
先日2014年4月22日に行われた、Google Cloud Platformセッションの模様がYoutubeに公開されてた。
地方民はこういうイベントになかなか参加できないので、動画配信はほんとに助かる。ありがたや。。。
動画の中で語られている Managed VM は地味だけど、なにげにすごい機能だと思う。GAEでできなかったほとんどの事がこれで解決できるかもしれない。
ただ、個人的に Managed VMでは、(スケールアウトしなくていいので)固定IPも使えるようになってほしい。なぜなら、Google App Engineで外部のシステムと連携するとき、相手側でIPアドレス制限されている事がある。だから、連携前にあらかじめこちらのIPアドレスを教えておくのだけど、GAEはPaaSなのでその範囲も広いし、いつ変わるかわからない。まぁ、IPアドレス制限という方法自体がすでに時代遅れな感じではあるけど。
ともかく、GAEではそういうシステムと連携する時は、間にGAEでないサーバーを中継させておく必要がある。なので、このためだけにOSやApacheのメンテが発生する。これは、ほんとにアホらしいことだ。もし、Managed VMで固定IPが使えれば、対外部システムとの処理だけそのインスタンスで行えばいいし、中継サーバーも不要になるので、だいぶ楽になるのだ。
Googleさん、よろしくおねがいします。
2014/05/01
Google Cloud Storage上の非公開動画を特定の人にだけ公開する
Google App Engine単体では(Blobstoreを使う以外)ファイルの読み書きはできないが、GCPのクラウドストレージ製品Google Cloud Storageを使えば、簡単にファイルを取り扱うことができる。今回は、ある特定の人にだけ動画を見せたい場合を想定して、Google Cloud Storageに置いた非公開な動画ファイルを、OpenIDでログインしている人にだけ見せる方法をメモしておく。
動画ファイルの配置
GCPのコンソールからGoogle Cloud Storageにファイルをアップロードする。(アップロード方法はまたの機会に。)ちなみに、動画ファイルはNHKクリエイティブ・ライブラリーから拝借した。今回保存したファイルパス(非公開)
/gcp-memo.appspot.com/movies/meteorite.mp4
動画再生ページの作成
videoタグを配置するページ(/member/movie)を作成。こちらもログイン必須のページにしておく。app.yamlでログイン必須指定する。- url: /member/(.*)今回も認証まわりの制御はGoogle App EngineのUsersライブラリを使う。過去記事参照。また、videoタグのsrcには、動画ダウンロードを行うスクリプト(/member/play_movie)を指定する。
script: controllers.front.member.application
secure: optional
login: required
<video autoplay="" controls="" src="/member/play_movie">
</video>
動画ダウンロードスクリプトの作成
/member/play_movie を実装する。上のapp.yamlで、/member配下は認証必須指定しているので、必ず認証された状態でアクセスされる。あとはダウンロード処理を実装すればいいだけ。from google.appengine.ext import blobstore
from google.appengine.ext.webapp import blobstore_handlers
class PlayMovie(blobstore_handlers.BlobstoreDownloadHandler):
def get(self):
blob_key = blobstore.create_gs_key('/gs/gcp-memo.appspot.com/movies/meteorite.mp4')
self.send_blob(blob_key)
url_map = [
('/member/play_movie', PlayMovie),
]
application = webapp.WSGIApplication(url_map, debug=True)
大きなサイズの動画でもDeadlineExceededErrorが出ることもないし、動画のシークも問題ない。ログを見ると14msで終わってるので、インスタンスを専有するわけでもなさそうだ。内部でどういうシーケンスになってるのか不明だが、これで目的の動作は果たせる。
【2015/12/10 追記】
内部のシーケンスについて、Google技術サポートに問い合わせる事があったので聞いてみると、Blobstore 経由で Cloud Storage のオブジェクトをクライアントに転送する場合、クライアントとインスタンスの経路の途中にある中継サーバに対して、Cloud Storage から直接データをクライアントに転送するように指示するそうです。なので、インスタンスを専有することもなく、大きめのファイルを転送できると。すばらしい!
ドコモIDでOpenID認証してみる
今回は、ドコモID(OpenID)を使ってユーザー認証させてみた。
Moduleをdispach.yamlで振り分ける方法
Moduleのyamlファイルの作成
今回は、以下の3つのModuleを作ってみる。・フロントエンド用(app.yaml) default
・バックオフィス用(back.yaml)
・JOB用(job.yaml)
まずは各Moduleのyamlファイルを作成する。
app.yaml
application: gcp-memo
module: default
version: 1
runtime: python27
api_version: 1
threadsafe: true
instance_class: F1
(以下省略)
admin.yaml
application: gcp-memo
module: admin
version: 1
runtime: python27
api_version: 1
threadsafe: true
instance_class: F1
(以下省略)
job.yaml
application: gcp-memo
module: job
version: 1
runtime: python27
api_version: 1
threadsafe: true
instance_class: B1
basic_scaling:
max_instances: 2
idle_timeout: 10m
(以下省略)
dispatch.yamlファイルの作成
続いて、これらのModuleを振り分けるdispatch.yamlを作成する。今回は、パスが/admin/に一致すればadminモジュールへ、/job/に一致すればjobモジュールが使用されるようにする。dispatch.yamlは、アプリケーションのルートディレクトリに配置する必要がある。Moduleのyamlファイルについては、ルートでも任意のディレクトリ配下でもOK。dispatch.yaml
application: gcp-memo
dispatch:
- url: "*/job/*"
module: job
- url: "*/admin/*"
module: admin
ローカルサーバーでの起動
以下で実行できる。パラメータにdispatch.yamlとモジュールのyamlファイルを渡す。実行ディレクトリは、アプリケーションのルート。モジュールのyamlがサブディレクトリ配下にある場合は、ルートからの相対パスで指定する。ローカルサーバーでの起動コマンド
dev_appserver.py dispatch.yaml app.yaml admin.yaml job.yamlコンソールには以下が表示され、http://localhost:8080 にアクセスすれば、適宜ディスパッチされる。
コンソールの表示
Starting dispatcher running at: http://localhost:8080
Starting module "default" running at: http://localhost:8081
Starting module "admin" running at: http://localhost:8082
Starting module "job" running at: http://localhost:8083
デプロイとdispatch.yamlの更新
本番環境にデプロイするときは、各モジュールのデプロイとdispatch.yamlの更新とでコマンドが別。モジュールのデプロイは、複数のyamlファイルを指定できる。デプロイ
appcfg.py update app.yaml job.yaml admin.yaml
dispatch.yamlの更新
appcfg.py update_dispatch ./※update_dispatchは忘れがち。