停用計費用量 (含通知)

本文說明如何設定在專案費用達到或超過預算時,自動停用專案的計費功能。停用專案的計費功能時,系統會終止專案中的所有 Google Cloud 服務,包括免費層服務。如要更精細地控制預算通知,請參閱「透過通知控制資源用量」。

如果您在 Google Cloud的支出有上限,可能就會因此想要限制費用。在這種情況下,當達到預算上限時,您可能願意關閉所有 Google Cloud 服務 Google Cloud 和使用,以停止產生費用。停用專案的計費功能,是避免該專案產生費用的有效方法。

限制

  • 產生費用和接收預算通知之間會有所延遲,因此在所有服務停止時,先前使用的用量可能尚未送達,而會產生更多費用。按照此範例中的步驟進行,仍無法保證您的費用不會超出預算。如果資金有限,請將預算上限設為低於可用資金,以因應帳單延遲。

  • 如果專案已鎖定帳單帳戶,您就無法停用專案的計費功能。如要進一步瞭解如何鎖定及解鎖專案,請參閱「確保專案與帳單帳戶之間的連結安全無虞」。

事前準備

開始之前,請務必完成下列工作:

  1. 啟用 Cloud Billing API
  2. 建立預算,範圍限定為單一專案
  3. 設定程式輔助預算通知

設定 Cloud Run 函式

如要停用專案的 Cloud Billing,請建立 Cloud Run 函式,並將其設定為呼叫 Cloud Billing API。

  1. 完成「建立 Cloud Run 函式」一文中的步驟。確認「觸發條件類型」設為預算使用的相同 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. 將「進入點」設為要執行的正確函式:

    Node.js

    將「Entry point」(進入點) 設為 stopBilling

    Python

    將「Entry point」(進入點) 設為 stop_billing

  5. 查看自動設定的環境變數清單,判斷是否需要手動將 GOOGLE_CLOUD_PROJECT 變數設為要停用 Cloud Billing 的專案。

  6. 按一下「部署」

設定服務帳戶權限

您的 Cloud Run 函式會以自動建立的服務帳戶執行。如要停用計費功能,請完成下列步驟,將服務帳戶權限授予給專案中需要修改的服務:

  1. 如要識別正確的服務帳戶,請查看 Cloud Run 函式的詳細資料。服務帳戶會列在頁面底部。
  2. 前往 Google Cloud 控制台的「IAM」IAM頁面,設定適當的權限。

    前往「IAM」頁面

  3. 如要修改帳單帳戶權限,請前往 Google Cloud 控制台的「帳單帳戶管理」頁面,將服務帳戶新增為 Cloud 帳單帳戶的主體,並設定適當的帳單帳戶權限

    前往 Cloud Billing 的「帳戶管理」頁面

進一步瞭解如何設定 Cloud Billing 帳戶的權限

測試 Cloud Billing 是否已停用

預算傳送通知後,指定專案就不會再有相關聯的 Cloud Billing 帳戶。如要確保函式正常運作,請按照「測試 Cloud Run 函式」一文中的步驟操作。

如果成功,專案就不會再顯示在 Cloud Billing 帳戶下,且專案中的資源均會停用,包括位於同一專案中的 Cloud Run 函式。

如要繼續使用專案中的 Google Cloud 資源,請在Google Cloud 控制台中,為專案手動重新啟用 Cloud Billing

後續步驟

請參閱其他程式輔助通知範例,瞭解如何執行下列操作: