使用 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 函式觸發條件類型。

    • --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. 在「服務帳戶」使用者介面中,按一下「+建立服務帳戶」

  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