2013年6月18日火曜日

37.Google Cloud Datastoreを試してみた GCE編 (2/3)

このエントリーをはてなブックマークに追加


33.Google Cloud Datastoreを試してみた 概要編 (1/3)
37.Google Cloud Datastoreを試してみた GCE編 (2/3)
38.Google Cloud Datastoreを試してみた その他環境編 (3/3)

Ryo Yamasaki(@vierjp)です。

途中で「App Engine SDK 1.8.1のリリース」「BigQueryの新機能」「GAE for PHP用WordPressプラグイン」を挟んで
引き続き「Google Cloud Datastore」について試したことを書いていきます。

第1回は概要や仕組み、注意点について書きました。
この第2回ではGoogle Compute EngineからCloud DatastoreのAPIを実行してみます。



◯Google Cloud Console と Cloudプロジェクト

Cloud DatastoreのAPI実行時に「プロジェクト」という概念が出てくるので、
先に「Google Cloud Console」とその「プロジェクト」について簡単に書こうと思います。

先日「Blog @vierjp : 27.Google I/Oで発表されたGoogle Cloud Platformの新機能」に書いた通り、
Google I/Oで「Google Cloud Console」が発表されました。

これまでプロダクト毎に分かれていたGoogle Cloud Platformの各プロダクトを
まとめて扱うための管理画面が「Google Cloud Console」で、
その各プロダクトをまとめる単位が「Cloudプロジェクト」になります。



上の画像のように、
一つの「Cloudプロジェクト」にはAppEngine・ComputeEngine・Cloud Storageなどの各プロダクトが含まれます。

「Cloudプロジェクト」と既存プロダクトとの関連は以下のようになります。

・AppEngineの"一つのアプリケーション"とプロジェクトが1:1で紐付いている
・AppEngineの「アプリケーションID」と Cloud Consoleの「Project ID」は同じ値である
・プロジェクトに紐づくCloud Datastore は"一つのGAEアプリケーションのDatastore"を示す
・既存のAPI Projectは「Cloudプロジェクト」と1:1で紐付いている(ほぼ同等)
という点がポイントです。



◯Cloudプロジェクトの作成

- 新規に作成したGAEアプリケーションのDatastoreを使用する場合

Google Cloud Consoleを開く
・画面左上の「CREATE PROJECT」ボタンを押す
・「Project name」と「Project ID」を入力する。

* デフォルトではかなり適当なID値が表示されますが、
Project IDの値はAPI利用時に使ったり、
GAEアプリケーションのアプリケーションID(=URLの一部)にもなるので、
自分で覚えやすい名前を明示的に指定するのがオススメ。


- 既存GAEアプリケーションのDatastoreを使用する場合
第一回に書いたように Cloud Datastore API の処理は App Engine のアプリで実行されますが、
Cloud Datastore APIの実行時には「Cloudプロジェクト」を指定します。
そのため、過去に作成済みのAppEngineアプリとCloudプロジェクトを紐付けておく必要があります。

1.AppEngineのAdmin Consoleを開く
2.Application SettingsのCloud Integration セクション内「Add Project」を押す

しばらく「進行中」になるので待ちます。
完了すると成功した旨表示され、「Application Settings」のBasics セクションに
「Google APIs Console Project Number:〜〜」の記述が追加されます。


これでそのGAEアプリケーションがCloudプロジェクトと紐付きます。
(エラーになって失敗するケースもあるようですが。。)

参考:Activate Google Cloud Datastore - Google Cloud Datastore



◯サンプルプログラム

ほとんど「Getting Started with Google Cloud Datastore and Java/Protobuf」のサンプルのままです。
コメントは全て日本語に直しました。

1.TriviaというKind名でEntityを一件putする
2.Keyを指定してgetする
3.入力した値とEntityから取得した数字(42)が一致するかを判定してメッセージを出力する
というコードです。


・pom.xml
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelversion>4.0.0</modelversion>
  <groupid>vier-test-client</groupid>
  <artifactid>vier-test-client</artifactid>
  <version>0.0.1-SNAPSHOT</version>
  <name>vier-test-client</name>

<repositories>
  <repository>
    <id>sonatype-snapshots</id>
    <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupid>com.google.apis</groupid>
    <artifactid>google-api-services-datastore-protobuf</artifactid>
    <version>v1beta1-rev1-1.0.0-SNAPSHOT</version>
  </dependency>
</dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactid>maven-eclipse-plugin</artifactid>
        <configuration>
          <downloadsources>true</downloadsources>
          <downloadjavadocs>true</downloadjavadocs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

・ClentTest.java
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.api.services.datastore.DatastoreV1.BlindWriteRequest;
import com.google.api.services.datastore.DatastoreV1.Entity;
import com.google.api.services.datastore.DatastoreV1.Key;
import com.google.api.services.datastore.DatastoreV1.LookupRequest;
import com.google.api.services.datastore.DatastoreV1.LookupResponse;
import com.google.api.services.datastore.DatastoreV1.Property;
import com.google.api.services.datastore.DatastoreV1.Value;
import com.google.api.services.datastore.client.Datastore;
import com.google.api.services.datastore.client.DatastoreException;
import com.google.api.services.datastore.client.DatastoreFactory;
import com.google.api.services.datastore.client.DatastoreHelper;
import com.google.api.services.datastore.client.DatastoreOptions;

public class ClientTest {
 private static final Logger logger = Logger.getLogger(ClientTest.class.getName());

 public static void main(String[] args) {
  if (args.length < 1) {
   System.err.println("Usage: ClientTest DATASET_ID");
   System.exit(1);
  }

  // 環境変数をログ出力
  logger.info("DATASTORE_DATASET:" + System.getenv("DATASTORE_DATASET"));
  logger.info("DATASTORE_HOST:" + System.getenv("DATASTORE_HOST"));
  logger.info("DATASTORE_SERVICE_ACCOUNT:" + System.getenv("DATASTORE_SERVICE_ACCOUNT"));
  logger.info("DATASTORE_PRIVATE_KEY_FILE:" + System.getenv("DATASTORE_PRIVATE_KEY_FILE"));

  // DatasetID(ProjectID) を実行時引数から取得する
  String datasetId = args[0];
  Datastore datastore = null;
  try {
   // 生成したCredentialをセットした「DatastoreOptions」を取得する。
   // Compute Engine上て動作している場合はComputeEngine用のCredentialを、
   // それ以外の環境で、かつ環境変数が適切に設定されている場合は
   // Serivce Account FlowのCredentialを生成する。
   DatastoreOptions.Builder builder = DatastoreHelper.getOptionsfromEnv();

   DatastoreOptions options = builder.dataset(datasetId).build();
   logger.info("options.getHost():" + options.getHost());
   logger.info("options.getDataset():" + options.getDataset());

   datastore = DatastoreFactory.get().create(options);

  } catch (GeneralSecurityException exception) {
   logger.severe("Security error connecting to the datastore: " + exception.getMessage());
   System.exit(1);
  } catch (IOException exception) {
   logger.severe("I/O error connecting to the datastore: " + exception.getMessage());
   System.exit(1);
  }

  try {
   // トランザクション外で更新するためのRPC requestを作成する
   BlindWriteRequest.Builder req = BlindWriteRequest.newBuilder();
   // 新規Entityを作成する
   Entity.Builder entity = Entity.newBuilder();
   // 一つのPathElementでKeyを生成する (親Keyなし)
   Key.Builder key = Key.newBuilder().addPathElement(
     Key.PathElement.newBuilder().setKind("Trivia").setName("hgtg"));
   entity.setKey(key);
   // Entityに2つのPropertyを追加する
   // utf-8文字列の「question」
   entity.addProperty(Property.newBuilder().setName("question")
     .addValue(Value.newBuilder().setStringValue("Meaning of Life?")));
   // 64bit integerの「answer」を追加する
   entity.addProperty(Property.newBuilder().setName("answer").addValue(Value.newBuilder().setIntegerValue(42)));
   // update or insertする「mutation」として「upsert」を指定する
   // 他にも「update」「insert」「insertAutoId」「delete」「force」がある
   // (「force」はユーザーが設定したread-onlyを無視するらしい)
   req.getMutationBuilder().addUpsert(entity);
   // 同期的にRPCを実行して結果を無視する
   // (「返り値を無視する」の意味であって、システムエラー時には例外が発生する)
   datastore.blindWrite(req.build());

   // KeyでEntityを「get」するための RPC リクエストを作成する
   LookupRequest.Builder lreq = LookupRequest.newBuilder();
   // 登録したEntityを「get」するためにKeyを一つ指定する
   // (おそらく複数指定するとbatch get)
   lreq.addKey(key);
   // RPCを実行して結果を取得する
   LookupResponse lresp = datastore.lookup(lreq.build());
   // 結果として一つのEntityを取得する.
   Entity entityFound = lresp.getFound(0).getEntity();
   // 「question」 propertyの値を取得する。
   String question = entityFound.getProperty(0).getValue(0).getStringValue();
   // 「answer」 propertyの値を取得する。
   Long answer = entityFound.getProperty(1).getValue(0).getIntegerValue();
   logger.info(question);
   // コンソールからの値入力待ち
   String result = System.console().readLine("> ");
   // 入力値とEntityから取得した「answer」(=42)が一致した場合
   if (result.equals(answer.toString())) {
    logger.info("fascinating, extraordinary and,when you think hard about it, completely obvious.");

    // 一致しなかった場合
   } else {
    logger.info("Don't Panic!");
   }
  } catch (DatastoreException exception) {
   // DatastoreAPI実行時の例外をcatchする.
   logger.severe("Error while doing datastore operation");
   logger.log(Level.SEVERE, "error", exception);
   System.exit(1);
  }
 }
}



◯Compute Engine から Cloud Datastore の API を使う

前回「Blog @vierjp : "33.Google Cloud Datastoreを試してみた 概要編 (1/3)"」に書いた以下のユースケースでは
Compute EngineからCloud Datastoreに接続します。

ユースケース1 App EngineのバックエンドとしてCompute Engineを利用する
ユースケース2 Compute Engineから使うNoSQLデータベースとして利用する

前回書いたとおりCompute EngineからCloud DatastoreのAPIを利用する場合は認証が簡単です。


- Google Compute Engineを有効にする
Google Cloud Consoleで操作対象の Datastore が属する Cloudプロジェクト を開いて Compute Engine を選択します。
Compute Engine を使用するためにはここで Billing設定を有効にする必要があります。


- gcutilの導入
Google Compute Engine では CUI のツールとして gcutil が提供されています。
今回の手順では gcutil が必要なので最初に準備します。(理由は後述)

gcutil はMac、Linux環境では他のプロダクトのCUIツール同様簡単に導入できますが、
Windows環境では「Cygwin」の導入も必要です。
(おそらく gcutil のコマンドの一部に内部的に ssh や scp コマンドを使うものがあるため)

・ダウンロードして展開する
wget https://google-compute-engine-tools.googlecode.com/files/gcutil-1.8.1.tar.gz
tar xzvpf gcutil-1.8.1.tar.gz -C $HOME
rm gcutil-1.8.1.tar.gz

・Pathに追加する
echo -e "\nexport PATH=\$PATH:\$HOME/gcutil-1.8.1" > $HOME/.bashrc
source $HOME/.bashrc

参考:gcutil Tool - Google Compute Engine


- GCEインスタンスの作成
gcutil --project [Project ID] addinstance [インスタンス名]\
 --image=projects/centos-cloud/global/images/centos-6-v20130522\
 --machine_type=f1-micro\
 --persistent_boot_disk=true\
 --service_account_scopes=\
https://www.googleapis.com/auth/userinfo.email,\
https://www.googleapis.com/auth/datastore

・Project ID
作成済みのCloudプロジェクトのIDを指定します。

・インスタンス名
ここでは好きな名前を指定すればOK。
覚えやすい名前にしましょう。

・image
OSイメージを指定する。
ここでは用意されているCentOS6のイメージを使いました。

・machine_type
インスタンスタイプを指定する。
提供されているインスタンスタイプの内容と料金は以下を参照
Google Compute Engine Pricing - Google Cloud Platform — Cloud Platform

・persistent_boot_disk
起動ディスクをPersistent Diskにするかどうか。
共有インスタンスの f1-micro はPersistent Diskの利用が必須なのでtrueを指定しています。
(Compute Engineについての詳しい話はまたそのうち)

・service_account_scopes
このGCEインスタンスから利用可能なScope
Googleの各API毎に要求するScopeが異なります。
「https://www.googleapis.com/auth/userinfo.email」はユーザーを特定するために定番で指定、
「https://www.googleapis.com/auth/datastore」はCloud DatastoreのAPIを利用するために指定します。


* 現状この手順は gcutil から行う必要があります
本来 GCEインスタンス の作成は Cloud Console の GUI からもできます。
その際にAPIの利用設定(Scopeの設定)をすることが可能ですが、
Cloud Datastore API については現状そこで利用設定をすることができません。(UI的に)
そのため gcutil のコマンドで明示的に Scope を指定してインスタンスを作成する必要があります。


- GCEインスタンスにsshで接続する
gcutil --project [プロジェクト名] ssh [インスタンス名]
インスタンス作成時に指定したプロジェクト名とインスタンス名を指定してsshで接続します。
任意のsshクライアントからIPアドレスを指定して接続することもできますが、
gcutil からなら覚えやすいインスタンス名で接続できるので楽です。


- JDKのインストール
Javaプログラムを実行するのでJDKをyumでインストールします。

yum list \*java-1\* | grep open
sudo yum install java-1.7.0-openjdk.x86_64
sudo yum install java-1.7.0-openjdk-devel.x86_64

- mavenのインストール
mavenをダウンロードして設定します。

wget http://ftp.riken.jp/net/apache/maven/maven-3/3.0.5/binaries/apache-maven-3.0.5-bin.tar.gz
tar xvzf apache-maven-3.0.5-bin.tar.gz
sudo mv apache-maven-3.0.5 /usr/share
sudo ln -s /usr/share/apache-maven-3.0.5 /usr/share/maven

export M2_HOME=/usr/share/maven
export M2=$M2_HOME/bin
export PATH=$M2:$PATH

最後に以下のコマンドで動作を確認。
mvn --version

- ローカルのファイルをGCEインスタンスに転送
gcutil --project [プロジェクト名] push [インスタンス名] \
/Users/User/Documents/workspace/vier-test-client/ /home/User/vier-test-client/
こちらも ssh 同様、インスタンス名を指定して gcutil からファイルの転送ができます。
内部的には scp コマンドが実行されているようです。


- プログラムの実行
mavenから実行します。

mvn clean compile
mvn exec:java -Dexec.mainClass="ClientTest" -Dexec.args="[Project ID]"
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building vier-test-client 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client >>>
[INFO] 
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client <<<
[INFO] 
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client ---
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_DATASET:null
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_HOST:null
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_SERVICE_ACCOUNT:null
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_PRIVATE_KEY_FILE:null
6 12, 2013 11:23:56 午前 com.google.api.services.datastore.client.DatastoreHelper getOptionsfromEnv
情報: Using Compute Engine credential.
6 12, 2013 11:23:56 午前 ClientTest main
情報: options.getHost():https://www.googleapis.com
6 12, 2013 11:23:56 午前 ClientTest main
情報: options.getDataset():***********
6 12, 2013 11:24:01 午前 ClientTest main
情報: Meaning of Life?
> 42
6 12, 2013 11:24:07 午前 ClientTest main
情報: fascinating, extraordinary and,when you think hard about it, completely obvious.

実行時引数は「Project ID」を指定します。(コード中では datasetId としているけど同じ値)
冒頭に書いたとおり「Cloudプロジェクト」 と 「Datastore」 は 1:1 で紐付いているので、
「Cloudプロジェクト」のIDを指定することでアクセス対象のDatastoreが確定します。


DatastoreHelper#getOptionsfromEnv()メソッドの中では、
1. Compute Engine用のCredentialの取得を試みて、取得できればそれを返す
 →Compute Engine上で実行した場合はCompute Engine用のCredentialを取得できる
  (実行ログ23行目の「Using Compute Engine credential.」はこの分岐のログ)

2. (1でCredentialを取得できなければ)Service Account Flow用のCredentialを生成して返す
 →Compute Engine用のCredentialを取得できなければ、環境変数にセットした情報を用いてCredentialを生成して返す
という処理が行われています。

よってローカル環境でテストする際やCompute Engine以外の環境でも、
環境変数の設定をするだけでプログラムの修正は無しに動作する作りになっています。

(Compute Engine から Scope設定 だけでアクセスできるのは同一プロジェクト内の Datastore だけなので、
ComputeEngine 優先のフローは 別Project の Datastore にアクセスしようとした場合に困りそうな気もしますが)



◯おまけ

GCEインスタンス上ではGoogle APIを実行するためのCredentialを以下のコードで取得できるようです。

  public static Credential getComputeEngineCredential()
      throws GeneralSecurityException, IOException {
    NetHttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
    ComputeCredential credential = new ComputeCredential(transport, new JacksonFactory());
    credential.refreshToken();
    return credential;
  }

参考:
com.google.api.services.datastore.client.DatastoreHelper#getComputeEngineCredential()

「ComputeCredential」は「google-api-client-1.15.0-rc.jar」に含まれているので、
ComputeEngineのインスタンスを生成する際に必要なScopeを指定することで、
おそらくCloud Datastore以外のAPIを使うときにも使えるでしょう。



◯参考リンク

Google Cloud Datastore 公式ドキュメント

Activate Google Cloud Datastore - Google Cloud Datastore

Getting Started with Google Cloud Datastore and Java/Protobuf

gcutil Tool - Google Compute Engine



◯次回

次回は Compute Engine 以外の環境から Cloud Datastore の API を試します。
今回は put と get だけでしたが、次回はクエリも実行してみようと思います。



このエントリーをはてなブックマークに追加