使用 Cloud Tasks 触发 Cloud Functions 函数


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

目标

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

费用

Cloud Tasks、Cloud Functions 和 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 Functions 和 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 Function,该函数是 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 Function

  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 Functions 触发器类型。

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

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

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

    1. Cloud 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 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
    

清理

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

删除资源

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

删除 Cloud Functions 函数

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

    转到 Cloud Functions 页面

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

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

删除 Cloud Tasks 队列

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

    转到 Cloud Tasks 队列页面

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

  3. 确认该操作。

删除项目

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

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

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

    转到“管理资源”

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

后续步骤