Firebase Extensions를 Cloud Functions로 이전

이 가이드에서는 지원 중단된 Firebase Extensions 환경에서 사용자가 자체 Firebase(2세대)용 Cloud Functions 코드베이스에 설치하고 배포하는 함수로 확장 프로그램을 마이그레이션하는 방법을 보여줍니다.

이것이 권장되는 이전 경로입니다. Firebase는 공식 npm에 상응하는 확장 프로그램 목록을 유지합니다. 이 가이드에서는 확장 프로그램을 만드는 방법을 안내합니다.

이 가이드에서는 Cloud Firestore에서 BigQuery로 스트리밍 확장 프로그램(firestore-bigquery-export)을 실행 예시로 사용합니다. 각 섹션은 이전 전의 확장 프로그램과 이전 후의 확장 프로그램이 @firebase-function-kits/firestore-bigquery-export 패키지로 어떻게 표시되는지 보여주는 작업 예시로 끝납니다.

가입하여 확장 프로그램 이전에 관한 자세한 정보와 도움을 받으세요.

Firebase Extensions에서 이전하는 방법에 관해 궁금한 점이 있으면 firebase-extensions-migrator-support-external@google.com으로 문의해 주세요. 2세대 함수 패키징, 테스트, 배포에 관한 자세한 정보로 가이드를 업데이트할 때 이 그룹에 이메일도 전송됩니다.

이 그룹에 가입하려면 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(...)를 사용하여 설치 시간 및 업데이트 시간 설정을 선언합니다. 이는 extension.yaml에서 선언하는 lifecycleEvents를 대체합니다.

Firebase Extensions 소스를 2세대 함수로 마이그레이션

(선택사항) Firebase 에이전트 기술을 사용한 자동 이전

공식 extension-to-functions-codebase AI 에이전트 기술을 사용하여 1~8단계(리소스 인벤토리, 트리거 업그레이드, 매개변수 및 보안 비밀 변환, 선언적 IAM, 수명 주기 후크, 패키지 README 생성)를 자동화할 수 있습니다.

스킬 설치하기

개발자 또는 AI 코딩 어시스턴트(Firebase의 Gemini, Cursor, Claude Code, GitHub Copilot)가 아직 스킬을 설치하지 않은 경우 스킬 CLI를 사용하여 다음 명령어를 실행합니다.

npx skills add firebase/agent-skills --skill extension-to-functions-codebase

스킬이 프로젝트에 설치되면 AI 코딩 어시스턴트가 마이그레이션 규칙과 변환 단계를 자동으로 따릅니다. 다음 프롬프트를 사용할 수 있습니다.

"extension-to-functions-codebase 기술의 안내에 따라 이 Firebase Extension을 게시 가능한 2세대 함수 키트 패키지로 이전하세요."

1. 확장 프로그램 인벤토리 작성

먼저 확장 프로그램의 인벤토리를 작성합니다. 확장 프로그램이 선언, 제공, 문서화하는 모든 항목의 전체 목록을 작성하여 모든 동작이 2세대 함수에서 정의된 대상을 갖고 마이그레이션 중에 손실되는 항목이 없도록 합니다.

다음 각 항목을 검토하고 발견한 내용을 기록합니다.

  • extension.yaml: 매개변수, 함수, 이벤트, IAM 역할, 필수 API, 보안 비밀, 수명 주기 후크를 선언합니다.

  • functions/: 함수 코드, 종속 항목, 빌드 구성, 트리거, 태스크 큐 함수가 포함됩니다.

  • 설정 단계, 경고, 결제 메모가 포함된 README.md, PREINSTALL.md, POSTINSTALL.md

  • scripts/: 가져오기, 백필, IAM, 복구 또는 마이그레이션 유틸리티와 확장 프로그램과 함께 제공하는 기타 도구가 포함됩니다.

그런 다음 extension.yaml의 각 항목에 대해 npm 패키지에서 어디로 이동할지 결정합니다.

  • 사용자 구성을 Cloud Functions 매개변수로 변환합니다(4단계).

  • 보안 비밀을 Cloud Functions 보안 비밀로 변환(4단계)

  • IAM 역할을 requiresRole(...) 선언으로 변환합니다(6단계).

  • 필요한 Google API를 적절한 경우 requiresAPI(...) 선언으로 변환합니다(6단계).

  • 설치 및 업데이트 후크를 afterFirstDeploy(...) 및 afterRedeploy(...) 선언으로 변환합니다(7단계).

  • 인스턴스 ID를 EXT_INSTANCE_ID에서 FIREBASE_KIT_INSTANCE_ID로 변환합니다(4단계).

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

firestore-bigquery-export/extension.yaml 및 functions/를 읽으면 다음 인벤토리가 생성됩니다.

extension.yaml 개수/값 전송 위치
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions 매개변수(4단계)
apis bigquery.googleapis.com requiresAPI(...)(6단계)
roles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...)(6단계)
resources 이벤트 트리거 1개(fsexportbigquery) + 태스크 큐 함수(initBigQuerySync, setupBigQuerySync) 내보낸 패키지 함수(3단계)
lifecycleEvents onInstall → initBigQuerySync, onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy(7단계)
인스턴스 ID 사용되지 않음(EXT_INSTANCE_ID 읽기 없음) 이전할 항목 없음
scripts/ import/(백필), gen-schema-view/ 스크립트로 유지(여기서는 범위 외)

분석. 확장 프로그램은 type: secret 매개변수를 선언하지 않으므로 4단계에서 보안 비밀을 마이그레이션할 필요가 없습니다. 이벤트 트리거는 이미 2세대입니다. 태스크 큐 함수만 아직 1세대입니다(3단계에서 관련됨).

2. package.json 업데이트

확장 프로그램의 package.json 파일을 업데이트합니다. 확장 프로그램 하나를 이전하는 경우 루트 package.json일 수 있습니다. 하나의 저장소에서 여러 확장 프로그램을 이전하는 경우 각 확장 프로그램에 자체 패키지를 지정합니다.

최소 SDK 버전: firebase-functions >= 7.4.0 및 firebase-admin >= 14.2.0를 종속 항목으로 선언합니다. firebase-functions 버전을 피어 종속 항목으로 선언하여 사용자의 Cloud Functions 프로젝트에 라이브러리가 작성된 것과 동일한 버전의 SDK가 포함되도록 합니다.

{
  "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.4.0"
  },
  "dependencies": {
    "firebase-functions": "^7.4.0",
    "firebase-admin": "^14.2.0"
  }
}

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

이전 확장 프로그램의 functions/package.json는 비공개이며 확장 프로그램 ID의 이름을 지정하고 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"
  }
}

After. 게시 가능한 패키지: 범위가 지정된 이름, exports 맵, firebase-functions가 peerDependencies로 이동됨

{
  "name": "@firebase-function-kits/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.4.0"
  },
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^7.4.0"
  }
}

3. 함수를 1세대에서 2세대로 업그레이드

확장 프로그램에서 여전히 1세대 함수를 내보내는 경우 각 트리거를 이에 상응하는 2세대 함수로 변환합니다. firebase-functions/... 모듈에서 가져오고 트리거 옵션에서 런타임 설정을 전달합니다.

Cloud Functions 2세대 업그레이드 가이드를 참고하세요. 특히 2세대 패치 이벤트 구조 분해를 사용하여 재작성 작업을 최소화하고 함수 로직을 재작성하지 않아도 됩니다. 2세대 SDK는 v1 매개변수를 이벤트 객체의 필드로 노출하므로 구조 분해/이름이 지정된 매개변수를 사용하고 비즈니스 로직을 변경하지 않고 유지할 수 있습니다.

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

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

1세대 함수와 2세대 함수의 차이점에 대한 전체 목록은 Cloud Functions 버전 비교를 참조하세요.

1세대 함수와 2세대 함수의 중요한 차이점은 2세대 함수가 트리거 리소스와 같은 위치에 있어야 한다는 것입니다. 사용자가 1세대 함수를 사용하는 확장 프로그램에서 2세대 함수를 사용하는 키트로 이전할 때 이 요구사항을 충족하기 위해 함수 위치를 변경해야 할 수 있습니다.

기존 위치가 보존되므로 사용자가 선택한 Cloud Functions 위치가 이전 중에 손상될 수 있습니다. 확장 프로그램의 이벤트 트리거 함수가 사용자 제공 함수 위치를 사용하는 경우 확장 프로그램에 새 매개변수를 추가하여 이벤트 트리거 위치를 수집하고 이를 새 함수 위치에 매핑하는 것이 좋습니다.

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

defineString("DATABASE_REGION", {
  label: "Firestore Instance Location",
  description:
    "Where is the Firestore database located? You can check your current database location at https://console.cloud.google.com/firestore/databases. The functions in this kit deploy to the Cloud Run region closest to this location.",
  input: select({
    "Multi-region (Europe - Belgium and Netherlands)": "eur3",
    "Multi-region (United States)": "nam5",
    "Multi-region (Iowa, North Virginia, and Oklahoma)": "nam7",
    "Iowa (us-central1)": "us-central1",
    // More locations...
  })
});

// Firestore multi-region locations are not Cloud Run regions; deploying a
// function to one hard-fails, so they map to a region inside the multi-region.
const MULTI_REGION_TO_FUNCTION_REGION: Record<string, string> = {
  nam5: "us-central1",
  nam7: "us-central1",
  eur3: "europe-west1",
};

/**
 * Maps a Firestore database location to the Cloud Run region the functions
 * should deploy to. The lookup is case-insensitive and ignores surrounding
 * whitespace, as the CLI's own region handling is. Regional locations pass
 * through lowercased; an unset or blank location returns `undefined`, meaning
 * the functions declare no region.
 */
export function firestoreLocationToFunctionRegion(
  location: string | undefined
): string | undefined {
  const normalized = location?.trim().toLowerCase();
  if (!normalized) {
    return undefined;
  }
  return MULTI_REGION_TO_FUNCTION_REGION[normalized] ?? normalized;
}

const functionRegion = firestoreLocationToFunctionRegion(
  process.env.DATABASE_REGION
);

export const fsexportbigquery = onDocumentWritten(
  {
    region: functionRegion,
    // Other configuration
  },
  (event) => handleDocumentWrite(event, getHandlerContext())
);

키트를 처음 배포할 때 사용자에게 DATABASE_REGION를 입력하라는 메시지가 표시되고 함수는 위의 해당 functionRegion에 배포되어 2세대 이벤트 트리거 함수의 위치 문제가 해결됩니다.

4. 확장 프로그램 매개변수 및 보안 비밀 변환

Params

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 CLI는 매개변수를 검색하고 .env, .env.<projectId>에서 값을 읽거나 배포 중에 사용자에게 메시지를 표시합니다. 기존 설치의 값이 이월되도록 동일한 매개변수 이름을 유지합니다.

코드에 선언된 매개변수 이름을 전혀 변경하지 않는 것이 중요합니다. 확장 프로그램 이전은 기존 최종 사용자 파라미터 값을 자동으로 보존하지만 이름이 변경되지 않은 경우에만 보존합니다.

실제 예시: Cloud 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,

After. defineString 1개: 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, analogous to "required: true" in extension.yaml
  input: { text: { nonEmpty: true } }
}),

매개변수 이름은 변경되지 않으므로 기존 .env는 계속 작동합니다.

인스턴스 ID

확장 프로그램은 확장 프로그램 런타임에서 삽입한 EXT_INSTANCE_ID에서 인스턴스 ID를 읽습니다. 함수 키트는 FIREBASE_KIT_INSTANCE_ID에서 인스턴스 ID를 읽습니다. Firebase CLI는 각 키트 인스턴스에 대해 이 ID를 firebase.json의 instances 맵에 있는 인스턴스 키로 설정합니다. CLI는 배포 시 검색 중에 에뮬레이터에서 배포된 함수에 이를 제공합니다.

인스턴스 ID는 매개변수가 아니므로 defineString로 선언하지 않습니다. 실제로 FIREBASE_...는 .env 파일의 예약된 프리픽스이므로 사용자가 설정하거나 재정의할 수 없습니다. CLI가 삽입하는 값은 매개변수 시스템에 표시되지 않습니다. 환경에서 직접 읽습니다.

// Before
const instanceId = process.env.EXT_INSTANCE_ID;

// After
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

변수는 패키지가 키트로 배포될 때만 설정됩니다. 코드가 독립형 코드베이스로도 배포되는 경우(9단계 참고) 선택사항으로 처리하거나 누락된 경우 명확한 메시지와 함께 빠른 실패를 수행합니다. 확장 프로그램이 인스턴스 ID를 사용자 대상 매개변수로 노출한 경우 이제 Firebase CLI가 값을 소유하므로 해당 매개변수를 삭제해야 합니다.

실제 예시: 사용자 데이터 삭제

(Stream Cloud Firestore to BigQuery 확장 프로그램은 인스턴스 ID를 읽지 않으므로 마이그레이션할 항목이 없습니다. 사용자 데이터 삭제 확장 프로그램은 이 ID를 사용하여 Pub/Sub 주제의 이름을 지정합니다.)

이전 확장 프로그램이 리소스에 사용한 ext- 프리픽스를 사용하여 config.ts에서 원시 환경 변수로 읽습니다.

// functions/src/config.ts
discoveryTopic: `ext-${process.env.EXT_INSTANCE_ID}-discovery`,
deletionTopic: `ext-${process.env.EXT_INSTANCE_ID}-deletion`,

After. 사용자가 주제 이름을 재정의할 수 있도록 두 개의 일반 매개변수에 사용되는 FIREBASE_KIT_INSTANCE_ID의 일반 process.env 읽기:

// src/config.ts
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

// Non-empty defaults so Pub/Sub trigger bindings resolve during deploy
// discovery without freezing an empty topic name into the manifest.
discoveryTopicName: defineString("DISCOVERY_TOPIC_NAME", {
  default: `kit-${instanceId}-discovery`,
}),
deletionTopicName: defineString("DELETION_TOPIC_NAME", {
  default: `kit-${instanceId}-deletion`,
}),

트리거 바인딩은 검색 시점에 확인되므로 기본값은 비어 있지 않아야 합니다. 빈 기본값이 주제 이름으로 배포 매니페스트에 작성됩니다. 또한 이 키트는 키트 컨텍스트 외부에서 실행되지 않도록 방어합니다. 변수가 누락된 경우 모듈 수준 기본값은 kit-undefined-discovery로 평가되므로 구성 로더는 다음과 같은 설명 오류와 함께 실패합니다.

// ...
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
if (!instanceId) {
  throw new Error(
    "FIREBASE_KIT_INSTANCE_ID is not set. It is provided automatically to " +
      "kit instances by firebase-tools >= 15.32.0; deploy or emulate this " +
      "kit with a supported CLI version."
  );
}
// ...

이 검사는 핸들러가 구성을 처음 확인할 때 실행되므로 누락된 변수가 있으면 함수가 kit-undefined-* 주제에 자동으로 바인딩되는 대신 명확한 런타임 오류가 발생합니다. 배포 자체를 거부하려면 검색 중에 실행되도록 모듈 범위에서 검사를 수행합니다. CLI는 firebase.json에서 인스턴스 ID를 파생하므로 구성 가능한 INSTANCE_ID가 없으며 여러 인스턴스에서 동기화할 필요가 없습니다.

보안 비밀

extension.yaml에서 type: 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,

After. defineString 1개 및 defineSecret 1개. 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();
    // ...
  }
);

5. 내부 태스크 큐 호출 마이그레이션

일부 확장 프로그램은 Firebase Admin SDK를 사용하여 함수 코드 내에서 자체 태스크 큐에 작업을 큐에 추가합니다. 이는 전달된 태스크를 수신하는 것과는 다릅니다(함수 업그레이드 및 수명 주기 후크 변환 섹션에서 설명). 여기서 코드는 queue.enqueue(...)를 호출하는 프로듀서입니다.

이전 버전의 Admin SDK에서는 동일한 확장 프로그램의 태스크 큐 함수를 타겟팅하기 위해 확장 프로그램이 자체 확장 프로그램 인스턴스 ID를 두 번째 매개변수로 전달해야 했습니다. firebase-admin 14.2.0부터는 이 작업이 필요하지 않으며 권장되지도 않습니다. 이제 Task Queue API는 기본적으로 동일한 컨텍스트(예: 확장 프로그램 또는 키트)의 태스크 큐를 타겟팅합니다. 코드에서 이 매개변수를 확장 프로그램 및 독립형 함수로 삭제하는 것이 안전하며 권장됩니다. 이 매개변수를 삭제하면 이식성과 향후 호환성이 보장됩니다.

큐에 추가 호출에 대한 다른 모든 항목(locations/<region>/functions/<name> 리소스 경로, 태스크 페이로드, 재시도 로직)은 동일하게 유지됩니다.

Cloud Tasks로 큐에 함수를 추가하는 방법에 대한 자세한 내용은 Cloud Tasks로 큐에 함수 추가를 참조하세요.

이전 1세대 확장 프로그램:

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

After. 2세대 확장 프로그램:

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

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

큐에 추가 호출이 프리픽스가 지정된 코드베이스를 타겟팅하는 경우 검색된 함수 이름에도 프리픽스가 지정됩니다(예: orders-syncBigQuery). 대체 함수 키트 인스턴스 검토 및 설치 및 함수 키트로 테스트를 참고하세요.

6. 필수 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가 더 좁은 모델을 지원하지 않는 한 코드베이스의 모든 함수가 해당 역할로 실행된다는 점을 사용자에게 문서화합니다.

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

이전 extension.yaml에 선언됨. 확장 프로그램 런타임에서 API를 사용 설정하고 관리 계정에 역할을 부여함:

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

After. requiresAPI 및 requiresRole를 사용하여 코드에서 선언됨:

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

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

확장 프로그램이 Eventarc 이벤트를 게시하는 경우 이벤트 게시에 필요한 적절한 역할과 API를 또한 설정해야 합니다. 이전에는 extensions.yaml를 변경하지 않고도 확장 프로그램에서 이를 처리했습니다. 커스텀 Eventarc 채널이 사용되는 경우에만 키트에서 이러한 권한을 요청하도록 코드에서 조건부로 이 작업을 수행할 수 있습니다.

if (!!process.env.EVENTARC_CHANNEL) {
  requiresRole("roles/eventarc.publisher");
  requiresAPI(
    "eventarcpublishing.googleapis.com",
    "Publishes the extension's custom events to its Eventarc channel."
  );
}

7. 수명 주기 후크 변환

확장 프로그램이 getExtensions().runtime()(예: setProcessingState 또는 setFatalError)를 호출하는 경우 정상적으로 배포된 2세대 함수에서 호출되면 오류가 발생하므로 해당 호출을 삭제합니다. 수명 주기 상태는 이제 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

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

이전 extension.yaml의 lifecycleEvents(확장 프로그램 런타임에 의해 구동됨):

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

After. 코드에 선언됨. 태스크는 첫 번째 배포 시 BigQuery를 프로비저닝합니다.

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

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

프로비저닝은 멱등성이므로 다시 실행하면 데이터 세트, 테이블, 뷰가 조정됩니다. 사용자는 firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME를 사용하여 수동으로 다시 실행할 수 있습니다.

8. 사용자를 위한 문서 설정

최소한 다음을 설명하는 패키지 README를 작성합니다.

  • 패키지에 필요한 .env 값입니다.
  • 패키지에 필요한 보안 비밀 및 기존 보안 비밀 값을 마이그레이션하는 방법
  • 패키지가 requiresRole(...)로 선언하는 IAM 역할입니다.
  • 패키지에서 사용 설정하거나 필요로 하는 Google API입니다.
  • 패키지가 선언하는 수명 주기 후크 및 수동으로 다시 실행하는 방법
  • 결제 메모
  • 기존 확장 프로그램과 비교하여 변경된 사항입니다.
  • 패키지가 인스턴스 ID(CLI에서 설정한 FIREBASE_KIT_INSTANCE_ID)를 가져오는 방법과 모든 인스턴스 함수가 kit-<instanceId>- 프리픽스로 배포되는 방법

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

README 패키지는 구체적인 '변경사항' 표를 제공합니다.

문제 확장 프로그램으로 @firebase-function-kits/firestore-bigquery-export 형태
구성 확장 프로그램 매개변수 .env를 통한 Cloud Functions 매개변수
IAM 확장 프로그램에서 부여함 requiresRole(...), 배포 시 적용됨
프로비저닝 확장 프로그램별 수명 주기 작업 작업 afterFirstDeploy/afterRedeploy개
함수 이름 ext-<instanceId>-fsexportbigquery fsexportbigquery(선택적으로 프리픽스 추가)
인스턴스 ID 확장 프로그램에 의해 삽입된 EXT_INSTANCE_ID FIREBASE_KIT_INSTANCE_ID: firebase.json에서 CLI에 의해 설정됨

9. 2세대 함수 테스트

이제 배포 시 확장 프로그램의 새 설치와 동일하게 작동하는 2세대 함수가 생성됩니다. 다음 단계는 그 과정에서 실수로 발생한 문제를 확인하고 해결하는 것입니다.

setGlobalOptions를 호출하여 기본 리전 또는 CPU와 같은 전역 옵션을 설정하려면 키트를 독립형 2세대 함수로 배포할 때만 이 작업을 수행해야 합니다. 키트가 npm 패키지로 설치되면 사용자는 래핑 코드에서 setGlobalOptions를 호출하여 이러한 매개변수를 구성하며, 이 작업이 두 번 발생하면 경고가 표시됩니다. FIREBASE_KIT_INSTANCE_ID 환경 변수를 확인하여 이 호출을 보호할 수 있습니다.

import { setGlobalOptions } from "firebase-functions";

if (!process.env.FIREBASE_KIT_INSTANCE_ID) {
  setGlobalOptions({
    region: "us-east1",
    maxInstances: 10,
  });
}

firebase-tools >= 15.32.0를 사용하고 있는지 확인하고 변환된 2세대 함수를 적절한 리소스가 있는 테스트 프로젝트에 배포하여 동작을 테스트합니다. 확장 프로그램을 테스트하여 이미 테스트 프로젝트를 설정한 경우 다음 명령어를 실행합니다.

firebase deploy --only functions

확장 프로그램의 Firebase Console에서 설치 양식을 작성한 것과 동일한 방식으로 매개변수 값을 입력하라는 메시지가 표시되는 마법사를 작성합니다.

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

Cloud Firestore에서 BigQuery로의 동기화를 엔드 투 엔드로 확인합니다.

  1. Firebase Console의 Cloud Firestore 페이지에서 COLLECTION_PATH(users)로 설정한 컬렉션이 아직 없으면 만듭니다.
  2. 값이 있는 필드가 포함된 bigquery-mirror-test이라는 문서를 만듭니다.
  3. Google Cloud 콘솔의 BigQuery 페이지에서 원시 변경 로그 테이블을 쿼리합니다. 문서 생성을 로깅하는 단일 행이 포함되어야 합니다.

    SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
    
  4. 최신 뷰를 쿼리합니다. 그러면 존재하는 유일한 문서(bigquery-mirror-test)의 최신 변경 이벤트가 반환됩니다.

    SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
    
  5. Cloud Firestore에서 bigquery-mirror-test 문서를 삭제합니다. 최신 뷰에서 사라지고 DELETE 이벤트가 원시 변경 로그 테이블에 추가됩니다.

    다음을 사용하여 단일 문서의 전체 기록을 검사할 수 있습니다.

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

확장 프로그램 테스트와의 차이점:

  • 트리거는 ext-<instanceId>-fsexportbigquery가 아닌 fsexportbigquery(키트가 아닌 일반적인 2세대 함수로 배포할 때는 프리픽스 없음)로 배포됩니다. Cloud Functions 대시보드 및 로그에서 해당 이름을 찾습니다.
  • 이제 코드가 Firebase Local Emulator Suite에서 일반 함수로 실행됩니다. .env.local를 사용하여 에뮬레이터에서 사용할 파라미터 값을 설정할 수 있습니다. Cloud Functions의 단위 테스트에 설명된 대로 firebase-functions-test SDK를 사용하여 코드를 단위 테스트할 수도 있습니다.
  • 프로비저닝은 더 이상 확장 프로그램 런타임에 의해 구동되지 않습니다. 배포 후 변경 로그 테이블이 누락된 경우 설정 태스크를 수동으로 다시 실행합니다(firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME). 태스크는 멱등성이므로 다시 실행하면 데이터 세트, 테이블, 뷰가 조정됩니다.
  • 매개변수 값은 설치 양식이 아닌 .env에서 가져오므로 .env가 완료되면 firebase deploy의 재실행은 비대화형입니다.

10. npm에 함수 키트 게시

확장 프로그램에서 2세대 함수로의 변환을 검증한 후에는 다음 가이드 중 하나를 사용하여 엔드 투 엔드 테스트를 위해 출시 후보를 npm에 게시할 수 있습니다.

키트가 npm에 게시되면 firebase functions:kits:install로 설치하고 확장 프로그램의 공식 대체 항목으로 나열할 수 있습니다.

출시 후보를 먼저 게시하는 것이 좋습니다. 키트는 패키지 이름과 버전으로 설치되므로 출시 전 버전을 사용하면 기본 latest 태그로 설치하는 사용자에게 미완성 패키지를 노출하지 않고도 레지스트리에 대해 실제 설치 흐름을 테스트할 수 있습니다.

게시하기 전에 다음 사항을 확인하세요.

  1. 패키지 이름을 선택합니다. 범위가 지정된 이름과 범위가 지정되지 않은 이름이 모두 작동합니다(위의 가이드 참고). 범위가 지정된 패키지는 기본적으로 비공개이므로 --access public를 전달합니다.
  2. 빌드하고 출시되는 항목을 확인합니다. main 및 types는 컴파일된 출력(이 예시에서는 lib/)을 가리키므로 해당 디렉터리가 게시된 tar 파일에 포함되어야 합니다. .npmignore 또는 files 허용 목록을 사용하고 npm pack --dry-run로 결과를 검사합니다. 빌드를 실행하는 prepublishOnly 스크립트는 오래된 출력이 게시되지 않도록 합니다.

기본적으로 안전한 게시를 지원하는 package.json 추가 기능:

{
  "files": ["lib", "README.md", "CHANGELOG.md"],
  "publishConfig": { "access": "public", "tag": "next" },
  "scripts": {
    "build": "tsc -b",
    "prepublishOnly": "npm run build && npm test"
  }
}

"publishConfig": { "tag": "next" } 필드는 일반 npm publish가 latest를 덮어쓰지 않도록 합니다.

출시 후보 분리:

예를 들어 로컬에서 버전을 0.0.2-rc.3에서 0.0.2-rc.4로 증가시키려면(package.json가 저장소 루트에 있는 경우 Git에서 커밋 및 태그 지정) 다음을 실행합니다.

npm version prerelease --preid rc

출시 후보를 게시하고 next 태그 아래의 npm에 @your-org/your-kit@0.0.2-rc.4를 등록하려면 다음 안내를 따르세요.

npm publish

npm 웹사이트에 새 버전이 표시되는 데 몇 분 정도 걸릴 수 있습니다. npm view는 레지스트리를 직접 읽습니다.

npm view @your-org/your-kit versions dist-tags

이 가이드의 11단계와 12단계를 완료하면 패키지를 안정화 버전으로 승격할 수 있습니다.

npm version 0.0.2
npm publish --tag latest
npm dist-tag add @your-org/your-kit@0.0.2 next

npm-shrinkwrap.json 참고: 패키지에 npm-shrinkwrap.json 파일을 포함하는 것이 좋습니다. 포함하지 않으면 CLI가 설치 시 사용자에게 경고합니다. 이렇게 하면 사용자가 테스트한 정확한 종속 항목을 사용하고 공급망 공격으로부터 보호할 수 있습니다. 그러나 shrinkwrap은 Cloud Functions 빌드(npm ci) 중을 포함하여 사용자 프로젝트에 그대로 적용되며, 여기서 개발 전용 항목은 EBADPLATFORM로 인해 실패할 수 있습니다. 게시된 shrinkwrap 사본에서 "dev": true 항목과 devDependencies를 삭제해야 할 수 있습니다.

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

네 번째 출시 후보 시점의 키트 package.json:

{
  "name": "@firebase-function-kits/firestore-bigquery-export",
  "version": "0.0.2-rc.4",
  "repository": {
    "type": "git",
    "url": "https://github.com/firebase/extensions.git",
    "directory": "kits/firestore-bigquery-export"
  },
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "engines": { "node": "22" },
  "scripts": { "build": "tsc -b" }
}

이 경우 키트는 모노 저장소에 있으므로 npm 레지스트리 링크가 올바른 폴더를 가리키도록 repository.directory를 포함해야 합니다. CHANGELOG.md에는 대기 중인 출시의 메모가 포함됩니다.

11. 함수 키트로 테스트

키트를 게시한 후에는 npm을 사용하여 키트를 테스트하는 것이 좋습니다.

firebase-tools 버전 >= 15.32.0를 사용 중인지 확인하고 키트를 설치합니다.

firebase functions:kits:install --package <your-package-name>@<your-prerelease-version>

이렇게 하면 npm에서 패키지를 다운로드하고 키트의 새 소스 디렉터리 내에 설정하고 확장 프로그램 설치 흐름과 유사한 첫 번째 인스턴스를 구성하는 과정을 안내합니다. 패키지를 로컬에 설치하고 설정한 후 배포를 실행하여 Google Cloud 프로젝트에 리소스를 만듭니다.

firebase deploy --only functions:<your-kit-instance-id>

설치 후 Firebase CLI는 설치 중에 선택한 정확한 인스턴스 ID와 함께 유사한 배포 명령어를 출력합니다.

9단계. 2세대 함수를 테스트합니다. 이제 키트를 사용하여 배포하므로 함수 이름은 kit-<instance-id>-<method-name>로 시작됩니다. 이렇게 하면 키트에 여러 인스턴스가 있을 수 있으며, 프로젝트에서 동일한 함수를 여러 번 배포할 수 있고, 각 인스턴스에는 고유한 이름이 지정됩니다.

12. 이전 교체 테스트

작동하는 확장 프로그램 인스턴스를 설정한 후 사용자 이전 가이드(firebase ext:migrate --package 또는 함수 키트 CLI 명령어 사용)를 사용하여 함수 키트를 이전 대체로 테스트할 수 있습니다.

13. 사용자 및 Google에 공식 확장 프로그램 교체 알림

함수 키트 대체가 준비되고 사용자가 마이그레이션해야 하는 npm 패키지로 제공되면 사용자 및 Google에 이 공식 대체에 대해 알립니다. 확장 프로그램을 호스팅하는 GitHub 저장소의 README.md 파일을 다음 정보로 업데이트합니다.

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="<your-extesion-id>" package="<your-npm-package-name>" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `<your-extension>` is deprecated. Migrate to the [<your-npm-package-name>](<link-to-your-npm-package>) package.

Google은 알려진 확장 프로그램 README에서 <!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->와 같은 주석을 검색하고 이를 사용하여 firebase-tools 저장소에 저장된 공식 대체 레지스트리를 replacements.json로 채웁니다. replacements.json를 확인하여 확장 프로그램에 대해 검사할 README.md를 확인할 수도 있습니다. 공식 교체 목록은 매주 업데이트됩니다.

실제 예시: Cloud Firestore에서 BigQuery로 스트리밍

firestore-bigquery-export 확장 프로그램 README.md에는 다음이 포함됩니다.

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `firebase/firestore-bigquery-export` is deprecated. Please migrate to the [`@firebase-function-kits/firestore-bigquery-export`](https://www.npmjs.com/package/@firebase-function-kits/firestore-bigquery-export) package.