Firebase と App Engine フレキシブル環境を使用した iOS アプリの作成

このチュートリアルでは、Firebase を使用したバックエンド データ ストレージやリアルタイム同期、ユーザー イベント ログに対応する iOS アプリの開発方法をご紹介します。App Engine フレキシブル環境で動作する Java サーブレットが、Firebase に保存された新しいユーザーログをリッスンして処理します。

このチュートリアルの手順では、FirebaseApp Engine フレキシブル環境を使用して目的のモバイルアプリを開発する方法を説明します。

ユーザーデータの処理またはイベントの統合がアプリ側で必要な場合は、App Engine フレキシブル環境を利用して Firebase を拡張すれば、リアルタイムの自動データ同期が利用できます。

Playchat サンプルアプリによりチャット メッセージが Firebase Realtime Database に保存され、デバイス間で自動的に同期されます。Playchat は、ユーザー イベントログも Firebase に書き込みます。データベースによるデータの同期方法について詳しくは、Firebase のドキュメントで動作の仕組みをご覧ください。

次の図に、Playchat クライアントのアーキテクチャを示します。

Playchat クライアント アーキテクチャ

App Engine フレキシブル環境で動作する一連の Java サーブレットが、リスナーとして Firebase に登録されます。これらのサーブレットが新しいユーザーイベント ログに応答し、ログデータを処理します。サーブレットはトランザクションを使用します。これにより、各ユーザーイベント ログを処理するサーブレットがただ 1 つであることが保証されます。

次の図に、Playchat サーバーのアーキテクチャを示します。

Playchat サーバー アーキテクチャ

アプリとサーブレットの間の通信は以下の 3 つの部分で発生します。

  • 新しいユーザーが Playchat にログインし、アプリがそのユーザーのロギング サーブレットをリクエストするために Firebase Realtime Database の /inbox/ の下にエントリを追加したとき。

  • いずれかのサーブレットが、このエントリの値を自分のサーブレット ID に変更して割り当てを受け入れたとき。このサーブレットは、自分以外のサーブレットが値を変更できないことを保証するために Firebase トランザクションを使用します。値が変更されると、他のすべてのサーブレットはリクエストを無視します。

  • ユーザーがログインまたはログアウトするか、新しいチャネルに切り替えたとき。このアクションを Playchat が /inbox/[SERVLET_ID]/[USER_ID]/ に記録します。ここで、[SERVLET_ID] はサーブレット インスタンスの ID、[USER_ID] はユーザーを表すハッシュ値です。

  • サーブレットが Inbox を監視して新しいエントリのログデータを収集するとき。

このサンプルアプリでは、ログデータがサーブレットによりローカルでコピーされ、ウェブページに表示されます。このアプリの本番環境バージョンでは、ログデータをサーブレットで処理することや、保存や分析のために Cloud StorageCloud BigtableBigQuery にコピーすることができます。

目標

このチュートリアルでは、以下の手順を紹介します。

  • iOS アプリ Playchat を作成する。このアプリはデータを Firebase Realtime Database に保存します。

  • App Engine フレキシブル環境で Java サーブレットを実行する。このサーブレットは Firebase に接続し、Firebase に格納されているデータが変更されたとき通知を受け取ります。

  • 上記の 2 つのコンポーネントで分散型のストリーミング バックエンド サービスを構築してログデータを収集し処理する。

費用

Firebase の使用は特定条件のもとで無料です。これらのサービスの使用量が Firebase 無料プランで指定された制限未満である場合、Firebase を使用しても料金はかかりません。

App Engine フレキシブル環境内のインスタンスについては、基盤となる Compute Engine 仮想マシンに対して費用がかかります。

始める前に

以下のソフトウェアをインストールします。

ターミナル ウィンドウで次のコマンドを実行して、Cloud SDK の App Engine Java コンポーネントをインストールします。

gcloud components install app-engine-java

サンプルコードのクローンの作成

  1. クライアント アプリのコードのクローンを作成します。

    git clone https://github.com/GoogleCloudPlatform/firebase-ios-samples
    
  2. バックエンド サーブレットのコードのクローンを作成します。

    git clone https://github.com/GoogleCloudPlatform/firebase-appengine-backend
    

Firebase プロジェクトの作成

  1. Firebase アカウントを作成するか、既存のアカウントにログインします。

  2. [プロジェクトを追加] をクリックします。

  3. [プロジェクト名] に「Playchat」と入力します。プロジェクトに割り当てられたプロジェクト ID は、このチュートリアルの複数の手順で使用するのでメモしておきます。

  4. 残りの設定手順を行い、[プロジェクトを作成] をクリックします。

  5. ウィザードでプロジェクトがプロビジョニングされたら、[続行] をクリックします。

  6. プロジェクトの [概要] ページで [設定] の歯車をクリックし、[プロジェクトの設定] をクリックします。

  7. [iOS アプリに Firebase を追加] をクリックします。

  8. [iOS バンドル ID] に「com.google.cloud.solutions.flexenv.PlayChat」と入力します。

  9. [アプリの登録] をクリックします。

  10. [設定ファイルのダウンロード] セクションの手順を行い、プロジェクトの PlayChat フォルダに GoogleService-Info.plist ファイルを追加します。

  11. [設定ファイルのダウンロード] セクションで、[次へ] をクリックします。

  12. CocoaPods の使用方法をメモし、プロジェクトの依存関係をインストールして管理します。サンプルコードでは、CocoaPods 依存関係マネージャーがすでに構成されています。

  13. 次のコマンドを実行して、依存関係をインストールします。コマンドが完了するまでに時間がかかる場合があります。

    pod install
    

    CocoaPods が Firebase の依存関係を見つけられない場合は、pod repo update を実行する必要があります。

    このステップが完了したら、この iOS アプリの以降のすべての開発に、新たに作成された .xcworkspace ファイルを .xcodeproj ファイルの代わりに使用します。

  14. [Firebase SDK の追加] セクションで [次へ] をクリックします。

  15. プロジェクトで Firebase の初期化に必要なコードをメモします。

  16. [初期化コードの追加] セクションで、[次へ] をクリックします。

  17. [アプリを実行してインストールを確認] セクションで [このステップをスキップ] をクリックします。

Realtime Database の作成

  1. Firebase コンソールの左側のメニューで、[開発] グループの [データベース] を選択します。

  2. [データベース] ページで [Realtime Database] セクションに移動し、[データベースの作成] をクリックします。

  3. [Realtime Database のセキュリティ ルール] ダイアログで、[テストモードで開始] を選択して [有効にする] をクリックします。

    この操作により、Firebase に保存したデータが表示されます。このチュートリアルの後のステップで、ウェブページを再度閲覧し、クライアント アプリとバックエンド サーブレットにより追加、変更されたデータを確認します。

  4. プロジェクトの Firebase URL をメモします。これは、リンクアイコンの横に https://[FIREBASE_PROJECT_ID].firebaseio.com/ という形式で表示されています。

Firebase プロジェクトの Google 認証の有効化

Firebase プロジェクトに接続するために、さまざまなログイン プロバイダを構成できます。このチュートリアルでは、ユーザーが Google アカウントを使用してログインできるように認証を設定する方法について説明します。

  1. Firebase コンソールの左側のメニューで、[開発] グループの [認証] をクリックします。

  2. [ログイン方法を設定] をクリックします。

  3. [Google] を選択し、[有効にする] をオンにして、[保存] をクリックします。

Firebase プロジェクトへのサービス アカウントの追加

バックエンド サーブレットでは、ログインに Google アカウントを使用しません。代わりに、サービス アカウントを使用して Firebase に接続します。以下の手順では、Firebase に接続するサービス アカウントを作成して、サービス アカウントの認証情報をサーブレットのコードに追加する方法について説明します。

  1. Firebase コンソールの左側のメニューで、Playchat プロジェクト ホームの横にある設定の歯車アイコンを選択し、[プロジェクトの設定] を選択します。

  2. [サービス アカウント]、[すべてのサービス アカウントを管理] の順に選択します。

  3. [サービス アカウントを作成] をクリックします。

  4. 以下を構成します。

    1. [サービス アカウント名] に、「playchat-servlet」と入力します。
    2. [役割] で、[プロジェクト] > [オーナー] を選択します。

    3. [新しい秘密鍵の提供] をオンにします。

    4. [キーのタイプ] で [JSON] を選択します。

  5. [作成] をクリックします。

  6. サービス アカウントの JSON キーファイルをダウンロードして、バックエンド サービス プロジェクト(firebase-appengine-backend)の src/main/webapp/WEB-INF/ ディレクトリに保存します。このファイル名は Playchat-[UNIQUE_ID].json という形式になります。

  7. src/main/webapp/WEB-INF/web.xml を編集して、初期化パラメータを以下のように設定します。

    • JSON_FILE_NAME を、ダウンロードした JSON キーファイルの名前に置き換えます。

    • FIREBASE_URL を、以前にメモした Firebase URL で置き換えます。

      <init-param>
        <param-name>credential</param-name>
        <param-value>/WEB-INF/JSON_FILE_NAME</param-value>
      </init-param>
      <init-param>
        <param-name>databaseUrl</param-name>
        <param-value>FIREBASE_URL</param-value>
      </init-param>
      

Google Cloud Platform プロジェクトの課金と API の有効化

GCP でバックエンド サービスを実行するには、プロジェクトの課金と API を有効にする必要があります。GCP プロジェクトは、Firebase プロジェクトの作成で作成したプロジェクトと同じものであり、同じプロジェクト ID が付いています。

  1. Google Cloud Platform Console で、Playchat プロジェクトを選択します。

    プロジェクト ページに移動

  2. Google Cloud Platform プロジェクトに対して課金が有効になっていることを確認します。 詳しくは、課金を有効にする方法をご覧ください。

  3. App Engine Admin、Compute Engine API を有効にします。

    APIを有効にする

バックエンド サービスのビルドとデプロイ

このサンプルでは、バックエンド サービスは Docker 構成を使用してホスティング環境を指定します。このようにカスタマイズするには、App Engine のスタンダード環境ではなくフレキシブル環境を使用する必要があります。

バックエンド サーブレットをビルドして App Engine フレキシブル環境にデプロイするには、Google App Engine Maven プラグインを使用できます。このプラグインは、このサンプルに含まれている Maven ビルドファイルですでに指定されています。

プロジェクトの設定

Maven でバックエンド サーブレットが正しくビルドされるようにするには、サーブレットのリソースを起動する Google Cloud Platform(GCP)プロジェクトを Maven に指定する必要があります。GCP プロジェクトと Firebase プロジェクトの ID は同じです。

  1. gcloud ツールが GCP へのアクセスに使用する、認証情報を入力します。

    gcloud auth login
    
  2. 次のコマンドを実行して、プロジェクトを Firebase プロジェクトに設定します。[FIREBASE_PROJECT_ID] は、以前にメモした Firebase プロジェクト ID に置き換えます。

    gcloud config set project [FIREBASE_PROJECT_ID]
    
  3. 構成を表示して、プロジェクトが設定されたことを確認します。

    gcloud config list
    

(省略可)ローカル サーバーでのサービスの実行

新しいバックエンド サービスを開発する際には、App Engine にデプロイする前にローカルでそのサービスを実行するのが有効です。これにより、変更の迅速な反復適用が可能になり、App Engine にデプロイする際のオーバーヘッドが回避されます。

ローカルでサーバーを実行する場合、Docker の構成は使用されず、App Engine 環境では実行されません。代わりに、Maven がすべての依存関係ライブラリをローカルにインストールし、Jetty 上でアプリが実行されます。

  1. firebase-appengine-backend ディレクトリで、次のコマンドによりローカルでバックエンド モジュールをビルドして実行します。

    mvn clean package appengine:run
    

    gcloud コマンドライン ツールを ~/google-cloud-sdk 以外のディレクトリにインストールした場合は、次に示すようにコマンドにインストール パスを追加します([PATH_TO_TOOL] の部分をそのディレクトリのパスに置き換えてください)。

    mvn clean package appengine:run -Dgcloud.gcloud_directory=[PATH_TO_TOOL]
    
  2. アプリケーション "Python.app" で受信ネットワーク接続を受け入れますか?」というメッセージが表示された場合は、[Allow] を選択します。

デプロイの終了後、http://localhost:8080/printLogs を開いて、バックエンド サービスが実行されていることを確認します。ウェブページに、「Inbox :」に続いて 16 桁の ID が表示されます。これは、ローカルマシンで実行されているサーブレットの Inbox ID です。

ページを更新してもこの ID は変わりません。ローカル サーバーは単一のサーブレット インスタンスを起動するからです。これはテストの際に役立ちます。Firebase Realtime Database にはサーブレット ID が 1 つしか保存されないためです。

ローカル サーバーをシャットダウンするには、Ctrl+C と入力します。

App Engine フレキシブル環境へのサービスのデプロイ

App Engine フレキシブル環境でバックエンド サービスを実行すると、App Engine は /firebase-appengine-backend/src/main/webapp/Dockerfiles の構成を使用して、サービスが実行されるホスティング環境を構築します。フレキシブル環境は複数のサーブレット インスタンスを起動し、需要に応じてその規模を拡大または縮小します。

  • firebase-appengine-backend ディレクトリで、次のコマンドによりローカルでバックエンド モジュールをビルドして実行します。

    mvn clean package appengine:deploy
    
    mvn clean package appengine:deploy -Dgcloud.gcloud_directory=[PATH_TO_GCLOUD]
    

ビルドを実行すると、「Sending build context to Docker daemon…」と表示されます。上記のコマンドにより、Docker の構成がアップロードされ、App Engine フレキシブル環境に設定されます。

デプロイが終了したら、https://[FIREBASE_PROJECT_ID].appspot.com/printLogs を開きます。ここで、[FIREBASE_PROJECT_ID]Firebase プロジェクトの作成で割り当てられた ID です。ウェブページに、「Inbox :」に続いて 16 桁の ID が表示されます。これは、App Engine フレキシブル環境で実行されているサーブレットの Inbox ID です。

ページを更新すると、この ID は定期的に変わります。これは、受信したクライアントからのリクエストを処理するために、App Engine が複数のサーブレット インスタンスを起動するためです。

iOS サンプルの URL スキームの更新

  1. Xcode で、PlayChat ワークスペースを開いた状態で PlayChat フォルダを開きます。

  2. GoogleService-Info.plist を開いて REVERSED_CLIENT_ID の値をコピーします。

  3. Info.plist を開いて、[key URL types] > [Item 0 (Editor)] > [URL Schemes] > [Item 0] の順に移動します。

  4. プレースホルダ値 [REVERSED_CLIENT_ID] を、GoogleService-Info.plist からコピーした値に置き換えます。

iOS アプリの実行とテスト

  1. Xcode で、PlayChat ワークスペースを開いた状態で [Product] > [Run] を選択します。

  2. アプリがシミュレータに読み込まれたら、Google アカウントでログインします。

    Playchat にログイン

  3. [books] チャンネルを選択します。

  4. メッセージを入力します。

    メッセージを送信

入力すると、Playchat アプリがメッセージを Firebase Realtime Database に保存します。Firebase はデータベースに保存されたデータをデバイス間で同期します。Playchat を実行するデバイスでは、ユーザーが [books] チャンネルを選択すると新しいメッセージが表示されます。

メッセージを送信

データの検証

Playchat アプリを使ってユーザー イベントをいくつか生成した後に、サーブレットがリスナーとして登録され、ユーザーイベント ログが収集されていることを確認します。

アプリの Firebase Realtime Database を開きます。ここで、[FIREBASE_PROJECT_ID]Firebase プロジェクトを作成で割り当てられた ID です。

https://console.firebase.google.com/project/[FIREBASE_PROJECT_ID]/database/data

Firebase Realtime Database の最下部(/inbox/ の下)に、接頭辞 client- の付いたノードのグループが表示され、その後に、ユーザー アカウントのログインを表すランダム生成のキーが続きます。以下の例の最後の項目 client-1240563753 の後には、ユーザーのログイベントを現在リッスンしているサーブレットを示す 16 桁の ID が続きます(この例では、0035806813827987)。

Firebase Realtime Database に保存されたデータ

その上(/inbox/ の下)には、現在割り当てられているすべてのサーブレットのサーブレット ID が表示されます。この例では、ログを収集しているサーブレットは 1 つだけです。/inbox/[SERVLET_IDENTIFIER] の下には、アプリによりそのサーブレットに書き込まれたユーザーログが示されます。

バックエンド サービスの App Engine ページ(https://[FIREBASE_PROJECT_ID].appspot.com/printLogs)を開きます。ここで、[FIREBASE_PROJECT_ID]Firebase プロジェクトの作成で割り当てられた ID です。このページには、生成されたユーザー イベントを記録するサーブレットの ID が表示されます。また、そのサーブレットの Inbox ID の下にも、記録されたイベントのログエントリが表示されます。

コードの確認

iOS アプリである Playchat は、FirebaseLogger というクラスを定義します。これは、Firebase Realtime Database にユーザー イベント ログを書き込むために使用されます。

import Firebase

class FirebaseLogger {
  var logRef: DatabaseReference!

  init(ref: DatabaseReference!, path: String!) {
    logRef = ref.child(path)
  }

  func log(_ tag: String!, message: String!) {
    let entry: LogEntry = LogEntry(tag: tag, log: message)
    logRef.childByAutoId().setValue(entry.toDictionary())
  }
}

新しいユーザーがログインすると、Playchat は requestLogger 関数を呼び出して Firebase Realtime Database の /requestLogger/ に新しいエントリを追加します。また、リスナーを設定して、サーブレットが割り当てを受け入れてエントリの値を変更したときに、Playchat が応答できるようにします。

サーブレットが値を変更すると、Playchat はリスナーを削除して、「Signed in」のログをサーブレットの Inbox に書き込みます。

func requestLogger() {
  ref.child(IBX + "/" + inbox!).removeValue()
  ref.child(IBX + "/" + inbox!)
    .observe(.value, with: { snapshot in
      print(self.inbox!)
      if snapshot.exists() {
        self.fbLog = FirebaseLogger(ref: self.ref, path: self.IBX + "/"
          + String(describing: snapshot.value!) + "/logs")
        self.ref.child(self.IBX + "/" + self.inbox!).removeAllObservers()
        self.msgViewController!.fbLog = self.fbLog
        self.fbLog!.log(self.inbox, message: "Signed in")
      }
    })
  ref.child(REQLOG).childByAutoId().setValue(inbox)
}

バックエンド サービス側では、サーブレット インスタンスが起動されると、MessageProcessorServlet.javainit(ServletConfig config) 関数が Firebase Realtime Database に接続し、リスナーを /inbox/ に追加します。

新しいエントリが /inbox/ に追加されると、サーブレットはその値を自身の ID に変更します。また、そのユーザーログの処理の割り当てをサーブレットが受け入れたことを示すシグナルが Playchat アプリに送信されます。サーブレットは Firebase のトランザクションを使用することで、値を変更して割り当てを受け入れるサーブレットがただ 1 つであることを保証します。

/*
 * Receive a request from a client and reply back its inbox ID.
 * Using a transaction ensures that only a single servlet instance replies
 * to the client. This lets the client know to which servlet instance
 * send consecutive user event logs.
 */
firebase.child(REQLOG).addChildEventListener(new ChildEventListener() {
  public void onChildAdded(DataSnapshot snapshot, String prevKey) {
    firebase.child(IBX + "/" + snapshot.getValue()).runTransaction(new Transaction.Handler() {
      public Transaction.Result doTransaction(MutableData currentData) {
        // Only the first servlet instance writes its ID to the client inbox.
        if (currentData.getValue() == null) {
          currentData.setValue(inbox);
        }
        return Transaction.success(currentData);
      }

      public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {}
    });
    firebase.child(REQLOG).removeValue();
  }
  // ...
});

サーブレットは、ユーザーのイベントログ処理の割り当てを受け入れた後、Playchat アプリが新しいログファイルをサーブレットの Inbox に書き込んだことを検出するリスナーを追加します。Inbox への書き込みを検出すると、サーブレットは Firebase Realtime Database から新しいログデータを取得します。

/*
 * Initialize user event logger. This is just a sample implementation to
 * demonstrate receiving updates. A production version of this app should
 * transform, filter, or load to another data store such as Google BigQuery.
 */
private void initLogger() {
  String loggerKey = IBX + "/" + inbox + "/logs";
  purger.registerBranch(loggerKey);
  firebase.child(loggerKey).addChildEventListener(new ChildEventListener() {
    public void onChildAdded(DataSnapshot snapshot, String prevKey) {
      if (snapshot.exists()) {
        LogEntry entry = snapshot.getValue(LogEntry.class);
        logs.add(entry);
      }
    }

    public void onCancelled(DatabaseError error) {
      localLog.warning(error.getDetails());
    }

    public void onChildChanged(DataSnapshot arg0, String arg1) {}

    public void onChildMoved(DataSnapshot arg0, String arg1) {}

    public void onChildRemoved(DataSnapshot arg0) {}
  });
}

クリーンアップ

このチュートリアルで使用するリソースについて、Google Cloud Platform アカウントに課金されないようにする手順は次のとおりです。

Google Cloud Platform と Firebase のプロジェクトの削除

課金を停止する最も簡単な方法は、このチュートリアルで作成したプロジェクトを削除することです。Firebase コンソールでプロジェクトを作成した場合でも、GCP Console でプロジェクトを削除できます。なぜなら、Firebase プロジェクトと GCP プロジェクトは同一であるからです。

  1. GCP Console で [プロジェクト] ページに移動します。

    プロジェクト ページに移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

デフォルト バージョンではない App Engine アプリを削除する

GCP プロジェクトと Firebase プロジェクトを削除しない場合は、App Engine フレキシブル環境のデフォルト バージョンではないアプリを削除すれば、課金をいくらか減らすことができます。

  1. GCP Console で、App Engine の [バージョン] ページに移動します。

    [バージョン] ページに移動

  2. デフォルト以外で削除するアプリのバージョンのチェックボックスを選択します。
  3. アプリのバージョンを削除するには、[削除]()をクリックします。

次のステップ

  • データの解析とアーカイブ - このサンプルでは、サーブレットはログデータをメモリ内のみに保存します。このサンプルの機能を拡張するには、Cloud StorageCloud BigtableGoogle Cloud DataflowBigQuery などのサービスを使用して、サーブレットによってデータがアーカイブ、変換、分析されるようにすることができます。

  • サーブレット間での負荷の均等分散 - App Engine では、自動スケーリングと手動スケーリングの両方が使用できます。自動スケーリングでは、フレキシブル環境でワークロードの変化が検出されると、クラスタで VM が追加または削除されます。手動スケーリングでは、トラフィックを処理するインスタンスの数を固定値で指定します。スケーリングの構成方法については、App Engine ドキュメントのサービス スケーリング設定をご覧ください。

    ユーザー アクティビティ ログは、Firebase Realtime Database にアクセスしてサーブレットに割り当てられるため、ワークロードが均等に分散されないことがあります。たとえば、特定のサーブレットが他のサーブレットよりも多くのユーザーイベント ログを処理する場合があります。

    それぞれの VM のワークロードを個別に制御するワークロード マネージャーを実装すれば効率が良くなります。このようなワークロード分散処理は、秒単位のリクエストのロギングや、同時クライアント数の監視といった指標に基づいて実行されます。

  • 未処理のユーザー イベント ログの回復 - このサンプルの実装内容では、サーブレット インスタンスがクラッシュしても、そのインスタンスに関連付けられたクライアント アプリは、ログイベントを Firebase Realtime Database のサーブレットの Inbox に送信し続けます。アプリの本番環境バージョンでは、バックエンド サービスでこの状況を検出して、未処理のユーザー イベント ログを回復する必要があります。

  • Cloud AI プロダクトを使用した追加機能の実装 - Cloud AI プロダクトおよびサービスを使用して ML ベースの機能を実装する方法です。たとえば、このサンプルの実装内容を拡張して、Speech-to-Text APITranslation APIText-to-Speech API を組み合わせた音声翻訳機能を実装できます。詳しくは、Android アプリに音声翻訳機能を追加するをご覧ください。

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...