OpenSocial in the Cloud(日本語訳)

(OpenSocial API v0.8)

Jason Cooper and Lane LiaBraaten, Google Developer Programs
September 2008

原文:OpenSocial in the Cloud

翻訳:伊藤 千光 (誤訳のご指摘などはこちらにコメントしてください)

目次

 

  1. OpenSocial in the Cloud
      1. (OpenSocial API v0.8)
    1. 目次
    2. 最初に
    3. Photo Pier アプリケーション
    4. 必要なソフトウェア
    5. クラウドへの移行
      1. アプリケーションからサーバーへの安全な通信
      2. アプリケーションとサーバーのインタラクション
        1. 新規画像のアップロード
        2. 現在のユーザーが所有する画像のフェッチ
        3. ユーザーの友達が所有する画像のフェッチ
        4. 画像へのタグ付け
        5. ユーザーが追加したタグのフェッチ
    6. Google App Engine
      1. データモデル
      2. 画像のアップロード
      3. 単一ユーザーの画像リストのフェッチ
      4. 複数ユーザーの画像リストのフェッチ
      5. 画像へのタグ付け
      6. 単一ユーザーのタグのフェッチ
      7. アプリケーションの公開
    7. Amazon S3
    8. 最適化
      1. getProxyUrl を利用する
      2. プロフィールページレンダリング用データのキャッシング
    9. リソース

最初に

いくつかの OpenSocial アプリケーションは、全体をクライアントサイドの JavaScript と HTML で記述することにより、ページの供給やアプリケーションデータの保存にコンテナを活用できます。この場合、あなたのサーバーにはコンテナから gadget specification をキャッシュするためのリクエストが送られるだけなので、アプリケーションは容易にスケーラブルになります。

しかしながら、アプリケーション自体をあなたのサーバーでホストする多くの理由があります。

サードパーティーのサーバーを使う OpenSocial アプリケーションをセットアップすることはとても簡単です。いくつかの注意点がありますが、しかし本当の問題はあなたのアプリケーションが成功したとき - 数百万のユーザーを獲得して秒間数千リクエストが送られる - に顕在化します。ソーシャルネットワークではアプリケーションがとても速く広まることがあるので、次のソーシャルアプリケーションをローンチする前に、もしあなたのアプリケーションの人気が出たら、いかにして素早くスケールアップするかを考えておかなければなりません。

残念ながら、スケーリングは素早く解決するのが難しく、実装が高くつく複雑な問題です。幸い、いくつかの企業がクラウド・コンピューティング・リソース — 仮想マシンでデータを保存し、プロセスを走らせる環境 — を提供しています。これらのコンピューティング・ソリューションが巨大なインフラを管理してくれるので、あなたはアプリケーションの開発に専念でき、「クラウド」が全てのリクエストとデータの保存を肩代わりしてくれます。

このチュートリアルでは、サードパーティー・サーバーに画像と関連するメタデータを保存する、簡単な画像共有アプリケーションを作成します。もしこのアプリケーションが数百万の画像を保存し、毎秒多数のリクエストを扱うようになると、一台の専用サーバーでは処理しきれません。そこで、このアプリケーションを調査し、バックエンドサーバーとのインタラクションを解析します。そして、最初に Google App Engine を使い、さらに Amazon S3 データ・ストレージ・サービスを活用して、アプリケーションをクラウドに実装します。最後に、アプリケーションを高速化し、クラウドにホストする料金を押さえるためにネットワーク・トラフィックを削減するいくつかの方法を検討します。

Photo Pier アプリケーション

Photo Pier は画像共有アプリケーションで、ユーザーは画像をアップロードし、それらにキーワードでタグ付けできます。また、ユーザーは友達がアップロードして、お気に入り画像としてプロフィールに表示するよう指定した画像も閲覧できます。

この機能を実現するためには、それぞれの画像ごとにいくつかのデータを保存する必要があります: 画像をアップロードしたユーザー(以降は所有者と呼びます)、一意な名前、そして所有者がタグとして設定した文字列のコレクションです。

必要なソフトウェア

このチュートリアルを完了するには、以下のリソースが必要です :

クラウドへの移行

もしこのアプリケーションが百万のユーザーと画像を持つまでに成長したら、共有ホスティングはもちろん、たとえ専用サーバーであっても、それらすべてを処理できる帯域幅と処理能力は持ち合わせないでしょう。多数のサーバーとネットワークインフラ、データベースの shard 、リクエストの負荷分散などに投資することもできますが、それには時間や資金、専門知識が必要です。もし、それらをアプリケーションの新機能の開発に費やしたいなら、今がクラウドへ移行する時です。

クラウドで走らせるであろうアプリケーションを設計する際は、アプリケーションとサーバーとのインタラクションに注意することが重要です。もし通信プロトコルとデータフォーマットが標準化されていれば、 OpenSocial アプリケーションに変更を加えることなく、容易にサーバーサイドの実装を変更できます。

アプリケーションからサーバーへの安全な通信

アプリケーションとサーバーの間を流れる個々のリクエストやレスポンスを考察する前に、 OpenSocial がいかにしてこの通信をセキュアに保っているかをおさえておきましょう。標準の makeRequest 呼び出しでは、誰もが関連するパラメータとともにサーバーにリクエストを送って、画像に不適当なタグを付けたり、さらには他のユーザーのプロフィールに画像をアップロードすることさえできてしまいます。これではユーザーを厄介な状況に追い込み、あなたとあなたのアプリケーションを大きなトラブルに巻き込むことになります。幸い、 OpenSocial はこの種の悪意ある動作を防ぐメカニズムを提供しています。

makeRequest メソッドはアプリケーションがサーバーへ送信するリクエストに OAuth によるデジタル署名を施すように設定できます。これは、サーバーがリクエストを受け取った際に、それが特定のコンテナでホストされたあなたのアプリケーションからのものであることを検証できるということを意味します。これを実装するには、 OpenSocial アプリケーション内の makeRequest 呼び出しでリクエストが署名されなければならないことを指示し、サーバーサイドのリクエストを処理するコードで有効な署名が存在することを検証します。

アプリケーションに対する変更はとても僅かです — 単にコンテナに SIGNED 認証を使用するように指示するパラメータを追加するだけです。

var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED;

var url = buildUrl(this.request_base_url,
                   
['photos'], [new Date().getTime()]);
gadgets
.io.makeRequest(url,
                       bind
(this.closeFetchOwnerPhotos(callback), this),
                       
params);

サーバーがリクエストを受信したとき、デジタル署名が有効なコンテナにより署名され、アプリケーション ID が正しいことを確認することで適切なアプリケーションから送信されたものであることを検証できます。それを行うコードは opensocial-resources wiki にあります。

リクエストを署名するとき、コンテナによってコンテナの ID とアプリケーションを利用している person の ID を含むいくつかのパラメータが追加されることに注意してください。 Photo Pier バックエンドはそれぞれの画像をどのユーザーに結びつけるかを知るためにそれらのパラメータを利用します。

アプリケーションとサーバーのインタラクション

前述の機能に基づいて、クライアントサイド・アプリケーションは以下のアクションでサーバーへのリクエスト行います。

新規画像のアップロード

画像をアップロードするために、アプリケーションはバイナリデータといくつかの追加パラメータを伴う HTTP POST リクエストを送信するフォームを作成します(ご心配なく、フォーム送信時にエンコーディングは自動的に処理されます — あなたがやるべきことは、エンドポイントの指定だけです)。

http://<base_url>/photo

注: このチュートリアルを通して <base_url> は一定の URL プリフィクスを意味します。すべてのリクエスト URL はこのプリフィクスで始まり、単にエンドポイントとクエリー文字列パラメータが変わるだけです。

サーバーがこのリクエストを受信すると、画像ファイルと(所有者の ID などの)メタデータをデータストアに保存します。レスポンスとして、 "画像が追加されました" というテキストと新しくアップロードされた画像を表示するための <img> タグを含む HTML が期待されます。

現在のユーザーが所有する画像のフェッチ

ユーザーが最初に canvas ページを読み込むとき、アプリケーションは表示する画像のリストを取得するリクエストをサーバーに送信する必要があります。アプリケーションは gadgets.io.makeRequest を使って以下の URL に HTTP GET リクエストを送信します。

http://<base_url>/photos?arg0=<TIMESTAMP>

サーバーがこのリクエストを受信すると、 oauth_consumer_key と opensocial_owner_id リクエストパラメータを使って現在のユーザーがアップロードした画像を特定します。それぞれの画像について、サーバーは画像の URL とタグのリストを返します。

このリクエストに対するレスポンスは以下の形式の JSON 文字列です。

{"resultsSet":[ { "url":"http://foo", "tags":["Aruba", "snorkling" },
               
{ "url":"http://bar", "tags":["wedding", "cake"] } ]
}

ユーザーの友達が所有する画像のフェッチ

ユーザーが "Friend's Photos" タブをクリックすると、アプリケーションはユーザーの友達がアップロードした画像を表示しなければなりません。サーバーは relationship データを保持していないので、アプリケーションは適切な画像をフェッチするためにユーザー ID のリストを送信しなければなりません。 fetchFriendPhotos メソッドは makeRequest メソッドを使い、ポストデータとして ID のリストを含む HTTP POST リクエストを送信します。この POST リクエストは以下の URL に送信されます。

http://<base_url>/photos?arg0=<TIMESTAMP>

これが先ほどの URL と同じであることに注意してください。 HTTP メソッドが GET ではなく POST なので、サーバーはこのリクエストを区別して扱います。リクエスト中のポストデータは以下の形式になります。

people=01495306580392390900,14088281537290874435

サーバーがリクエストを受信すると、このデータをパースしてそれぞれの ID に関連する画像をフェッチします。レスポンスは以下の形式の JSON 文字列になります。

{"resultsCollection":[
             
{ "name" : "01495306580392390900",
               
"photos" : [ { "url":"http://foo", "tags":["Aruba", "snorkling", "fish"] },
                           
{ "url":"http://bar", "tags":["snorkling", "shipwreck"] } ] },
             
{ "name" : "14088281537290874435",
               
"photos" : [ { "url":"http://baz", "tags":["food", "pasta", "linguini"] },
                           
{ "url":"http://raz", "tags":["food", "dessert", "apple pie"] } ] } ]
}

画像へのタグ付け

画像にテキストタグを追加するために、アプリケーションはテキストとともに EXTENDED_PHOTO_NAME (これには所有者の OpenSocial ID とアップロード時に設定された画像名が含まれます)をサーバーに送信します。アプリケーションは gadgets.io.makeRequest 関数を使って以下の URL に HTTP POST リクエストを送信します。

http://<base_url>/photo/<EXTENDED_PHOTO_NAME>

実際のタグのテキストはポストデータとして送信されます。サーバーがリクエストを受信すると、 EXTENDED_PHOTO_NAME をパースして、タグ付けされる画像の特定にその情報を利用します。そしてデータベース上でタグが画像に関連付けられます。タグ付けが成功すると、レスポンスは "Tag added!" というプレーンテキストになります。

ユーザーが追加したタグのフェッチ

最後に、ユーザーが追加したすべてのタグを取得するリクエストを追加します。タグに関連する画像をユーザーがより簡単に選択できるように、タグのセットはドロップダウンリストで表示されます。アプリケーション gadgets.io.makeRequest 関数を呼び出して、以下の URL に GET リクエストを送信します。

http://<base_url>/tags

レスポンスは以下のような文字列化された JSON オブジェクトです:

{"tags":["Aruba", "snorkeling", "fish", "shipwreck"]}

Google App Engine

この時点で、開発マシンに必要なソフトウェアがすべてインストールされている必要があります。

この App Engine プロジェクトは以下のいくつかのファイルと少しのサードパーティーライブラリを含んでいます。

このアプリケーションの app.yaml ファイルはとても単純です。

application: datastore
version
: 1
runtime
: python
api_version
: 1

handlers
:
- url: /scripts
  static_dir: scripts

- url: /
modules/.*
  script
: modules.py

- url: /.*
  script
: main.py

上記のように、このファイルには複数のハンドラ・スクリプトや静的コンテンツの場所を指定できます。このファイルのさらなる詳細については Configuring an App をご参照ください。

アプリケーションのコーディングを始めましょう。最初に、アプリケーションがリクエストを受け取るために使用するさまざまな URL のためのハンドラクラスを定義する必要があります。 OpenSocial in the Cloud resource bundle には以下のリクエスト・ハンドラのスケルトン・コードが cloud.py として収録されています。

# cloud.py

import sys
sys
.path.append('lib')

import re
import cgi
import urllib
import simplejson

from google.appengine.ext import webapp
from google.appengine.ext import db
from math import floor
from time import time

# Signature validation required libraries import
import base64
import hashlib
import oauth

from Crypto.PublicKey import RSA
from Crypto.Util import number

# Local port; change if another process is running on 8080
PORT
= '8080'

class RootHandler(webapp.RequestHandler):
 
def get(self):
    self
.response.out.write("RootHandler received a GET request")

class TagsHandler(webapp.RequestHandler):    
 
def get(self):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return

    self
.response.out.write("TagsHandler received a GET request")

class PhotosHandler(webapp.RequestHandler):    
 
def get(self):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return

    self
.response.out.write("PhotosHandler received a GET request")

 
def post(self):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return

    self
.response.out.write("PhotosHandler received a POST request")

class PhotoHandler(webapp.RequestHandler):    
 
def get(self):
    self
.response.out.write("PhotoHandler received a GET request")
 
 
def post(self):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return

    self
.response.out.write("PhotoHandler received a POST request")

def _isValidSignature(self):

 
# Code lab hack:
 
# If the container is 'appengine' (e.g. app is running on localhost), return True
 
if self.request.get('oauth_consumer_key') == 'appengine':
   
return True

 
# Construct a RSA.pubkey object
  exponent
= 65537
  public_key_str
= """0x\
00b1e057678343866db89d7dec2518\
99261bf2f5e0d95f5d868f81d600c9\
a101c9e6da20606290228308551ed3\
acf9921421dcd01ef1de35dd3275cd\
4983c7be0be325ce8dfc3af6860f7a\
b0bf32742cd9fb2fcd1cd1756bbc40\
0b743f73acefb45d26694caf4f26b9\
765b9f65665245524de957e8c547c3\
58781fdfb68ec056d1"""

  public_key_long
= long(public_key_str, 16)
  public_key
= RSA.construct((public_key_long, exponent))

 
# Rebuild the message hash locally
  oauth_request
= oauth.OAuthRequest(http_method=self.request.method,
                                     http_url
=self.request.url,
                                     parameters
=self.request.params.mixed())
  message
= '&'.join((oauth.escape(oauth_request.get_normalized_http_method()),
                      oauth
.escape(oauth_request.get_normalized_http_url()),
                      oauth
.escape(oauth_request.get_normalized_parameters()),))
  local_hash
= hashlib.sha1(message).digest()

 
# Apply the public key to the signature from the remote host
  sig
= base64.decodestring(urllib.unquote(self.request.params.mixed()["oauth_signature"]))
  remote_hash
= public_key.encrypt(sig, '')[0][-20:]

 
# Verify that the locally-built value matches the value from the remote server.
 
if local_hash==remote_hash:
   
return True
 
else:
   
return False

コードを規律正しく洗練された状態に保つために、エンドポイントからクラスへのマッピングを main.py という独立したファイルに配置しています。このファイルの内容は以下のようになります。

import cloud

def main():  
  application
= webapp.WSGIApplication([('/', cloud.RootHandler),
                                       
('/tags', cloud.TagsHandler),
                                       
('/photos', cloud.PhotosHandler),
                                       
('/photo/.*', cloud.PhotoHandler),
                                       
('/photo', cloud.PhotoHandler)],
                                        debug
=True)
                                       
  user
= cloud.User.get_by_key_name(''.join(['appengine', '00000000000000000000']))
 
if not user:
    _initializeDatastore
()
 
 
# Start application
  wsgiref
.handlers.CGIHandler().run(application)
 
if __name__ == "__main__":
  main
()

先に進む前に、少し立ち止まって上記の _isValidSignature 関数を見てください。電子署名リクエストをサポートしたプロダクション環境ではこのコードが実行され、 OAuth 仕様に従ってパラメータをエンコードしてコンテナが送信したダイジェストを確認することでリクエストの信憑性を検証します。もしそれらが一致すればリクエストは本物であることがわかりますが、そうでなければ偽装されているのでアプリケーションはすぐに終了しなければなりません。しかしながら、今はプロダクション環境ではなくローカルで実行しているので、このルーチンの先頭に小さなセクションを追加して、もしコンテナが 'appengine' (このサンプルのための仮のコンテナです)であれば、単に true を返すようにしています。

もしデジタル署名を生成するソーシャルネットワークにこのアプリケーションをデプロイするときは、必ずこのセクションを削除してください。

これで単純なアプリケーションができたので、開発用 Web サーバーでテストしましょう。もしまだやっていなければ、 SDK をダウンロードして解凍してください。そして google_appengine ディレクトリで './dev_appserver.py <your_app_directory>' を実行し、ブラウザからアプリケーションにアクセスできることを確認してください(デフォルトの URL は http://localhost:8080/ です)。

データモデル

Google App Engine はリレーショナル・データベースの代わりにオブジェクト・モデル・データストアを使用します。これはつまり、アプリケーションで使用するデータ要素は単に db.model クラスを継承した Python オブジェクトとして定義すればよいことを意味します。

Photo Pier アプリケーションはデータストア内に 2 種類のオブジェクトを保持します : ユーザーと画像です。

class User(db.Model):
  container
= db.StringProperty()    # the container this user came from
  containerId
= db.StringProperty()  # the ID provided by the container for this user
 
class Photo(db.Model):
  name
= db.StringProperty()         # a unique ID for the photo
  content
= db.BlobProperty()        # the binary data of the image
  contentType
= db.StringProperty()  # the type of image (e.g. .jpg, .gif, etc.)
  user
= db.ReferenceProperty(User)  # a reference to the user that uploaded this image (like a foreign key)
  tags
= db.StringListProperty()     # a list of tags for this photo

見てのとおり、 Google App Engine のデータストアはたくさんのデータ型をサポートしています。完全なリストは Types and Property Classes をご参照ください。

画像のアップロード

以下の PhotoHandler クラスの実装は、(上記の webapp.WSGIApplication コンストラクタで定義されているとおり) /photo エンドポイントへの HTTP POST リクエストを受信したときに常に呼び出される post メソッドを定義しています。 post メソッドは、まず最初に container と personid パラメータに対応するユーザーがデータストアに存在するかどうかを確認し、もし見つからなければ作成します。そして画像のバイナリデータを読み込み、 blob としてデータストアに保存します。

class PhotoHandler(webapp.RequestHandler):    
 
def get(self):
    self
.response.out.write("PhotoHandler received a GET request")
 
 
def post(self):
    form
= cgi.FieldStorage()
   
    fileItem
= form['file']
    personId
= form.getfirst('personId')
    container
= form.getfirst('container')

    self
.response.headers['Content-Type'] = 'text/html'

    photo
= createPhoto(container, personId, fileItem)
   
if photo:
      self
.response.out.write('Photo added.')
      self
.response.out.write(''.join(['<img src="http://localhost:',
                                       PORT
,
                                       
'/photo/',
                                       container
,
                                       
':',
                                       personId
,
                                       
':',
                                       photo
.name,
                                       
'" width="50"/><br/>']))


def getUser(container, personId):
  user
= User.get_or_insert(''.join([container, personId]), container=container, personId=personId)
 
return user
       
def createPhoto(container, personId, fileItem):
  user
= getUser(container, personId)

  name
= ''.join([str(int(floor(time()))), fileItem.filename])
  key  
= ''.join([container, personId, '_', name])

  photo
= Photo(key_name=key)
  photo
.user = user
  photo
.name = name
  photo
.content = db.Blob(fileItem.file.read())
  photo
.contentType = fileItem.type    
  photo
.put()

 
return photo

createdPhoto から呼ばれている getUser メソッドの記述に注目してください。このメソッドは与えられた container ID 属性に対応するユーザーを App Engine データストアに問い合わせます。もしマッチするものが見つかれば、呼び出し元にそれを返します。そうでなければ、新しいユーザー・インスタンスをデータストア内に作成してそれを返します。 App Engine では、モデルクラスの get_or_insert メソッドでこれをとても簡単に実現できます。

画像をアップロードするこのテクニックは安全でないことに注意してください。このシステムでは任意のサーバーが関連するパラメータとともに HTTP POST リクエストを送信し、任意のユーザーとして画像をアップロードできてしまいます。この記事では扱いませんが、画像をアップロードするリクエストに含めるためのワンタイム・トークンを OpenSocial アプリケーションに要求するメカニズムを提供することが可能です。

単一ユーザーの画像リストのフェッチ

この PhotosHandler クラスの実装は get メソッドによりすべての HTTP GET リクエストを処理します。再び、署名されたリクエストからのパラメータでユーザーを識別し、 getPhotos メソッドを介してユーザーの画像をフェッチします。下で示しているように、画像情報は simplejson ライブラリの dumps メソッドで JSON 文字列に変換されて self.response.out オブジェクトに書き出されます。

class PhotosHandler(webapp.RequestHandler):    
 
def get(self):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return
   
    personId
= self.request.get('opensocial_owner_id')
    container
= self.request.get('oauth_consumer_key')

    photos
= getPhotos(container, personId)

    self
.response.headers['Content-Type'] = 'text/plain'
    self
.response.out.write(simplejson.dumps({'resultsSet': photos}))

fetchPhotosForUser メソッド(訳注 : get メソッドの間違い?)が getPhotos 関数を呼び、そこからさらに指定された person がアップロードした全ての画像をデータストアから取得する getPhotosForUser が呼ばれます。返された Photo オブジェクト内の関連する情報は simplejson ライブラリで「文字列化」できる Python の辞書オブジェクト(連想配列もしくはマップとも呼ばれます)に格納されます。

def getPhotos(container, personId):
  retArray
= []

  photos
= getPhotosForUser(container, personId)

 
if photos:
   
for objt in photos:
      photo
= {}
      photo
['url'] = ''.join(['http://localhost:', PORT, '/photo/', container, ':', personId, ':', objt.name])
      photo
['tags'] = objt.tags

      retArray
.append(photo)

 
return retArray

def getPhotosForUser(container, personId):
  user
= getUser(container, personId)

 
return db.GqlQuery("SELECT * FROM Photo WHERE user = :1", user)

getPhotosForUser メソッドが db.GqlQuery メソッドでデータストアから Photo オブジェクトのコレクションをフェッチする方法に注意してください。

複数ユーザーの画像リストのフェッチ

サーバーは relationship データを保持していないので、 PhotosHandler クラスはリクエストのポストデータ内の ID リストを調べます。その後、それぞれの ID に対して getPhotos 関数を呼び出してその結果を結合し、ひとつの JSON 文字列として返します。このレスポンスと、単一ユーザーの画像リストのレスポンスは僅かに違います: ひとつの配列を返す代わりに、ひとりの person ごとにひとつずつ、複数の配列を返します。いくつか上のセクションのサンプルレスポンスを参照してください。

class PhotosHandler(webapp.RequestHandler):    
 
def post(self):
    form
= cgi.FieldStorage()

    peopleIds
= urllib.unquote(form.getfirst('people')).split(',')
   
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return

    personId
= self.request.get('opensocial_owner_id')
    container
= self.request.get('oauth_consumer_key')

    photoSetCollection
= []

   
for id in peopleIds:
      photoSet
= {}
      photoSet
['name'] = id
      photoSet
['photos'] = getPhotos(container, id)

     
if len(photoSet['photos']) > 0:
        photoSetCollection
.append(photoSet)

    self
.response.headers['Content-Type'] = 'text/plain'
    self
.response.out.write(simplejson.dumps({'resultsCollection': photoSetCollection}))

リクエスト中のポストデータは URL エンコードされているので、 post メソッドが pserson ID のコンマ区切りリストを分割する前に urllib.unquote を使っていることに注目してください。

画像へのタグ付け

画像にタグを追加するために、 PhotoHandler クラスは渡された名前と所有者に関連する Photo をデータベースに問い合わせます(名前と所有者はリクエストの URL として渡されます — 例えば .../photo/<CONTAINER>:<ID>:<PHOTO_NAME> などとして)。この形式は関連する Photo オブジェクトをデータストアから見つけ出すのをとても簡単にします。いったん見つけたら、 Photo オブジェクトは新しいタグで更新されます。

PhotoHandler が別のタイプの POST リクエスト — 画像アップロード — を処理することを思い出してください。リクエストとともに渡されるパラメータを調べることで POST リクエストのタイプを識別できます。フォームで送信されたアップロードリクエストは 'file' メンバを持っているはずです。もしこのメンバがあればアップロードを続行し、そうでなければタグのポストとして処理します。

def post(self):
  form
= cgi.FieldStorage()
 
if not form.has_key('file'):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return
   
    personId
= self.request.get('opensocial_owner_id')
    container
= self.request.get('oauth_consumer_key')

   
if form.getfirst('text'):
      textTag
= urllib.unquote(form.getfirst('text'))

    match
= re.search(r'^http://.*/photo/([\w\.]*?):([\w\.]*?):([\w\.]*)', urllib.unquote(self.request.uri))
   
if match:
      photo
= getPhoto(match.group(1), match.group(2), match.group(3))
     
if photo:
        self
.response.headers['Content-Type'] = 'text/plain'
       
if 'textTag' in locals():
          addTextTagToPhoto
(photo, textTag)
          self
.response.out.write('Text tag added successfully')          
 
else:
   
# See file upload code above

def addTextTagToPhoto(photo, textTag):
  photo
.tags.append(textTag)
  photo
.put()

メソッドの一番最初に条件付きブロックが追加されていることに注目してください。もし form オブジェクトが 'file' キーを持っていれば、このリクエストはアップロードフォームからのものなので、ポストデータを Photo オブジェクトとしてデータストアに保存します。そうでなければ、 URI をパースして画像の名前と所有者を割り出し、画像を取得し、そして addTextTag 関数を呼び出してデータストア内の Photo オブジェクトを更新します。

単一ユーザーのタグのフェッチ

このリクエストはとても直感的です。リクエストのエンドポイントが /tags であれば TagsHandler クラスが呼び出され、 getTagsForUser 関数の助けを借りてユーザーの画像をフェッチし、すべてのタグを収集してリストにします。そして、このリストから重複を削除し、 simplejson ライブラリを使って文字列化したものが結果として返されます。

class TagsHandler(webapp.RequestHandler):    
 
def get(self):
   
if not _isValidSignature(self):
      self
.response.out.write('SIGNATURE INVALID')
     
return
   
    personId
= self.request.get('opensocial_owner_id')
    container
= self.request.get('oauth_consumer_key')

    tags
= getTagsForUser(container, personId)

    self
.response.headers['Content-Type'] = 'text/plain'
    self
.response.out.write(simplejson.dumps({'tags': list(set(tags))}))

def getTagsForUser(container, personId):
  tags
= []

  photos
= getPhotosForUser(container, personId)

 
if photos:
   
for objt in photos:
     
for tag in objt.tags:
        tags
.append(tag)

 
return tags

list(set(tags)) はリストから重複を削除する便利で効果的な方法です。

アプリケーションの公開

ここまで、開発用のアプリケーション・サーバーを使用してきましたが、 orkut や MySpace のような OpenSocial コンテナが Google App Engine アプリケーションにアクセスできるようにするためには、アプリケーションを公開する必要があります。そのためには、 My Applications ページで新しいアプリケーションを作成します — 現在の App Engine では 3 つのアプリケーションしか作成できないので、おそらくなにか汎用的な、 username-dev のような名前にしたいでしょう。そして app.yaml ファイルがそのアプリケーション名を含むように更新してください。

google_appengine ディレクトリで './appcfg.py update <your_app_directory>' を実行してアプリケーションディレクトリを公開します。ブラウザで http://your_app_name.appspot.com/ がアクセスできることを確認してください。

Amazon S3

S3 は Amazon によって提供されているファイル/データストレージの Web サービスです。我々のニーズにとって都合よいことに、 1 バイトから 5 ギガバイトまでの任意のファイルを保存でき、クラウド・サービスであるため<限りなく>スケーラブルです。高速に、いくつでもファイルをアップロードできます。もちろん、それにはコストがかかりますが、データセンターでスペースをレンタルするよりとても安価です(そして言うまでもなく遥かに便利です)。

S3 は REST ベースのサービスなので、 HTTP が有効な任意の開発環境から利用できます。さらに素晴らしいことに、あなたが快適と思ういかなる言語でも確実に S3 と相互運用できる多数のオープンソース・クライアント・ライブラリが存在します。この記事では Python についてのみ説明しますが、サンプルコードは PHP, Java, Ruby などに簡単に移植でき、あなたが選択した言語の S3 クライアントライブラリが利用できることを当然のように期待できます。以下のセクションでは、そのようなライブラリのひとつを使って上記のサンプルを改変します — 画像のバイナリとテキストタグをデータストアに保存する代わりに、実際のファイルとメタデータを S3 に保存します。

このセクションを続けるには、ライブラリが必要とするアクセスキーとシークレットキーを置き換えるために S3 開発者登録を行う必要があります。登録はとても簡単です。キーを取得したら、以下のコードを cloud.py の先頭に追加してください。

# Amazon AWS S3 import
import S3

# Amazon AWS parameters
AWS_ACCESS_KEY_ID
= <YOUR_ACCESS_KEY>
AWS_SECRET_ACCESS_KEY
= <YOUR_SECRET_KEY>

BUCKET_NAME
= ''.join([AWS_ACCESS_KEY_ID.lower(), '.cloud'])

上で定義した Python クラスにとても小さな変更を施す必要があります。データのポストやフェッチをデータストアの代わりに S3 から行うようにヘルパー関数を再実装します。

createPhoto 関数から始めましょう。画像バイナリを App Engine データストアにポストするのに使われていたものです。ここでは上でインポートした S3 ライブラリを使って S3 にアップロードするように再実装します。

def createPhoto(container, personId, fileItem):
  user
= getUser(container, personId)
 
  name
= ''.join([str(int(floor(time()))), fileItem.filename])
  key  
= ''.join([container, personId, '_', name])

  headers
= {
   
'x-amz-acl':'public-read',
   
'Content-Type': fileItem.type,
   
'x-amz-meta-tags': ''
 
}  
     
  conn
= S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, True)
 
  conn
.put(BUCKET_NAME, key, fileItem.file.read(), headers)

 
return {'name': name}

ユーザー情報の管理は引き続き App Engine のデータストアで行いたいので、 getUser は変更しません。設定したいヘッダを格納した新しい辞書オブジェクトを定義していることに注目してください(最後のヘッダは実際には画像にタグを保存するために使われます)。あとは、ライブラリの AWSAuthConnection コンストラクタを呼び出して S3 への接続を開き、それを使って "put" することで新規ファイルを指定した bucket にアップロードするだけです。

次に、画像情報を S3 から取得できるようにします。このように getPhotosForUser を変更します。

def getPhotosForUser(container, personId):
  retArray
= []

  conn
= S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, True)
 
  listResponse
= conn.list_bucket(BUCKET_NAME, {'prefix': ''.join([container, personId, '_'])})
 
 
if listResponse.entries:
   
for entry in listResponse.entries:
      match
= re.search(r'^.*?_(.*)$', entry.key)
     
if match:
        getResponse
= conn.get(BUCKET_NAME, entry.key)
       
if getResponse.http_response.status_code==200:
          photo
= {
           
'name': match.group(1),
           
'tags': []
         
}
         
if getResponse.object.metadata.has_key('tags'):
            tags
= getResponse.object.metadata['tags']
            photo
['tags'] = tags.split('|')
       
          retArray
.append(photo)
 
 
return retArray

サービスとの接続を開いた後、このコードはユーザーに所属する backet 内のすべてのオブジェクトをフェッチします(アップロード時にコンテナと ID をファイル名の前に付けているので、このように与えられたプリフィクスにマッチするもののみを返すように指示することで、簡単に指定されたユーザーの画像をサービスに問い合わせることができます)。いったん全ての画像が利用可能になれば、それぞれの画像に対して個別に content-type とメタデータ(タグ)を取得するためのリクエストを発行し、それをリストに格納して返します。

addTextTagToPhoto メソッドは、サービスから画像を取得して、適切なヘッダを設定してからオブジェクトを "put" する必要があるので、少し大きくなります。

def addTextTagToPhoto(photo, textTag):
  headers
= {
   
'Content-Type': photo['contentType']
 
}
 
 
if not photo['tags'] == '':
    headers
['x-amz-meta-tags'] = ''.join([photo['tags'], '|', textTag])
 
else:
    headers
['x-amz-meta-tags'] = textTag    
 
  conn
= S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, True)
 
  conn
.put(BUCKET_NAME, photo['key'], photo['content'], headers)

事実上、そのほかに変更の必要がある関数は、 S3 からデータをフェッチしてそれをブラウザに返す(もちろん、適切な content-type を指定した後に) getPhoto だけです。

def getPhoto(container, personId, photoName):
  key
= ''.join([container, personId, '_', photoName])
 
  conn
= S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, True)
  response
= conn.get(BUCKET_NAME, key)

  photo
= None
 
if response.http_response.status_code==200:
   
if response.http_response.headers.has_key('content-type'):
      photo
= {
       
'contentType': response.http_response.headers['content-type'],
       
'content': response.http_response.content,
       
'name': photoName,
       
'key': key
     
}
     
     
if response.object.metadata.has_key('tags'):
        photo
['tags'] = response.object.metadata['tags']
     
else:
        photo
['tags'] = ''
   
 
return photo

以上の関数を差し替えて、僅かな追加の変更(すなわち、すべてのオブジェクト参照(例 : photo.name)を photo['name'] のような辞書参照に変更する)を施せば、 Google App Engine ストレージ・バックエンドの S3 への移行が完了します。

最適化

クラウドでのコーディング時によくある誤解は、ストレージスペースや CPU サイクル、帯域幅が無制限であると考えることです。クラウド・ホスティング・プロバイダは、理論的には、アプリケーションが必要するすべてのリソースを提供できますが、クラウドへのホスティングは無料ではないので、それらのリソースはあなたの予算で制限されます。幸い、 OpenSocial はサーバーへの負荷を軽減するために画像やデータをキャッシュするいくつかのメカニズムを提供しています。

getProxyUrl を利用する

"Friend's Photos" タブを表示するのに必要なトラフィック量を考えてみましょう。平均的なユーザーには 10 人の友達がいて、それぞれが 20 の画像(それぞれ 500KB)をアップロードしていると仮定すると、このページのレンダリングは 100MB をサーバーに要求します。もしアプリケーションの人気が出て、このタブが一日に 10,000 ビューを獲得すると、このタブだけで 1TB のトラフィックとなります!

ガジェット・インフラストラクチャはサーバーへの負荷を削減するために積極的にデータをキャッシュするよう設計されていますが、なにをキャッシュすべきかは指定しなければなりません。それには gadgets.io.getProxyUrl メソッドでキャッシュ画像の URL を取得し、それをアプリケーションの HTML で使用します。

function showImage() {
  imgUrl
= 'http://www.example.com/i_heart_apis_sm.png';
  cachedUrl
= gadgets.io.getProxyUrl(imgUrl);

  html
= ['<img src="', cachedUrl, '">'];
  document
.getElementById('dom_handle').innerHTML = html.join('');
};

showImage
();

これで大部分のリクエストはキャッシュ URL に送られるので、サーバーの画像への帯域幅は大幅に削減されます。キャッシュからの利益を最大化するには、コンテンツに適した cache control ヘッダが設定されていることを確認してください。キャッシュに関するさらに詳細な情報は、 OpenSocial Latency Combat Field Manual をご参照ください。

プロフィールページレンダリング用データのキャッシング

OpenSocial コンテナサイトに最も多く表示されるのがプロフィール・ページです。もし Photo Pier の人気が出てきて、 100,000 ユーザーが一日にそれぞれ 10 人のプロフィールを閲覧すると、プロフィールに表示する画像の URL を取得するためだけに、一日に百万リクエスト(毎秒 11 リクエスト以上)を送信することになります。

サーバーへのトラフィックを削減する方法のひとつがプロフィール・ビューの表示に必要なデータの保存に OpenSocial の永続化 API を使用することです。そうすれば、もはやアプリケーションはプロフィール・ビューを表示するためにサーバーにアクセスする必要はありません。

Photo Pier の場合、プロフィールのスライドショーに含める画像 URL のリストを要求しています。このデータをデータベースに保存する代わりに永続化 API を使うことができます。ユーザーがスライドショーに表示する画像を選択したときに、この情報をコンテナに保存できます。

updateFavoritesData: function(value, photoUrl) {
 
var req = opensocial.newDataRequest();
           
 
if (value == true) {
   
this.profilePhotoSet.push(photoUrl);
 
} else {
   
var index = this.profilePhotoSet.indexOf(photoUrl);
   
if (index != -1) {
     
this.profilePhotoSet.splice(index, 1);
   
}
 
}
  req
.add(req.newUpdatePersonAppDataRequest(opensocial.IdSpec.PersonId.VIEWER,
                                           
'favoritePhotos',
                                            gadgets
.json.stringify(this.profilePhotoSet)));
  req
.send();
};

そして、プロフィール・ビューを表示するときは、単にこのデータをサーバーではなくコンテナに要求します。

fetchOpenSocialData: function() {
 
var req = opensocial.newDataRequest();
 
var ownerIdSpec = opensocial.newIdSpec({'userId':'OWNER', 'groupId':'SELF'});

  req
.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.OWNER), 'owner');
  req
.add(req.newFetchPersonAppDataRequest(ownerIdSpec, 'favoritePhotos'), 'profilePhotoUrls');
           
  req
.send(closeFetchOpenSocialData);  
};
closeFetchOpenSocialData
: function(resp) {
 
var ownerResp = resp.get('owner');
 
var photoUrlsResp = resp.get('profilePhotoUrls');
 
 
if (!ownerResp.hadError() && !photoUrlsResp.hadError()) {
   
var ownerData = photoUrlsResp.getData()[this.owner.getId()];
   
if (ownerData) {
      profilePhotoSet
= gadgets.json.parse(gadgets.util.unescapeString(ownerData['favoritePhotos']));
     
/* Now that we've fetched the photo URLs, render them */
   
}
 
}
};

サーバーへのトラフィック削減に加えて、このテクニックには高速という利点もあります — 永続化 API へのデータ要求はサーバーへの往復を行うよりもかなり高速です。

リソース

クラウド・アプリケーションのコーディングを始めるとき、間違いなくいくつかの疑問が浮かぶでしょう。ここに最初の一歩となるリソースを掲載しておきます。

この記事のライセンスは原文に従いますが、基本的に Creative Commons Attribution 2.5 License になるはずです。詳細は原文をご参照ください。