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

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

应用架构

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

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

地理位置要求

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

  • Compute Engine 实例:在区域和地区中列出的任何区域中皆受支持。
  • Cloud Run functions:在位置中列出的区域中受支持。
  • Pub/Sub 消息:Pub/Sub 是一项全球服务,因此在全球皆受支持。
  • 具有 Pub/Sub 目标的 Cloud Scheduler 作业:在任何Google Cloud 位置皆受支持。

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

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

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

如需了解如何确保 Cloud Run functions 的安全,请参阅 Cloud Run functions 安全概览。如需比较 HTTP 触发器和 Pub/Sub 触发器,请参阅 Cloud Run functions 触发器文档。

设置 Compute Engine 实例

控制台

  1. 在 Google Cloud 控制台中,前往虚拟机实例页面。
    前往“虚拟机实例”页面
  2. 点击创建实例
  3. 名称设置为 dev-instance
  4. 标签下,点击添加标签
  5. 点击添加标签
  6. Key 输入 env,为 Value 输入 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 Run functions 部署由 Pub/Sub 触发的函数

创建和部署函数

Console

创建启动函数。

  1. 前往 Google Cloud 控制台中的 Cloud Run 函数页面。
    前往 Cloud Run 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": "^10.0.0",
        "mocha": "^10.0.0",
        "proxyquire": "^2.0.0",
        "sinon": "^18.0.0"
      },
      "dependencies": {
        "@google-cloud/compute": "^4.0.0"
      }
    }
    
  17. 点击页面底部的部署 (Deploy)。

创建停止函数。

  1. 您应位于 Google Cloud 控制台中的 Cloud Run 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": "^10.0.0",
        "mocha": "^10.0.0",
        "proxyquire": "^2.0.0",
        "sinon": "^18.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

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

Console

停止实例

  1. 前往 Google Cloud 控制台中的 Cloud Run 函数页面。
    前往 Cloud Run 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 Run 函数页面。
    前往 Cloud Run 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

创建作业

Console

创建启动作业。

  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

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

Console

停止实例

  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