停用带通知的结算使用情况

本文档介绍了如何在费用达到或超过项目预算时自动停用项目的结算功能。为项目停用结算功能后,您将终止该项目中的所有 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

    入口点设置为 stopBilling

    Python

    入口点设置为 stop_billing

  5. 查看自动设置的环境变量的列表,以确定是否需要手动将 GOOGLE_CLOUD_PROJECT 变量设置为您要停用 Cloud Billing 的项目。

  6. 点击部署

配置服务账号权限

Cloud Run 函数作为自动创建的服务账号运行。如需停用结算功能,您需要通过完成以下步骤,向该服务账号授予项目中需要修改的任何服务的相应权限:

  1. 通过查看 Cloud Run 函数的详细信息,确定正确的服务账号。服务账号显示在页面底部。
  2. 前往 Google Cloud 控制台中的 IAM 页面,设置相应权限。

    转到 IAM 页面

  3. 如需修改结算账号权限,请在 Google Cloud 控制台中前往“结算”账号管理页面,将服务账号添加为 Cloud Billing 账号上的正文,然后设置适当的结算账号权限

    前往 Cloud Billing 中的“账号管理”页面

详细了解如何配置 Cloud Billing 账号的权限

测试 Cloud Billing 是否已停用

当预算发出通知时,指定的项目将不再拥有关联的 Cloud Billing 账号。如需确保您的函数按预期运行,请按照测试 Cloud Run 函数中的步骤操作。

如果成功,Cloud Billing 账号下将不再显示此项目,且此项目中的资源(包括同一项目中的 Cloud Run 函数)已停用。

如需继续使用项目中的 Google Cloud 资源,请在Google Cloud 控制台中为项目手动重新启用 Cloud Billing

后续步骤

查看其他程序化通知示例,了解如何执行以下操作: