Préparer les extensions Firebase à la migration vers Cloud Functions

Ce guide vous explique comment migrer vos extensions de l'environnement obsolète Firebase Extensions vers une fonction que vos utilisateurs installent et déploient dans leur propre Cloud Functions pour Firebase (2e génération) codebase.

Il s'agit du chemin de migration recommandé. Firebase gérera une liste d'extensions avec des équivalents npm officiels. Ce guide vous expliquera comment créer les vôtres .

Tout au long de ce guide, l'extension Stream Firestore to BigQuery (firestore-bigquery-export) est utilisée comme exemple. Chaque section se termine par un exemple concret qui montre à quoi ressemblait cette extension avant la migration et à quoi elle ressemble après, en tant que package @firebase/firestore-bigquery-export.

Inscrivez-vous pour obtenir plus d'informations et d'aide sur la migration des extensions

Si vous avez des questions sur la migration depuis Firebase Extensions, vous pouvez nous contacter à l'adresse firebase-extensions-migrator-support-external@google.com. Nous enverrons également un e-mail à ce groupe lorsque nous mettrons à jour le guide avec plus d'informations sur l'empaquetage, les tests et la distribution de vos fonctions de 2e génération.

Pour rejoindre ce groupe, envoyez un message à firebase-extensions-migrator-support-external+subscribe@google.com, qui répondra par un e-mail de demande d'adhésion. Vous devez répondre à cet e-mail, et non cliquer sur le bouton "Rejoindre ce groupe".

Avant de commencer

Pour effectuer cette migration, vous utiliserez les fonctionnalités suivantes de Cloud Functions :

  • Configuration paramétrée. Chaque paramètre que vous déclarez dans extension.yaml devient un paramètre défini dans le code de votre package.

  • Rôles IAM déclaratifs et API requises. Chaque rôle que vous déclarez dans extension.yaml devient un appel requiresRole(...), et chaque API devient un appel requiresAPI(...) dans le code de votre fonction. Au moment du déploiement, la Firebase CLI Firebase accorde les rôles déclarés à un compte de service d'environnement d'exécution géré et active les API déclarées en votre nom.

  • Événements de cycle de vie pour les codebases Cloud Functions. Cloud Functions Les codebases sont désormais compatibles avec les événements de cycle de vie analogues à Firebase Extensions. Déclarez la configuration au moment de l'installation et de la mise à jour avec les hooks de cycle de vie afterFirstDeploy(...) et afterRedeploy(...). Ils remplacent les lifecycleEvents que vous déclarez dans extension.yaml.

Inventaire de l'extension

Commencez par effectuer un inventaire de votre extension : une liste complète de tout ce que l'extension déclare, fournit et documente, afin que chaque comportement ait une destination définie dans la fonction de 2e génération et que rien ne soit perdu lors de la migration.

Examinez chacun des éléments suivants et notez ce que vous trouvez :

  • extension.yaml, qui déclare vos paramètres, fonctions, événements, rôles IAM, API requises, secrets et hooks de cycle de vie.

  • functions/, qui contient le code de votre fonction, les dépendances, la configuration de compilation, les déclencheurs et les fonctions de file d'attente de tâches.

  • README.md, PREINSTALL.md et POSTINSTALL.md, qui contiennent les étapes de configuration, les avertissements et les notes de facturation.

  • scripts/, qui contient tous les utilitaires d'importation, de remplissage, IAM, de réparation ou de migration, ainsi que tous les autres outils que vous fournissez avec l'extension.

Ensuite, pour chaque élément de extension.yaml, déterminez où il se trouve dans le package npm :

  • Convertissez la configuration utilisateur en Cloud Functions paramètres (section 5).

  • Convertissez les secrets en Cloud Functions secrets (section 6).

  • Convertissez les rôles IAM en déclarations requiresRole(...) (section 8).

  • Convertissez les API Google requises en déclarations requiresAPI(...) le cas échéant (section 8).

  • Convertissez les hooks d'installation et de mise à jour en déclarations afterFirstDeploy(...) et afterRedeploy(...) (section 8).

Exemple concret : Stream Firestore to BigQuery

La lecture de firestore-bigquery-export/extension.yaml et de functions/ génère l'inventaire suivant :

Dans extension.yaml Nombre / valeur Où cela se trouve
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions paramètres (section 5)
apis bigquery.googleapis.com requiresAPI(...) (section 7)
rôles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (section 7)
ressources 1 déclencheur d'événement (fsexportbigquery) + fonctions de file d'attente de tâches (initBigQuerySync, setupBigQuerySync) Fonctions de package exportées (section 3)
lifecycleEvents onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy (section 9)
scripts/ import/ (remplissage), gen-schema-view/ Conservé en tant que scripts (hors champ ici)

L'extension ne déclare aucun paramètre de type secret. Il n'y a donc rien à migrer dans la section 6 de ce guide. Le déclencheur d'événement est déjà de 2e génération. Seules les fonctions de file d'attente de tâches sont encore de 1re génération (pertinentes dans la section 3).

Mettre à jour package.json

Mettez à jour le fichier package.json de votre extension. Si vous migrez une seule extension, il peut s'agir du package.json racine. Si vous migrez plusieurs extensions dans un seul dépôt, attribuez à chaque extension son propre package.

Versions minimales du SDK. Déclarez firebase-functions >= 7.3 et firebase-admin >= 14.2.0 comme dépendances. Déclarez également votre version de firebase-functions comme dépendance peer.

{
  "name": "<package-name>",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/index.d.ts",
      "default": "./lib/index.js"
    }
  },
  "engines": {
    "node": ">=22"
  },
  "peerDependencies": {
    "firebase-functions": "^7.3.0"
  },
  "dependencies": {
    "firebase-functions": "^7.3.0",
    "firebase-admin": "^14.2.0"
  }
}

Déclarez firebase-functions comme dépendance peer en plus de votre dépendance normale, afin que le projet Cloud Functions de vos utilisateurs dispose de la même version du SDK que celle avec laquelle votre bibliothèque a été écrite.

Exemple concret : Stream Firestore to BigQuery

Avant. Le fichier functions/package.json de l'extension est privé, nomme l'ID de l'extension et déclare firebase-functions comme dépendance directe :

{
  "name": "firestore-bigquery-export",
  "main": "lib/index.js",
  "private": true,
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^6.3.2"
  }
}

Après. Un package publiable : nom limité, carte d'exportation et firebase-functions déplacé vers peerDependencies :

{
  "name": "@firebase/firestore-bigquery-export",
  "version": "0.1.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".":     { "types": "./lib/index.d.ts", "default": "./lib/index.js" },  },
  "engines": { "node": ">=22" },
  "peerDependencies": { "firebase-functions": "^7.3.0" },
  "dependencies": {
      "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
      "firebase-admin": "^14.2.0",
      "firebase-functions": "^7.3.0"
    }
}

Mettre à niveau les fonctions de la 1re génération à la 2e génération

Si votre extension exporte toujours des fonctions de 1re génération, convertissez chaque fonction en son équivalent de 2e génération. Importez à partir des modules firebase-functions/... et transmettez les paramètres d'exécution dans les options de la fonction.

Vous pouvez minimiser les efforts de réécriture grâce à la déstructuration d'événements corrigés de 2e génération et éviter de réécrire la logique de votre fonction car le SDK de 2e génération expose désormais les paramètres V1 en tant que champs dans l'objet d'événement, ce qui vous permet d'utiliser des paramètres déstructurés/nommés et de conserver votre logique métier inchangée.

Avant. 1re génération :

import * as functions from "firebase-functions/v1";

export const sync = functions.firestore
  .document("{collectionId}/{documentId}")
  .onWrite(async (change, context) => {
    await handleWrite(change.before, change.after, context.params);
  });

Après. 2e génération :

import { onDocumentWritten } from "firebase-functions/firestore";

export const syncV2 = onDocumentWritten(
  { document: "{collectionId}/{documentId}" },
  async ({change,context}) =>
    await handleWrite(change.before, change.after, context.params);
);

Consultez la Cloud Functions comparaison des versions pour obtenir une liste complète des différences entre les fonctions de 1re et de 2e génération.

Convertir les paramètres et les secrets d'extension

Convertir les paramètres

Chaque paramètre que vous déclarez dans extension.yaml devient un Cloud Functions paramètre.

Convertissez les lectures directes de l'environnement :

const collectionPath = process.env.COLLECTION_PATH;

en paramètres Cloud Functions :

import { defineString } from "firebase-functions/params";
import { onDocumentWritten} from "firebase-functions/firestore";

const collectionPath = defineString("COLLECTION_PATH");

// Pass the param directly when used as a placeholder (e.g. trigger path)
export const sync = onDocumentWritten(
  { document: collectionPath },
  async (event) => {
    // Call .value() to read the string inside a handler
    const path = collectionPath.value();
    await handleWrite(path, event);
  }
);

Utilisez collectionPath.value() pour lire la chaîne dans un gestionnaire. Utilisez collectionPath directement lorsqu'un espace réservé est attendu, tel qu'un chemin de déclencheur de fonction .

La Firebase CLI détecte vos paramètres et lit leurs valeurs à partir de .env, .env.projectId, ou invite vos utilisateurs lors du déploiement. Conservez les mêmes noms de paramètres afin que les valeurs d'une installation existante soient conservées.

Il est important de ne pas modifier les noms de paramètres déclarés dans votre code du tout. La migration d'extension conservera automatiquement la valeur de paramètre de l'utilisateur final existant, mais uniquement lorsque les noms sont inchangés.

Exemple concret : Stream Firestore to BigQuery Avant. Un paramètre déclaré dans extension.yaml, lu comme une variable d'environnement brute dans config.ts :

# extension.yaml
-   param: COLLECTION_PATH
  label: Collection path
  type: string
  required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,

Après. Un defineString ; la CLI le détecte et le lit à partir de .env :

// src/config.ts
import { defineString } from "firebase-functions/params";

collectionPath: defineString("COLLECTION_PATH", {
  label: "Collection path",
  // We now support "nonEmpty: true" to ensure a value other than the empty string
  // is entered, analagous to "required: true" in extensions.yaml
  input: { text: { nonEmpty: true} }
}),

Le nom du paramètre est inchangé. Un fichier .env existant continue donc de fonctionner.

Convertir les secrets

Dans extension.yaml, vous déclarez des secrets avec le type : secret. L'environnement d'exécution Extensions les stocke et les lie, de sorte que le code de votre extension puisse lire directement process.env.PARAM_NAME. Dans une codebase Cloud Functions typique, vous déclarez et liez explicitement chaque secret :

import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";

const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);

Une fois votre extension migrée vers un package/kit npm, les références secrètes seront gérées dans le fichier .env de l'utilisateur final. Il est important de ne pas modifier les noms de secrets déclarés dans votre code du tout. Lors de la migration, les secrets de l'utilisateur final seront migrés en conséquence.

Exemple concret : Trigger Email From Cloud Firestore

Avant. MAIL_COLLECTION et SMTP_PASSWORD lus comme des variables d'environnement brutes dans config.ts :

# extension.yaml
-   param: MAIL_COLLECTION
  label: Email documents collection
  type: string
  default: mail
  required: true

-   param: SMTP_PASSWORD
  label: SMTP password
  type: secret
// functions/src/config.ts
mailCollection: process.env.MAIL_COLLECTION,
smtpPassword: process.env.SMTP_PASSWORD,

Après. Un defineString et un defineSecret ; la CLI détecte les deux et les lit à partir de .env :

import { defineString, defineSecret } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";

const mailCollection = defineString("MAIL_COLLECTION",{ label: "Email documents collection",
 default: "mail"
});

const smtpPassword = defineSecret("SMTP_PASSWORD", { label: "SMTP password" });

export const processQueue = onDocumentWritten(
  { document: `${mailCollection}/{documentId}`, secrets: [smtpPassword] },
  async (event) => {
    const collection = mailCollection.value();
    const password = smtpPassword.value();
    // ...
  }
);

Migrer les appels internes de file d'attente de tâches

Certaines extensions mettent en file d'attente le travail sur leurs propres files d'attente de tâches à partir du code de leur fonction, à l'aide du Firebase Admin SDK. Cela diffère de la réception d'une tâche distribuée (traitée dans les sections Mettre à niveau les fonctions et Convertir les hooks de cycle de vie). Ici, votre code est le producteur qui appelle queue.enqueue(...).

Les versions précédentes de Admin SDK exigeaient que les extensions transmettent leur propre ID d'instance d'extension en tant que deuxième paramètre pour cibler une fonction de file d'attente de tâches dans la même extension. À partir de `firebase-admin` 14.2.0, cela n'est ni requis ni recommandé. L'API Task Queue cible désormais par défaut les files d'attente de tâches dans le même contexte (par exemple, l'extension). Il est sûr et recommandé de supprimer ce paramètre dans votre code à la fois en tant qu'extension et en tant que fonctions autonomes. La suppression de ce paramètre garantit la portabilité et la compatibilité ascendante.

Tout le reste concernant l'appel d'enfilement (chemin d'accès aux ressources locations/region/functions/name, charge utile de la tâche et logique de nouvelle tentative) reste identique.

Pour en savoir plus sur l'enfilement de fonctions avec Cloud Tasks, consultez /docs/functions/task-functions.

Avant. Extension de 1re génération

import { getFunctions } from "firebase-admin/functions";

const queue = getFunctions().taskQueue(
  `locations/${config.location}/functions/syncBigQuery`,
  process.env.EXT_INSTANCE_ID, // extension instance ID, injected by the runtime
);
await queue.enqueue(taskData);

Après. Extension de 2e génération

import { getFunctions } from "firebase-admin/functions";
import { region } from "firebase-functions/params";

const queue = getFunctions().taskQueue(
  `locations/${region.value()}/functions/syncBigQuery`);
await queue.enqueue(taskData);

Si votre appel d'enfilement cible une codebase préfixée, le nom de la fonction détectée est également préfixé (par exemple, orders-syncBigQuery).

Déclarer les API et les rôles IAM requis

Déplacez les exigences IAM et API de votre extension hors de extension.yaml et dans le code :

import { requiresAPI, requiresRole } from "firebase-functions"

requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");

Avec la sécurité déclarative, la Firebase CLI crée ou met à jour un compte de service d'environnement d'exécution géré pour la codebase et lui accorde l'union de tous les rôles déclarés. Indiquez à vos utilisateurs que toutes les fonctions de la codebase s'exécutent avec ces rôles, sauf si l'API finale est compatible avec un modèle plus étroit.

Exemple concret : Stream Firestore to BigQuery

Avant. Déclaré dans extension.yaml ; l'environnement d'exécution Extensions a activé l'API et accordé les rôles à un compte géré :

apis:
  -   apiName: bigquery.googleapis.com
roles:
  -   role: bigquery.dataEditor
  -   role: datastore.user
  -   role: bigquery.user

Après. Déclaré dans le code avec requiresAPI et requiresRole :

import { requiresAPI, requiresRole } from "firebase-functions/";
requiresAPI("bigquery.googleapis.com",
  "Needed to write changelog rows and views");
requiresRole("roles/biguqery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");

Convertir les hooks de cycle de vie

Si votre extension appelle getExtensions().runtime(), par exemple setProcessingState ou setFatalError, supprimez ces appels, car ils généreront une erreur s'ils sont appelés à partir d'une fonction de 2e génération déployée normalement. L'état du cycle de vie est désormais géré par afterFirstDeploy et afterRedeploy lorsque ce suivi d'état n'est pas utilisé.

Firebase Extensions peut exécuter la configuration lorsqu'un utilisateur installe, met à jour ou reconfigure une extension. Dans votre package npm, déclarez des actions de cycle de vie équivalentes dans le code.

Pour une configuration unique :

import { afterFirstDeploy } from "firebase-functions/lifecycle";
import { onTaskDispatched } from "firebase-functions/tasks";

export const runInitialSetup = onTaskDispatched(async (request) => {
  await initializeResources(request.data);
});

afterFirstDeploy({
  task: {
    function: "runInitialSetup",
    body: {}
  }
});

Pour les mises à jour de configuration ou de code :

import { afterRedeploy } from "firebase-functions/lifecycle";

afterRedeploy({
  task: {
    function: "runInitialSetup",
    body: { reconcile: true }
  }
});

Rendez vos actions de cycle de vie idempotentes. Vos utilisateurs devront peut-être les réexécuter manuellement en cas d'échec de la distribution ou de l'exécution :

firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME

Exemple concret : Stream Firestore to BigQuery

Avant. lifecycleEvents dans extension.yaml, géré par l'environnement d'exécution Extensions :

lifecycleEvents:
  onInstall:
    function: initBigQuerySync
    processingMessage: Configuring BigQuery Sync.
  onUpdate:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync
  onConfigure:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync

Après. Déclaré dans le code ; la tâche provisionne BigQuery lors du premier déploiement :

import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";

afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });

Le provisionnement est idempotent. Une nouvelle exécution réconcilie donc l'ensemble de données, la table et les vues. Les utilisateurs peuvent réexécuter manuellement avec firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Documenter la configuration pour vos utilisateurs

  • Rédigez un fichier README de package qui explique au minimum :

  • Les valeurs .env requises par le package.

  • Les secrets requis par le package et comment migrer les valeurs secrètes existantes.

  • Les rôles IAM que le package déclare avec requiresRole(...).

  • Les API Google que le package active ou requiert.

  • Les hooks de cycle de vie que le package déclare et comment les réexécuter manuellement.

  • Les notes de facturation.

  • Les modifications apportées par rapport à l'extension d'origine.

Exemple concret : Stream Firestore to BigQuery

Le fichier README du package fournit un tableau concret des modifications apportées :

Problème Comme l'extension Comme @firebase/firestore-bigquery-export
Config Paramètres d'extension Paramètres de fonctions via .env
IAM Accordé par Extensions requiresRole(...), appliqué lors du déploiement
Provisionnement Tâche de cycle de vie par Extensions Tâche afterFirstDeploy / afterRedeploy
Noms des fonctions ext-instanceId-fsexportbigquery fsexportbigquery (préfixé éventuellement)

Tester votre fonction de 2e génération

Vous devriez maintenant disposer d'une fonction de 2e génération qui, une fois déployée, se comportera de manière identique à une nouvelle installation de votre extension. La dernière étape consiste à vérifier et à corriger les problèmes introduits accidentellement en cours de route.

Assurez-vous d'utiliser firebase-tools >= 15.24.0 et déployez votre fonction de 2e génération convertie dans un projet de test avec les ressources appropriées pour tester son comportement. Si vous avez déjà configuré un projet de test à partir du test de votre extension, utilisez la commande :

firebase deploy --only functions

Après avoir saisi cette commande, remplissez l'assistant qui s'affiche en vous demandant les valeurs des paramètres de la même manière que vous auriez rempli le formulaire d'installation dans la Firebase console pour l'extension.

Exemple concret : Stream Firestore to BigQuery

Nous vérifions la synchronisation de bout en bout entre Cloud Firestore et BigQuery :

  1. Dans la console Cloud Firestore, créez la collection que vous avez définie comme COLLECTION_PATH (utilisateurs) si elle n'existe pas déjà.
  2. Créez un document nommé bigquery-mirror-test contenant des champs avec des valeurs.
  3. Dans la console BigQuery, interrogez la table brute de journal des modifications. Elle doit contenir une seule ligne enregistrant la création du document :
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Interrogez la dernière vue, qui doit renvoyer le dernier événement de modification pour le seul document présent : bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Supprimez le bigquery-mirror-test document dans Cloud Firestore. Il disparaît de la dernière vue et un événement DELETE est ajouté à la table brute de journal des modifications.

Vous pouvez inspecter l'historique complet d'un seul document avec :

SELECT *
   FROM `PROJECT_ID.analytics.users_raw_changelog`
   WHERE document_name = "bigquery-mirror-test"
   ORDER BY timestamp ASC

Différences par rapport au test de l'extension :

  • Le déclencheur se déploie en tant que fsexportbigquery (éventuellement préfixé par la codebase), et non ext-&lt;instanceId&gt;-fsexportbigquery. Recherchez ce nom dans le Cloud Functions tableau de bord et les journaux.
  • Votre code s'exécutera désormais dans la Firebase Local Emulator Suite en tant que fonctions normales. Vous pouvez définir la valeur des paramètres à utiliser dans l'émulateur avec .env.local. Vous pouvez également effectuer des tests unitaires de votre code à l'aide du SDK firebase-functions-test, comme décrit dans la section Tests unitaires de Cloud Functions
  • Le provisionnement n'est plus géré par l'environnement d'exécution Extensions. Si la table de journal des modifications est manquante après le déploiement, réexécutez manuellement la tâche de configuration : firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. La tâche est idempotente. Sa réexécution réconcilie donc l'ensemble de données, la table et les vues.
  • Les valeurs des paramètres proviennent de .env plutôt que du formulaire d'installation. Les réexécutions de firebase deploy ne sont donc pas interactives une fois que .env est terminé.