🧅

【Cloud Functions for Firebase】onCall関数の使い方とリファレンス 【第二世代】

に公開

onCall関数(HTTP 呼び出し可能関数)の特徴

  • 簡潔な通信: SDKを用いることでクライアントアプリケーションから簡潔に呼び出すことができ、HTTPリクエストやレスポンスの詳細を意識する必要がありません。
  • セキュリティ: Firebase AuthenticationやApp Checkを活用して、認証済みのクライアントからのリクエストのみを許可することが可能です。
  • スケーラビリティ: Firebaseのインフラストラクチャにより、トラフィックの増加に応じて自動的にスケールします。

主なユースケース

基本的には以下のケースを想定してこの関数を選択することになるかと思います。

  • データの取得や更新: クライアントからのリクエストに応じて、データベースのデータを取得または更新する。
  • サーバーサイドロジックの実行: クライアントで実行するには不適切なロジック(例: サードパーティAPIの呼び出し)をサーバー側で処理する。
  • 認証済みリクエストの処理: Firebase AuthenticationやApp Checkを利用して、認証済みユーザーのみがアクセス可能な機能を提供する。

個人的な考えを以下にまとめておきましたので、参考にしてください。

意見: なるべく低頻度でセキュリティが重要な処理に利用し、高頻度での呼び出しを避ける

個人的には、サーバーサイドロジックの実行認証済みリクエストの処理が主たる利用理由になるかと思います。
 Functionsを通してFireStoreにアクセスすると二重でコストがかかってしまうため、運用コストを跳ね上げる原因になりかねません。可能な限り、FireStoreにおけるSecurity Rulesとデータ構造の設計で吸収すべきです。
 ただし、Security Rulesとデータ構造の工夫だけでは難しいロジックも存在するため、その場合は素直にFunctionsを利用すべきだと考えています。

意見: onCallonRequestの使い分け

onCall(HTTP 呼び出し可能関数)とonRequest(HTTP 関数)どちらもFirebase Functionsの機能であり、両方ともHTTPリクエストを処理するための関数です。両者はほとんどの点で似通っているため、どちらを選択するか迷うことが多いかと思います。そのため、筆者が考える使い分けの基準を以下に記載しました。

関数 特徴
onCall Firebase SDKを使用してクライアントから直接呼び出すことが可能。Firebase AuthenticationやApp Checkによるトークンが自動付与される。
onRequest HTTPリクエストを直接処理するため、柔軟性が高い。

選択のポイント

認証やApp Checkが必要な場合や、内部からの呼び出し以外を想定しない場合(多くの場合がこちらにに相当すると思います)はonCallが適していますが、外部からのアクセスが必要な場合はonRequestを選択すると良いでしょう。

onCallに関する注意

onCall関数はSDKなしには呼び出すことができないように見えますが、そうではありません。
仕様に従えば、誰でも呼び出すことは可能です。当然のことではありますが、受け取るデータには常に悪意のある内容が含まれることを前提に開発を進めてください。

onCall関数の実装例

server.ts
import { defineSecret } from "firebase-functions/params";
import {
  CallableOptions,
  CallableRequest,
  CallableResponse,
  onCall,
} from "firebase-functions/v2/https";

const apiKey = defineSecret("SOME_SECRET");
export const helloWorld = onCall(
  {
    region: "asia-northeast1",
    secrets: [apiKey],
    minInstances: 0,
    maxInstances: 1,
    cors:["localhost"]
  } as CallableOptions,
  async (request: CallableRequest, response: CallableResponse | undefined) => {
    // リクエストデータの取得
    const data = request.data;

    // 認証情報の確認
    const auth = request.auth;

    // 処理の実行
    const result = { message: "Hello from Firebase!" };

    // レスポンスの返却
    return result;
  },
);

上記のコードは、Firebase Functions の onCall を使用して、クライアントから直接呼び出すことができる HTTPS Callable Function を定義しています。

主なポイント

1. defineSecret の使用

const apiKey = defineSecret("SOME_SECRET");
  • defineSecret は Firebase の Secret Manager に保存された機密情報を使用するための関数です。
  • このコードでは、SOME_SECRET という名前のシークレットを定義し、関数内で利用可能にしています。
  • ローカルで開発している場合、.env.localファイル内の記述でオーバーライドすることができます。

2. オプションの設定

export const helloWorld = onCall(
  {
    region: "asia-northeast1",
    secrets: [apiKey],
    minInstances: 0,
    maxInstances: 1,
    cors:["localhost"],
  } as CallableOptions,
  async (request: CallableRequest, response: CallableResponse | undefined) => {
    ...
  },
);
  • region: 関数がデプロイされるリージョンを指定します。この例では asia-northeast1(東京リージョン)を指定しています。
  • secrets: 関数内で使用するシークレットを指定します。
  • minInstancesmaxInstances: 関数のスケーリング設定を指定します。ここでは、最小 0 インスタンス、最大 1 インスタンスに設定されています。
  • cors: クロスオリジンリクエストを許可するドメインを指定します。ここではlocalhostからのアクセスを許可していますが、必要に応じて他のドメインも追加してください。

3. リクエストデータの取得

const data = request.data;
  • クライアントから送信されたデータを取得します。
  • 例えば、クライアントが JSON データを送信した場合、この data オブジェクトにその内容が格納されます。

4. 認証情報の確認

const auth = request.auth;
  • リクエストを送信したクライアントの認証情報を取得します。
  • 例えば、Firebase Authentication を使用している場合、ここでユーザーの UID やトークン情報を確認できます。

5. 処理の実行

const result = { message: "Hello from Firebase!" };
  • 必要な処理を実行し、その結果を result に格納します。
  • この例では、単純にメッセージを返すだけの処理を行っています。

6. レスポンスの返却

return result;
  • クライアントに対して処理結果を返却します。
  • onCall 関数では、return で返したデータがクライアントに送信されます。

クライアントサイドでの使用例

クライアント側では、Firebase SDK を使用してこの関数を呼び出すことができます。以下は JavaScript クライアントの例です。

import { getFunctions, httpsCallable } from "firebase/functions";

const firebaseConfig = { //プロジェクトによって内容は異なります
  apiKey: "****",
  authDomain: "****",
  projectId: "****",
  storageBucket: "****",
  messagingSenderId: "****",
  appId: "****",
  measurementId: "****",
};

const app = initializeApp(firebaseConfig); 
const functions = getFunctions(app, "asia-northeast1");
const helloWorld = httpsCallable(functions, "helloWorld");

// localのエミュレーターに接続する
if (window.location.hostname === "localhost") {
    console.log("[Emulator mode]");
    connectFunctionsEmulator(functions, "localhost", 5001);
  }

helloWorld({ message: "hi!" }).then(
  (result) => {
    console.log(result.data); // { message: "Hello from Firebase!" }
  },
  (reason) => {
    console.error(reason); // エラーが発生した場合の処理
  },
);

リファレンス

CallableOptions

CallableOptions.enforceAppCheck

  • : boolean | undefined
  • 説明: Firebase App Checkが有効かどうかを示します。true の場合、無効なトークンを含むリクエストは自動的に401 (Unauthorized) エラーで応答します。false の場合、無効なトークンを含むリクエストでは request.appundefined に設定されます。

CallableOptions.consumeAppCheckToken

  • : boolean | undefined
  • 説明: Firebase App Checkトークンをリクエスト時に消費するかどうかを決定します。デフォルトは false です。
  • 備考:
    • このオプションを true に設定すると、App Checkのリプレイ保護機能が有効になります。すでに消費されたトークンは request.app.alreadyConsumed プロパティが true に設定されます。
    • この機能を有効にすると、App Checkバックエンドへの追加のネットワーク呼び出しが発生し、パフォーマンスに影響を与える可能性があります。また、選択した認証プロバイダーのクォータを早く消費する可能性があります。
    • セキュリティが重要な低頻度の操作やコストのかかる操作を保護する場合にのみ、この機能を使用してください。
    • このオプションは enforceAppCheck オプションには影響しません。
      • enforceAppChecktrue に設定すると、無効なApp Checkトークンを含むリクエストは自動的に401 (Unauthorized) エラーで応答します。
      • 有効だが消費済みのApp Checkトークンを含むリクエストは自動的に拒否されません。その代わりに、request.app.alreadyConsumed プロパティが true に設定されます。このプロパティを用いて追加のセキュリティチェックを要求する、またはリクエストを拒否するなどのさらなる判断を行うことができます。

CallableOptions.heartbeatSeconds

  • : number | null | undefined
  • 説明: 接続を維持するためにハートビートメッセージを送信する間隔(秒単位)。null に設定するとハートビートが無効になります。
  • デフォルト値: 30秒

CallableOptions.authPolicy(廃止予定)

  • : (auth: AuthData | null, data: T) => boolean | Promise<boolean> | undefined
  • 説明: リクエストが認証されているかどうかを判断するためのコールバック関数。
  • 備考:
    • 再利用可能な認証ポリシーをオプションオブジェクトとして渡すために設計されています。
    • 組み込みの再利用可能なポリシーとして isSignedInhasClaim が存在します。
  • 注意: このオプションは将来的に廃止されます。

CallableOptions.omit

  • : boolean | Expression<boolean>
  • 説明: この関数をデプロイまたはエミュレートしない場合に true を設定します。

CallableOptions.region

  • : SupportedRegion | string | Array<SupportedRegion | string> | Expression<string> | ResetValue
  • 説明: グローバルオプションを上書きし、この関数をデプロイするリージョンを指定します。複数のリージョンを指定することも可能です。
  • 関連文献

CallableOptions.cors

  • : string | boolean | RegExp | Array<string | RegExp>
  • 説明: この関数へのリクエストでCORSを許可するかどうかを指定します。デフォルトでは、https.CallableFunction の場合は true、それ以外は false です。

CallableOptions.memory

  • : options.MemoryOption | Expression<number> | ResetValue
  • 説明: 関数に割り当てるメモリ量を指定します。

CallableOptions.timeoutSeconds

  • : number | Expression<number> | ResetValue
  • 説明: 関数のタイムアウト時間(秒単位)を指定します。
  • 備考:
    • 第二世代関数の最小タイムアウトは 1秒 です。
    • 関数の種類によって最大タイムアウトが異なります:
      • イベントトリガー関数: 最大 540秒 (9分)。
      • HTTPSおよびonCall関数: 最大 3,600秒 (1時間)。
      • タスクキュー関数: 最大 1,800秒 (30分)。
  • :
    const options = {
      timeoutSeconds: 300, // 5分
    };
    
  • : number | Expression<number> | ResetValue
  • 説明: 常に実行中のインスタンスの最小数を指定します。

CallableOptions.maxInstances

  • : number | Expression<number> | ResetValue
  • 説明: 同時に実行可能なインスタンスの最大数を指定します。

CallableOptions.concurrency

  • : number | Expression<number> | ResetValue
  • 説明: 関数が同時に処理できるリクエスト数を指定します。デフォルトはCPUに応じて異なります。

CallableOptions.cpu

  • : number | "gcf_gen1"
  • 説明: 関数に割り当てるCPUの割合を指定します。デフォルトはメモリサイズに応じて異なります。

CallableOptions.vpcConnector

  • : string | Expression<string> | ResetValue
  • 説明: 関数を接続するVPCコネクタを指定します。

CallableOptions.vpcConnectorEgressSettings

  • : options.VpcEgressSetting | ResetValue
  • 説明: VPCコネクタのEgress設定を指定します。

CallableOptions.serviceAccount

  • : string | Expression<string> | ResetValue
  • 説明: 関数を実行するための特定のサービスアカウントを指定します。

CallableOptions.ingressSettings

  • : options.IngressSetting | ResetValue
  • 説明: この関数がどこから呼び出されるかを制御するための設定を指定します。

CallableOptions.labels

  • : Record<string, string>
  • 説明: 関数に設定するユーザーラベルを指定します。

CallableOptions.secrets

CallableOptions.invoker

  • : "public" | "private" | string | string[]
  • 説明: HTTPS関数のアクセス制御を設定します。

CallableRequest<T>

CallableRequest.data

  • : T
  • 説明: クライアントから送信されたデータ。任意の型を持つことができます。
  • :
    const inputData = request.data;
    console.log(inputData);
    

CallableRequest.auth?

  • : AuthData | undefined
  • 説明: 認証済みユーザーの情報を含むオブジェクト。認証されていない場合は undefined になります。
  • プロパティ:
    • uid: 認証されたユーザーの一意の識別子。
    • token: 認証およびユーザー情報を含むデコード済みIDトークン。
  • :
    if (request.auth) {
      console.log(`User ID: ${request.auth.uid}`);
      console.log(`Token: ${JSON.stringify(request.auth.token)}`);
    }
    
  • 関連文献

CallableRequest.app?

  • : AppCheckData | undefined
  • 説明: Firebase App Checkトークンに関する情報。App Checkが有効でない場合は undefined になります。
  • プロパティ:
    • appId: App Checkトークンによって証明されたFirebaseアプリのID。
    • token: デコードされたApp Checkトークン。
    • alreadyConsumed (オプション): トークンがすでに消費されたかどうかを示します。
      • false: このトークンがApp Checkサービスによって初めて確認され、将来の使用のために消費済みとしてマークされたことを示します。
      • true: トークンが以前にApp Checkサービスによって消費済みとしてマークされたことを示します。この場合、リクエストを拒否する、または追加のセキュリティチェックを要求するなどの追加措置を検討してください。
  • :
    if (request.app) {
      console.log(`App ID: ${request.app.appId}`);
      console.log(`Token: ${request.app.token}`);
      if (request.app.alreadyConsumed) {
        console.log("This token has already been consumed.");
      } else {
        console.log("This token is being consumed for the first time.");
      }
    }
    

CallableRequest.instanceIdToken?

  • : string | undefined
  • 説明: FirebaseインスタンスID。特定のインスタンスを識別するために使用されるようです。(あいまいな解説を避けるため、現時点では解説しません。)
  • :
    if (request.instanceIdToken) {
      console.log(`Instance ID Token: ${request.instanceIdToken}`);
    }
    
  • 関連しそうな文献

CallableRequest.rawRequest

  • : Request
  • 説明: 元のHTTPリクエストオブジェクト。高度な操作が必要な場合に使用します。
  • :
console.log(request.rawRequest.headers);

CallableRequest.acceptsStreaming

  • : boolean
  • 説明: リクエストがストリーミングリクエストであるかどうかを示します。このプロパティを使用することで、request.acceptsStreamingfalse の場合に response.sendChunk を呼び出すためのチャンクのストリーム生成を回避し、パフォーマンスを最適化できます。ただし、acceptsStreamingfalse の場合でも response.sendChunk を呼び出すことは安全であり、何も影響を与えません。
  • :
    if (request.isStreaming) {
      response.sendChunk("Streaming data...");
    } else {
      console.log("This is not a streaming request.");
    }
    
  • 関連文献

CallableResponse

CallableResponse.sendChunk

  • : (chunk: T) => Promise<boolean>
  • 説明: レスポンスボディのチャンクをクライアントに書き込みます。このメソッドは複数回呼び出すことで、データを段階的にストリーミングできます。
  • 戻り値: データが書き込まれたかどうかを示すPromiseを返します。例えば、リクエストがストリーミングリクエストでない場合はfalseを返します。ネットワークエラーが発生した場合はPromiseが拒否されます。
  • :
    if (request.acceptsStreaming) {
      response.sendChunk("Chunk 1").then((written) => {
        if (written) {
          console.log("Chunk 1 was sent successfully.");
        } else {
          console.log("Chunk 1 was not sent.");
        }
      });
    }
    

CallableResponse.signal

  • : AbortSignal

  • 説明: クライアントが切断されたり、リクエストが途中で終了した場合にトリガーされます。

  • プロパティ

    • aborted (読み取り専用):
      関連付けられた AbortController が中止を通知した場合に true を返し、それ以外の場合は false を返します。

    • reason (読み取り専用):
      操作が中止された理由を提供します。この値は中止の理由を説明する任意の値やオブジェクトです。

    • onabort:
      abort イベントが発生したときにトリガーされるイベントハンドラプロパティです。このプロパティには、イベントを処理する関数を設定できます。null または (this: AbortSignal, event: Event) => any 型の関数を指定します。

    • throwIfAborted():
      シグナルの aborted フラグが true に設定されている場合、例外をスローします。このメソッドは、中止された場合に操作を即座に終了するために使用されます。

      • 例:
      import { onCall } from "firebase-functions/v2/https";
      
      export const processData = onCall(async (request, response) => {
        if (!response) return;
        const signal = response.signal;
      
        try {
          // 長時間実行される処理の例
          for (let i = 0; i < 10; i++) {
            // 中止されているか確認
            signal.throwIfAborted();
      
            // 処理の実行
            console.log(`Processing chunk ${i + 1}`);
            await new Promise((resolve) => setTimeout(resolve, 1000)); // 1秒待機
          }
      
        return { message: "Processing completed successfully." };
        } catch (error) {
          if (signal.aborted) {
            console.error("Request was aborted by the client.");
            return { error: "Request aborted by the client." };
          }
          throw error; // その他のエラーを再スロー
        }
      });
      

    この例では、signal.throwIfAborted() をループ内で呼び出すことで、クライアントがリクエストを中止した場合に即座に処理を停止します。

参考文献

あとがき

Firebaseのドキュメントは、各ドキュメントが独立性が高いです。その分、関連情報へのアプローチが難しく、特にリファレンス面で非常に貧弱だったため、今回この記事を執筆しました。
リファレンスに記載されている内容はSDKに書かれている内容を英訳しただけにすぎませんが、個人的にはいちいち英語を読まなくて済むようになり、非常に満足しています。

また、バイブコーディングが今後主流になるにつれて、ますます技術への理解が重要になります。今後もFirebaseに関する記事を執筆していく予定ですが、バイブコーディングに必要な「方針を決める能力」「間違いを修正する能力」を鍛えるために、個人的な考えも含めたものをお出ししようと考えています。

Discussion