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

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

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

תהליך ההעברה

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

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

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

עדכון ייבוא

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

חבילת המשנה 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 מהדור השני יש העדפה לייבוא מודולרי, צריך לעדכן את ההגדרות של הטריגרים כדי לשקף את הייבוא ששונה מהשלב הקודם.

הארגומנטים שמועברים לקריאות חוזרות (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) => {
  /* ... */
});

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

פונקציות מהדור השני לא תומכות ב-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 באמצעות Firebase CLI. מומלץ מאוד להשתמש בשיטה הזו, כי היא היעילה והמאובטחת ביותר להעברת ההגדרה.

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

    משתמשים בפקודה 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 כדי לאשר גישה ל-Firebase APIs, אבל פונקציות מהדור השני משתמשות בחשבון השירות שמוגדר כברירת מחדל ב-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 (בדור השני).

שימוש במקביליות

יתרון משמעותי של פונקציות מהדור השני הוא היכולת של מופע פונקציה יחיד לטפל ביותר מבקשה אחת בו-זמנית. הפעולה הזו יכולה לצמצם באופן משמעותי את מספר ההפעלות במצב התחלתי (cold start) שמשתמשי הקצה חווים. כברירת מחדל, מספר הבקשות המקבילות מוגדר ל-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.

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

  1. משנים את שם הפונקציה בקוד הפונקציות. לדוגמה, משנים את השם של resizeImage ל-resizeImageSecondGen.
  2. פורסים את הפונקציה כך שגם הפונקציה המקורית מדור ראשון וגם הפונקציה מדור שני יפעלו.
    1. במקרה של טריגרים מסוג callable,‏ Task Queue ו-HTTP, צריך להתחיל להפנות את כל הלקוחות לפונקציה מהדור השני על ידי עדכון קוד הלקוח בשם או בכתובת ה-URL של הפונקציה מהדור השני.
    2. עם טריגרים ברקע, פונקציות מהדור הראשון ומהדור השני יגיבו לכל אירוע מיד לאחר הפריסה.
  3. אחרי שכל התעבורה מועברת, מוחקים את הפונקציה מהדור הראשון באמצעות הפקודה firebase functions:delete של firebase CLI.
    1. אופציונלי: משנים את השם של הפונקציה מהדור השני כך שיהיה זהה לשם של הפונקציה מהדור הראשון.