Подготовка расширений Firebase к миграции в Cloud Functions

В этом руководстве показано, как перенести ваши расширения из устаревшей среды Firebase Extensions в функцию, которую ваши пользователи будут устанавливать и развертывать в своей собственной кодовой базе Cloud Functions for Firebase (2-го поколения).

Это рекомендуемый путь миграции. Firebase будет поддерживать список расширений с официальными аналогами в npm; это руководство поможет вам создать свой собственный список.

В данном руководстве в качестве примера используется расширение Stream Firestore to BigQuery (firestore-bigquery-export). Каждый раздел заканчивается примером , демонстрирующим, как выглядело это расширение до миграции и как оно выглядит после миграции в виде пакета @firebase/firestore-bigquery-export.

Зарегистрируйтесь, чтобы получить дополнительную информацию и помощь по миграции расширений.

Если у вас есть вопросы по миграции с Firebase Extensions вы можете связаться с нами по адресу firebase-extensions-migrator-support-external@google.com . Мы также будем отправлять электронные письма этой группе по мере обновления руководства с дополнительной информацией о сборке, тестировании и распространении ваших функций второго поколения.

Чтобы присоединиться к этой группе, отправьте сообщение на адрес firebase-extensions-migrator-support-external+subscribe@google.com , в ответ вы получите электронное письмо с запросом на вступление. Вам необходимо ответить на это письмо, а не нажимать кнопку «Присоединиться к этой группе».

Прежде чем начать

Для завершения миграции вам понадобятся следующие функции Cloud Functions :

  • Параметризованная конфигурация . Каждый параметр, объявленный в extension.yaml становится определенным параметром в коде вашего пакета.

  • Декларативные роли IAM и необходимые API. Каждая роль, объявленная в extension.yaml становится вызовом requiresRole(...) , а каждый API — вызовом requiresAPI(...) в коде вашей функции. Во время развертывания Firebase CLI предоставляет объявленные роли управляемой учетной записи службы среды выполнения и включает объявленные API от вашего имени.

  • События жизненного цикла для кодовых баз Cloud Functions . Кодовые базы Cloud Functions теперь поддерживают события жизненного цикла, аналогичные тем, что используются в Firebase Extensions . Объявляйте события установки и обновления с помощью хуков жизненного цикла afterFirstDeploy(...) и afterRedeploy(...) . Они заменяют lifecycleEvents которые вы объявляете в extension.yaml .

Проведите инвентаризацию пристройки.

Начните с инвентаризации вашего расширения: составьте полный список всего, что расширение объявляет, включает в себя и документирует, чтобы каждое поведение имело определенное место назначения во второй функции и ничего не было потеряно при миграции.

Проанализируйте каждый из следующих пунктов и отметьте, что вы обнаружили:

  • extension.yaml , в котором объявляются ваши параметры, функции, события, роли IAM, необходимые API, секреты и хуки жизненного цикла.

  • functions/ , где находится код ваших функций, зависимости, конфигурация сборки, триггеры и функции очереди задач.

  • README.md , PREINSTALL.md и POSTINSTALL.md содержат шаги установки, предупреждения и примечания по выставлению счетов.

  • scripts/ , который содержит любые утилиты для импорта, заполнения, управления идентификацией и доступом (IAM), восстановления или миграции, а также любые другие инструменты, которые вы поставляете вместе с расширением.

Затем для каждого элемента в extension.yaml определите, куда он будет помещен в npm-пакете:

  • Преобразуйте пользовательскую конфигурацию в параметры Cloud Functions (раздел 5).

  • Преобразование секретов в секреты Cloud Functions (раздел 6).

  • Преобразуйте роли IAM в объявления requiresRole(...) (раздел 8).

  • При необходимости преобразуйте необходимые API Google в объявления requiresAPI(...) (раздел 8).

  • Преобразуйте обработчики установки и обновления в объявления afterFirstDeploy(...) и afterRedeploy(...) (раздел 8).

Пример работы: передача данных из Firestore в BigQuery

При чтении файлов firestore-bigquery-export/extension.yaml и functions/ выводится следующий список файлов:

В файле extension.yaml Количество / значение Куда это денется
параметры 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Параметры Cloud Functions (раздел 5)
апи bigquery.googleapis.com требует API(...) (раздел 7)
роли bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (раздел 7)
ресурсы 1 триггер события (fsexportbigquery) + функции очереди задач (initBigQuerySync, setupBigQuerySync) Функции экспортируемого пакета (раздел 3)
события жизненного цикла onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy (раздел 9)
скрипты/ импорт/ (заполнение), gen-schema-view/ Сохранено в виде скриптов (здесь это выходит за рамки данной статьи).

Расширение не объявляет тип: секретные параметры, поэтому в разделе 6 этого руководства ничего переносить не нужно. Триггер событий уже относится ко второму поколению; только функции очереди задач по-прежнему относятся к первому поколению (это актуально в разделе 3).

Обновите файл package.json.

Обновите файл package.json вашего расширения. Если вы переносите одно расширение, это может быть корневой файл package.json . Если вы переносите много расширений в одном репозитории, присвойте каждому расширению свой собственный файл package.json.

Минимальные версии SDK. Укажите firebase-functions >= 7.3 и firebase-admin >= 14.2.0 в качестве зависимостей. Также укажите вашу версию firebase-functions в качестве зависимой пары.

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

В дополнение к вашей обычной зависимости, укажите firebase-functions в качестве зависимой библиотеки, чтобы в проекте Cloud Functions ваших пользователей использовалась та же версия SDK, что и при написании вашей библиотеки.

Пример работы: передача данных из Firestore в BigQuery

Раньше. Файл functions/package.json расширения является приватным, в нём указывается идентификатор расширения, и firebase-functions объявляется прямой зависимостью:

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

После этого пакет, готовый к публикации: имя с указанием области видимости, карта экспорта и firebase-functions перемещены в 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"
    }
}

Обновление функций с 1-го поколения до 2-го поколения.

Если ваше расширение по-прежнему экспортирует функции первого поколения, преобразуйте каждую функцию в её эквивалент второго поколения. Импортируйте функции из модулей firebase-functions/... и передайте параметры времени выполнения в параметрах функции.

Вы можете свести к минимуму усилия по переписыванию кода, используя деструктуризацию событий второго поколения, и избежать переписывания логики функций, поскольку SDK второго поколения теперь предоставляет параметры версии 1 в качестве полей в объекте события, что позволяет использовать деструктурированные/именованные параметры и сохранять бизнес-логику без изменений.

Раньше. 1-е поколение:

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

После. 2-е поколение:

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

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

Полный список различий между функциями первого и второго поколений можно найти в сравнении версий Cloud Functions .

Преобразование параметров и секретов расширения.

Преобразовать параметры

Каждый параметр, который вы указываете в extension.yaml становится параметром Cloud Functions .

Преобразование прямых чтений из среды:

const collectionPath = process.env.COLLECTION_PATH;

в параметры 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);
  }
);

Используйте collectionPath.value() для чтения строки внутри обработчика; используйте collectionPath напрямую там, где ожидается заполнитель, например, путь к триггеру функции.

Интерфейс командной строки Firebase обнаруживает ваши параметры и считывает их значения из файлов .env, projectId или запрашивает их у пользователей во время развертывания. Сохраняйте те же имена параметров, чтобы значения из существующей установки перенеслись.

Важно, чтобы вы вообще не меняли имена параметров, объявленных в вашем коде. Миграция расширений автоматически сохранит существующие значения параметров конечного пользователя, но только если имена останутся неизменными.

Рабочий пример: потоковая передача данных из Firestore в BigQuery . Параметр, объявленный в extension.yaml , считывается как необработанная переменная окружения в config.ts :

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

После этого команда defineString обнаруживает CLI и считывает данные из файла .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} }
}),

Имя параметра остается неизменным, поэтому существующий файл .env продолжает работать.

Преобразовать секреты

В файле extension.yaml вы объявляете секреты с типом: secret. Среда выполнения расширений хранит и связывает их, поэтому ваш код расширения может напрямую считывать process.env.PARAM_NAME. В типичном коде Cloud Functions вы объявляете и связываете каждый секрет явно:

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

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

После переноса вашего расширения в npm-пакет/комплект, управление секретными ссылками будет осуществляться в файле .env конечного пользователя. Важно не изменять имена секретов, объявленные в вашем коде . В процессе миграции секреты конечного пользователя будут перенесены соответствующим образом.

Пример работы: Отправка электронного письма из Cloud Firestore

Ранее переменные MAIL_COLLECTION и SMTP_PASSWORD считывались как необработанные переменные окружения в файле 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,

После этого. Один defineString и один defineSecret ; CLI обнаруживает оба и считывает их из файла .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();
    // ...
  }
);

Перенос вызовов внутренней очереди задач

Некоторые расширения добавляют задачи в свои собственные очереди задач из кода своих функций, используя Firebase Admin SDK . Это отличается от получения отправленной задачи (рассмотрено в разделах «Функции обновления» и «Круты жизненного цикла преобразования »). В данном случае ваш код выступает в роли производителя , который вызывает queue.enqueue(...).

В предыдущих версиях Admin SDK расширениям требовалось передавать свой собственный идентификатор экземпляра расширения в качестве второго параметра для обращения к функции очереди задач в том же расширении. Начиная с версии `firebase-admin` 14.2.0, это не требуется и не рекомендуется. API очереди задач теперь по умолчанию будет обращаться к очередям задач в том же контексте (например, в расширении). Безопасно и даже рекомендуется удалить этот параметр в вашем коде как для расширений, так и для отдельных функций. Удаление этого параметра обеспечивает переносимость и обратную совместимость.

Все остальное, касающееся вызова enqueue — путь к ресурсу locations/ region /functions/ name , полезная нагрузка задачи и ваша логика повторных попыток — остается неизменным.

Дополнительную информацию о добавлении функций в очередь с помощью Cloud Tasks см. в файле /docs/functions/task-functions .

Ранее. Расширение первого поколения.

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

После. Расширение 2-го поколения.

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

Если ваш вызов функции добавления в очередь нацелен на кодовую базу с префиксом, то и имя обнаруженной функции будет иметь префикс (например, orders-syncBigQuery ).

Укажите необходимые API и роли IAM.

Перенесите требования IAM и API вашего расширения из файла extension.yaml в код:

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

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

При использовании декларативной безопасности Firebase CLI создает или обновляет управляемую учетную запись службы среды выполнения для кодовой базы и предоставляет ей объединение всех объявленных ролей. Задокументируйте для пользователей, что все функции в кодовой базе выполняются с этими ролями, если только окончательный API не поддерживает более узкую модель.

Пример работы: передача данных из Firestore в BigQuery

Ранее. Объявлялось в файле extension.yaml; среда выполнения расширений включала API и предоставляла роли управляемой учетной записи:

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

После этого. Объявлено в коде с помощью requiresAPI и 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");

Преобразовать хуки жизненного цикла

Если ваше расширение вызывает getExtensions().runtime() , например, setProcessingState или setFatalError , удалите эти вызовы, так как они вызовут ошибку, если будут вызваны из обычно развернутой функции второго поколения. Состояние жизненного цикла теперь управляется функциями afterFirstDeploy и afterRedeploy , где отслеживание состояния не используется.

Firebase Extensions могут запускать настройку при установке, обновлении или перенастройке расширения пользователем. В вашем npm-пакете объявите эквивалентные действия жизненного цикла в коде.

Для однократной настройки:

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

Для обновления конфигурации или кода:

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

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

Сделайте действия жизненного цикла идемпотентными. Вашим пользователям может потребоваться повторно запустить их вручную, если отправка или выполнение завершится неудачей:

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

Пример работы: передача данных из Firestore в BigQuery

Ранее использовались lifecycleEvents в extension.yaml , управляемые средой выполнения расширений:

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

После этого. Объявлено в коде; задача инициализирует BigQuery при первом развертывании:

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

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

Процесс подготовки ресурсов является идемпотентным, поэтому повторный запуск согласует набор данных, таблицу и представления. Пользователи могут повторно запустить процесс вручную с помощью команды firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Настройка документов для ваших пользователей

  • Напишите файл README для пакета, в котором, как минимум, будет объяснено следующее:

  • Значения .env , необходимые для работы пакета.

  • Секреты, необходимые для работы пакета, и как перенести существующие секретные значения.

  • Роли IAM, которые объявляются в пакете с помощью requiresRole(...) .

  • API-интерфейсы Google, которые включает или требует данный пакет.

  • Объявленные в пакете механизмы жизненного цикла и способы их ручного повторного запуска.

  • Примечания к выставлению счетов.

  • Что изменилось по сравнению с первоначальным расширением.

Пример работы: передача данных из Firestore в BigQuery

В файле README пакета содержится подробная таблица с описанием изменений:

Беспокойство В качестве расширения Как @firebase/firestore-bigquery-export
Конфигурация Параметры расширения Параметры функций передаются через файл .env.
Я Предоставлено в рамках продления срока действия. requiresRole(...), применяется при развертывании
Предоставление ресурсов Задача жизненного цикла, созданная расширениями. задача afterFirstDeploy / afterRedeploy
Названия функций ext- instanceId -fsexportbigquery fsexportbigquery (с возможностью добавления префикса)

Проверьте работу функций вашего устройства второго поколения.

Теперь у вас должна быть функция второго поколения, которая после развертывания будет вести себя идентично новой установке вашего расширения. Последний шаг — проверить и исправить любые проблемы, случайно возникшие в процессе.

Убедитесь, что вы используете firebase-tools >= 15.24.0, и разверните преобразованную функцию второго поколения в тестовый проект с соответствующими ресурсами для проверки её поведения. Если у вас уже есть тестовый проект, созданный после тестирования вашего расширения, используйте команду:

firebase deploy --only functions

После ввода этой команды заполните появившийся мастер, запрашивающий значения параметров, точно так же, как вы заполняли бы форму установки в консоли Firebase для расширения.

Пример работы: передача данных из Firestore в BigQuery

Мы проверяем сквозную синхронизацию Cloud Firestore и BigQuery :

  1. В консоли Cloud Firestore создайте коллекцию, указанную в качестве COLLECTION_PATH (users), если она еще не существует.
  2. Создайте документ с именем bigquery-mirror-test, содержащий любые поля с любыми значениями.
  3. В консоли BigQuery выполните запрос к исходной таблице changelog. Она должна содержать одну строку, регистрирующую создание документа:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Запросите последнюю версию представления , и вы получите последнее событие изменения для единственного присутствующего документа: bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Удалите документ bigquery-mirror-test в Cloud Firestore . Он исчезнет из списка последних изменений, и в таблицу исходного журнала изменений будет добавлено событие DELETE .

Вы можете просмотреть полную историю отдельного документа с помощью:

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

Отличия от результатов тестирования расширения:

  • Триггер развертывается как fsexportbigquery (опционально с префиксом codebase), а не ext-&lt;instanceId&gt;-fsexportbigquery . Найдите это имя на панели мониторинга и в журналах Cloud Functions .
  • Теперь ваш код будет выполняться в Firebase Local Emulator Suite как обычные функции. Вы можете задать значения параметров для использования в эмуляторе с помощью .env.local . Вы также можете протестировать свой код с помощью SDK `firebase-functions-test`, как описано в разделе «Модульное тестирование Cloud Functions
  • Процесс подготовки ресурсов больше не управляется средой выполнения расширений. Если после развертывания отсутствует таблица журнала изменений, повторно запустите задачу настройки вручную: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME . Задача является идемпотентной, поэтому ее повторный запуск согласует набор данных, таблицу и представления.
  • Значения параметров берутся из .env , а не из формы установки, поэтому повторный запуск команды firebase deploy становится неинтерактивным после завершения обработки файла .env .