שדרוג פונקציות של Node.js מדור ראשון לדור שני

אם אתם משתמשים באפליקציות עם פונקציות מהדור הראשון, כדאי לכם לשקול לעבור לדור השני לפי ההוראות במדריך הזה. פונקציות מהדור השני משתמשות ב-Cloud Run כדי לספק ביצועים טובים יותר, הגדרה טובה יותר, מעקב טוב יותר ועוד.

הדוגמאות במסמך הזה מבוססות על שימוש ב-JavaScript עם מודולים של CommonJS (ייבוא בסגנון require), אבל אותם עקרונות חלים על JavaScript עם ESM (ייבוא בסגנון import … from) ו-TypeScript.

תהליך ההעברה

פונקציות מהדור הראשון ומהדור השני יכולות להתקיים זו לצד זו באותו קובץ מקור. כך תוכלו להעביר את בסיס הקוד שלכם חלק אחר חלק, כשתהיו מוכנים. עם זאת, שימו לב ששילוב החבילות הזה לא פועל בתוך פונקציה נפרדת אחת.

מומלץ להעביר פונקציה אחת בכל פעם, ולבצע בדיקה ואימות לפני שממשיכים.

אימות הגרסאות של Firebase CLI ו-firebase-functions

חשוב לוודא שאתם משתמשים בגרסה 12.00 לפחות של ה-CLI של Firebase ובגרסה 4.3.0 לפחות של firebase-functions. כל גרסה חדשה יותר תתמוך גם בדור השני וגם בדור הראשון.

עדכון הייבוא

פונקציות מהדור השני מיובאות מחבילת המשנה v2 ב-SDK של firebase-functions. נתיב הייבוא השונה הזה הוא כל מה שנדרש ל-CLI של Firebase כדי לקבוע אם לפרוס את קוד הפונקציה כפונקציה מהדור הראשון או מהדור השני.

חבילת המשנה v2 היא מודולרית, ומומלץ לייבא רק את המודול הספציפי שאתם צריכים.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

אחרי: דור שני

// explicitly import each trigger
const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");

עדכון הגדרות הטריגרים

מכיוון שערכת ה-SDK מהדור השני מעדיפה ייבוא מודולרי, צריך לעדכן את הגדרות הטריגרים כדי לשקף את הייבוא שהשתנה מהשלב הקודם.

הארגומנטים שמועברים לקריאות חוזרות (callbacks) של טריגרים מסוימים השתנו. בדוגמה הזו, שימו לב שהארגומנטים של הקריאה החוזרת (callback) של onDocumentCreated אוחדו לאובייקט event אחד. בנוסף, לחלק מהטריגרים יש תכונות הגדרה חדשות ונוחות, כמו האפשרות cors של הטריגר onRequest.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.date = functions.https.onRequest((req, res) => {
  // ...
});

exports.uppercase = functions.firestore
  .document("my-collection/{docId}")
  .onCreate((change, context) => {
    // ...
  });

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");

exports.date = onRequest({cors: true}, (req, res) => {
  // ...
});

exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  /* ... */
});

איך לצמצם את מאמצי הכתיבה מחדש באמצעות פירוק מבנה של JavaScript

אם לפונקציות יש גופים מורכבים שמסתמכים במידה רבה על הקשר של דור ראשון או על פרמטרים ספציפיים לספק (כמו message או snapshot), אתם יכולים להשתמש בעזרי התאימות של דור ראשון שמובנים ב-SDK של דור שני.

ערכת ה-SDK מהדור השני מתקנת באופן אוטומטי את אובייקט האירוע באמצעות פונקציות getter שתואמות לחתימות מהדור הראשון. כך אפשר להשתמש בפירוק מבנים (destructuring) ב-JavaScript כדי לחלץ את המאפיינים האלה ישירות בחתימה של ה-handler, וכך לצמצם את הצורך לשכתב את הלוגיקה של הפונקציה.

הפניה למיפוי ספקים

ספק ארגומנטים בדור ראשון פירוק אירועים מתוקנים מדור שני
Pub/Sub (message, context) ({ message, context }) => { ... }
Cloud Firestore (snapshot, context) ({ snapshot, context }) => { ... }
Cloud Storage (object, context) ({ object, context }) => { ... }
Realtime Database (snapshot, context) ({ snapshot, context }) => { ... }
Remote Config (version, context) ({ version, context }) => { ... }
Cloud Scheduler (context) ({ context }) => { ... }
תור המשימות (data, context) ({ data, context }) => { ... }

לפני (דור ראשון):

export const myPubSubV1 = functions.pubsub.topic("my-topic").onPublish((message, context) => {
  const data = message.json;
  const eventId = context.eventId;
  // ... rest of the logic
});

חלופה חדשה (דור שני עם פירוק מבנה):

import { onMessagePublished } from "firebase-functions/v2/pubsub";

export const myPubSubV2 = onMessagePublished("my-topic", ({ message, context }) => {
  // No need to change the function body!
  const data = message.json;      // Uses v1 Message wrapper
  const eventId = context.eventId; // Uses v1 EventContext map
  // ... rest of the logic
});

שימוש בהגדרות עם פרמטרים

בפונקציות מהדור השני אין תמיכה ב-functions.config, ובמקום זאת יש ממשק מאובטח יותר להגדרה דקלרטיבית של פרמטרים של הגדרות אישיות בתוך בסיס הקוד. במודול החדש params, ה-CLI חוסם את הפריסה אלא אם לכל הפרמטרים יש ערך תקין, כדי לוודא שפונקציה לא נפרסת עם הגדרות חסרות.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.getQuote = functions.https.onRequest(async (req, res) => {
  const quote = await fetchMotivationalQuote(functions.config().apiKey);
  // ...
});

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {defineSecret} = require("firebase-functions/params");

// Define the secret parameter
const apiKey = defineSecret("API_KEY");

exports.getQuote = onRequest(
  // make the secret available to this function
  { secrets: [apiKey] },
  async (req, res) => {
    // retrieve the value of the secret
    const quote = await fetchMotivationalQuote(apiKey.value());
    // ...
  }
);

אם יש לכם הגדרת סביבה קיימת עם functions.config, העבירו את ההגדרה הזו כחלק מהשדרוג לדור השני.

ה-API של functions.config הוצא משימוש ויושבת במרץ 2027. אחרי התאריך הזה, פריסות עם functions.config ייכשלו.

כדי למנוע כשלים בפריסה, צריך להעביר את ההגדרות האישיות ל-Cloud Secret Manager באמצעות ה-CLI של Firebase. מומלץ מאוד להשתמש בשיטה הזו, כי היא הדרך היעילה והבטוחה ביותר להעביר את ההגדרות האישיות.

  1. הגדרות אישיות של ייצוא באמצעות ה-CLI של Firebase

    משתמשים בפקודה config export כדי לייצא את הגדרת הסביבה הקיימת לסוד חדש ב-Cloud Secret Manager:

    $ firebase functions:config:export
    i  This command retrieves your Runtime Config values (accessed via functions.config())
       and exports them as a Secret Manager secret.
    
    i  Fetching your existing functions.config() from your project...  ✔
       Fetched your existing functions.config().
    
    i  Configuration to be exported:
    ⚠  This may contain sensitive data. Do not share this output.
    
    {
       ...
    }
    
    ✔ What would you like to name the new secret for your configuration? RUNTIME_CONFIG
    
    ✔  Created new secret version projects/project/secrets/RUNTIME_CONFIG/versions/1```
    
  2. עדכון קוד הפונקציה כדי לקשר סודות

    כדי להשתמש בהגדרות האישיות ששמורות בסוד החדש ב-Cloud Secret Manager, צריך להשתמש ב-API של defineJsonSecret בקוד המקור של הפונקציה. בנוסף, חשוב לוודא שהסודות מקושרים לכל הפונקציות שצריכות אותם.

    לפני

    const functions = require("firebase-functions/v1");
    
    exports.myFunction = functions.https.onRequest((req, res) => {
      const apiKey = functions.config().someapi.key;
      // ...
    });
    

    אחרי

    const { onRequest } = require("firebase-functions/v2/https");
    const { defineJsonSecret } = require("firebase-functions/params");
    
    const config = defineJsonSecret("RUNTIME_CONFIG");
    
    exports.myFunction = onRequest(
      // Bind secret to your function
      { secrets: [config] },
      (req, res) => {
        // Access secret values via .value()
        const apiKey = config.value().someapi.key;
        // ...
    });
    
  3. פריסת פונקציות

    פורסים את הפונקציות המעודכנות כדי להחיל את השינויים ולקשר את ההרשאות לסוד.

    firebase deploy --only functions:<your-function-name>
    

הגדרת אפשרויות של סביבת זמן ריצה

ההגדרה של אפשרויות זמן הריצה השתנתה בין הדור הראשון לדור השני. בדור השני נוספה גם יכולת חדשה להגדיר אפשרויות לכל הפונקציות.

לפני: דור ראשון

const functions = require("firebase-functions/v1");

exports.date = functions
  .runWith({
    // Keep 5 instances warm for this latency-critical function
    minInstances: 5,
  })
  // locate function closest to users
  .region("asia-northeast1")
  .https.onRequest((req, res) => {
    // ...
  });

exports.uppercase = functions
  // locate function closest to users and database
  .region("asia-northeast1")
  .firestore.document("my-collection/{docId}")
  .onCreate((change, context) => {
    // ...
  });

אחרי: דור שני

const {onRequest} = require("firebase-functions/v2/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");
const {setGlobalOptions} = require("firebase-functions/v2");

// locate all functions closest to users
setGlobalOptions({ region: "asia-northeast1" });

exports.date = onRequest({
    // Keep 5 instances warm for this latency-critical function
    minInstances: 5,
  }, (req, res) => {
  // ...
});

exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  /* ... */
});

עדכון של חשבון השירות שמוגדר כברירת מחדל (אופציונלי)

בפונקציות מהדור הראשון, חשבון השירות שמוגדר כברירת מחדל הוא Google App Engine, והוא משמש כדי לאשר גישה לממשקי ה-API של Firebase. בפונקציות מהדור השני, חשבון השירות שמוגדר כברירת מחדל הוא Compute Engine. ההבדל הזה יכול לגרום לבעיות בהרשאות של פונקציות שהועברו לדור השני, במקרים שבהם נתתם הרשאות מיוחדות לחשבון השירות של הדור הראשון. אם לא שיניתם את ההרשאות של חשבון השירות, אתם יכולים לדלג על השלב הזה.

הפתרון המומלץ הוא להקצות באופן מפורש את App Engine, חשבון השירות שמשמש כברירת מחדל בדור הראשון, לפונקציות שרוצים להעביר לדור השני, וכך לעקוף את ברירת המחדל של הדור השני. כדי לעשות את זה, צריך לוודא שבכל פונקציה שהועברה מוגדר הערך הנכון של serviceAccountEmail:

const {onRequest} = require("firebase-functions/https");
const {onDocumentCreated} = require("firebase-functions/v2/firestore");
const {setGlobalOptions} = require("firebase-functions");

// Use the App Engine default service account for all functions
setGlobalOptions({serviceAccountEmail: '<my-project-number>@<wbr>appspot.gserviceaccount.com'});

// Now I use the App Engine default service account.
exports.date = onRequest({cors: true}, (req, res) => {
  // ...
});

// I do too!
exports.uppercase = onDocumentCreated("my-collection/{docId}", (event) => {
  // ...
});

לחלופין, אתם יכולים לשנות את הפרטים של חשבון השירות כך שיתאימו לכל ההרשאות הנדרשות גם בחשבון השירות שמשמש כברירת מחדל ב-App Engine (לדור הראשון) וגם בחשבון השירות שמשמש כברירת מחדל ב-Compute Engine (לדור השני).

שימוש בו-זמני

אחד היתרונות המשמעותיים של פונקציות מהדור השני הוא היכולת של מופע פונקציה יחיד לטפל ביותר מבקשה אחת בו-זמנית. כך אפשר לצמצם באופן משמעותי את מספר ההפעלות במצב התחלתי שמשתמשי הקצה חווים. כברירת מחדל, מספר הפעולות בו-זמנית מוגדר ל-80, אבל אפשר להגדיר אותו לכל ערך מ-1 עד 1,000:

const {onRequest} = require("firebase-functions/v2/https");

exports.date = onRequest({
    // set concurrency value
    concurrency: 500
  },
  (req, res) => {
    // ...
});

כוונון של בו-זמניות יכול לשפר את הביצועים ולהפחית את עלות הפונקציות. מידע נוסף על בו-זמניות זמין במאמר אישור בקשות בו-זמניות.

בדיקת השימוש במשתנים גלובליים

פונקציות מהדור הראשון שנכתבו בלי להתחשב בו-זמניות עשויות להשתמש במשתנים גלובליים שמוגדרים ונקראים בכל בקשה. כשמפעילים בו-זמניות ומכונה אחת מתחילה לטפל בכמה בקשות בבת אחת, יכולים להיווצר באגים בפונקציה כי בקשות מקבילות מתחילות להגדיר ולקרוא משתנים גלובליים בו-זמנית.

בזמן השדרוג, אתם יכולים להגדיר את המעבד של הפונקציה ל-gcf_gen1 ולהגדיר את concurrency ל-1 כדי לשחזר את ההתנהגות של הדור הראשון:

const {onRequest} = require("firebase-functions/v2/https");

exports.date = onRequest({
    // TEMPORARY FIX: remove concurrency
    cpu: "gcf_gen1",
    concurrency: 1
  },
  (req, res) => {
    // ...
});

עם זאת, לא מומלץ להשתמש בפתרון הזה לטווח הארוך, כי הוא מבטל את יתרונות הביצועים של פונקציות מהדור השני. במקום זאת, בדקו את השימוש במשתנים גלובליים בפונקציות שלכם, והסירו את ההגדרות הזמניות האלה כשתהיו מוכנים.

העברת התנועה לפונקציות החדשות מהדור השני

כמו בשינוי האזור או סוג הטריגר של הפונקציה, תצטרכו לתת לפונקציה מהדור השני שם חדש ולהעביר אליה את התנועה בהדרגה.

אי אפשר לשדרג פונקציה מהדור הראשון לדור השני עם אותו שם ולהריץ את firebase deploy. אם תעשו את זה, תופיע השגיאה הבאה:

Upgrading from GCFv1 to GCFv2 is not yet supported. Please delete your old function or wait for this feature to be ready.

אסטרטגיית ההעברה תלויה בסוג הטריגר שבו הפונקציה משתמשת.

העברת טריגרים מסוג Callable,‏ תור משימות ו-HTTP

הטריגרים האלה הם הפעלות ישירות. מכיוון שלפונקציה מהדור השני יהיה שם חדש (וכתובת URL חדשה לטריגרים של HTTP), אפשר להעביר את התנועה על ידי עדכון הלקוחות.

  1. משנים את שם הפונקציה בקוד (לדוגמה, משנים את myCallable ל-myCallableV2).
  2. פורסים את הפונקציה. עכשיו פועלות גם הפונקציות מהדור הראשון וגם הפונקציות מהדור השני.
  3. מעדכנים את קוד הלקוח או את הגורם שמבצע את הקריאה כך שיפנו לשם או לכתובת ה-URL של הפונקציה החדשה מהדור השני.
  4. אחרי שכל התנועה עברה לפונקציה החדשה, מוחקים את הפונקציה מהדור הראשון באמצעות הפקודה firebase functions:delete של ה-CLI של Firebase.

העברת טריגרים ברקע

טריגרים ברקע (כמו טריגרים של Pub/Sub,‏ Cloud Firestore ו-Cloud Storage) מגיבים לאירועים בפרויקט. כדי לא לפספס אירועים במהלך המעבר, צריך להפעיל באופן זמני את הפונקציות מהדור הראשון ומהדור השני זו לצד זו.

במהלך תקופת המעבר, שתי הפונקציות יופעלו באותו אירוע. זה אומר שהלוגיקה העסקית תפעל פעמיים בכל אירוע. לפני שממשיכים, חשוב לוודא שהפונקציה אידמפוטנטית.

  1. מוסיפים את הפונקציה מהדור השני לצד הפונקציה מהדור הראשון, כך שהפונקציה הקיימת מהדור הראשון נשארת בקוד, ומוסיפים את הפונקציה מהדור השני שמאזינה לאותו מקור אירועים.

    import * as functions from "firebase-functions/v1";
    import { onMessagePublished } from "firebase-functions/v2/pubsub";
    
    // --- Existing 1st gen function ---
    export const myPubSub = functions.pubsub.topic("my-topic").onPublish((message, context) => {
      console.log("V1 handler running for event:", context.eventId);
      // ... existing v1 function logic ...
    });
    
    // --- New v2 passthrough function ---
    export const myPubSubV2 = onMessagePublished("my-topic", async ({ message, context }) => {
      console.log("v2 handler triggering V1 for event:", context.eventId);
      // Call the v1 function's handler
      await myPubSub.run(message, context);
    });
    
  2. מריצים את firebase deploy. שתי הפונקציות פעילות עכשיו ומאזינות לאותם אירועים.

  3. מוודאים שהפונקציה מהדור השני מקבלת תנועה. עוקבים אחרי היומנים של שתי הפונקציות. מוודאים שהפונקציה מהדור השני מופעלת בכל האירועים ושהקריאות מצליחות.

  4. אחרי שמוודאים שהפונקציה פועלת כמו שצריך, מעבירים את הלוגיקה העסקית בפועל מהפונקציה מהדור הראשון לגוף הפונקציה מהדור השני. אם השתמשתם בשיטת ההעברה, הסירו את הקריאה ל-myPubSub.run().

    import * as functions from "firebase-functions/v1";
    import { onMessagePublished } from "firebase-functions/v2/pubsub";
    
    // --- Existing v1 function (to be removed next) ---
    export const myPubSub = functions.pubsub.topic("my-topic").onPublish((message, context) => {
      console.log("v1 handler running for event:", context.eventId);
      // ... existing v1 function logic ...
    });
    
    // --- New v2 function with full logic ---
    export const myPubSubV2 = onMessagePublished("my-topic", ({ message, context }) => {
      console.log("v2 handler running for event:", context.eventId);
      // ... existing v1 function logic WAS MOVED HERE ...
    });
    

    פורסים את השינוי.

  5. מסירים את הגדרת הפונקציה מהדור הראשון מהקוד ופורסים מחדש. ב-CLI תופיע בקשה למחוק את הפונקציה מהדור הראשון מ-Google Cloud.