安排 Compute Engine 虚拟机启动或停止


本教程介绍了如何使用 Cloud Scheduler 和 Cloud Functions 通过资源标签定期自动启动和停止 Compute Engine 实例。

目标

  • 使用 Cloud Functions 编写和部署一组启用和停止 Compute Engine 实例的函数。
  • 使用 Cloud Scheduler 创建一组作业,调度带 dev 资源标签的实例在周一至周五的 09:00-17:00 运行,以匹配典型的工作时间。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

  • Cloud Scheduler
  • 云端函数
  • Pub/Sub
  • Compute Engine

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

准备工作

  1. 为 Cloud Scheduler 设置环境。

    设置您的环境

  2. 启用 Cloud Functions, Pub/Sub, and Compute Engine API。

    启用 API

应用架构

此解决方案包含以下 Google Cloud 组件:

显示 Cloud Scheduler 通过 Pub/Sub 调度 Compute Engine 实例的系统架构图

地理位置要求

某些组件仅在特定区域受支持:

  • Compute Engine 实例:在区域和可用区中列出的任何区域中均受支持。
  • Cloud Functions:在位置中列出的区域受支持。
  • Pub/Sub 消息:由于 Pub/Sub 是一项全球服务,因此全球支持。
  • 带有 Pub/Sub 目标的 Cloud Scheduler 作业:在任何 Google Cloud 位置均受支持。

为什么不用 HTTP 取代 Pub/Sub?

您可能希望使用 Cloud Functions HTTP 触发器取代 Pub/Sub 触发器以简化此架构。

本教程使用 Pub/Sub 作为 Cloud Functions 触发器,因为此方法以前比使用 HTTP 更安全。不过,HTTP 也是一个有效的选择,现在可以通过要求进行身份验证加以保护。

如需了解如何保护 Cloud Functions 函数,请参阅 Cloud Functions 安全性概览。如需比较 HTTP 和 Pub/Sub 触发器,请参阅 Cloud Functions 触发器文档。

设置 Compute Engine 实例

控制台

  1. 前往 Google Cloud 控制台中的虚拟机实例页面。
    转到“虚拟机实例”页面
  2. 点击创建实例
  3. 名称设置为 dev-instance
  4. 标签下,点击添加标签
  5. 点击添加标签
  6. 对于,输入 env;对于,输入 dev
  7. 对于区域,选择 us-west1
  8. 对于地区,选择 us-west1-b
  9. 点击保存
  10. 点击该页面底部的创建

gcloud

gcloud compute instances create dev-instance \
    --network default \
    --zone us-west1-b \
    --labels=env=dev

通过 Cloud Functions 部署由 Pub/Sub 触发的函数

创建和部署函数

控制台

创建启动函数。

  1. 转到 Google Cloud 控制台中的 Cloud Functions 页面。
    转到 Cloud Functions 页面
  2. 点击创建函数
  3. 环境部分,选择第 1 代
  4. 函数名称设置为 startInstancePubSub
  5. 保留区域的默认值。
  6. 对于触发器类型,选择 Cloud Pub/Sub
  7. 选择 Cloud Pub/Sub 主题部分,点击创建主题
  8. 此时将显示创建主题对话框。
    1. 主题 ID 下,输入 start-instance-event
    2. 点击创建以完成对话框。
  9. 点击触发器框底部的保存
  10. 点击页面底部的下一步
  11. 对于运行时,选择 Node.js 16 或更高版本。
  12. 对于入口点,请输入 startInstancePubSub
  13. 在代码编辑器的左侧,选择 index.js
  14. 使用以下代码替换入门代码:

    const compute = require('@google-cloud/compute');
    const instancesClient = new compute.InstancesClient();
    const operationsClient = new compute.ZoneOperationsClient();
    
    async function waitForOperation(projectId, operation) {
      while (operation.status !== 'DONE') {
        [operation] = await operationsClient.wait({
          operation: operation.name,
          project: projectId,
          zone: operation.zone.split('/').pop(),
        });
      }
    }
    
    /**
     * Starts Compute Engine instances.
     *
     * Expects a PubSub message with JSON-formatted event data containing the
     * following attributes:
     *  zone - the GCP zone the instances are located in.
     *  label - the label of instances to start.
     *
     * @param {!object} event Cloud Function PubSub message event.
     * @param {!object} callback Cloud Function PubSub callback indicating
     *  completion.
     */
    exports.startInstancePubSub = async (event, context, callback) => {
      try {
        const project = await instancesClient.getProjectId();
        const payload = _validatePayload(event);
        const options = {
          filter: `labels.${payload.label}`,
          project,
          zone: payload.zone,
        };
    
        const [instances] = await instancesClient.list(options);
    
        await Promise.all(
          instances.map(async instance => {
            const [response] = await instancesClient.start({
              project,
              zone: payload.zone,
              instance: instance.name,
            });
    
            return waitForOperation(project, response.latestResponse);
          })
        );
    
        // Operation complete. Instance successfully started.
        const message = 'Successfully started instance(s)';
        console.log(message);
        callback(null, message);
      } catch (err) {
        console.log(err);
        callback(err);
      }
    };
    
    /**
     * Validates that a request payload contains the expected fields.
     *
     * @param {!object} payload the request payload to validate.
     * @return {!object} the payload object.
     */
    const _validatePayload = event => {
      let payload;
      try {
        payload = JSON.parse(Buffer.from(event.data, 'base64').toString());
      } catch (err) {
        throw new Error('Invalid Pub/Sub message: ' + err);
      }
      if (!payload.zone) {
        throw new Error("Attribute 'zone' missing from payload");
      } else if (!payload.label) {
        throw new Error("Attribute 'label' missing from payload");
      }
      return payload;
    };
  15. 在代码编辑器的左侧,选择 package.json

  16. 使用以下代码替换入门代码:

    {
      "name": "cloud-functions-schedule-instance",
      "version": "0.1.0",
      "private": true,
      "license": "Apache-2.0",
      "author": "Google Inc.",
      "repository": {
        "type": "git",
        "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
      },
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "test": "c8 mocha -p -j 2 test/*.test.js --timeout=20000"
      },
      "devDependencies": {
        "c8": "^8.0.0",
        "mocha": "^10.0.0",
        "proxyquire": "^2.0.0",
        "sinon": "^16.0.0"
      },
      "dependencies": {
        "@google-cloud/compute": "^4.0.0"
      }
    }
    
  17. 点击页面底部的部署 (Deploy)。

创建停止函数。

  1. 转到 Google Cloud 控制台中的 Cloud Functions 页面。
  2. 点击创建函数
  3. 环境部分,选择第 1 代
  4. 函数名称设置为 stopInstancePubSub
  5. 保留区域的默认值。
  6. 对于触发器类型,选择 Cloud Pub/Sub
  7. 选择 Cloud Pub/Sub 主题部分,点击创建主题
  8. 此时将显示创建主题对话框。
    1. 主题 ID 下,输入 stop-instance-event
    2. 点击创建以完成对话框。
  9. 点击触发器框底部的保存
  10. 点击页面底部的下一步
  11. 对于运行时,选择 Node.js 16 或更高版本。
  12. 对于入口点,请输入 stopInstancePubSub
  13. 在代码编辑器的左侧,选择 index.js
  14. 使用以下代码替换入门代码:

    const compute = require('@google-cloud/compute');
    const instancesClient = new compute.InstancesClient();
    const operationsClient = new compute.ZoneOperationsClient();
    
    async function waitForOperation(projectId, operation) {
      while (operation.status !== 'DONE') {
        [operation] = await operationsClient.wait({
          operation: operation.name,
          project: projectId,
          zone: operation.zone.split('/').pop(),
        });
      }
    }
    
    /**
     * Stops Compute Engine instances.
     *
     * Expects a PubSub message with JSON-formatted event data containing the
     * following attributes:
     *  zone - the GCP zone the instances are located in.
     *  label - the label of instances to stop.
     *
     * @param {!object} event Cloud Function PubSub message event.
     * @param {!object} callback Cloud Function PubSub callback indicating completion.
     */
    exports.stopInstancePubSub = async (event, context, callback) => {
      try {
        const project = await instancesClient.getProjectId();
        const payload = _validatePayload(event);
        const options = {
          filter: `labels.${payload.label}`,
          project,
          zone: payload.zone,
        };
    
        const [instances] = await instancesClient.list(options);
    
        await Promise.all(
          instances.map(async instance => {
            const [response] = await instancesClient.stop({
              project,
              zone: payload.zone,
              instance: instance.name,
            });
    
            return waitForOperation(project, response.latestResponse);
          })
        );
    
        // Operation complete. Instance successfully stopped.
        const message = 'Successfully stopped instance(s)';
        console.log(message);
        callback(null, message);
      } catch (err) {
        console.log(err);
        callback(err);
      }
    };
    
    /**
     * Validates that a request payload contains the expected fields.
     *
     * @param {!object} payload the request payload to validate.
     * @return {!object} the payload object.
     */
    const _validatePayload = event => {
      let payload;
      try {
        payload = JSON.parse(Buffer.from(event.data, 'base64').toString());
      } catch (err) {
        throw new Error('Invalid Pub/Sub message: ' + err);
      }
      if (!payload.zone) {
        throw new Error("Attribute 'zone' missing from payload");
      } else if (!payload.label) {
        throw new Error("Attribute 'label' missing from payload");
      }
      return payload;
    };
  15. 在代码编辑器的左侧,选择 package.json

  16. 使用以下代码替换入门代码:

    {
      "name": "cloud-functions-schedule-instance",
      "version": "0.1.0",
      "private": true,
      "license": "Apache-2.0",
      "author": "Google Inc.",
      "repository": {
        "type": "git",
        "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
      },
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "test": "c8 mocha -p -j 2 test/*.test.js --timeout=20000"
      },
      "devDependencies": {
        "c8": "^8.0.0",
        "mocha": "^10.0.0",
        "proxyquire": "^2.0.0",
        "sinon": "^16.0.0"
      },
      "dependencies": {
        "@google-cloud/compute": "^4.0.0"
      }
    }
    
  17. 点击页面底部的部署 (Deploy)。

gcloud

创建 Pub/Sub 主题。

gcloud pubsub topics create start-instance-event
gcloud pubsub topics create stop-instance-event

获取代码

  1. 下载代码。

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

  2. 转到正确的目录。

    cd nodejs-docs-samples/functions/scheduleinstance/
    

创建启动和停止函数。

您应该已位于 nodejs-docs-samples/functions/scheduleinstance/ 目录中。

gcloud functions deploy startInstancePubSub \
    --trigger-topic start-instance-event \
    --runtime nodejs18 \
    --allow-unauthenticated
gcloud functions deploy stopInstancePubSub \
    --trigger-topic stop-instance-event \
    --runtime nodejs18 \
    --allow-unauthenticated

(可选)验证函数能否正常运行

控制台

停止实例

  1. 转到 Google Cloud 控制台中的 Cloud Functions 页面。
    转到 Cloud Functions 页面
  2. 点击名为 stopInstancePubSub 的函数。
  3. 您应该会看到许多标签页:常规触发器权限测试。点击测试标签页。
  4. 对于触发事件,输入以下内容:

    {"data":"eyJ6b25lIjoidXMtd2VzdDEtYiIsICJsYWJlbCI6ImVudj1kZXYifQo="}
    

    • 这只是 {"zone":"us-west1-b", "label":"env=dev"} 的 base64 编码的字符串

    • 如果您想要编码生成自己的字符串,则可以随意使用任何在线 base64 编码工具

  5. 点击测试函数按钮。

  6. 当函数完成运行时,您应该会看到输出下面显示了 Successfully stopped instance dev-instance。完成运行可能最多需要 60 秒。

    • 如果您看到的是 error: 'Error: function failed to load.',只需等待 10 秒左右以便函数完成部署,然后重试。

    • 如果您看到的是 error: 'Error: function execution attempt timed out.',直接继续下一步,以确定实例是否只是需要较长时间完成关闭。

    • 如果函数完成了运行,但没有显示任何内容,则可能也只是超时。直接继续下一步,以确定实例是否只是需要较长时间完成关闭。

  7. 前往 Google Cloud 控制台中的虚拟机实例页面。
    转到“虚拟机实例”页面

  8. 验证名为 dev-instance 的实例的名称旁边是否有灰色方块,该方块表示实例已停止。完成关闭可能最多需要 30 秒。

    • 如果似乎未完成关闭,尝试点击页面顶部的刷新

启动实例

  1. 转到 Google Cloud 控制台中的 Cloud Functions 页面。
    转到 Cloud Functions 页面
  2. 点击名为 startInstancePubSub 的函数。
  3. 您应该会看到许多标签页:常规触发器权限测试。点击测试标签页。
  4. 对于触发事件,输入以下内容:

    {"data":"eyJ6b25lIjoidXMtd2VzdDEtYiIsICJsYWJlbCI6ImVudj1kZXYifQo="}
    

    • 同样,这只是 {"zone":"us-west1-b", "label":"env=dev"} 的 base64 编码的字符串
  5. 点击测试函数按钮。

  6. 当函数完成运行时,您应该会看到输出下面显示了 Successfully started instance dev-instance

  7. 前往 Google Cloud 控制台中的虚拟机实例页面。
    转到“虚拟机实例”页面

  8. 验证名为 dev-instance 的实例的名称旁边是否有绿色对勾标记,该标记表示实例正在运行。完成启动可能最多需要 30 秒。

gcloud

停止实例

  1. 调用以下函数来停止实例。

    gcloud functions call stopInstancePubSub \
        --data '{"data":"eyJ6b25lIjoidXMtd2VzdDEtYiIsICJsYWJlbCI6ImVudj1kZXYifQo="}'
    
    • 这只是 {"zone":"us-west1-b", "label":"env=dev"} 的 base64 编码的字符串

    • 如果您想编码生成自己的字符串,可以使用任何工具。 以下示例中使用了 base64 命令行工具:

      echo '{"zone":"us-west1-b", "label":"env=dev"}' | base64
      
      eyJ6b25lIjoidXMtd2VzdDEtYiIsICJsYWJlbCI6ImVudj1kZXYifQo=
      

    函数完成后,您将看到以下内容:

    result: Successfully stopped instance dev-instance
    

    完成运行可能最多需要 60 秒。

    • 如果您看到以下错误:

      error: 'Error: function failed to load.`
      

      只需等待 10 秒左右以便函数完成部署,然后重试。

    • 如果您看到以下错误:

      error: `Error: function execution attempt timed out.`
      

      直接继续下一步,以确定实例是否只是需要较长时间完成关闭。

    • 如果您未看到任何结果,该函数可能只是超时。 直接继续下一步,以确定实例是否只是需要较长时间完成关闭。

  2. 检查实例的状态是否为 TERMINATED。完成关闭可能最多需要 30 秒。

    gcloud compute instances describe dev-instance \
        --zone us-west1-b \
        | grep status
    
    status: TERMINATED
    

启动实例

  1. 调用以下函数来启动实例。

    gcloud functions call startInstancePubSub \
        --data '{"data":"eyJ6b25lIjoidXMtd2VzdDEtYiIsICJsYWJlbCI6ImVudj1kZXYifQo="}'
    
    • 同样,这只是 {"zone":"us-west1-b", "label":"env=dev"} 的 base64 编码的字符串

    函数完成后,您将看到以下内容:

    result: Successfully started instance dev-instance
    
  2. 检查实例的状态是否为 RUNNING。完成启动可能最多需要 30 秒。

    gcloud compute instances describe dev-instance \
        --zone us-west1-b \
        | grep status
    
    status: RUNNING
    

设置 Cloud Scheduler 作业以调用 Pub/Sub

创建作业

控制台

创建启动作业。

  1. 前往 Google Cloud 控制台中的 Cloud Scheduler 页面。
    转到 Cloud Scheduler 页面
  2. 点击创建作业
  3. 保留默认区域。
  4. 名称设置为 startup-dev-instances
  5. 对于频率,请输入 0 9 * * 1-5
    • 此操作会在周一至周五的上午 9 点执行。
  6. 对于时区,请选择相应的国家/地区和时区。此示例将使用 United StatesLos Angeles
  7. 点击继续
  8. 对于目标类型,选择 Pub/Sub
  9. 从主题下拉菜单中选择start-instance-event
  10. 对于消息,请输入以下内容:
    {"zone":"us-west1-b","label":"env=dev"}
    
  11. 点击创建

创建停止作业。

  1. 您应该会转到 Google Cloud 控制台中的 Cloud Scheduler 页面。
  2. 点击创建作业
  3. 保留默认区域,然后点击页面底部的下一步
  4. 名称设置为 shutdown-dev-instances
  5. 对于频率,请输入 0 17 * * 1-5
    • 此操作会在周一至周五的 17:00 执行。
  6. 对于时区,请选择相应的国家/地区和时区。此示例将使用 United StatesLos Angeles
  7. 点击继续
  8. 对于目标类型,选择 Pub/Sub
  9. 从主题下拉菜单中选择“stop-instance-event”。
  10. 对于消息,请输入以下内容:
    {"zone":"us-west1-b","label":"env=dev"}
    
  11. 点击创建

gcloud

创建启动作业。

gcloud scheduler jobs create pubsub startup-dev-instances \
    --schedule '0 9 * * 1-5' \
    --topic start-instance-event \
    --message-body '{"zone":"us-west1-b", "label":"env=dev"}' \
    --time-zone 'America/Los_Angeles' \
    --location us-central1

创建停止作业。

gcloud scheduler jobs create pubsub shutdown-dev-instances \
    --schedule '0 17 * * 1-5' \
    --topic stop-instance-event \
    --message-body '{"zone":"us-west1-b", "label":"env=dev"}' \
    --time-zone 'America/Los_Angeles' \
    --location us-central1

(可选)验证作业是否能正常运行

控制台

停止实例

  1. 前往 Google Cloud 控制台中的 Cloud Scheduler 页面。
    转到 Cloud Scheduler 页面
  2. 对于名为 shutdown-dev-instances 的作业,点击页面最右侧的立即运行按钮。
  3. 前往 Google Cloud 控制台中的虚拟机实例页面。
    转到“虚拟机实例”页面
  4. 验证名为 dev-instance 的实例的名称旁边是否有灰色方块,该方块表示实例已停止。完成关闭可能最多需要 30 秒。

启动实例

  1. 前往 Google Cloud 控制台中的 Cloud Scheduler 页面。
    转到 Cloud Scheduler 页面
  2. 对于名为 startup-dev-instances 的作业,点击页面最右侧的立即运行按钮。
  3. 前往 Google Cloud 控制台中的虚拟机实例页面。
    转到“虚拟机实例”页面
  4. 验证名为 dev-instance 的实例的名称旁边是否有绿色对勾标记,该标记表示实例正在运行。完成启动可能最多需要 30 秒。

gcloud

停止实例

  1. 运行调度程序作业以停止实例。

    gcloud beta scheduler jobs run shutdown-dev-instances
    
  2. 检查实例的状态是否为 TERMINATED。完成关闭可能最多需要 30 秒。

    gcloud compute instances describe dev-instance \
        --zone us-west1-b \
        | grep status
    
    status: TERMINATED
    

启动实例

  1. 运行调度程序作业以启动实例。

    gcloud beta scheduler jobs run startup-dev-instances
    
  2. 检查实例的状态是否为 RUNNING。完成启动可能最多需要 30 秒。

    gcloud compute instances describe dev-instance \
        --zone us-west1-b \
        | grep status
    
    status: RUNNING
    

清理

完成本教程后,您可以清理您创建的资源,让它们停止使用配额,以免产生费用。以下部分介绍如何删除或关闭这些资源。

删除 Cloud Scheduler 作业

  1. 在 Google Cloud 控制台中,转到 Cloud Scheduler 页面。

    转到 Cloud Scheduler 页面

  2. 点击作业旁边的复选框。

  3. 点击页面顶部的删除按钮并确认删除操作。

删除 Pub/Sub 主题

  1. 前往 Google Cloud 控制台中的 Pub/Sub 页面。

    转到 Pub/Sub 页面

  2. 点击主题旁边的复选框。

  3. 在页面顶部,点击删除并确认删除操作。

删除通过 Cloud Functions 部署的函数

  1. 进入 Google Cloud 控制台中的 Cloud Functions 页面。

    转到 Cloud Functions 页面

  2. 点击函数旁边的复选框。

  3. 点击页面顶部的删除按钮并确认删除操作。

删除 Compute Engine 实例

要删除 Compute Engine 实例,请运行以下命令:

  1. 在 Google Cloud 控制台中,转到虚拟机实例页面。

    转到“虚拟机实例”

  2. 选中要删除的实例。
  3. 如需删除实例,请点击更多操作,点击删除,然后按照说明操作。

删除项目

若要避免产生费用,最简单的方法是删除您为本教程创建的项目。

如需删除项目,请执行以下操作:

  1. 在 Google Cloud 控制台中,进入管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

后续步骤