使用通知控制资源用量

本文档介绍了如何使用预算通知来有选择地控制资源使用情况。

为项目停用结算功能后,所有服务都会停止,且所有资源最终会被删除。如果您需要更精细的回答,则可以有选择地控制资源。例如,您可以停止某些 Compute Engine 资源,但 Cloud Storage 资源保持不变。仅停止部分资源可降低费用,而不会完全停用您的环境。

在以下示例中,项目使用许多 Compute Engine 虚拟机 (VM) 运行研究,并将结果存储在 Cloud Storage 存储分区中。此 Cloud Run 函数使用预算通知作为触发器,在超出预算后会关停所有 Compute Engine 实例,但不会影响存储的结果。

准备工作

在开始之前,您必须完成以下任务:

  1. 启用 Cloud Billing API
  2. 创建预算
  3. 设置程序化预算通知

设置 Cloud Run 函数

  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 instancesClient = new InstancesClient();
    const ZONE = 'us-central1-a';
    
    exports.limitUse = 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})`;
      }
    
      const instanceNames = await _listRunningInstances(PROJECT_ID, ZONE);
      if (!instanceNames.length) {
        return 'No running instances were found.';
      }
    
      await _stopInstances(PROJECT_ID, ZONE, instanceNames);
      return `${instanceNames.length} instance(s) stopped successfully.`;
    };
    
    /**
     * @return {Promise} Array of names of running instances
     */
    const _listRunningInstances = async (projectId, zone) => {
      const [instances] = await instancesClient.list({
        project: projectId,
        zone: zone,
      });
      return instances
        .filter(item => item.status === 'RUNNING')
        .map(item => item.name);
    };
    
    /**
     * @param {Array} instanceNames Names of instance to stop
     * @return {Promise} Response from stopping instances
     */
    const _stopInstances = async (projectId, zone, instanceNames) => {
      await Promise.all(
        instanceNames.map(instanceName => {
          return instancesClient
            .stop({
              project: projectId,
              zone: zone,
              instance: instanceName,
            })
            .then(() => {
              console.log(`Instance stopped successfully: ${instanceName}`);
            });
        })
      );
    };

    Python

    import base64
    import json
    import os
    from googleapiclient import discovery
    PROJECT_ID = os.getenv("GCP_PROJECT")
    PROJECT_NAME = f"projects/{PROJECT_ID}"
    ZONE = "us-west1-b"
    
    
    def limit_use(data, context):
        pubsub_data = base64.b64decode(data["data"]).decode("utf-8")
        pubsub_json = json.loads(pubsub_data)
        cost_amount = pubsub_json["costAmount"]
        budget_amount = pubsub_json["budgetAmount"]
        if cost_amount <= budget_amount:
            print(f"No action necessary. (Current cost: {cost_amount})")
            return
    
        compute = discovery.build(
            "compute",
            "v1",
            cache_discovery=False,
        )
        instances = compute.instances()
    
        instance_names = __list_running_instances(PROJECT_ID, ZONE, instances)
        __stop_instances(PROJECT_ID, ZONE, instance_names, instances)
    
    
    def __list_running_instances(project_id, zone, instances):
        """
        @param {string} project_id ID of project that contains instances to stop
        @param {string} zone Zone that contains instances to stop
        @return {Promise} Array of names of running instances
        """
        res = instances.list(project=project_id, zone=zone).execute()
    
        if "items" not in res:
            return []
    
        items = res["items"]
        running_names = [i["name"] for i in items if i["status"] == "RUNNING"]
        return running_names
    
    
    def __stop_instances(project_id, zone, instance_names, instances):
        """
        @param {string} project_id ID of project that contains instances to stop
        @param {string} zone Zone that contains instances to stop
        @param {Array} instance_names Names of instance to stop
        @return {Promise} Response from stopping instances
        """
        if not len(instance_names):
            print("No running instances were found.")
            return
    
        for name in instance_names:
            instances.stop(project=project_id, zone=zone, instance=name).execute()
            print(f"Instance stopped successfully: {name}")
    
    

  4. 入口点设置为要执行的正确函数:

    Node.js

    入口点设置为 limitUse

    Python

    入口点设置为 limit_use

  5. 查看自动设置的环境变量列表,并确定是否需要手动将 GCP_PROJECT 变量设置为运行虚拟机的项目。

  6. 设置 ZONE 参数。此参数是指超出预算时停止实例的地区。

  7. 点击部署

配置服务账号权限

Cloud Run 函数作为自动创建的服务账号运行。为了控制使用量,您需要将服务账号权限授予项目中需要修改的任何服务,方法是完成以下步骤:

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

    转到 IAM 页面

测试实例是否已停止

如需确保函数按预期运行,请按照测试 Cloud Run 函数中的步骤操作。

如果成功, Google Cloud 控制台中的 Compute Engine 虚拟机将停止。

后续步骤

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