Primeiros passos com jobs do Cloud Run

1. Introdução

96d07289bb51daa7.png.

Visão geral

Os serviços do Cloud Run são ideais para contêineres que são executados indefinidamente e recebem solicitações HTTP, mas os jobs do Cloud Run podem ser mais adequados para contêineres executados até serem concluídos e que não atendem a solicitações. Por exemplo, o processamento de registros de um banco de dados, o processamento de uma lista de arquivos de um bucket do Cloud Storage ou uma operação de longa duração, como calcular o valor de Pi, funcionam bem se implementados como um job do Cloud Run.

Os jobs não podem atender a solicitações ou fazer detecções em uma porta. Isso significa que, ao contrário dos serviços do Cloud Run, os jobs não devem incluir um servidor da Web. Em vez disso, os contêineres de jobs são encerrados quando terminam a função deles.

Nos jobs do Cloud Run, é possível executar várias cópias do contêiner em paralelo especificando várias tarefas. Cada tarefa representa uma cópia em execução do contêiner. É útil usar várias tarefas se cada uma delas puder processar independentemente um subconjunto dos dados. Por exemplo, o processamento de 10.000 registros do Cloud SQL ou de 10.000 arquivos do Cloud Storage pode ser feito mais rapidamente com 10 tarefas processando 1.000 registros ou arquivos, cada uma em paralelo.

Fluxo de trabalho de jobs

É simples usar jobs do Cloud Run, com apenas duas etapas:

  1. Crie um job. Isso encapsula toda a configuração necessária para executar o job, como a imagem de contêiner, a região e as variáveis de ambiente.
  2. Execute o job. Isso cria uma nova execução do job. Como opção, configure o job para ser executado em uma programação usando o Cloud Scheduler.

Limitações do pré-lançamento

Durante o pré-lançamento, os jobs do Cloud Run têm as seguintes restrições:

  • No máximo 50 execuções (do mesmo job ou de jobs diferentes) podem ser feitas por projeto e por região.
  • É possível ver seus jobs existentes, iniciar execuções e monitorar o status da execução na página "Jobs do Cloud Run" no Console do Cloud. Use gcloud para criar novos jobs, já que o Console do Cloud não é compatível com a criação de novos jobs.
  • Não use jobs do Cloud Run para cargas de trabalho de produção. Não há garantia de confiabilidade ou de desempenho. Os jobs do Cloud Run podem sofrer alterações incompatíveis com versões anteriores, sem aviso prévio antes da disponibilidade geral.

Neste codelab, você primeiro vai explorar um aplicativo Node.js para fazer capturas de tela de páginas da Web e armazená-las no Cloud Storage. Em seguida, crie uma imagem de contêiner para o aplicativo, execute-a como um job no Cloud Run, atualize o job para processar mais páginas da Web e execute o job em uma programação com o Cloud Scheduler.

O que você vai aprender

  • Como usar um app para fazer capturas de tela de páginas da Web.
  • Como criar uma imagem de contêiner para o aplicativo.
  • Como criar um job do Cloud Run para o aplicativo.
  • Como executar o aplicativo como um job do Cloud Run.
  • Como atualizar o job.
  • Como programar o job com o Cloud Scheduler.

2. Configuração e requisitos

Configuração de ambiente personalizada

  1. Faça login no Console do Google Cloud e crie um novo projeto ou reutilize um existente. Crie uma conta do Gmail ou do Google Workspace, se ainda não tiver uma.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • O Nome do projeto é o nome de exibição para os participantes do projeto. Ele é uma string de caracteres que não é usada pelas APIs do Google e pode ser atualizada a qualquer momento.
  • O ID do projeto precisa ser exclusivo em todos os projetos do Google Cloud e não pode ser alterado após a definição. O Console do Cloud gera automaticamente uma string única, geralmente não importa o que seja. Na maioria dos codelabs, você precisará fazer referência ao ID do projeto, que geralmente é identificado como PROJECT_ID. Então, se você não gostar dele, gere outro ID aleatório ou crie um próprio e veja se ele está disponível. Em seguida, ele fica "congelado" depois que o projeto é criado.
  • Há um terceiro valor, um Número de projeto, que algumas APIs usam. Saiba mais sobre esses três valores na documentação.
  1. Em seguida, você precisará ativar o faturamento no Console do Cloud para usar os recursos/APIs do Cloud. A execução deste codelab não será muito cara, se tiver algum custo. Para encerrar os recursos e não gerar cobranças além deste tutorial, siga as instruções de "limpeza" encontradas no final do codelab. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Iniciar o Cloud Shell

Embora o Google Cloud e o Spanner possam ser operados remotamente do seu laptop, neste codelab usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Cloud.

No Console do Google Cloud, clique no ícone do Cloud Shell na barra de ferramentas superior à direita:

55efc1aaa7a4d3ad.png

O provisionamento e a conexão com o ambiente levarão apenas alguns instantes para serem concluídos: Quando o processamento for concluído, você verá algo como:

7ffe5cbb04455448.png

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Todo o trabalho neste laboratório pode ser feito apenas com um navegador.

Configurar a gcloud

No Cloud Shell, defina o ID do projeto e a região para implantar o job do Cloud Run. Salve-as como variáveis PROJECT_ID e REGION. É possível escolher uma região em um dos locais do Cloud Run.

PROJECT_ID=[YOUR-PROJECT-ID]
REGION=[YOUR-REGION]
gcloud config set core/project $PROJECT_ID
gcloud config set run/region $REGION

Ativar APIs

Ative todos os serviços necessários:

gcloud services enable \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  run.googleapis.com

3. Buscar o código

Primeiro, você vai explorar um aplicativo Node.js para fazer capturas de tela de páginas da Web e armazená-las no Cloud Storage. Mais tarde, você vai criar uma imagem de contêiner para o aplicativo e executá-la como um job no Cloud Run.

No Cloud Shell, execute o seguinte comando para clonar o código do aplicativo deste repositório:

git clone https://github.com/GoogleCloudPlatform/jobs-demos.git

Acesse o diretório que contém o aplicativo:

cd jobs-demos/screenshot

Você verá este layout de arquivo:

screenshot
 |
 ├── Dockerfile
 ├── README.md
 ├── screenshot.js
 ├── package.json

Veja uma breve descrição de cada página:

  • screenshot.js contém o código Node.js do aplicativo.
  • package.json define as dependências da biblioteca.
  • Dockerfile define a imagem do contêiner.

4. Explorar o código

Para explorar o código, use o editor de texto integrado clicando no botão Open Editor na parte superior da janela do Cloud Shell.

f78880c00c0af1ef.png

Veja uma breve explicação de cada arquivo.

screenshot.js

screenshot.js primeiro adiciona o Puppeteer e o Cloud Storage como dependências O Puppeteer é uma biblioteca do Node.js que você usa para fazer capturas de tela de páginas da Web:

const puppeteer = require('puppeteer');
const {Storage} = require('@google-cloud/storage');

Há uma função initBrowser para inicializar o Puppeteer e uma função takeScreenshot para fazer capturas de tela de um determinado URL:

async function initBrowser() {
  console.log('Initializing browser');
  return await puppeteer.launch();
}

async function takeScreenshot(browser, url) {
  const page = await browser.newPage();

  console.log(`Navigating to ${url}`);
  await page.goto(url);

  console.log(`Taking a screenshot of ${url}`);
  return await page.screenshot({
    fullPage: true
  });
}

Em seguida, há uma função para acessar ou criar um bucket do Cloud Storage e outra para fazer upload da captura de tela de uma página da Web para um bucket:

async function createStorageBucketIfMissing(storage, bucketName) {
  console.log(`Checking for Cloud Storage bucket '${bucketName}' and creating if not found`);
  const bucket = storage.bucket(bucketName);
  const [exists] = await bucket.exists();
  if (exists) {
    // Bucket exists, nothing to do here
    return bucket;
  }

  // Create bucket
  const [createdBucket] = await storage.createBucket(bucketName);
  console.log(`Created Cloud Storage bucket '${createdBucket.name}'`);
  return createdBucket;
}

async function uploadImage(bucket, taskIndex, imageBuffer) {
  // Create filename using the current time and task index
  const date = new Date();
  date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
  const filename = `${date.toISOString()}-task${taskIndex}.png`;

  console.log(`Uploading screenshot as '${filename}'`)
  await bucket.file(filename).save(imageBuffer);
}

Por fim, a função main é o ponto de entrada:

async function main(urls) {
  console.log(`Passed in urls: ${urls}`);

  const taskIndex = process.env.CLOUD_RUN_TASK_INDEX || 0;
  const url = urls[taskIndex];
  if (!url) {
    throw new Error(`No url found for task ${taskIndex}. Ensure at least ${parseInt(taskIndex, 10) + 1} url(s) have been specified as command args.`);
  }
  const bucketName = process.env.BUCKET_NAME;
  if (!bucketName) {
    throw new Error('No bucket name specified. Set the BUCKET_NAME env var to specify which Cloud Storage bucket the screenshot will be uploaded to.');
  }

  const browser = await initBrowser();
  const imageBuffer = await takeScreenshot(browser, url).catch(async err => {
    // Make sure to close the browser if we hit an error.
    await browser.close();
    throw err;
  });
  await browser.close();

  console.log('Initializing Cloud Storage client')
  const storage = new Storage();
  const bucket = await createStorageBucketIfMissing(storage, bucketName);
  await uploadImage(bucket, taskIndex, imageBuffer);

  console.log('Upload complete!');
}

main(process.argv.slice(2)).catch(err => {
  console.error(JSON.stringify({severity: 'ERROR', message: err.message}));
  process.exit(1);
});

Observe o seguinte sobre o método main:

  • Os URLs são transmitidos como argumentos.
  • O nome do bucket é transmitido como a variável de ambiente BUCKET_NAME definida pelo usuário. O nome do bucket precisa ser globalmente exclusivo em todo o Google Cloud.
  • Uma variável de ambiente CLOUD_RUN_TASK_INDEX é transmitida pelos jobs do Cloud Run. Os jobs do Cloud Run podem executar várias cópias do aplicativo como tarefas únicas. CLOUD_RUN_TASK_INDEX representa o índice da tarefa em execução. O padrão é zero quando o código é executado fora dos jobs do Cloud Run. Quando o aplicativo é executado como várias tarefas, cada tarefa/contêiner escolhe o URL pelo qual é responsável, faz uma captura de tela e salva a imagem no bucket.

package.json

O arquivo package.json define o aplicativo e especifica as dependências do Cloud Storage e do Puppeteer:

{
  "name": "screenshot",
  "version": "1.0.0",
  "description": "Create a job to capture screenshots",
  "main": "screenshot.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Google LLC",
  "license": "Apache-2.0",
  "dependencies": {
    "@google-cloud/storage": "^5.18.2",
    "puppeteer": "^13.5.1"
  }
}

Dockerfile

O Dockerfile define a imagem do contêiner para o aplicativo com todas as bibliotecas e dependências necessárias:

FROM node:17-alpine

# Installs latest Chromium (92) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      npm

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy all files
COPY . .

# Run everything after as a non-privileged user.
USER pptruser

ENTRYPOINT ["node", "screenshot.js"]

5. Criar e publicar a imagem de contêiner

O Artifact Registry é o serviço de armazenamento e gerenciamento de imagens de contêiner no Google Cloud. Para mais informações, consulte Como trabalhar com imagens de contêiner. O Artifact Registry pode armazenar imagens de contêiner do Docker e do OCI em um repositório do Docker.

Crie um novo repositório do Artifact Registry chamado containers:

gcloud artifacts repositories create containers --repository-format=docker --location=$REGION

Crie e publique a imagem do contêiner:

gcloud builds submit -t $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1

Após alguns minutos, a imagem do contêiner é criada e hospedada no Artifact Registry.

62e50ebe805f9a9c.png

6. Criar um job

Antes de criar um job, você precisa criar uma conta de serviço para executar esse job.

gcloud iam service-accounts create screenshot-sa --display-name="Screenshot app service account"

Atribua o papel storage.admin à conta de serviço para que ela possa ser usada para criar buckets e objetos.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --role roles/storage.admin \
  --member serviceAccount:screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

Agora está tudo pronto para você criar um job do Cloud Run que inclui a configuração necessária para executá-lo.

gcloud beta run jobs create screenshot \
  --image=$REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1 \
  --args="https://example.com" \
  --args="https://cloud.google.com" \
  --tasks=2 \
  --task-timeout=5m \
  --set-env-vars=BUCKET_NAME=screenshot-$PROJECT_ID \
  --service-account=screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

Isso cria um job do Cloud Run sem executá-lo.

Observe como as páginas da Web são transmitidas como argumentos. O nome do bucket para salvar as capturas de tela é transmitido como uma variável de ambiente.

É possível executar várias cópias do seu contêiner em paralelo especificando várias tarefas a serem executadas com a sinalização --tasks. Cada tarefa representa uma cópia em execução do contêiner. É útil usar várias tarefas se cada uma delas puder processar independentemente um subconjunto dos dados. Para facilitar esse processo, cada tarefa reconhece o índice, que é armazenado na variável de ambiente CLOUD_RUN_TASK_INDEX. Seu código é responsável por determinar qual tarefa processa qual subconjunto dos dados. Observe o --tasks=2 neste exemplo. Isso garante que dois contêineres sejam executados para os dois URLs que queremos processar.

Cada tarefa pode ser executada por até 1 hora. É possível diminuir esse tempo limite usando a sinalização --task-timeout, como fizemos neste exemplo. Todas as tarefas precisam ser concluídas corretamente para que o job seja concluído. Por padrão, os jobs com falha não serão executados novamente. É possível configurar novas tentativas quando elas falharem. Se alguma tarefa exceder o número de novas tentativas, todo o job falhará.

Por padrão, o job será executado com o maior número possível de tarefas em paralelo. Isso será igual ao número de tarefas do seu job, até um máximo de 100. É recomendável definir um paralelismo mais baixo para jobs que acessam um back-end com escalonabilidade limitada. Por exemplo, um banco de dados que suporta um número limitado de conexões ativas. É possível reduzir o paralelismo com a sinalização --parallelism.

7. Executar um job

Antes de executar o job, liste-o para ver se ele foi criado:

gcloud beta run jobs list

✔
JOB: screenshot
REGION: $REGION
LAST RUN AT:
CREATED: 2022-02-22 12:20:50 UTC

Execute o job com o seguinte comando:

gcloud beta run jobs execute screenshot

Isso executa o job. É possível listar as execuções atuais e antigas:

gcloud beta run jobs executions list --job screenshot

...
JOB: screenshot
EXECUTION: screenshot-znkmm
REGION: $REGION
RUNNING: 1
COMPLETE: 1 / 2
CREATED: 2022-02-22 12:40:42 UTC

Descreva a execução. Você vai ver a marca de seleção verde e a mensagem tasks completed successfully:

gcloud beta run jobs executions describe screenshot-znkmm
✔ Execution screenshot-znkmm in region $REGION
2 tasks completed successfully

Image:           $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot at 311b20d9...
Tasks:           2
Args:            https://example.com https://cloud.google.com
Memory:          1Gi
CPU:             1000m
Task Timeout:    3600s
Parallelism:     2
Service account: 11111111-compute@developer.gserviceaccount.com
Env vars:
  BUCKET_NAME    screenshot-$PROJECT_ID

Você também pode verificar a página "Jobs do Cloud Run" no Console do Cloud para ver o status:

e59ed4e532b974b1.png

Se você verificar o bucket do Cloud Storage, pode ver os dois arquivos de captura de tela criados:

f2f86e60b94ba47c.png

Às vezes, pode ser necessário interromper uma execução antes que ela seja concluída, talvez porque você percebeu que precisa executar o job com parâmetros diferentes ou que há um erro no código e que não quer usar um tempo de computação desnecessário.

Para interromper a execução do job, é necessário excluí-la:

gcloud beta run jobs executions delete screenshot-znkmm

8. Atualizar um job

As novas versões do contêiner não são selecionadas automaticamente por jobs do Cloud Run na próxima execução. Se você alterar o código do job, será necessário recriar o contêiner e atualizar o job. O uso de imagens marcadas ajuda a identificar qual versão da imagem está sendo usada no momento.

Da mesma maneira, também é preciso atualizar o job se você quiser atualizar alguma das variáveis de configuração. As execuções subsequentes do job usarão as novas definições de contêiner e configurações.

Atualize o job e mude as páginas em que o app faz capturas de tela na sinalização --args. Atualize também a sinalização --tasks para refletir o número de páginas.

gcloud beta run jobs update screenshot \
  --args="https://www.pinterest.com" \
  --args="https://www.apartmenttherapy.com" \
  --args="https://www.google.com" \
  --tasks=3

Execute o job novamente. Esse tempo é transmitido na sinalização --wait para aguardar a conclusão das execuções:

gcloud beta run jobs execute screenshot --wait

Após alguns segundos, mais três capturas de tela serão adicionadas ao bucket:

ce91c96dcfd271bb.png

9. Programar um job

Até o momento, este codelab mostra como executar jobs manualmente. Em um cenário real, você provavelmente quer executar jobs em resposta a um evento ou a uma programação. É possível fazer isso usando a API REST do Cloud Run. Vamos ver como executar o job de captura de tela em uma programação usando o Cloud Scheduler.

Primeiro, verifique se a API Cloud Scheduler está ativada:

gcloud services enable cloudscheduler.googleapis.com

Crie um job do Cloud Scheduler para executar o job do Cloud Run todos os dias às 9h:

PROJECT_NUMBER="$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)')"

gcloud scheduler jobs create http screenshot-scheduled --schedule "0 9 * * *" \
   --http-method=POST \
   --uri=https://$REGION-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/$PROJECT_ID/jobs/screenshot:run \
   --oauth-service-account-email=$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
   --location $REGION

Verifique se o job do Cloud Scheduler foi criado e está pronto para chamar o job do Cloud Run:

gcloud scheduler jobs list

ID: screenshot-scheduled
LOCATION: $REGION
SCHEDULE (TZ): 0 9 * * * (Etc/UTC)
TARGET_TYPE: HTTP
STATE: ENABLED

Para testar, acione o Cloud Scheduler manualmente:

gcloud scheduler jobs run screenshot-scheduled

Em alguns segundos, você verá mais três capturas de tela adicionadas pela chamada do Cloud Scheduler:

971ea598020cf9ba.png

10. Parabéns

Parabéns, você concluiu o codelab.

O que vimos

  • Como usar um app para fazer capturas de tela de páginas da Web.
  • Como criar uma imagem de contêiner para o aplicativo.
  • Como criar um job do Cloud Run para o aplicativo.
  • Como executar o aplicativo como um job do Cloud Run.
  • Como atualizar o job.
  • Como programar o job com o Cloud Scheduler.