使用 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) => {
  // 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. 將「New principals」(新增主體) 設為 allAuthenticatedUsers

    5. 設定「Role」(角色)

      • 第 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 Tasks 可透過這個服務帳戶建立及新增 OIDC 權杖。

  1. 服務帳戶使用者介面中,按一下「+CREATE SERVICE ACCOUNT」

  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 functions」頁面。

    前往 Cloud Run functions 頁面

  2. 按一下函式旁邊的核取方塊。

  3. 按一下頁面頂端的 [Delete] (刪除) 按鈕,並確認您要刪除工作。

刪除 Cloud Tasks 佇列

  1. 在主控台中開啟 Cloud Tasks 佇列頁面。

    前往 Cloud Tasks 佇列頁面

  2. 選取要刪除的佇列名稱,然後按一下 [刪除佇列]

  3. 確認上述動作。

刪除專案

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。

刪除專案的方法如下:

  1. 前往 Google Cloud 控制台的「Manage resources」(管理資源) 頁面。

    前往「Manage resources」(管理資源)

  2. 在專案清單中選取要刪除的專案,然後點選「Delete」(刪除)
  3. 在對話方塊中輸入專案 ID,然後按一下 [Shut down] (關閉) 以刪除專案。

後續步驟