使用 Cloud Tasks 触发 Cloud Run 函数

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

了解代码

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

创建任务

索引页面使用 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 functions 触发器类型。

    • --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 functions 函数对请求进行身份验证。此服务账号允许 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