Activar funciones de Cloud Run con Cloud Tasks

En este tutorial se muestra cómo usar Cloud Tasks en una aplicación de App Engine para activar una función de Cloud Run y enviar un correo programado.

Información sobre el código

En esta sección se explica el código de la aplicación y cómo funciona.

Crear la tarea

La página de índice se publica mediante controladores en app.yaml. Las variables necesarias para crear tareas se transfieren como variables de entorno.

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

Este código crea el endpoint /send-email. Este endpoint gestiona los envíos de formularios de la página de índice y transfiere esos datos al código de creación de tareas.

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! 💌');
});

Este código crea la tarea y la envía a la cola de Cloud Tasks. El código crea la tarea de la siguiente manera:

  • Especifica el tipo de segmentación como HTTP Request.

  • Especificar el HTTP method que se va a usar y el URL del objetivo.

  • Asigna el valor application/json al encabezado Content-Type para que las aplicaciones posteriores puedan analizar la carga útil estructurada.

  • Añadir un correo de cuenta de servicio para que Cloud Tasks pueda proporcionar credenciales al destino de la solicitud, que requiere autenticación. La cuenta de servicio se crea por separado.

  • Comprobando que la fecha introducida por el usuario no supere el máximo de 30 días y añadiéndola a la solicitud como campo 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;

Crear el correo

Este código crea la función de Cloud Run que es el destino de la solicitud de Cloud Tasks. Usa el cuerpo de la solicitud para crear un correo y enviarlo a través de la API SendGrid.

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);
  }
};

Preparar la aplicación

Configurar SendGrid

  1. Crea una cuenta de SendGrid.

  2. Crea una clave de API de SendGrid:

    1. Inicia sesión en tu cuenta de SendGrid.

    2. En el menú de navegación de la izquierda, abre Configuración y haz clic en Claves de API.

    3. Haz clic en Crear clave de API y selecciona acceso restringido. En el encabezado Envío de correo, selecciona Acceso completo.

    4. Copia la clave de API cuando se muestre (solo se mostrará una vez, así que asegúrate de pegarla en algún sitio para poder usarla más adelante).

Descargar el código fuente

  1. Clona el repositorio de aplicaciones de muestra en la máquina local:

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git
    
  2. Accede al directorio que contiene el código de muestra:

    cd cloud-tasks/
    

Desplegar la función de Cloud Run

  1. Ve al directorio function/:

    cd function/
    
  2. Despliega la función:

    gcloud functions deploy sendEmail --runtime nodejs14 --trigger-http \
      --no-allow-unauthenticated \
      --set-env-vars SENDGRID_API_KEY=SENDGRID_API_KEY \

    Sustituye SENDGRID_API_KEY por tu clave de API.

    Este comando usa marcas:

    • --trigger-http para especificar el tipo de activador de Cloud Run Functions.

    • --no-allow-unauthenticated para especificar la invocación de la función requiere autenticación.

    • --set-env-var para definir sus credenciales de SendGrid.

  3. Define el control de acceso de la función para permitir solo a los usuarios autenticados.

    1. Selecciona la función sendEmail en la interfaz de usuario de Cloud Run Functions.

    2. Si no ves la información de los permisos de sendEmail, haz clic en MOSTRAR PANEL DE INFORMACIÓN en la esquina superior derecha.

    3. Haz clic en el botón Añadir directores situado arriba.

    4. Asigna el valor allAuthenticatedUsers a Nuevos directores.

    5. Define el rol.

      • Funciones de primera generación: asigna el rol Cloud Function Invoker
      • Funciones de segunda generación: asigna el rol Cloud Run Invoker
    6. Haz clic en GUARDAR.

Crear una cola de Cloud Tasks

  1. Crea una cola con el siguiente comando gcloud:

    gcloud tasks queues create my-queue --location=LOCATION

    Sustituye LOCATION por la ubicación que prefieras para la cola (por ejemplo, us-west2). Si no especificas la ubicación, gcloud CLI elegirá la predeterminada.

  2. Comprueba que se haya creado correctamente:

    gcloud tasks queues describe my-queue --location=LOCATION

    Sustituye LOCATION por la ubicación de la cola.

Creando una cuenta de servicio

La solicitud de Cloud Tasks debe proporcionar credenciales en el encabezado Authorization para que la función de Cloud Run autentique la solicitud. Esta cuenta de servicio permite que Cloud Tasks cree y añada un token OIDC para ello.

  1. En la interfaz de usuario de cuentas de servicio, haz clic en + CREAR CUENTA DE SERVICIO.

  2. Añade un nombre de cuenta de servicio(nombre visible) y selecciona Crear.

  3. Define el Rol y haz clic en Continuar.

    • Funciones de primera generación: asigna el rol Cloud Function Invoker
    • Funciones de segunda generación: asigna el rol Cloud Run Invoker
  4. Selecciona Hecho.

Desplegar el endpoint y el creador de tareas en App Engine

  1. Ve al directorio app/:

    cd ../app/
    
  2. Actualiza las variables de app.yaml con tus valores:

    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"

    Para consultar tu posición en la cola, usa el siguiente comando:

    gcloud tasks queues describe my-queue --location=LOCATION

    Sustituye LOCATION por la ubicación de la cola.

    Para encontrar la URL de tu función, usa el siguiente comando:

    gcloud functions describe sendEmail
  3. Despliega la aplicación en el entorno estándar de App Engine con el siguiente comando:

    gcloud app deploy
  4. Abre la aplicación para enviar una postal por correo electrónico:

    gcloud app browse