使用 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. 在「Welcome to App Engine」(歡迎使用 App Engine) 頁面,按一下「Create Application」(建立應用程式)

    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)