השבתת השימוש בחיוב באמצעות התראות

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

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

מגבלות

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

  • אי אפשר להשבית את החיוב בפרויקט נעול בחשבון לחיוב. למידע נוסף על נעילה ופתיחה של פרויקטים, קראו את המאמר אבטחת הקישור בין פרויקט לחשבון שלו לחיוב.

לפני שמתחילים

לפני שמתחילים צריך לבצע את המשימות האלה:

  1. הפעלת Cloud Billing API
  2. יצירת תקציב שמוגבל לפרויקט אחד
  3. יצירת התראות פרוגרמטיות בקשר לתקציב

הגדרת פונקציית Cloud Run

כדי להשבית את החיוב ב-Cloud בפרויקט, יוצרים פונקציית Cloud Run ומגדירים אותה לקריאה ל-Cloud Billing API.

  1. אתם יכולים להיעזר בהוראות שבמאמר יצירה של פונקציית Cloud Run. חשוב לוודא שהגדרתם את Trigger type כך שישתמש באותו נושא Pub/Sub שהוגדר לשימוש בתקציב.
  2. מוסיפים את יחסי התלות הבאים:

    Node.js

    מעתיקים את הקוד הבא לקובץ package.json:‏

    {
      "name": "cloud-functions-billing",
      "private": "true",
      "version": "0.0.1",
      "description": "Examples of integrating Cloud Functions with billing",
      "main": "index.js",
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "compute-test": "c8 mocha -p -j 2 test/periodic.test.js --timeout=600000",
        "test": "c8 mocha -p -j 2 test/index.test.js --timeout=5000 --exit"
      },
      "author": "Ace Nassri <anassri@google.com>",
      "license": "Apache-2.0",
      "dependencies": {
        "@google-cloud/billing": "^4.0.0",
        "@google-cloud/compute": "^4.0.0",
        "google-auth-library": "^9.0.0",
        "googleapis": "^143.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^10.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^18.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    מעתיקים את הקוד הבא לקובץ requirements.txt:

    slackclient==2.9.4
    google-api-python-client==2.131.0
    

  3. מעתיקים את הקוד הבא לפונקציית Cloud Run:‏

    Node.js

    const {CloudBillingClient} = require('@google-cloud/billing');
    const {InstancesClient} = require('@google-cloud/compute');
    
    const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
    const PROJECT_NAME = `projects/${PROJECT_ID}`;
    const billing = new CloudBillingClient();
    
    exports.stopBilling = async pubsubEvent => {
      const pubsubData = JSON.parse(
        Buffer.from(pubsubEvent.data, 'base64').toString()
      );
      if (pubsubData.costAmount <= pubsubData.budgetAmount) {
        return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
      }
    
      if (!PROJECT_ID) {
        return 'No project specified';
      }
    
      const billingEnabled = await _isBillingEnabled(PROJECT_NAME);
      if (billingEnabled) {
        return _disableBillingForProject(PROJECT_NAME);
      } else {
        return 'Billing already disabled';
      }
    };
    
    /**
     * Determine whether billing is enabled for a project
     * @param {string} projectName Name of project to check if billing is enabled
     * @return {bool} Whether project has billing enabled or not
     */
    const _isBillingEnabled = async projectName => {
      try {
        const [res] = await billing.getProjectBillingInfo({name: projectName});
        return res.billingEnabled;
      } catch (e) {
        console.log(
          'Unable to determine if billing is enabled on specified project, assuming billing is enabled'
        );
        return true;
      }
    };
    
    /**
     * Disable billing for a project by removing its billing account
     * @param {string} projectName Name of project disable billing on
     * @return {string} Text containing response from disabling billing
     */
    const _disableBillingForProject = async projectName => {
      const [res] = await billing.updateProjectBillingInfo({
        name: projectName,
        resource: {billingAccountName: ''}, // Disable billing
      });
      return `Billing disabled: ${JSON.stringify(res)}`;
    };

    Python

    # WARNING: The following action, if not in simulation mode, will disable billing
    # for the project, potentially stopping all services and causing outages.
    # Ensure thorough testing and understanding before enabling live deactivation.
    
    import base64
    import json
    import os
    import urllib.request
    
    from cloudevents.http.event import CloudEvent
    import functions_framework
    
    from google.api_core import exceptions
    from google.cloud import billing_v1
    from google.cloud import logging
    
    billing_client = billing_v1.CloudBillingClient()
    
    
    def get_project_id() -> str:
        """Retrieves the Google Cloud Project ID.
    
        This function first attempts to get the project ID from the
        `GOOGLE_CLOUD_PROJECT` environment variable. If the environment
        variable is not set or is None, it then attempts to retrieve the
        project ID from the Google Cloud metadata server.
    
        Returns:
            str: The Google Cloud Project ID.
    
        Raises:
            ValueError: If the project ID cannot be determined either from
                        the environment variable or the metadata server.
        """
    
        # Read the environment variable, usually set manually
        project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
        if project_id is not None:
            return project_id
    
        # Otherwise, get the `project-id`` from the Metadata server
        url = "http://metadata.google.internal/computeMetadata/v1/project/project-id"
        req = urllib.request.Request(url)
        req.add_header("Metadata-Flavor", "Google")
        project_id = urllib.request.urlopen(req).read().decode()
    
        if project_id is None:
            raise ValueError("project-id metadata not found.")
    
        return project_id
    
    
    @functions_framework.cloud_event
    def stop_billing(cloud_event: CloudEvent) -> None:
        # TODO(developer): As stoping billing is a destructive action
        # for your project, change the following constant to False
        # after you validate with a test budget.
        SIMULATE_DEACTIVATION = True
    
        PROJECT_ID = get_project_id()
        PROJECT_NAME = f"projects/{PROJECT_ID}"
    
        event_data = base64.b64decode(
            cloud_event.data["message"]["data"]
        ).decode("utf-8")
    
        event_dict = json.loads(event_data)
        cost_amount = event_dict["costAmount"]
        budget_amount = event_dict["budgetAmount"]
        print(f"Cost: {cost_amount} Budget: {budget_amount}")
    
        if cost_amount <= budget_amount:
            print("No action required. Current cost is within budget.")
            return
    
        print(f"Disabling billing for project '{PROJECT_NAME}'...")
    
        is_billing_enabled = _is_billing_enabled(PROJECT_NAME)
    
        if is_billing_enabled:
            _disable_billing_for_project(
                PROJECT_NAME,
                SIMULATE_DEACTIVATION
            )
        else:
            print("Billing is already disabled.")
    
    
    def _is_billing_enabled(project_name: str) -> bool:
        """Determine whether billing is enabled for a project.
    
        Args:
            project_name: Name of project to check if billing is enabled.
    
        Returns:
            Whether project has billing enabled or not.
        """
        try:
            print(f"Getting billing info for project '{project_name}'...")
            response = billing_client.get_project_billing_info(name=project_name)
    
            return response.billing_enabled
        except Exception as e:
            print(f'Error getting billing info: {e}')
            print(
                "Unable to determine if billing is enabled on specified project, "
                "assuming billing is enabled."
            )
    
            return True
    
    
    def _disable_billing_for_project(
        project_name: str,
        simulate_deactivation: bool,
    ) -> None:
        """Disable billing for a project by removing its billing account.
    
        Args:
            project_name: Name of project to disable billing.
            simulate_deactivation:
                If True, it won't actually disable billing.
                Useful to validate with test budgets.
        """
    
        # Log this operation in Cloud Logging
        logging_client = logging.Client()
        logger = logging_client.logger(name="disable-billing")
    
        if simulate_deactivation:
            entry_text = "Billing disabled. (Simulated)"
            print(entry_text)
            logger.log_text(entry_text, severity="CRITICAL")
            return
    
        # Find more information about `updateBillingInfo` API method here:
        # https://cloud.google.com/billing/docs/reference/rest/v1/projects/updateBillingInfo
        try:
            # To disable billing set the `billing_account_name` field to empty
            project_billing_info = billing_v1.ProjectBillingInfo(
                billing_account_name=""
            )
    
            response = billing_client.update_project_billing_info(
                name=project_name,
                project_billing_info=project_billing_info
            )
    
            entry_text = f"Billing disabled: {response}"
            print(entry_text)
            logger.log_text(entry_text, severity="CRITICAL")
        except exceptions.PermissionDenied:
            print("Failed to disable billing, check permissions.")

  4. מגדירים את Entry point לפונקציה הנכונה להרצה:

    Node.js

    מגדירים את Entry point ל-stopBilling.

    Python

    מגדירים את Entry point ל-stop_billing.

  5. בדקו את רשימת משתני הסביבה שמוגדרים אוטומטית כדי להחליט אם אתם צריכים להגדיר ידנית את המשתנה GOOGLE_CLOUD_PROJECT כך שיכיל את הפרויקט שבו אתם רוצים להשבית את החיוב ב-Cloud.

  6. לוחצים על DEPLOY.

הגדרת ההרשאות לחשבון השירות

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

  1. כדי לזהות את חשבון השירות הנכון, אתם יכולים לבדוק את הפרטים של פונקציית Cloud Run. חשבון השירות מופיע בתחתית הדף.
  2. נכנסים לדף IAM במסוף Google Cloud כדי להגדיר את ההרשאות המתאימות.

    כניסה לדף IAM

  3. כדי לשנות את ההרשאות בחשבון לחיוב, נכנסים לדף Account management בחלק של החיוב במסוף Google Cloud . אחר כך מוסיפים את חשבון השירות בתור חשבון ראשי בחשבון לחיוב ב-Cloud ומגדירים את ההרשאות המתאימות לחשבון לחיוב.

    לדף Account management בחיוב ב-Cloud

למידע נוסף על הגדרת הרשאות לחשבונות לחיוב ב-Cloud

בדיקה אם החיוב ב-Cloud מושבת

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

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

כדי להמשיך להשתמש במשאבי Google Cloud בפרויקט, צריך להפעיל מחדש באופן ידני את החיוב ב-Cloud בשביל הפרויקט במסוףGoogle Cloud .

המאמרים הבאים

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