AWS Lambda上でFirebase Admin Node.js SDKを使用した際にinitializeApp()が上手く動かずハマった話

2022.05.27

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、AWS Lambda上でFirebase Admin Node.js SDKを使用した際に、initializeApp()が上手く動かずハマった際の対処について共有します。

事象、調査

次のFirebase Cloud Messagingによるメッセージ送信を行うsend-firebase-messageModuleをAWS Lambda(Node.js)上で実装します。このsendMessageFunctionはhandlerではなく、親Moduleで読み込まれて実行されます。

send-firebase-message.ts

import * as firebaseAdmin from 'firebase-admin';

const FIREBASE_PROJECT_ID = process.env.FIREBASE_PROJECT_ID as string;
const FIREBASE_CLIENT_EMAIL = process.env.FIREBASE_CLIENT_EMAIL as string;

export const sendMessage = async (
  firebasePrivateKey: string,
  fcmToken: string,
  messageTitle: string,
  messageBody?: string
) => {
  firebaseAdmin.initializeApp({
    credential: firebaseAdmin.credential.cert({
      projectId: FIREBASE_PROJECT_ID,
      clientEmail: FIREBASE_CLIENT_EMAIL,
      privateKey: firebasePrivateKey,
    }),
  });

  const params = {
    notification: {
      title: messageTitle,
      body: messageBody,
    },
    token: fcmToken,
  };

  await firebaseAdmin.messaging().send(params);
};

ここで使用しているinitializeApp()は、Firebaseのアプリインスタンスを作成および初期化するための関数で、Firebase Admin Node.js SDKを使用する際に必ず実行する必要です。

さてこのLambdaを実行すると、1回目は成功しましたが、2回目以降で次のようなエラーが発生するようになりました。

{
  "errorType": "Error",
  "errorMessage": "The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.",
  "trace": [
    "Error: The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.",
    "    at FirebaseAppError2.FirebaseError2 [as constructor] (/var/task/index.js:49489:28)",
    "    at FirebaseAppError2.PrefixedFirebaseError2 [as constructor] (/var/task/index.js:49520:28)",
    "    at new FirebaseAppError2 (/var/task/index.js:49537:28)",
    "    at AppStore2.initializeApp (/var/task/index.js:71423:19)",
    "    at FirebaseNamespaceInternals2.initializeApp (/var/task/index.js:206852:33)",
    "    at FirebaseNamespace2.initializeApp (/var/task/index.js:207039:30)",
    "    at sendMessage (/var/task/index.js:207179:17)",
    "    at sendPushNotification (/var/task/index.js:207202:9)",
    "    at processTicksAndRejections (internal/process/task_queues.js:95:5)",
    "    at async Runtime.handler (/var/task/index.js:207208:3)"
  ]
}

エラーメッセージの翻訳です。default Firebase appがすでに存在していることが原因のようです。LambdaのWarm Startによる影響でしょうか。

デフォルトのFirebaseアプリはすでに存在します。これは、2番目の引数としてアプリ名を指定せずにinitializeApp()を複数回呼び出したことを意味します。ほとんどの場合、initializeApp()を呼び出す必要があるのは1回だけです。ただし、複数のアプリを初期化する場合は、initializeApp()に2番目の引数を渡して、各アプリに一意の名前を付けます。

そこでメッセージに従って、initializeApp()の2番目の引数に必ず一意となるアプリ名を指定してみます。

send-firebase-message.ts

  firebaseAdmin.initializeApp(
    {
      credential: firebaseAdmin.credential.cert({
        projectId: FIREBASE_PROJECT_ID,
        clientEmail: FIREBASE_CLIENT_EMAIL,
        privateKey: firebasePrivateKey,
      }),
    },
    String(new Date())
  );

変更をデプロイし、Lambdaを実行すると、次はエラーが変わりました。

{
  "errorType": "Error",
  "errorMessage": "The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services.",
  "trace": [
    "Error: The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services.",
    "    at FirebaseAppError2.FirebaseError2 [as constructor] (/var/task/index.js:49489:28)",
    "    at FirebaseAppError2.PrefixedFirebaseError2 [as constructor] (/var/task/index.js:49520:28)",
    "    at new FirebaseAppError2 (/var/task/index.js:49537:28)",
    "    at AppStore2.getApp (/var/task/index.js:71441:17)",
    "    at FirebaseNamespaceInternals2.app (/var/task/index.js:206856:33)",
    "    at FirebaseNamespace2.app (/var/task/index.js:207042:30)",
    "    at FirebaseNamespace2.ensureApp (/var/task/index.js:207053:22)",
    "    at FirebaseNamespace2.fn (/var/task/index.js:206911:26)",
    "    at sendMessage (/var/task/index.js:207207:23)",
    "    at sendPushNotification (/var/task/index.js:207216:9)"
  ]
}

エラーメッセージの翻訳です。次はdefault Firebase appが存在しないためエラーとなっています。(どっちやねん!と思わず突っ込みました。)恐らく、default Firebase appが未作成の場合(LambdaがCold startの場合)はアプリ名を指定せずにdefault Firebase appを1つ以上作成する必要があるようです。

デフォルトのFirebaseアプリは存在しません。 Firebaseサービスを使用する前に、必ずinitializeApp()を呼び出してください。

解決

次のようにFirebase appが1つも作成されていない場合のみにinitializeApp()を行うようにすれば、AWS Lambda上でも正常に動作するようになりました。

send-firebase-message.ts(修正版)

import * as firebaseAdmin from 'firebase-admin';

const FIREBASE_PROJECT_ID = process.env.FIREBASE_PROJECT_ID as string;
const FIREBASE_CLIENT_EMAIL = process.env.FIREBASE_CLIENT_EMAIL as string;

export const sendMessage = async (
  firebasePrivateKey: string,
  fcmToken: string,
  messageTitle: string,
  messageBody?: string
) => {
  if (firebaseAdmin.apps.length === 0) {
    firebaseAdmin.initializeApp({
      credential: firebaseAdmin.credential.cert({
        projectId: FIREBASE_PROJECT_ID,
        clientEmail: FIREBASE_CLIENT_EMAIL,
        privateKey: firebasePrivateKey,
      }),
    });
  }

  const params = {
    notification: {
      title: messageTitle,
      body: messageBody,
    },
    token: fcmToken,
  };

  await firebaseAdmin.messaging().send(params);
};

おわりに

AWS Lambda上でFirebase Admin Node.js SDKを使用した際にinitializeApp()が上手く動かずハマった話でした。

AWS SDKと比べると独特な仕様な気がしますが、これから使うこともあると思うので慣れていきたいです。

参考

以上