FirebaseGoogle Cloud 

Firebase Local Emulator Suiteを使って、Firebaseアプリケーションの開発環境を構築しましょう!

はいさい!ちゅらデータのAustin John Mayer、通称、井上オースティンです。
今日のお題は、Firebase Local Emulator Suiteです。

Firebase Local Emulator Suiteとは

Firebase Local Emulator Suite は、Firebaseの様々なバックエンドサービスをローカルでエミュレートするソフトです。

正式ドキュメントの説明を引用すると

Firebase Local Emulator Suite は、Cloud Firestore、Realtime Database、Cloud Storage、Authentication、Cloud Functions、Pub/Sub、Firebase Hosting、Firebase Extensions を使用してアプリをローカルでビルドおよびテストするデベロッパー向けの高度なツールセットです

つまり、すべてのサービスを本番環境と全く同じ条件で再現できるのです。
Firebaseで開発した経験がおありの方ならば、このソフトのありがたさと重要性がわかるはずです。

なぜ重要なのか

Firebaseでは、バックエンドの機能は全てFirebaseを介して実装します。

これは多くの場合、とてもありがたいことなのですが、開発の際にRealtime Databaseの設定を試しに変えたり、Firebase Storageのルールをチューニングしたいなど、間違えた操作でアプリケーション全体に影響を及ぼしかねないのです。

もちろん、開発用のFirebaseプロジェクトを作って、そこで開発用に操作したりすることもできます。
しかし、この方法は費用もかかるでしょうし、セキュリティ上の問題もあるでしょうし、複数の開発者が同時に使おうとすると、話になりません 。
そこで、やはり、ローカルで好き勝手ができる環境を用意したい!
好き勝手ができる、これはエンジニアにとって非常に重要なことで、Firebase Local Emulator Suiteを使う最も大きなメリットに思えます。

チュートリアル用の環境を構築する

本記事ではFirebaseの模擬プロジェクトを作っていきますが、読者さんも一緒に再現できるように環境構築を先に説明します。

環境構築の手順

1.Viteをセットアップする
2.新しいFirebaseプロジェクトを立ち上げる

この部分が不要な方は、飛ばしてください。

Viteを使い、開発環境を一気に立てよう。

Viteを使うと、あっという間にフロントエンドの開発環境構築ができます。

yarn create vite firebase-demo --template vanilla-ts
cd firebase-demo/
yarn install

そして次に、npmパッケージのfirebaseをインストールします。

yarn add firebase

すると、必要な準備が終わったので、以下のコマンドを実行してローカルサーバーを起動させましょう。

yarn dev

ターミナルに出力されるViteのローカルサーバーのアドレスを開いておきましょう。

Viteのデフォルトの画面が出てくれたら、OKです!

Firebaseプロジェクトをセットアップする

以前の記事でも説明しましたが、次はFirebaseコンソールで新規プロジェクトを作成します。

Firebase Console(https://console.firebase.google.com/)

ログインしたら、ダッシュボードで プロジェクトを追加 のボタンを クリックして案内に従ってプロジェクトを作成します。

プロジェクト作成が終わると、Firebaseプロジェクトのダッシュボードに遷移します。
ここから、プロジェクト設定を開き、下にスクロールして、“のアイコンをクリックしてWebアプリケーションの登録を行います。Hostingは不要ですが、筆者はとりあえずつけておきました。
登録が終わると、Firebaseのコンフィグ情報が表示されます。これをコピーして、src/firebase.tsというファイルを作成して貼り付けます。

typescript
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyBA7E5uMhwOMum2TPqTpQvV5Q-ki6EkX9E",
  authDomain: "austin-blog.firebaseapp.com",
  projectId: "austin-blog",
  storageBucket: "austin-blog.appspot.com",
  messagingSenderId: "234954796390",
  appId: "1:234954796390:web:8695aaee9ee2567d222ca7",
};

// Initialize Firebase
export const app = initializeApp(firebaseConfig);

最後のconst app = …にexportを追加しておきましょう。

Firebase CLIをインストールする

ローカルから上記のFirebaseコンソールで作ったプロジェクトとのやりとりができるように、Firebase CLIをインストールする必要があります。
最も簡単なやり方は、npmを使ってグローバルパッケージとしてインストールする方法です。

npm install -g firebase-tools

Firebase CLIでプロジェクトにログインする

次は、Firebase CLIを先ほど作ったプロジェクトにリンクさせます。
まず、Firebase CLIにGoogleアカウントの認証情報を渡さないといけないので、ログインしましょう。

firebase login

すると、ブラウザが開かれますが、Googleアカウントを選択し、「許可」をクリックしましょう。

認証ができていれば、上記のプロジェクトにアクセスすることができます。
続いて、以下のコマンド実行し、プロジェクトに接続するのと同時に、Realtime Databaseも作ります。

firebase init

CLIでHostingとRealtime Databaseを選んで、「既存のプロジェクトを仕様する」も選択し、先ほど作ったプロジェクトの名前を選びます。

Realtime Databaseは作成していないので、y と答えて作ってもらいましょう!

サーバーの場所はなるべき物理的に近いものがいいので、asia-southeast1にしましょう。

また、セキュリティルールの設定ファイルの名前を問われますが、デフォルトでいいのでEnterを押して続けます。

最後にHostingの設定を問われますが、以下の通りで問題ありません。ダイレクトリの設定だけ、Viteの出力ダイレクトリの./dist/に設定しましょう。

すべてのセットアップが完了すると、firebase.jsonというファイルが作成されます。ここにプロジェクトの各設定が入っており、後々のemulatorsでも使います。

json firebase.json
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

Realtime Databaseのルールを設定する

先ほどのRealtime Databaseのセットアップで、database.rules.jsonというファイルが作成されましたが、こちらはデフォルトで読み込みも書き込みもできない設定になっています。
今回のデモのために、便宜的に以下のようにルールを書いて全てを可能にします。
しかし、読者はもちろんお分かりだと思いますが、一応念のために断っておきます。
実際の本番環境だったら、以下の設定のような設定は絶対にやってはいけないことです。正しくルールの設定をしましょう。

ルールの設定についてはこちら(https://firebase.google.com/docs/database/security)をご参照いただければと思います。

json database.rules.json
{
  "rules": {
    ".read": true,
    ".write": true
  }
}

この設定をCLIでプロジェクトに反映させます。以下のコマンドを実行すれば、すべてのローカルの設定をアップロードしてデプロイできます。

firebase deploy

実行すると、以下のように登録しているすべてのFirebaseサービスを更新してくれます。Firebaseの力です!

TypeScriptで簡単なTODOアプリケーションを作ります。

最後に、上記のRealtime Databaseを使ったTODOアプリケーションを簡単に作成します。
このアプリケーションを作成するために、Firebaseが提供するnpmパッケージを使います。
最初に、index.htmlを以下のように修正します。

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Firebase Todo</title>
  </head>
  <body>
    <h1>Firebase Todo App</h1>
    <firebase-todo></firebase-todo>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

src/main.tsを以下のように編集します。

typescript src/main.ts
import "./style.css";
import { getDatabase, push, ref, get, remove } from "firebase/database";
import { app } from "./firebase";

class FirebaseTodo extends HTMLElement {
  private form!: HTMLFormElement;
  private ul!: HTMLUListElement;
  private database: ReturnType<typeof getDatabase>;

  constructor() {
    super();
    this.database = getDatabase(app);
  }

  connectedCallback() {
    this.innerHTML = `
    <style>
      .flex-column {
        display: flex;
        flex-direction: column;
      }
    </style>
    <div class="flex-column">
      <form>
        <input type="text" name="title" required>
        <button type="submit">Add</button>
      </form>
      <ul></ul>
    </div>`;
    this.form = this.querySelector("form")!;
    this.ul = this.querySelector("ul")!;

    this.form.addEventListener("submit", this.handleFormSubmission);
    this.updateTodoList();
  }

  private fetchTodos() {
    return get(ref(this.database, "/todos")).then((snapshot) => {
      const data = snapshot.val() as Record<string, string> | null;
      return data ? Object.entries(data) : [];
    });
  }

  private updateTodoList() {
    this.fetchTodos().then((todos) => {
      Array.from(this.ul.children).forEach((li) => li.removeEventListener("click", this.handleLiClick));
      this.ul.innerHTML = "";
      todos.forEach(([key, todo]) => {
        const li = document.createElement("li");
        li.textContent = todo;
        li.id = key;
        li.addEventListener("click", this.handleLiClick);
        this.ul.appendChild(li);
      });
    });
  }

  private handleLiClick: EventListener = (event) => {
    const li = event.currentTarget;
    if (!(li instanceof HTMLLIElement)) throw TypeError();
    const { id } = li;
    remove(ref(this.database, `/todos/${id}`)).then(() => this.updateTodoList());
  };

  private handleFormSubmission: EventListener = (event) => {
    event.preventDefault();
    const formData = new FormData(this.form);
    const todoTitle = formData.get("title")!.toString().trim();
    if (!todoTitle) return;
    push(ref(this.database, "/todos"), todoTitle).then(() => {
      this.form.reset();
      this.updateTodoList();
    });
  };
}

window.customElements.define("firebase-todo", FirebaseTodo);

筆者が大好きなWeb Componentsを使って簡単に書いてしまいましたが、以下のコマンドを実行してHostingにデプロイしてみましょう。

yarn build
firebase deploy

さすがFirebase!さすがWeb Components!

Firebase Local Emulator Suiteの使い方

ここまで事前準備で本番環境を使うTodoアプリケーションのプロジェクトを作成してきました。
これからは、このプロジェクトの開発環境を、Firebase Local Emulator Suiteを使って構築していきます。
その中で、Firebase Local Emulator Suiteの使い方を二つ紹介します。

・Javaをローカルにインストールして実行する方法
・Dockerを使って実行する方法

Javaをローカルにインストールして実行する方法

Firebase Local Emulator SuiteはJava SDKを使って起動します。
なので、真っ先にやらないといけないのは、Javaをローカルにインストールすることです!

こちら(https://www.oracle.com/jp/java/technologies/downloads/)の手順に従ってインストールしてください。

Local Emulator Suiteの設定を追加する

次、CLIを使ってEmulator Suiteの設定を調整します。
お馴染みのコマンドを実行します。

firebase init

すると、前回と同じく様々なオプションが表示されますが、Emulatorsを選択して進めましょう。

必要な機能を自動で選択してくれますが、`Authentication`など、本番にまだ追加していない機能も選択できます。
本番環境に新しいFirebaseサービスを導入したい時に、まずローカルで試すということができます。

それらのサービスのポート設定を問われますが、基本的にデフォルトで進めましょう。
Emulator UIは、Firebaseコンソール上で設定、情報を確認することができるのと同じように、ローカルのエミュレーターが保持している設定および情報をブラウザ上で確認するためのものです。

これで設定は終わりますが、もう一度`firebase.json`を見ると、上記の設定変更が反映されていることがわかります。

json firebase.json
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "emulators": {
    "database": {
      "port": 9000
    },
    "hosting": {
      "port": 5000
    },
    "ui": {
      "enabled": true
    }
  }
}

ちなみに、gitのレポには、基本的に firebase.jsonをコミットするべきです。

Local Emulator Suiteを起動させる

インストールと設定が終わっているので、次は起動です!
以下のコマンドを実行すれば設定したサービスのエミュレーターを立ち上げることができます。

firebase emulators:start

実行をすると以下のような表示が出ます。
※ 筆者の場合はそれらのポートが違うアプリケーションで使われているので変えています!

このEmulator UIのリンクを開くと以下のような画面が表示されます。

このUIで動いているサービスの詳細情報が確認できます。
後でTodoがRealtime Databaseに入るところを確認しましょう!

アプリケーションをLocal Emulator Suiteに接続するようにする

Local Emulator Suiteはパソコン上で起動していますが、Viteのローカルサーバーで動いているアプリケーションはまだ本番環境に繋がってしまいます。
これを解決しましょう。
src/firebase.tsを開いて、環境がdevelopmentかどうかで特殊なコードを実行します。

typescript src/firebase.ts
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getDatabase, connectDatabaseEmulator } from "firebase/database";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyBA7E5uMhwOMum2TPqTpQvV5Q-ki6EkX9E",
  authDomain: "austin-blog.firebaseapp.com",
  databaseURL: "https://austin-blog-default-rtdb.asia-southeast1.firebasedatabase.app",
  projectId: "austin-blog",
  storageBucket: "austin-blog.appspot.com",
  messagingSenderId: "234954796390",
  appId: "1:234954796390:web:8695aaee9ee2567d222ca7",
};

// Initialize Firebase
export const app = initializeApp(firebaseConfig);

const isDev = import.meta.env.MODE === "development";
if (isDev) {
  const db = getDatabase();
  connectDatabaseEmulator(
    db,
    "localhost",
    9001 // ここはfirebase.jsonに入っている設定に合わせましょう!
  );
}

これだけやればもうバッチリです!
Authenticationなど、他のサービスを使っている場合は、同様に`connect***Emulator`の関数を使ってエミュレーターを使うように設定します。

動作確認

フロントエンドの設定も追加したので、エミュレーターを使ってみましょう!

きちんとUIにも出ていますし、本番環境と全く同じ動作をしていますね。
最後に残ったTodoは永遠に消えない気がしますが…

ダミーデータをダンプする

こうしてFirebase Local Emulator SuiteをJavaで起動することができました。
Dockerで起動させる説明に入る前に、Emulator Suiteの初期データを保存する方法を紹介したいと思います。

Emulator Suiteを起動して様々な情報を保存していくかと思いますが、実は、これはすべて一時的なメモリーでEmulator Suiteを停止すれば、すべてが消えるのです。

CI/CDで自動テストを導入したい場合にこれはちょっと厄介ですね。特に、Authenticationを使っている場合、毎度ユーザーをUIから追加しないといけなくて大変です。
しかし、ご安心ください、ここまで話してしまえばお分かりかと思いますが、この問題を解決するためにある便利な機能があります。
emulators:exportというコマンドを使えば、一時的に保存されている情報を出力することができます。

データ出力の方法

まず、出力先のダイレクトリを作りましょう。`dump/`というフォルダーを作成しておきます。実行時に存在しなかったら作ってくれるので任意です。

そして、Emulator Suiteが起動中の状態で以下のコマンドを実行します。

firebase emulators:export dump

以下のように出力されればOKです。

要注意:出力先を./などにすると、エラーが発生してすべてのファイルが削除されます!

筆者は最初、ルートダイレクトリにコミットしたいと思い、上記のような設定で実行したのですが、.gitまでもすべて丸ごと削除されてしまい、Pushしていなかったコミットが消えました。気をつけてください。

dump/に出力された中身を見てみましょう!

まずこのfirebase-export-metadata.jsonですが、以下のような内容です。

`json
{
  "version": "11.13.0",
  "database": {
    "version": "4.9.0",
    "path": "database_export"
  }
}

そしてdatabase_exportは以下のような内容です。複数のRealtime Databaseに対応するために、Database名ごとに出力されるようです。
筆者の場合は、一つしかDatabaseがなかったのでaustin-blog-default-rtdb.jsonのみです。

json
{"todos":{"-NDaEuSyGaa3FCpji_Z6":"Buy a ps5"}}

こうすると、現実と同じように永遠にこのTodoが残ります。

起動時にダンプを読み込ませる

出力ができたので次は立ち上げる時にどうやって読み込ませるかです。
今動いているエミュレーターをctr + cで停止した上で、以下のコマンドを実行すればできます。

firebase emulators:start --import=./dump

正しく設定されていれば以下のような画面が表示されます。

そしてアプリケーションを開くと、ちゃんとダンプデータが読み込まれていることがわかります。

これでとりあえず一通り、Firebase Local Emulator Suiteの使い方を紹介しました!

Firebase Local Emulator SuiteをDocker化する

次は、開発環境をより簡単に構築できるように、上記のエミュレーターをDocker上で起動させる方法を紹介します。
Docker化するには以下の順番があります。

1. firebase.jsonのemulator設定を調整する
2. Emulator Suite用のDockerfile作成
3. Viteを起動させる用のDockerfile作成
4. docker-compose.yamlの作成

下記これらをやっていきます。

なぜDocker上で起動させる必要があるのか

本番環境と独立した開発環境を構築する一つの理由にもなるのですが、CI/CDで自動テストをGitHub Actions、もしくはTravis上で実行している場合、Java SDKがインストールされているかどうか、Firebase CLIをどうやって実行できるか、様々な課題が出てきます。

また、ローカルで実行している環境と、CI/CDで実行していう環境が異なると、自動テストが失敗した時にエラーを再現させにくくなります。
数行のコマンドで簡単にCI/CDに組み込めるようにしたいので、やはりDocker一択です。

firebase.jsonの設定を調整する

まず最初に、Dockerコンテナ上で起動させるので、firebase.jsonの設定を微妙に調整する必要があります。
エミュレーターのホストの設定は、デフォルト値だとlocalhostになりますが、これはDocker上だと都合が悪くて、0.0.0.0にしたいのです。
そのために、各サービスのポート設定の他に、ホストの設定も追加します。
そして、UIも明示的に4000番などのポートにしましょう。

json
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
  },
  "emulators": {
    "database": {
      "host": "0.0.0.0",
      "port": 9001
    },
    "hosting": {
      "host": "0.0.0.0",
      "port": 5001
    },
    "ui": {
      "host": "0.0.0.0",
      "enabled": true,
      "port": 4000
    }
  }
}

Emulator Suite用のDockerfileを作成する

次、Emulator Suiteを起動させるコンテナを作るためのDockerfileを作成します。
ここで悩ましいのはどのイメージをベースにするべきかです。
筆者は最初、JDKを元に作成してみましたが、Firebase CLIをインストールするところで引っかかって断念しました。
最も手取り早かったのは、Nodeを元にJava SDKをインストールする方法でした。もちろん、Ubuntuをベースにすることもできます。

ここでは、Nodeで解決できた方法を紹介しますが、複数の選択肢があることも共有します。
emulators.Dockerfileというファイルを作成し、その中に以下のようなコマンドを埋め込みます。

FROM node
WORKDIR /app

RUN apt-get update && apt-get install default-jre -y
RUN npm install -g firebase-tools

COPY .firebaserc firebase.json database.rules.json /app/
COPY ./dump /app/dump

EXPOSE 9001 5001 4000

CMD [ "firebase", "emulators:start", "--import=./dump" ]

上記の記述の通り、default-jreとfirebase-toolsをインストールする必要があります。
また、COPYではEmulator Suiteが使う設定ファイルをイメージ内に持ってきます。
EXPOSEは任意ですが、明示的にfirebase.jsonの設定に合わせます。
CMDでデフォルトのコマンドとして、先ほど実行したコマンドを入れます。

Viteを起動させる用のDockerfile作成

ViteもDocker上で起動させたら完璧です。
「完璧」を目指していきましょう!
まず、最初に、vite.config.tsをルートダイレクトリに追加します。
中身は以下の通りです。

typescript
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
  server: {
    host: "0.0.0.0",
    port: 3000
  },
});

このファイルを追加するのは、明示的にポートの指定ができるようにするのと、上記のfirebase.jsonと同様にDockerのコンテナ上で起動させるので、localhostじゃなくて0.0.0.0にする必要があるからです。
続いて、vite.Dockefileというファイルを作成して、以下のような中身にします。

FROM node
WORKDIR /app

COPY package.json yarn.lock /app/
RUN yarn install

EXPOSE 3000

VOLUME [ "/app/node_modules" ]

CMD [ "yarn", "dev" ]

srcなどのファイルは何もコピーしていないのはわざとで、次のdocker-compose.yamlでボリュームの設定でコンテナにローカルのファイルを露呈させます。
そうすると、開発している時に、編集するとすぐに変わります。

docker-compose.yamlの作成

最後に、これらのDockerfileを総括する、指揮するdocker-compose.yamlを作成します。

services:
  emulator:
    build:
      context: .
      dockerfile: emulators.Dockerfile
    ports:
      - "9001:9001"
      - "5001:5001"
      - "4000:4000"
    tty: true
  vite:
    build:
      context: .
      dockerfile: vite.Dockerfile
    volumes:
      - ./:/app
    ports:
      - "3000:3000"

これを以下のコマンドでとりあえず立ち上げてみましょう!
※Firebase Local Emulator Suiteを停止することをお忘れなく!

docker-compose up

実行すると、ポートの競合・コンフリクトがなければ無事に起動するはずです!

そしてViteも元のファイルを編集すればちゃんと拾ってくれます!

また、これまでと同じようにFirebase Local Emulator SuiteのUIもアクセスできます。

まとめ

これまでFirebaseのプロジェクトでFirebase Local Emulator Suiteを使って、完璧な開発環境を構築する方法を紹介してきましたが、いかがでしょうか?

Firebaseのメリットでもデメリットでもある、バックエンドを触らなくていいというところは、デメリットとしてこういう開発環境の構築が難しいところがありますが、Firebase Local Emulator Suiteを使えば簡単にその問題を解決できるので、Firebaseが最強であるという結論には、変わりないです。
また、そこにDockerもうまいこと組み合わせることでJavaをインストールするなどの面倒な問題も無くなります。

筆者は、社内で Firebaseを使っていこうぜ というムードが出る以前にFirebaseの熱を感じていた一人なので、こうしてFirebaseの楽しさを少しでもわかっていただければと思います。

今度のFirebaseをテーマにした記事を書いていきたいと思います。

・Firestoreでページネーションを実装する
・App Checkで我がFirebaseアプリケーションを守ろう

お楽しみにお待ちいただければと思います!

このページをシェアする: