使用 Cloud Tasks 触发 Cloud Run 函数


本教程介绍如何在 App Engine 应用中使用 Cloud Tasks 触发 Cloud Run 函数并定期发送电子邮件。

目标

  • 了解每个组件中的代码。
  • 创建一个 SendGrid 账号。
  • 下载源代码。
  • 部署 Cloud Run 函数,以接收 Cloud Tasks 请求并通过 SendGrid API 发送电子邮件。
  • 创建 Cloud Tasks 队列。
  • 创建服务账号以对您的 Cloud Tasks 请求进行身份验证。
  • 部署允许用户发送电子邮件的客户端代码。

费用

Cloud Tasks、Cloud Run 函数和 App Engine 均提供免费层级,因此只要您在特定产品的免费层级内运行本教程,就不会产生额外费用。要了解详情,请参阅价格

准备工作

  1. 选择或创建 Google Cloud 项目。

    转到 App Engine 页面

  2. 在您的项目中初始化 App Engine 应用:

    1. 欢迎使用 App Engine网页点击创建应用

    2. 为您的应用选择一个区域。此位置将用作 Cloud Tasks 请求的 LOCATION_ID 参数,因此请记下该位置。请注意,App Engine 命令中名为 europe-west 和 us-central 的两个位置在 Cloud Tasks 命令中分别称为 europe-west1 和 us-central1。

    3. 选择 Node.js 作为语言,选择标准作为环境。

    4. 如果系统弹出启用结算窗口,请选择您的结算账号。如果您目前没有结算账号,请点击创建结算账号并按照向导中的说明操作。

    5. 开始页面,点击下一页。您稍后会处理此内容。

  3. 启用 Cloud Run 函数和 Cloud Tasks API。

    启用 API

  4. 安装并初始化 gcloud CLI

了解代码

此部分介绍了应用代码及其工作原理。

创建任务

索引页面使用 app.yaml 中的处理程序提供。创建任务所需的变量将作为环境变量传入。

runtime: nodejs16

env_variables:
  QUEUE_NAME: "my-queue"
  QUEUE_LOCATION: "us-central1"
  FUNCTION_URL: "https://<region>-<project_id>.cloudfunctions.net/sendEmail"
  SERVICE_ACCOUNT_EMAIL: "<member>@<project_id>.iam.gserviceaccount.com"

# Handlers for serving the index page.
handlers:
  - url: /static
    static_dir: static
  - url: /
    static_files: index.html
    upload: index.html

此代码会创建端点 /send-email。此端点会处理从索引页面提交的表单,并将该数据传递给任务创建代码。

app.post('/send-email', (req, res) => {
  // Set the task payload to the form submission.
  const {to_name, from_name, to_email, date} = req.body;
  const payload = {to_name, from_name, to_email};

  createHttpTaskWithToken(
    process.env.GOOGLE_CLOUD_PROJECT,
    QUEUE_NAME,
    QUEUE_LOCATION,
    FUNCTION_URL,
    SERVICE_ACCOUNT_EMAIL,
    payload,
    date
  );

  res.status(202).send('📫 Your postcard is in the mail! 💌');
});

此代码实际上会创建任务并将其发送到 Cloud Tasks 队列。代码通过以下方式构建任务:

  • 目标类型指定为 HTTP Request

  • 指定要使用的 HTTP method 和目标的 URL

  • Content-Type 标头设置为 application/json,以便下游应用可以解析结构化载荷。

  • 添加服务账号电子邮件,以便 Cloud Tasks 可以向请求目标提供凭据,这需要身份验证。此服务账号是单独创建的。

  • 检查以确保用户输入日期不超过 30 天,并将其作为字段 scheduleTime 添加到请求中。

const MAX_SCHEDULE_LIMIT = 30 * 60 * 60 * 24; // Represents 30 days in seconds.

const createHttpTaskWithToken = async function (
  project = 'my-project-id', // Your GCP Project id
  queue = 'my-queue', // Name of your Queue
  location = 'us-central1', // The GCP region of your queue
  url = 'https://example.com/taskhandler', // The full url path that the request will be sent to
  email = '<member>@<project-id>.iam.gserviceaccount.com', // Cloud IAM service account
  payload = 'Hello, World!', // The task HTTP request body
  date = new Date() // Intended date to schedule task
) {
  // Imports the Google Cloud Tasks library.
  const {v2beta3} = require('@google-cloud/tasks');

  // Instantiates a client.
  const client = new v2beta3.CloudTasksClient();

  // Construct the fully qualified queue name.
  const parent = client.queuePath(project, location, queue);

  // Convert message to buffer.
  const convertedPayload = JSON.stringify(payload);
  const body = Buffer.from(convertedPayload).toString('base64');

  const task = {
    httpRequest: {
      httpMethod: 'POST',
      url,
      oidcToken: {
        serviceAccountEmail: email,
        audience: url,
      },
      headers: {
        'Content-Type': 'application/json',
      },
      body,
    },
  };

  const convertedDate = new Date(date);
  const currentDate = new Date();

  // Schedule time can not be in the past.
  if (convertedDate < currentDate) {
    console.error('Scheduled date in the past.');
  } else if (convertedDate > currentDate) {
    const date_diff_in_seconds = (convertedDate - currentDate) / 1000;
    // Restrict schedule time to the 30 day maximum.
    if (date_diff_in_seconds > MAX_SCHEDULE_LIMIT) {
      console.error('Schedule time is over 30 day maximum.');
    }
    // Construct future date in Unix time.
    const date_in_seconds =
      Math.min(date_diff_in_seconds, MAX_SCHEDULE_LIMIT) + Date.now() / 1000;
    // Add schedule time to request in Unix time using Timestamp structure.
    // https://googleapis.dev/nodejs/tasks/latest/google.protobuf.html#.Timestamp
    task.scheduleTime = {
      seconds: date_in_seconds,
    };
  }

  try {
    // Send create task request.
    const [response] = await client.createTask({parent, task});
    console.log(`Created task ${response.name}`);
    return response.name;
  } catch (error) {
    // Construct error for Stackdriver Error Reporting
    console.error(Error(error.message));
  }
};

module.exports = createHttpTaskWithToken;

创建电子邮件

此代码会创建 Cloud Run 函数,将其作为 Cloud Tasks 请求的目标。它使用请求正文来构建电子邮件并通过 SendGrid API 发送它。

const sendgrid = require('@sendgrid/mail');

/**
 * Responds to an HTTP request from Cloud Tasks and sends an email using data
 * from the request body.
 *
 * @param {object} req Cloud Function request context.
 * @param {object} req.body The request payload.
 * @param {string} req.body.to_email Email address of the recipient.
 * @param {string} req.body.to_name Name of the recipient.
 * @param {string} req.body.from_name Name of the sender.
 * @param {object} res Cloud Function response context.
 */
exports.sendEmail = async (req, res) => {
  // Get the SendGrid API key from the environment variable.
  const key = process.env.SENDGRID_API_KEY;
  if (!key) {
    const error = new Error(
      'SENDGRID_API_KEY was not provided as environment variable.'
    );
    error.code = 401;
    throw error;
  }
  sendgrid.setApiKey(key);

  // Get the body from the Cloud Task request.
  const {to_email, to_name, from_name} = req.body;
  if (!to_email) {
    const error = new Error('Email address not provided.');
    error.code = 400;
    throw error;
  } else if (!to_name) {
    const error = new Error('Recipient name not provided.');
    error.code = 400;
    throw error;
  } else if (!from_name) {
    const error = new Error('Sender name not provided.');
    error.code = 400;
    throw error;
  }

  // Construct the email request.
  const msg = {
    to: to_email,
    from: 'postcard@example.com',
    subject: 'A Postcard Just for You!',
    html: postcardHTML(to_name, from_name),
  };

  try {
    await sendgrid.send(msg);
    // Send OK to Cloud Task queue to delete task.
    res.status(200).send('Postcard Sent!');
  } catch (error) {
    // Any status code other than 2xx or 503 will trigger the task to retry.
    res.status(error.code).send(error.message);
  }
};

准备应用

设置 SendGrid

  1. 创建一个 SendGrid 账号。

  2. 创建 SendGrid API 密钥:

    1. 登录您的 SendGrid 账号

    2. 在左侧导航栏中,打开设置,然后点击 API 密钥

    3. 点击创建 API 密钥,然后选择受限访问权限。在邮件发送标头下,选择完整访问权限

    4. 复制所显示的 API 密钥(您只能看到一次,请务必将其粘贴到别处,方便日后使用)。

下载源代码

  1. 将示例应用代码库克隆到本地机器:

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git
    
  2. 转到包含示例代码的目录:

    cd cloud-tasks/
    

部署 Cloud Run 函数

  1. 导航到 function/ 目录:

    cd function/
    
  2. 部署函数的方法如下:

    gcloud functions deploy sendEmail --runtime nodejs14 --trigger-http \
      --no-allow-unauthenticated \
      --set-env-vars SENDGRID_API_KEY=SENDGRID_API_KEY \

    SENDGRID_API_KEY 替换为您的 API 密钥。

    此命令使用以下标志:

    • --trigger-http:用于指定 Cloud Run 函数触发器类型。

    • --no-allow-unauthenticated 以指定函数调用需要进行身份验证。

    • 使用 --set-env-var 以设置您的 SendGrid 凭据

  3. 将该函数的访问权限控制设置为仅允许经过身份验证的用户。

    1. Cloud Run functions 界面中选择 sendEmail 函数。

    2. 如果您没有看到 sendEmail 的权限信息,请点击右上角的显示信息面板

    3. 点击上方的添加主账号按钮。

    4. 新的主账号设置为 allAuthenticatedUsers

    5. 设置角色

      • 第 1 代函数:将角色设置为 Cloud Function Invoker
      • 第 2 代函数:将角色设置为 Cloud Run Invoker
    6. 点击保存

创建 Cloud Tasks 队列

  1. 使用以下 gcloud 命令创建队列

    gcloud tasks queues create my-queue --location=LOCATION

    LOCATION 替换为您的首选值 队列的位置,例如 us-west2。如果您 未指定位置,则 gcloud CLI 会选择默认值。

  2. 验证是否已成功创建队列:

    gcloud tasks queues describe my-queue --location=LOCATION

    LOCATION 替换为队列位置。

创建服务账号

Cloud Tasks 请求必须在 Authorization 标头中提供凭据,以便 Cloud Run 函数对请求进行身份验证。此服务账号允许 Cloud 任务为此创建和添加 OIDC 令牌。

  1. 服务账号界面,点击 +创建服务账号

  2. 添加服务账号名称(易记显示名),然后选择创建

  3. 设置角色,然后点击继续

    • 第 1 代函数:将角色设置为 Cloud Function Invoker
    • 第 2 代函数:将角色设置为 Cloud Run Invoker
  4. 选择完成

将端点和任务创建者部署到 App Engine

  1. 导航到 app/ 目录:

    cd ../app/
    
  2. 使用您的值更新 app.yaml 中的变量:

    env_variables:
      QUEUE_NAME: "my-queue"
      QUEUE_LOCATION: "us-central1"
      FUNCTION_URL: "https://<region>-<project_id>.cloudfunctions.net/sendEmail"
      SERVICE_ACCOUNT_EMAIL: "<member>@<project_id>.iam.gserviceaccount.com"

    如需查找您的队列位置,请使用以下命令:

    gcloud tasks queues describe my-queue --location=LOCATION

    LOCATION 替换为队列位置。

    如需查找函数网址,请使用以下命令:

    gcloud functions describe sendEmail
  3. 使用以下命令将应用部署到 App Engine 标准环境:

    gcloud app deploy
  4. 打开应用,以电子邮件的形式发送明信片:

    gcloud app browse

清除数据

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

删除资源

您可以清理在 Google Cloud 上创建的资源,以免这些资源占用配额,日后产生费用。以下部分介绍如何删除或关闭这些资源。

删除 Cloud Run 函数

  1. 前往 Google Cloud 控制台中的 Cloud Run 函数页面。

    前往 Cloud Run 函数页面

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

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

删除 Cloud Tasks 队列

  1. 在控制台打开 Cloud Tasks 队列页面。

    转到 Cloud Tasks 队列页面

  2. 选择要删除的队列名称,然后点击删除队列

  3. 确认该操作。

删除项目

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

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

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

后续步骤