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. コンソールの左側のメニューで、[開発] グループの [データベース] を選択します。

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

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

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

  5. データベースの [ルール] タブで、読み取り / 書き込みのセキュリティ ルールがあることを確認します。例:

    {
      "rules": {
        ".read": true,
        ".write": true
      }
    }
    
  6. プロジェクトの 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. [完了] をクリックします。

  5. 作成したサービス アカウントをクリックし、[キーを追加]、[新しいキーを作成] の順にクリックします。

  6. [キーのタイプ] の横にある [JSON] をクリックし、[作成] をクリックしてキーをダウンロードします。

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

  8. 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 プロジェクトの課金と API を有効にする

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

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

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

  2. Google Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  3. App Engine Admin and 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
    
  4. App Engine を初めて使用する場合は、App Engine アプリを初期化します

    gcloud app create
    

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

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

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

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

    mvn clean package appengine:run
    

    ~/google-cloud-sdk 以外のディレクトリに gcloud コマンドライン ツールをインストールした場合は、次に示すようにコマンドにインストール パスを追加します([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 の下にも、記録されたイベントのログエントリが表示されます。

コードの確認

Playchat iOS アプリは、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 コンソールでプロジェクトを作成しましたが、Firebase プロジェクトと Cloud プロジェクトは同一であるため、Cloud Console でも削除できます。

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] ページに移動

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

App Engine アプリのデフォルト以外のバージョンの削除

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

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

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

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

次のステップ

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

  • サーブレット間での負荷の均等分散 - 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 アプリに音声翻訳機能を追加するをご覧ください。