Preparare le estensioni Firebase per la migrazione a Cloud Functions

Questa guida mostra come eseguire la migrazione delle estensioni dall'ambiente Firebase Extensions ritirato a una funzione che gli utenti installano ed eseguono il deployment nel proprio Cloud Functions per la base di codice Firebase (2ª gen.).

Questo è il percorso di migrazione consigliato. Firebase manterrà un elenco di estensioni con equivalenti npm ufficiali. Questa guida ti mostrerà come creare la tua.

In questa guida, l'estensione Stream Firestore to BigQuery (firestore-bigquery-export) viene utilizzata come esempio. Ogni sezione termina con un esempio pratico che mostra l'aspetto dell'estensione prima della migrazione e dopo, come pacchetto @firebase/firestore-bigquery-export.

Registrati per ricevere maggiori informazioni e assistenza sulla migrazione delle estensioni

Se hai domande su come eseguire la migrazione da Firebase Extensions, puoi contattarci all'indirizzo firebase-extensions-migrator-support-external@google.com. Invieremo un'email a questo gruppo anche quando aggiorneremo la guida con ulteriori informazioni su pacchettizzazione, test e distribuzione delle funzioni di 2ª gen.

Per unirti a questo gruppo, invia un messaggio all'indirizzo firebase-extensions-migrator-support-external+subscribe@google.com, che risponderà con un'email di richiesta di iscrizione. Devi rispondere a questa email, non fare clic sul pulsante "Iscriviti a questo gruppo".

Prima di iniziare

Per completare questa migrazione, utilizzerai le seguenti funzionalità di Cloud Functions:

  • Configurazione parametrizzata. Ogni parametro dichiarato in extension.yaml diventa un parametro definito nel codice del pacchetto.

  • Ruoli IAM dichiarativi e API richieste. Ogni ruolo dichiarato in extension.yaml diventa una chiamata requiresRole(...) e ogni API diventa una chiamata requiresAPI(...) nel codice della funzione. Al momento del deployment, la CLI Firebase concede i ruoli dichiarati a un service account di runtime gestito e abilita le API dichiarate per tuo conto.

  • Eventi del ciclo di vita per i codebase Cloud Functions. Le codebase Cloud Functions ora supportano gli eventi del ciclo di vita analoghi a Firebase Extensions. Dichiara la configurazione al momento dell'installazione e dell'aggiornamento con gli hook del ciclo di vita afterFirstDeploy(...) e afterRedeploy(...). Questi sostituiscono lifecycleEvents che dichiari in extension.yaml.

Inventario dell'estensione

Inizia con un inventario della tua estensione: un elenco completo di tutto ciò che l'estensione dichiara, spedisce e documenta, in modo che ogni comportamento abbia una destinazione definita nella funzione di 2ª gen. e nulla vada perso durante la migrazione.

Esamina ciascuno dei seguenti elementi e annota ciò che trovi:

  • extension.yaml, che dichiara parametri, funzioni, eventi, ruoli IAM, API richieste, secret e hook del ciclo di vita.

  • functions/, che contiene il codice della funzione, le dipendenze, la configurazione della build, i trigger e le funzioni della coda di attività.

  • README.md, PREINSTALL.md e POSTINSTALL.md, che contengono passaggi di configurazione, avvisi e note di fatturazione.

  • scripts/, che contiene eventuali utilità di importazione, backfill, IAM, riparazione o migrazione e qualsiasi altro strumento fornito insieme all'estensione.

Poi, per ogni elemento in extension.yaml, decidi dove inserirlo nel pacchetto npm:

  • Converti la configurazione utente in parametri Cloud Functions (sezione 5).

  • Converti i secret in secret Cloud Functions (sezione 6).

  • Converti i ruoli IAM in dichiarazioni requiresRole(...) (sezione 8).

  • Converti le API di Google richieste in dichiarazioni requiresAPI(...), se opportuno (sezione 8).

  • Converti gli hook di installazione e aggiornamento in dichiarazioni afterFirstDeploy(...) e afterRedeploy(...) (sezione 8).

Esempio elaborato: trasmetti in streaming Firestore a BigQuery

La lettura di firestore-bigquery-export/extension.yaml e functions/ produce questo inventario:

In extension.yaml Conteggio / valore Dove vengono inviati
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Parametri Cloud Functions (sezione 5)
API bigquery.googleapis.com requiresAPI(...) (sezione 7)
ruoli bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (sezione 7)
risorse 1 trigger evento (fsexportbigquery) + funzioni di code di attività (initBigQuerySync, setupBigQuerySync) Funzioni del pacchetto esportato (sezione 3)
lifecycleEvents onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy (sezione 9)
scripts/ import/ (backfill), gen-schema-view/ Mantenuti come script (non rientrano nell'ambito di questo articolo)

L'estensione non dichiara alcun tipo: parametri secret, quindi non c'è nulla da migrare nella sezione 6 di questa guida. Il trigger di eventi è già di 2ª gen.; solo le funzioni della coda delle attività sono ancora di 1ª gen. (pertinente nella sezione 3).

Aggiorna package.json

Aggiorna il file package.json dell'estensione. Se esegui la migrazione di un'estensione, puoi utilizzare la package.json principale. Se esegui la migrazione di molte estensioni in un unico repository, assegna a ogni estensione il proprio pacchetto.

Versioni minime dell'SDK. Dichiara firebase-functions >= 7.3 e firebase-admin >= 14.2.0 come dipendenze. Dichiara anche la tua versione di firebase-functions come dipendenza 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"
  }
}

Dichiara firebase-functions come dipendenza peer oltre alla normale dipendenza, in modo che il progetto Cloud Functions dei tuoi utenti abbia la stessa versione dell'SDK con cui è stata scritta la tua libreria.

Esempio elaborato: trasmetti in streaming Firestore a BigQuery

Prima. Il file functions/package.json dell'estensione è privato, indica l'ID estensione e dichiara firebase-functions come dipendenza diretta:

{
  "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"
  }
}

Dopo. Un pacchetto pubblicabile: nome con ambito, una mappa delle esportazioni e firebase-functions spostato in 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"
    }
}

Esegui l'upgrade delle funzioni dalla 1ª alla 2ª gen.

Se l'estensione esporta ancora funzioni di 1ª gen., converti ogni funzione nel suo equivalente di 2ª gen. Importa dai moduli firebase-functions/... e passa le impostazioni di runtime nelle opzioni della funzione.

Puoi ridurre al minimo gli sforzi di riscrittura con la destrutturazione degli eventi patch di 2ª gen. ed evitare di riscrivere la logica della funzione perché l'SDK di 2ª gen. ora espone i parametri V1 come campi nell'oggetto evento, consentendoti di utilizzare parametri destrutturati/denominati e mantenere invariata la logica di business.

Prima. 1ª gen.:

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);
  });

Dopo. 2ª gen.:

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

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

Consulta il confronto tra le versioni Cloud Functions per un elenco completo delle differenze tra le funzioni di 1ª gen. e 2ª gen.

Convertire i parametri e i secret dell'estensione

Converti parametri

Ogni parametro che dichiari in extension.yaml diventa un parametro Cloud Functions.

Converti le letture dirette dell'ambiente:

const collectionPath = process.env.COLLECTION_PATH;

nei parametri 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);
  }
);

Utilizza collectionPath.value() per leggere la stringa all'interno di un gestore; utilizza collectionPath direttamente dove è previsto un segnaposto, ad esempio un percorso di trigger di funzione.

La CLI Firebase rileva i parametri e legge i relativi valori da .env, .env.projectId o chiede agli utenti durante la distribuzione. Mantieni gli stessi nomi dei parametri, in modo che i valori di un'installazione esistente vengano trasferiti.

È importante non modificare in alcun modo i nomi dei parametri dichiarati nel codice. La migrazione delle estensioni manterrà automaticamente il valore del parametro utente finale esistente, ma solo se i nomi rimangono invariati.

Esempio elaborato: trasmetti in streaming Firestore a BigQuery Prima. Un parametro dichiarato in extension.yaml, letto come variabile di ambiente non elaborata in config.ts:

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

Dopo. Uno defineString; la CLI lo rileva e legge da .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} }
}),

Il nome del parametro è invariato, quindi un file .env esistente continua a funzionare.

Convertire i secret

In extension.yaml, dichiari i secret con type: secret. L'ambiente di runtime delle estensioni le memorizza e le associa, in modo che il codice dell'estensione possa leggere direttamente process.env.PARAM_NAME. In un codebase Cloud Functions tipico, dichiari e associ ogni secret in modo esplicito:

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

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

Una volta eseguita la migrazione dell'estensione a un pacchetto/kit npm, i riferimenti ai secret verranno gestiti nel file .env dell'utente finale. È importante non modificare in alcun modo i nomi dei secret dichiarati nel codice. Durante la migrazione, i secret degli utenti finali verranno migrati di conseguenza.

Esempio elaborato: Trigger Email From Cloud Firestore

Prima. MAIL_COLLECTION e SMTP_PASSWORD vengono letti come variabili di ambiente non elaborate in 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,

Dopo. Un defineString e un defineSecret; la CLI rileva entrambi e legge da .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();
    // ...
  }
);

Migrazione delle chiamate alla coda di attività interna

Alcune estensioni mettono in coda il lavoro nelle proprie code di attività dall'interno del codice della funzione, utilizzando Firebase Admin SDK. Questa operazione è diversa dalla ricezione di un'attività inviata (descritta nelle sezioni Funzioni di upgrade e Hook del ciclo di vita di conversione). Qui il codice è il producer che chiama queue.enqueue(...).

Le versioni precedenti di Admin SDK richiedevano che le estensioni trasmettessero il proprio ID istanza estensione come secondo parametro per indirizzare una funzione di coda di attività nella stessa estensione. A partire da `firebase-admin` 14.2.0, questo non è né richiesto né consigliato. L'API Task Queue ora ha come target le code di attività nello stesso contesto (ad es. estensione) per impostazione predefinita. È sicuro e consigliabile rimuovere questo parametro nel codice sia come estensione che come funzioni autonome. La rimozione di questo parametro garantisce la portabilità e la compatibilità futura.

Tutto il resto della chiamata enqueue, ovvero il percorso della risorsa locations/region/functions/name, il payload dell'attività e la logica di ripetizione, rimane invariato.

Per maggiori dettagli sull'accodamento delle funzioni con Cloud Tasks, consulta la pagina /docs/functions/task-functions.

Prima. Estensione di 1ª gen.

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);

Dopo. Estensione di 2ª gen.

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

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

Se la chiamata di accodamento ha come target un codebase con prefisso, anche il nome della funzione rilevato ha un prefisso (ad esempio, orders-syncBigQuery).

Dichiarare le API e i ruoli IAM richiesti

Sposta i requisiti IAM e API della tua estensione da extension.yaml nel codice:

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

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

Con la sicurezza dichiarativa, la CLI Firebase crea o aggiorna un service account di runtime gestito per la base di codice e gli concede l'unione di tutti i ruoli dichiarati. Documenta per i tuoi utenti che tutte le funzioni nel codebase vengono eseguite con questi ruoli, a meno che l'API finale non supporti un modello più ristretto.

Esempio elaborato: trasmetti in streaming Firestore a BigQuery

Prima. Dichiarata in extension.yaml; il runtime delle estensioni ha attivato l'API e ha concesso i ruoli a un account gestito:

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

Dopo. Dichiarato nel codice con requiresAPI e 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");

Convertire gli hook del ciclo di vita

Se la tua estensione chiama getExtensions().runtime(), ad esempio setProcessingState o setFatalError, elimina queste chiamate, in quanto genereranno un errore se chiamate da una funzione di 2ª gen. normalmente implementata. Lo stato del ciclo di vita è ora determinato da afterFirstDeploy e afterRedeploy, dove il monitoraggio dello stato non viene utilizzato.

Firebase Extensions può eseguire la configurazione quando un utente installa, aggiorna o riconfigura un'estensione. Nel pacchetto npm, dichiara azioni del ciclo di vita equivalenti nel codice.

Per la configurazione una tantum:

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: {}
  }
});

Per gli aggiornamenti di configurazione o del codice:

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

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

Rendi idempotenti le azioni del ciclo di vita. Se l'invio o l'esecuzione non va a buon fine, gli utenti potrebbero doverli eseguire di nuovo manualmente:

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

Esempio elaborato: trasmetti in streaming Firestore a BigQuery

Prima. lifecycleEvents in extension.yaml, generato dal runtime delle estensioni:

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

Dopo. Dichiarato nel codice; il provisioning dell'attività BigQuery al primo deployment:

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

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

Il provisioning è idempotente, quindi una nuova esecuzione riconcilia il set di dati, la tabella e le viste. Gli utenti possono eseguire nuovamente l'operazione manualmente con firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Configurazione dei documenti per gli utenti

  • Scrivi un README del pacchetto che spieghi, come minimo:

  • I valori di .env richiesti dal pacchetto.

  • I secret richiesti dal pacchetto e come eseguire la migrazione dei valori dei secret esistenti.

  • I ruoli IAM dichiarati dal pacchetto con requiresRole(...).

  • Le API di Google che il pacchetto abilita o richiede.

  • Gli hook del ciclo di vita dichiarati dal pacchetto e come eseguirli di nuovo manualmente.

  • Note di fatturazione.

  • Cosa è cambiato rispetto all'estensione originale.

Esempio elaborato: trasmetti in streaming Firestore a BigQuery

Il file README del pacchetto include una tabella concreta "Che cosa è cambiato":

Problema Come estensione As @firebase/firestore-bigquery-export
Configurazione Parametri di estensione Parametri delle funzioni tramite .env
IAM Concessa dalle estensioni requiresRole(...), applicato al deployment
In fase di provisioning Attività del ciclo di vita per estensioni afterFirstDeploy / afterRedeploy task
Nomi delle funzioni ext-instanceId-fsexportbigquery fsexportbigquery (con prefisso facoltativo)

Testare la funzione di 2ª gen.

Ora dovresti avere una funzione di 2ª gen. che, una volta eseguito il deployment, si comporterà in modo identico a una nuova installazione della tua estensione. L'ultimo passaggio consiste nel verificare e risolvere eventuali problemi introdotti accidentalmente durante la procedura.

Assicurati di utilizzare firebase-tools >= 15.25.1 e di eseguire il deployment della funzione di 2ª gen. convertita in un progetto di test con le risorse appropriate per testarne il comportamento. Se hai già configurato un progetto di test per testare l'estensione, utilizza il comando:

firebase deploy --only functions

Dopo aver inserito questo comando, compila la procedura guidata risultante che ti chiede i valori dei parametri nello stesso modo in cui avresti compilato il modulo di installazione nella console Firebase per l'estensione.

Esempio elaborato: trasmetti in streaming Firestore a BigQuery

Verifichiamo la sincronizzazione end-to-end da Cloud Firestore a BigQuery:

  1. Nella console Cloud Firestore, crea la raccolta che hai impostato come COLLECTION_PATH (users) se non esiste già.
  2. Crea un documento denominato bigquery-mirror-test contenente campi con valori qualsiasi.
  3. Nella console BigQuery, esegui una query della tabella del log delle modifiche non elaborato. Deve contenere una sola riga che registra la creazione del documento:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Esegui una query sulla visualizzazione più recente, che dovrebbe restituire l'ultimo evento di modifica per l'unico documento presente: bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Elimina il documento bigquery-mirror-test in Cloud Firestore. Scompare dalla visualizzazione più recente e alla tabella del log delle modifiche non elaborato viene aggiunto un evento DELETE.

Puoi esaminare la cronologia completa di un singolo documento con:

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

Differenze rispetto al test dell'estensione:

  • Il trigger viene eseguito il deployment come fsexportbigquery (con prefisso codebase facoltativo), non come ext-&lt;instanceId&gt;-fsexportbigquery. Cerca questo nome nella dashboard e nei log di Cloud Functions.
  • Il codice verrà ora eseguito in Firebase Local Emulator Suite come funzioni normali. Puoi impostare il valore dei parametri da utilizzare nell'emulatore con .env.local. Puoi anche testare le unità del tuo codice utilizzando l'SDK firebase-functions-test come descritto in Test delle unità di Cloud Functions
  • Il provisioning non è più gestito dal runtime delle estensioni. Se la tabella del log delle modifiche non è presente dopo l'implementazione, esegui di nuovo manualmente l'attività di configurazione: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. L'attività è idempotente, quindi la sua esecuzione riconcilia il set di dati, la tabella e le viste.
  • I valori dei parametri provengono da .env anziché dal modulo di installazione, pertanto le ripetizioni di firebase deploy non sono interattive una volta completato .env.