Firebase Extensionsを自作して公開するまでのステップ
背景
2023年5月に開催された Google I/O 2023 にて、これまで Google や限定されたサードパーティしか公開できなかった Firebase Extensions が、個人でも公開できるようになりました。
Firebase Extensions を利用することで、アプリケーションコードやデプロイ、サービスアカウントの作成などの手間を省略でき、簡単に迅速なアプリケーション開発ができるようになります。また、複数の Firebase Projects を利用している場合でも GUI のみでインストールが完了するため、再利用性が高い点も特徴です。
Firebase Extensions を自作することで、前述したメリットを享受できる関数を自由に開発でき再利用が可能になります。まとめると、以下のような関数が拡張化するのに適しています。
- サービスアカウントや IAM のセットアップが面倒
- アプリケーションコードの開発・デプロイが面倒
- Firebase プロジェクトを跨いで再利用したい
本記事は、2023年7月7日に開催された PORT Firebase meetup の発表にてお伝えしきれなかった開発方法に焦点を当てて記載しています。Firebase Extensions の背景や概略などについては下記の登壇資料をご参照ください。
Setup
Firebase Extensions といっても、アプリケーションコードは Cloud Functions でデプロイされるため、普段プロジェクトで関数を書いているのとほぼ同様の開発者体験で開発ができます。
ドキュメントが充実しているので、まずはこちらを参照するのがおすすめです。本ドキュメントをベースに、個人的に若干分かりづらいと感じた箇所やポイントを中心に記載していきます。
initする
Firebase CLI を使って開発をしていきます。以下のコマンドで拡張を開発するためのテンプレートディレクトリが生成されます。開発言語は JavaScript または TypeScript を選択できます。
firebase ext:dev:init
ファイル構造については Structure of an extension にも記載されていますが、ざっくり分類すると、アプリケーションコードを書く functions
、パラメータを設定する extensions.yaml
、 それ以外のドキュメント類となります。
example-extension
├── functions
│ ├── integration-tests
│ │ ├── extensions
│ │ │ └── example-extension.env
│ │ ├── firebase.json
│ │ └── integration-test.spec.js
│ ├── index.js
│ └── package.json
├── README.md
├── PREINSTALL.md
├── POSTINSTALL.md
├── CHANGELOG.md
├── icon.png
└── extension.yaml
init コマンドを叩くとデフォルトで greetTheWorld
という HTTP トリガー関数が定義されています。extensions.yaml
の resources
に記載されている関数名(greetTheWorld
)が、 index.ts
の exports.greetTheWorld
に対応します。当然、resources
に記載する項目を増やせば、複数の関数を定義できます。
import * as functions from "firebase-functions";
exports.greetTheWorld = functions.https.onRequest(
(req: functions.Request, res: functions.Response) => {
const consumerProvidedGreeting = process.env.GREETING;
const instanceId = process.env.EXT_INSTANCE_ID;
const greeting = `${consumerProvidedGreeting} World from ${instanceId}`;
res.send(greeting);
});
resources:
- name: greetTheWorld
type: firebaseextensions.v1beta.function
description: >-
HTTP request-triggered function that responds with a specified greeting message
properties:
httpsTrigger: {}
runtime: "nodejs16"
モノレポな構成
私は執筆時点で2つ拡張を公開していますが、GitHub のリポジトリはモノレポ構成にしています。
特にドキュメントに記載されているわけではないですが、以下に代表されるようなリポジトリがその構成にしていたので真似しました。
- firebase/extensions
- invertase/firebase-extensions
- rowyio/firebase-extensions
- yamankatby/firebase-extensions
複数の拡張をルートに置いたり、ディレクトリにまとめたりでそれぞれ若干構成は異なりますが、設定ファイルの共有など参考になる点は多いはずです。また、firebase/extensions
や invertase/firebase-extensions
は個人公開できる前から開発していたためか、ディレクトリ構造が init で生成されるテンプレートと異なっているので注意です(個人開発という観点では yamankatby/firebase-extensions
が最も参考になりました)。
Build
開発サイクルについては Recommended development cycle に記載されております。大きく分解すると以下の3ステップに分類できます。後ろのステップに連れてイテレーションサイクルが遅くなります。
- Firebase Emulator Suite を使って手元で開発
- ローカルパスを指定して実プロジェクトにインストール
- プレリリース版でアップロード・発行リンクを配布しインストール
1. Firebase Emulator Suiteで開発
基本的には、Cloud Functions を Emulator Suite で開発するサイクルと一緒です。extensions.yaml
で拡張の利用者設定するパラメータに何を要求するのかを考える点が拡張開発の特徴といったところでしょうか。
関数の開発準備
TypeScript で Cloud Functions の開発をする時と同様に、以下のコマンドでターミナルから複数のタブを使ってトランスパイルとエミュレーターを起動します。
# トランスパイル - `./functions`
npm run build:watch
# Emulator Suiteの起動 - `./functions/integration-tests`
firebase emulators:start --only extensions \
--inspect-functions \
--project demo-test
エミュレータを起動すると新規作成時には greetTheWorld
という HTTP トリガー関数が定義されているので、コンソールに出力される URL を叩くと「Hello World from greet-the-world」が返ってくるはずです。
functions[us-central1-ext-greet-the-world-greetTheWorld]: http function initialized (http://127.0.0.1:5001/demo-test/us-central1/ext-greet-the-world-greetTheWorld).
ちなみに Firebase プロジェクトを選択する に記載の通り demo-
という接頭辞をつけると、デモプロジェクトとしてエミュレートできます。上記のコマンドも demo-test
とすることで、デモプロジェクトを利用しています。
ひとまずここまでで自前の関数を修正して変更反映されるサイクルが実現できると思います。以下の gif では、greetTheWorld
関数のテキスト変更が反映される様子です(Zenn が3MB 上限のため gif が荒くて恐縮です)。
Demo |
---|
新しいトリガーを追加する
これまでは普通に関数を開発していただけですので、extensions.yaml
を弄って拡張開発をしていきます。設定項目はたくさんあるのですべての紹介はできないですが、新しいトリガー関数を追加してみます。利用できるトリガーは Supported function triggers に記載されています。利用するトリガーによって、利用できる Cloud Functions が第1世代・第2世代かは異なりますので、以下のドキュメントにて確認します。
試しに Firestore をトリガーする関数を追加してみます。ちなみに Firestore トリガーで利用できる Cloud Functions は第1世代です。
まず、index.ts
を変更します。
exports.usersCreateTrigger = firestore
.document('users/{userId}')
.onCreate((snapshot, context) => {
logger.info(JSON.stringify(snapshot.data()))
})
次に extensions.yaml
の resource を追加します。
resources:
- name: usersCreateTrigger
type: firebaseextensions.v1beta.function
properties:
eventTrigger:
eventType: providers/cloud.firestore/eventTypes/document.write
# 本来は`demo-test`の部分を{$PROJECT_ID}として可変にする
resource: projects/demo-test/databases/(default)/documents/users/{userId}
デフォルトでは Firestore のエミュレータも入っていないので追加します。
{
"emulators": {
+ "firestore": {
+ "port": 8080
+ },
# …
},
"extensions": {
"greet-the-world": "../.."
}
}
最後にエミュレータを起動します。--only
で起動するエミュレータを制限している場合は firestore
指定を忘れないようにしましょう。
firebase emulators:start --only extensions,firestore \
--inspect-functions \
--project demo-test
上手くいくと、users
コレクション配下に新しいドキュメントを追加する度に関数が呼ばれることを確認できます。
Firestore Trigger Demo |
---|
このようにして、新しいトリガーを追加・変更しながら Firebase Emulator Suite を使って開発のイテレーションを回していくのが基本的な方法となります。全トリガーの紹介はできないので、適宜ご自身の拡張の用途にあったトリガーを選択して開発してみたください。
2. ローカルパスを指定して実プロジェクトにインストール
Emulator Suite である程度開発・テストが完了したら、Firebase の実プロジェクトにインストールしてアドホックなテストを実施します。extensions.yaml
で指定したパラメータやドキュメントが想定通りに表示されているかなど、実際に拡張を利用する立場でインストールするステップを体験します。
また、外部 API を叩く必要があったり Google Cloud 側のトリガーを利用するなどで、Emulator Suite 上では開発ができない場合もあります。その場合は、若干イテレーション速度は落ちてしまうのですが本セクションの方法で動作確認します。
動作確認用にあらかじめセットアップした実プロジェクトを用意しておくと良いです。ここからは自作云々関係なく普通に拡張をインストールするのと同様です。
firebase ext:install ./path/to/extension/directory --project=<project-id>
# 出力結果
❯ firebase ext:install ../sample-extension
i extensions: ensuring required API firebaseextensions.googleapis.com is enabled...
✔ extensions: required API firebaseextensions.googleapis.com is enabled
i extensions: Checking project IAM policy...
✔ extensions: Project IAM policy OK
✔ Uploaded extension source code
Extension: Greet the world
Description: Sends the world a greeting.
Version: 0.0.1
License: Apache-2.0
Resources created:
- greetTheWorld (Cloud Function (1st gen)): HTTP request-triggered function that responds with a specified greeting message
- usersCreateTrigger (Cloud Function (1st gen))
extensions.yaml
で設定したパラメータがプロンプトとして尋ねられるので、ユーザーに入力してほしい項目をセットするなど調整いします。インストールが完了すると、firebase.json
に自作した拡張が追加されます。
{
# …
"extensions": {
# …
+ "greet-the-world": "../sample-extension"
}
}
最後にデプロイして完了です。インストールした実プロジェクトで、開発した拡張が動作するか確認してみてください。
firebase deploy --only extensions
ちなみに個人的な感覚なのですが、後述するインストールリンクを発行する方法が便利なので、個人的には本セクションで紹介したローカルパス指定でのインストールはあまり使いません。
3. プレリリース版でアップロード・発行リンクを配布しインストール
個人的にはこれがすごく便利でおすすめです。以下のコマンドでプレリリース版として Upload することで、インストールリンクを発行できます。
firebase ext:dev:upload htsuruo/sample-extension --local
✔ Uploaded extension source code
✔ Successfully uploaded htsuruo/sample-extension@0.0.1-rc.0
i extensions: Install Link
インストールリンクは以下の形式となっており、自由に Firebase プロジェクトを選択し GUI でインストールできます。
リンクをクリックすると、おなじみの Firebase のプロジェクト選択の画面に遷移するので、拡張をインストールしたいプロジェクトを選択し完了です。
また、上記はローカルからアップロードしましたが、公開リポジトリにしておくと GitHub から直接アップロードもできます。以下は htsuruo/back-up-firestore-to-storage
を開発したときの例ですが、これで GitHub から beta
版としてアップロードできます。
❯ firebase ext:dev:upload htsuruo/sample-extension
プレリリース版のバージョンについて
以下のドキュメントにも記載がありますが、プレリリース版はバージョン番号を自動でインクリメントしてくれるため、指定する必要はありません。ちょっとしたドキュメントの修正内容を確認したい場合も、これのおかげでガンガン新しいバージョンをアップロードできるので、比較的早くイテレートできます(とはいえ、インストール自体に5分程度はかかりますが)。
Notice you don't specify a version number—this value comes from the extension.yaml file. When you upload a pre-release extension version, the stage and upload number is appended to the version. For example, if extension.yaml specifies version 1.0.1 and you upload a release candidate, it would result in the version 1.0.1-rc.0; uploading another release candidate of the same version would automatically increment the count, resulting in 1.0.1-rc.1, and so on.
https://firebase.google.com/docs/extensions/publishers/upload-and-publish#first-upload
Publish
公開に当たって実施することは主に4点です。
- Extension Publisher としての登録
- PREINSTALL・POSTINSTALL などの各種ドキュメントの作成
- stable 版でアップロード
- 審査提出および承認
1. Extension Publisher としての登録
これは初回のみで単に登録するだけですが、パブリッシャーの規約 もあるので、一通り読んで問題ないことを確認してください。
2. PREINSTALL・POSTINSTALL などの各種ドキュメントの作成
ドキュメントはこちらです。
発表スライドでも触れているので割愛します。
ext:dev:init
で生成されるテンプレートには README が含まれていないので、以下のコマンドを叩くと、各種ドキュメントが統合されて README.md
として出力できます。
`firebase ext:info . --markdown > README.md`
3. stable版でアップロード
開発フェーズでのプレリリース版ではローカルからアップロードできましたが、stable
版では GitHub リポジトリ経由からのみとなります。stable
にアップロードすることは、つまり公開用審査に提出することなのでこの時点で、オープンソースとして準備できている必要があります。
以下は私が公開している拡張 back-up-firestore-to-storage
の例ですが、以下のコマンドでアップロードしています。
firebase ext:dev:upload htsuruo/back-up-firestore-to-storage \
--repo https://github.com/htsuruo/firebase-extensions \
--stage "stable" \
--root back-up-firestore-to-storage \
--ref back-up-firestore-to-storage/v0.0.2
また、テンプレート生成時には typescript
依存が devDependencies
となっているため、dependencies
に移動させておく必要があります。
ext:dev:upload
では --production
フラグが付くため devDependencies
のパッケージは利用されず、tsc
コマンドが通らずエラーになります。Issue もありますが、少しの手間なので改善されるかは不明です。
4. 審査提出および承認
stable 版のアップロードが完了したら、パブリッシャーコンソールから審査に提出します。
審査期間は、実績ベースですが早いときで1日、遅い時で3日程度かかりました。今はまだパブリッシャーが少ない印象ですが、今後拡張も増えていくにつれて審査期間も伸びてくると思います。
審査については発表スライドでも触れているのでこちらをご参照ください。
まとめ
本記事では、個人でも開発および公開が可能になった Firebase Extensions について、開発ステップにフォーカスして紹介しました。Firebase Emulator Suite を使ったローカルでの開発に慣れていれば、extensions.yaml
ファイルやドキュメントを調整するだけですぐに拡張化できますし、慣れていない場合でもドキュメントが充実しているのでぜひチャレンジしてみてください。
今回の内容で、シンプルな拡張であれば公開まで辿り着けるように紹介したつもりですが、まだドキュメントの一部にすぎないので、もし躓いたりおかしいなと思った点はドキュメントを読んで理解してみてください。
Discussion