使用 Cloud Tasks 来触发 Cloud Functions 函数

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

目标

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

费用

Cloud Tasks、Cloud Functions 和 App Engine 具有免费层级,因此只要您在给定产品的免费层级中运行教程,就不会产生额外费用。要了解详情,请参阅价格

准备工作

  1. 选择或创建 GCP 项目。

    转到“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. 安装并初始化 Cloud SDK

了解代码

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

创建任务

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

runtime: nodejs10

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,
      },
      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 Functions 函数,该函数是 Cloud Task 请求的目标。它使用请求正文来构建电子邮件并通过 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. 部署该函数,将 sendgrid_api_key 替换为您的密钥:

    gcloud functions deploy sendEmail --runtime nodejs8 --trigger-http --set-env-vars SENDGRID_API_KEY=sendgrid_api_key
    

    此命令使用以下标志:

    • 使用 --trigger-http 以指定 Cloud Functions 触发器类型。

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

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

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

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

    3. Cloud Functions Invoker 标头下,删除成员 allUsers

    4. 点击上方的添加成员按钮。

    5. 新成员设置为 allAuthenticatedUsers

    6. 角色设置为 Cloud Function Invoker

    7. 点击保存

创建 Cloud Tasks 队列

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

    gcloud tasks queues create my-queue
    
  2. 验证是否已成功创建队列:

    gcloud tasks queues describe my-queue
    

创建服务帐号

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

  1. IAM 和管理员|服务帐号界面,请点击 +创建服务帐号

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

  3. 选择角色 Cloud Functions 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
    

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

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

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

    gcloud app browse
    

清理

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

删除资源

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

删除 Cloud Functions 函数

  1. 转到 Cloud Functions 网页。

    转到 Cloud Functions 页面

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

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

删除 Cloud Task 队列

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

    转到 Cloud Tasks 队列页面

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

  3. 确认该操作。

删除项目

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

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

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”页面

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

后续步骤