見出し画像

Firebaseプロジェクトを新規作成してCloud FunctionsとCloud Firestoreのローカル開発環境を整備してみた

はじめに

こんにちは、SHIFTの開発部門に所属している Katayama です。

Firestore でドキュメントが更新されたらそれをトリガーに何らかの処理をするアプリケーションをトライアルで開発することになった。
開発環境としてローカルで開発・実行できるようにするために、過去記事で Localstack と serverless フレームワークを使ってローカル環境を整えたように、今回はローカルでの Cloud Functions の実行・検証をできるようにしてみようと思う。

Firebase プロジェクトの初期化

firebase-tools で Firebase にログインする

まずは、firebase-tools を global インストール(yarn global add firebase-tools)する。その後、Firebase にログインするために、firebase login コマンドを実行する。

すると、以下のように OAuth2.0 の認可フローを開始する認可リクエストの URL がターミナルに表示されるので、それをブラウザに張り付ける。

ブラウザに URL を張り付けると、アカウントの選択をした後、以下のような画面(認可画面)が開くので「許可」をする(Google アカウントにログインしていない場合は認証画面が認可画面の先に開く)。

許可をすると redirect_uri にリダイレクトするが、ホスト名が localhost になっているので、自分の環境に合わせてホスト名を書き換える必要がある場合もある(私の場合、Windows 上で Virtualbox を利用して Linux 環境を構築していたので、ホストオンリーアダプター経由で Linux にアクセスする必要があり、localhost を 192.168.56.2 に変えた。

Windows で Virtualbox に Linux 環境を構築する方法はこの記事を参照)。
firebase-tools の方で立っているローカルサーバー 9005 にアクセスできると、以下のようにログインができた旨が画面に表示される。

firebase init でプロジェクトを初期化する

まずは Firebase のダッシュボードから、プロジェクトを作成する(プロジェクトに作成後、firebase init firestore, firebase init functions を実行する)。

任意のプロジェクト名を付ける。

Google アナリティクスは導入してもしなくても OK。

続いて、今回利用する Cloud Firestore のデータベースの作成をする。この手順を端折ってしまうと firebase init firestore をしても以下のようなエラーが出てしまう。

Error: It looks like you haven't used Cloud Firestore in this project before. Go to https://console.firebase.google.com/project/{project_id}/firestore to create your Cloud Firestore database.

以下の図にあるように、左のメニューから Firestore Database を選択し、「データベースの作成」を行う(公式サイト:Cloud Firestore データベースを作成するも参照)。

作成を行うと、モードとロケーションの選択をする画面が出てくるので、それぞれ設定する。
モードについては今回は Admin SDK からのアクセスしかしないので本番モードにする。
ロケーションは一番近い Tokyo にした。

データベースが作成できたら、firebase init firestore コマンドで初期化する。

続いて、firebase init functions コマンドで、Cloud Functions の初期化を行う。

ここまで一番最初のプロジェクトの設定は終了になる。
続いて、functions の開発のために ESLint のルールや Node.js の環境周りの設定を行う。

functions の開発のための調整

ES Module 化

Node.js で ES Module を利用するために、package.json に"type": "module"を追記する。

この変更により ESLint の設定ファイルの拡張子を.js から.cjs にする必要がある(以下、Configuration Filesからの引用)。

JavaScript (ESM) - use .eslintrc.cjs when running ESLint in JavaScript packages that specify "type":"module" in their package.json. Note that ESLint does not support ESM configuration at this time.

ESLint のルール設定

以下を追加で設定する。

  • airbnb-base JavaScript の開発をするのであれば必須ともいえるルール

  • eslint-config-prettier コードフォーマッティングを Prettier で行うので、競合するルールを Off にする

最終的には以下のような config ファイルになった。

functions/.eslintrc.cjsrequire('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
	parserOptions: {
		ecmaVersion: 2023
	},
	extends: ['eslint:recommended', 'google', 'airbnb-base', 'prettier'],
	rules: {},
	overrides: [
		{
			files: ['**/*.spec.*'],
			env: {
				mocha: true
			},
			rules: {}
		}
	]
};

Prettier の設定

前提として、ディレクトリ構成は以下のようになっている。

$ tree -I node_modules
.
├── README.md
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│   ├── index.js
│   ├── package.json
│   └── yarn.lock
...

上記のようなプロジェクトに対して、functions のコードもそうだが、firebase.json など Firebase の設定に関するファイルもフォーマッティングしたいと思ったので、./functions ではなくプロジェクトルートの方で設定する事にした。
package.json をプロジェクトルートに追加し、以下のようなディレクトリ構成になるようにした。

.
├── .prettierrc.json
...
├── package.json
└── yarn.lock

package.json の中身は以下。

package.json{
	...
	"license": "MIT",
	"private": true,
	"scripts": {
		"style": "prettier --ignore-unknown --write ."
	},
	"devDependencies": {
		"prettier": "^2.8.4"
	}
}

これで yarn style コマンドを実行すれば、プロジェクト内のファイルのフォーマッティングができる。
ついでに、.vscode/settings.json を以下のように設定する事で、ファイルの保存時に自動でフォーマッティングするようにできる。

.vscode/settings.json{
	"editor.defaultFormatter": "esbenp.prettier-vscode",
	"editor.formatOnSave": true
}

Git hooks による ESLint のチェック、自動フォーマッティング、コミットメッセージチェックとライセンスチェック

最後に、Git hooks を設定して Git コミット時に ESLint を実行したり、自動でコードフォーマッティングするようにしたいと思う。
Git hooks を設定するにあたってはsimple-git-hooksを利用する。
また、lint-stagedを利用してステージに追加されているファイルのみ対してコマンドを実行するようにする。

最終的な package.json の設定としては以下のようになった。

./package.json{
	...
	"scripts": {
		...,
		"prepare": "npx simple-git-hooks",
		"postinstall": "cd functions && yarn install"
	},
	"lint-staged": {
		"*": "npx prettier --ignore-unknown --write",
		"*.js|*.cjs|*.mjs": "npx eslint --ignore-path .gitignore",
		"./yarn.lock": "npx license-checker --development --failOn \"GPL;AGPL;LGPL;NGPL\" --summary",
		"./functions/yarn.lock": "npx license-checker --start ./functions --production --failOn \"GPL;AGPL;LGPL;NGPL\" --summary"
	},
	"simple-git-hooks": {
		"pre-commit": "npx lint-staged --verbose",
		"commit-msg": "npx commitlint -e"
	}
}

※lint-staged の"./functions/yarn.lock"についてだが、./functions に Cloud Functions のコードが存在し、そこにも package.json が存在する関係上、functions のパッケージが更新された時にライセンスチェックを行いたいのでこのような設定になっている。
また、"--start ./functions"オプションにより、どこの package.json のライセンスをチェックするか?を指定している。

※ESLint はプロジェクトルートでも依存に追加し、./.eslintrc.cjs には以下のような設定をした(./functions/.eslintrc.cjs の設定内容は./.eslintrc.cjs とマージされ、同じ設定は./functions/.eslintrc.cjs が優先される)。

module.exports = {
	env: { node: true },
	parserOptions: {
		ecmaVersion: 2023
	},
	extends: ['eslint:recommended', 'prettier']
};
$ tree -I "node_modules|.git" -a
.
├── .eslintrc.cjs
├── .firebaserc
├── .gitignore
├── .prettierrc.json
├── .vscode
│   └── settings.json
├── README.md
├── commitlint.config.cjs
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│   ├── .eslintrc.cjs
│   ├── index.js
│   ├── package.json
│   └── yarn.lock
├── package.json
└── yarn.lock

※commitlint の設定についてはコミットリント(commitlint)を導入し、コミットメッセージを lint するを参照。

Cloud Functions を実装して Deploy する

Cloud Functions を実装する

今回は以下の 2 つの関数を実装する。

  • addMessage() クエリーパラメーターで text を受け取り、それを Firestore の messages というドキュメントに保存する

  • makeUppercase() Firestore の messages にデータが追加されたら、それを Uppercase に変換し uppercase というフィールドを追加する

実装としては特に難しい事はなく、以下のようになるだろう。

index.jsimport functions from 'firebase-functions';
import admin from 'firebase-admin';

admin.initializeApp();

export const addMessage = functions
	.region('asia-northeast1')
	.https.onRequest(async (req, res) => {
		const original = req.query.text;

		const writeResult = await admin
			.firestore()
			.collection('messages')
			.add({ original });

		res.json({ result: `Message with ID: ${writeResult.id} added.` });
	});

export const makeUppercase = functions
	.region('asia-northeast1')
	.firestore.document('/messages/{documentId}')
	.onCreate((snap, context) => {
		const { original } = snap.data();
		functions.logger.log('Uppercasing', context.params.documentId, original);

		const uppercase = original.toUpperCase();
		return snap.ref.set({ uppercase }, { merge: true });
	});

少し上記のコードに関して補足をする。

  • functions.region('asia-northeast1').https.onRequest https.onRequest()にある通り、Http の Request と Express の res のオブジェクトを引数に受け取る関数で、実装としては Express と全く同じような実装ができる

  • functions.region('asia-northeast1') 関数のリージョンを変更するに書かれている通り、関数のリージョンを指定するための構文(CLI で deploy する際のオプションにリージョンを指定できるものがないため、コードで指定する他ない模様)。

  • onCreate Cloud Firestore 関数トリガーに書かれている通り、Firestore のイベントをトリガーとして処理を実行できる仕組みがある。今回は「ドキュメントが最初に書き込まれたときに」という条件で関数を実行している。

  • return snap.ref.set({ uppercase }, { merge: true }) makeUppercase() 関数を追加するに以下のように書かれている通り、onCreate の callback 関数内で非同期処理がある場合は、その戻りは Promise にする必要がある。

You must return a Promise when performing asynchronous tasks inside a Functions such as writing to Firestore."

上記のコードを実際に実行してみたいと思うが、Firebase にはFirebase Local Emulator Suiteというのがあるので、本番に Deploy する前にローカル環境でエミュレーションを利用して実行できるか?検証してみたいと思う。

Firebase Local Emulator Suite を利用して、ローカル環境で Cloud Functions を実行する

まず、Local Emulator Suite のインストールに書かれている通り、Java 11 以上が必要になるので、OpenJDK11 をインストールする("sudo yum install java-11-openjdk")。

続いて firebase init emulators コマンドでエミュレータ用の設定を初期化する。

上記のコマンドを実行すると、firebase.json に emulators の設定が追加される。

firebase.json{
	...
	"emulators": {
		"functions": {
			"port": 5001
		},
		"firestore": {
			"port": 8081
		},
		"ui": {
			"enabled": true
		},
		"singleProjectMode": true
	}
}

ここまで出来たら、あとは firebase emulators:start コマンドを実行してエミュレータを起動するだけ。

エミュレータが起動すると以下のように Emulator UI を開けるようになる。

初期の段階では Functions emulator のログにはセットアップが完了した事が分かるログが出力されており、Firestore emulator の方はドキュメントが何もない状態である事が確認できる。

実際に Cloud Functions をローカルで実行する。
エンドポイントは firebase emulators:start コマンドを実行した際に表示されているものになるので、ブラウザ上で"http://localhost:5001/{project_id}/us-central1/addMessage?text=uppercaseme" を叩くと、以下の動画の通り、Cloud Functions が起動し、Cloud Firestore にドキュメントが追加され、Cloud Firestore へのドキュメント追加をトリガーに makeUppercase()関数がトリガーされ、uppercase というフィールドがドキュメントに追加される事が確認できる。

ちなみに、Emulator UI の Logs を確認すると、Cloud Functions が実行された事をログからも確認できる。

※firestore の port だが、8080 では動作しないため 8081 を指定した。

※エミュレーターなので、Cloud Firestore に REST API の呼び出し機能が備わっており、例えば以下のように直接データを API で参照するという事もできる(POST、DELETE も)。

$ curl -X GET "http://127.0.0.1:8081/v1/projects/{project_id}/databases/(default)/documents/messages" -H 'Authorization: Bearer owner'
{
  "documents": [{
    "name": "projects/{project_id}/databases/(default)/documents/messages/6qBtf73FlSh87jXKhGNB",
    "fields": {
      "original": {
        "stringValue": "uppercaseme"
      },
      "uppercase": {
        "stringValue": "UPPERCASEME"
      }
    },
    "createTime": "2023-03-20T10:59:36.910828Z",
    "updateTime": "2023-03-20T10:59:37.016585Z"
  }, {
    "name": "projects/{project_id}/databases/(default)/documents/messages/NKSqQAKeonp86FXrRfqF",
    "fields": {
      "original": {
        "stringValue": "uppercaseme"
      },
      "uppercase": {
        "stringValue": "UPPERCASEME"
      }
    },
    "createTime": "2023-03-20T10:59:11.224603Z",
    "updateTime": "2023-03-20T10:59:15.077388Z"
  }]
}

※今回、Virtualbox 上の Linux 環境でエミュレーターを実行していたので、以下のように VS Code の Port Forward を利用した。

まとめとして

今回は Firebase Cloud Functions と Cloud Firestore をローカル環境でエミュレーションし、動作確認をローカル環境でできるようにするという事をやってみた。
Firebase を利用したアプリ開発でも、今回利用したエミュレーターを活用する事で Deploy する事なく動作確認ができ、開発者体験を向上できるのではないかと思った。
また、セキュリティールールなどの検証を行う事もできるので、ルール不備でデータが流出みたいなことを未然に防げるだろう。

ただ、Cloud Functions については推奨: App Check で不正行為を防止するにあるような追加設定も別に必要になるだろう。それについてはまた別の記事で取り上げる事にする。

※おまけに、今回実装した Cloud Functions・Cloud Firestore を本番環境に Deploy する、をやってみた結果があるので良ければそちらも参照下さい。

おまけ

Cloud Functions を本番環境に Deploy する

firebase deploy コマンドを実行する。実行すると以下のように Deploy が成功する。

Deploy 後には"Function URL (addMessage(asia-northeast1)):"に記載されている URL にクエリーパラメーター text=uppercase を付けて呼び出すと、本番環境の Cloud Firestore にデータが登録される事が確認できる。

※Cloud Functions を利用するにはプランをBlazeにする必要がある。無料枠があるのでいきなり課金にはならないが、従量課金なので利用時には注意が必要(外部か実行できないようにする対策などが必要になるだろう)。

Error: Your project {project_id} must be on the Blaze (pay-as-you-go) plan to complete this command. Required API cloudbuild.googleapis.com can't be enabled until the upgrade is complete. To upgrade, visit the following URL:

https://console.firebase.google.com/project/{project_id}/usage/details

※ちなみに、firebase deploy は差分があれば depploy が実行されるので、例えば firestore.rules だけを修正して firebase deploy を行うと、以下のように functions の deploy は skip される。

※意図しない課金を防ぐ意味でも Deploy 後のお掃除をお忘れなく。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。
経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。
SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。
その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/