1. Visão geral
Neste codelab, você vai usar o laboratório anterior e adicionar um serviço de miniatura. O serviço de miniaturas é um contêiner da Web que pega fotos grandes e cria miniaturas delas.
À medida que a foto é enviada para o Cloud Storage, uma notificação é enviada pelo Cloud Pub/Sub para um contêiner da Web do Cloud Run, que redimensiona as imagens e as salva novamente em outro bucket do Cloud Storage.

O que você vai aprender
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub
2. Configuração e requisitos
Configuração de ambiente autoguiada
- 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.



- 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.
- 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.
Inicie 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 GCP, clique no ícone do Cloud Shell na barra de ferramentas localizada no canto superior direito:

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:

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.
3. Ativar APIs
Neste laboratório, você vai precisar do Cloud Build para criar imagens de contêiner e do Cloud Run para implantar o contêiner.
Ative as duas APIs no Cloud Shell:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
A operação será concluída com sucesso:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Criar outro bucket
Você vai armazenar miniaturas das imagens enviadas em outro bucket. Vamos usar gsutil para criar o segundo bucket.
No Cloud Shell, defina uma variável para o nome exclusivo do bucket. O Cloud Shell já tem GOOGLE_CLOUD_PROJECT definido como o ID exclusivo do seu projeto. Você pode anexar isso ao nome do bucket. Em seguida, crie um bucket público multirregional na Europa com acesso uniforme no nível:
BUCKET_THUMBNAILS=thumbnails-$GOOGLE_CLOUD_PROJECT gsutil mb -l EU gs://$BUCKET_THUMBNAILS gsutil uniformbucketlevelaccess set on gs://$BUCKET_THUMBNAILS gsutil iam ch allUsers:objectViewer gs://$BUCKET_THUMBNAILS
No final, você terá um novo bucket público:

5. Clonar o código
Clone o código e acesse o diretório que contém o serviço:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop cd serverless-photosharing-workshop/services/thumbnails/nodejs
Você terá o seguinte layout de arquivo para o serviço:
services
|
├── thumbnails
|
├── nodejs
|
├── Dockerfile
├── index.js
├── package.json
Dentro da pasta thumbnails/nodejs, há três arquivos:
index.jscontém o código Node.jspackage.jsondefine as dependências da bibliotecaDockerfiledefine a imagem do contêiner
6. Explorar o código
Para explorar o código, use o editor de texto integrado clicando no botão Open Editor na parte de cima da janela do Cloud Shell:

Você também pode abrir o editor em uma janela dedicada do navegador para ter mais espaço na tela.
Dependências
O arquivo package.json define as dependências de biblioteca necessárias:
{
"name": "thumbnail_service",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"bluebird": "^3.7.2",
"express": "^4.17.1",
"imagemagick": "^0.1.3",
"@google-cloud/firestore": "^4.9.9",
"@google-cloud/storage": "^5.8.3"
}
}
A biblioteca do Cloud Storage é usada para ler e salvar arquivos de imagem no Cloud Storage. Firestore para atualizar os metadados da imagem. O Express é um framework da Web JavaScript / Node. O módulo body-parser é usado para analisar solicitações recebidas com facilidade. O Bluebird é usado para processar promessas, e o Imagemagick é uma biblioteca para manipular imagens.
Dockerfile
Dockerfile define a imagem do contêiner para o aplicativo:
FROM node:14-slim
# installing Imagemagick
RUN set -ex; \
apt-get -y update; \
apt-get -y install imagemagick; \
rm -rf /var/lib/apt/lists/*; \
mkdir /tmp/original; \
mkdir /tmp/thumbnail;
WORKDIR /picadaily/services/thumbnails
COPY package*.json ./
RUN npm install --production
COPY . .
CMD [ "npm", "start" ]
A imagem base é o Node 14, e a biblioteca imagemagick é usada para manipulação de imagens. Alguns diretórios temporários são criados para armazenar arquivos de imagem originais e em miniatura. Em seguida, os módulos NPM necessários para nosso código são instalados antes de iniciar o código com npm start.
index.js
Vamos analisar o código em partes para entender melhor o que esse programa está fazendo.
const express = require('express');
const imageMagick = require('imagemagick');
const Promise = require("bluebird");
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');
const app = express();
app.use(express.json());
Primeiro, exigimos as dependências necessárias e criamos nosso aplicativo da Web Express, além de indicar que queremos usar o analisador de corpo JSON, já que as solicitações recebidas são apenas payloads JSON enviados por uma solicitação POST ao nosso aplicativo.
app.post('/', async (req, res) => {
try {
// ...
} catch (err) {
console.log(`Error: creating the thumbnail: ${err}`);
console.error(err);
res.status(500).send(err);
}
});
Estamos recebendo esses payloads no URL base / e encapsulando nosso código com alguma lógica de tratamento de erros para ter mais informações sobre por que algo pode estar falhando no nosso código. Para isso, basta consultar os registros que serão visíveis na interface do Stackdriver Logging no console da Web do Google Cloud.
const pubSubMessage = req.body;
console.log(`PubSub message: ${JSON.stringify(pubSubMessage)}`);
const fileEvent = JSON.parse(Buffer.from(pubSubMessage.message.data, 'base64').toString().trim());
console.log(`Received thumbnail request for file ${fileEvent.name} from bucket ${fileEvent.bucket}`);
Na plataforma Cloud Run, as mensagens do Pub/Sub são enviadas por solicitações HTTP POST, como payloads JSON do seguinte formato:
{
"message": {
"attributes": {
"bucketId": "uploaded-pictures",
"eventTime": "2020-02-27T09:22:43.255225Z",
"eventType": "OBJECT_FINALIZE",
"notificationConfig": "projects/_/buckets/uploaded-pictures/notificationConfigs/28",
"objectGeneration": "1582795363255481",
"objectId": "IMG_20200213_181159.jpg",
"payloadFormat": "JSON_API_V1"
},
"data": "ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN...FQUU9Igp9Cg==",
"messageId": "1014308302773399",
"message_id": "1014308302773399",
"publishTime": "2020-02-27T09:22:43.973Z",
"publish_time": "2020-02-27T09:22:43.973Z"
},
"subscription": "projects/serverless-picadaily/subscriptions/gcs-events-subscription"
}
Mas o que é realmente interessante nesse documento JSON é o que está contido no atributo message.data, que é apenas uma string, mas codifica o payload real em Base 64. Por isso, o código acima está decodificando o conteúdo Base 64 desse atributo. Esse atributo data, depois de decodificado, contém outro documento JSON que representa os detalhes do evento do Cloud Storage, que, entre outros metadados, indica o nome do arquivo e do bucket.
{
"kind": "storage#object",
"id": "uploaded-pictures/IMG_20200213_181159.jpg/1582795363255481",
"selfLink": "https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg",
"name": "IMG_20200213_181159.jpg",
"bucket": "uploaded-pictures",
"generation": "1582795363255481",
"metageneration": "1",
"contentType": "image/jpeg",
"timeCreated": "2020-02-27T09:22:43.255Z",
"updated": "2020-02-27T09:22:43.255Z",
"storageClass": "STANDARD",
"timeStorageClassUpdated": "2020-02-27T09:22:43.255Z",
"size": "4944335",
"md5Hash": "QzBIoPJBV2EvqB1EVk1riw==",
"mediaLink": "https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg?generation=1582795363255481&alt=media",
"crc32c": "hQ3uHg==",
"etag": "CLmJhJu08ecCEAE="
}
Estamos interessados nos nomes da imagem e do bucket, já que nosso código vai buscar essa imagem do bucket para o tratamento da miniatura:
const bucket = storage.bucket(fileEvent.bucket);
const thumbBucket = storage.bucket(process.env.BUCKET_THUMBNAILS);
const originalFile = path.resolve('/tmp/original', fileEvent.name);
const thumbFile = path.resolve('/tmp/thumbnail', fileEvent.name);
await bucket.file(fileEvent.name).download({
destination: originalFile
});
console.log(`Downloaded picture into ${originalFile}`);
Estamos recuperando o nome do bucket de armazenamento de saída de uma variável de ambiente.
Temos o bucket de origem, cuja criação de arquivo acionou nosso serviço do Cloud Run, e o bucket de destino, onde vamos armazenar a imagem resultante. Estamos usando a API integrada path para fazer o processamento de arquivos locais, já que a biblioteca imagemagick vai criar a miniatura localmente no diretório temporário /tmp. Usamos await para uma chamada assíncrona para fazer o download do arquivo de imagem enviado.
const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
srcPath: originalFile,
dstPath: thumbFile,
width: 400,
height: 400
});
console.log(`Created local thumbnail in ${thumbFile}`);
O módulo imagemagick não é muito compatível com async / await. Por isso, estamos encapsulando-o em uma promessa JavaScript (fornecida pelo módulo Bluebird). Em seguida, chamamos a função assíncrona de redimensionamento / corte que criamos com os parâmetros dos arquivos de origem e destino, bem como as dimensões da miniatura que queremos criar.
await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);
Depois que o arquivo de miniatura for enviado para o Cloud Storage, também vamos atualizar os metadados no Cloud Firestore para adicionar uma flag booleana indicando que a miniatura dessa imagem foi gerada:
const pictureStore = new Firestore().collection('pictures');
const doc = pictureStore.doc(fileEvent.name);
await doc.set({
thumbnail: true
}, {merge: true});
console.log(`Updated Firestore about thumbnail creation for ${fileEvent.name}`);
res.status(204).send(`${fileEvent.name} processed`);
Quando a solicitação termina, respondemos à solicitação HTTP POST que o arquivo foi processado corretamente.
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Started thumbnail generator on port ${PORT}`);
});
No final do arquivo de origem, temos as instruções para que o Express inicie o aplicativo da Web na porta padrão 8080.
7. Testar localmente
Teste o código localmente para garantir que ele funcione antes de implantar na nuvem.
Na pasta thumbnails/nodejs, instale as dependências do npm e inicie o servidor:
npm install; npm start
Se tudo der certo, o servidor será iniciado na porta 8080:
Started thumbnail generator on port 8080
Use CTRL-C para sair.
8. Criar e publicar a imagem de contêiner
O Cloud Run executa contêineres, mas primeiro é necessário criar a imagem do contêiner (definida em Dockerfile). O Google Cloud Build pode ser usado para criar imagens de contêiner e hospedá-las no Container Registry.
Na pasta thumbnails/nodejs em que Dockerfile está, execute o seguinte comando para criar a imagem do contêiner:
gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
Depois de um ou dois minutos, o build vai ser concluído:

A seção "histórico" do Cloud Build também mostra o build bem-sucedido:

Clique no ID do build para acessar a visualização de detalhes. Na guia "Artefatos do build", confira se a imagem do contêiner foi enviada ao Cloud Registry (GCR):

Se quiser, verifique se a imagem do contêiner é executada localmente no Cloud Shell:
docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
Ele vai iniciar o servidor na porta 8080 no contêiner:
Started thumbnail generator on port 8080
Use CTRL-C para sair.
9. Implantar no Cloud Run
Antes de fazer a implantação no Cloud Run, defina a região do Cloud Run como uma das regiões compatíveis e a plataforma como managed:
gcloud config set run/region europe-west1 gcloud config set run/platform managed
Para verificar se a configuração está definida:
gcloud config list ... [run] platform = managed region = europe-west1
Execute o comando a seguir para implantar a imagem do contêiner no Cloud Run:
SERVICE_NAME=thumbnail-service
gcloud run deploy $SERVICE_NAME \
--image gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service \
--no-allow-unauthenticated \
--update-env-vars BUCKET_THUMBNAILS=$BUCKET_THUMBNAILS
Observe a flag --no-allow-unauthenticated. Isso torna o serviço do Cloud Run um serviço interno que só será acionado por contas de serviço específicas.
Se a implantação for bem-sucedida, você vai ver a seguinte saída:

Se você acessar a interface do console do Cloud, também vai ver que o serviço foi implantado com sucesso:

10. Eventos do Cloud Storage para o Cloud Run via Pub/Sub
O serviço está pronto, mas ainda é necessário fazer eventos do Cloud Storage para o serviço do Cloud Run recém-criado. O Cloud Storage pode enviar eventos de criação de arquivos pelo Cloud Pub/Sub, mas há algumas etapas para que isso funcione.
Crie um tópico do Pub/Sub como o pipeline de comunicação:
TOPIC_NAME=cloudstorage-cloudrun-topic gcloud pubsub topics create $TOPIC_NAME
Crie notificações do Pub/Sub quando os arquivos forem armazenados no bucket:
BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES
Crie uma conta de serviço para a assinatura do Pub/Sub que vamos criar mais tarde:
SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name "Cloud Run Pub/Sub Invoker"
Conceda à conta de serviço permissão para invocar um serviço do Cloud Run:
SERVICE_NAME=thumbnail-service gcloud run services add-iam-policy-binding $SERVICE_NAME \ --member=serviceAccount:$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \ --role=roles/run.invoker
Se você ativou a conta de serviço do Pub/Sub até 8 de abril de 2021, conceda o papel iam.serviceAccountTokenCreator à conta de serviço do Pub/Sub:
PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
--member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator
Pode levar alguns minutos para que as mudanças do IAM sejam propagadas.
Por fim, crie uma assinatura do Pub/Sub com a conta de serviço:
SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --format 'value(status.url)') gcloud pubsub subscriptions create $TOPIC_NAME-subscription --topic $TOPIC_NAME \ --push-endpoint=$SERVICE_URL \ --push-auth-service-account=$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com
Você pode verificar se uma assinatura foi criada. Acesse o Pub/Sub no console, selecione o tópico gcs-events e, na parte de baixo, você vai encontrar a assinatura:

11. Testar o serviço
Para testar se a configuração está funcionando, faça upload de uma nova imagem no bucket uploaded-pictures e verifique se as novas imagens redimensionadas aparecem conforme o esperado no bucket thumbnails.
Você também pode verificar os registros para ver as mensagens de registro aparecerem à medida que as várias etapas do serviço do Cloud Run são concluídas:

12. Limpeza (opcional)
Se você não pretende continuar com os outros laboratórios da série, limpe os recursos para economizar custos e ser um bom usuário da nuvem. É possível limpar os recursos individualmente da seguinte maneira.
Excluir o bucket:
gsutil rb gs://$BUCKET_THUMBNAILS
Exclua o serviço:
gcloud run services delete $SERVICE_NAME -q
Exclua o tópico Pub/Sub:
gcloud pubsub topics delete $TOPIC_NAME
Se preferir, exclua todo o projeto:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
13. Parabéns!
Agora está tudo pronto:
- Criou uma notificação no Cloud Storage que envia mensagens do Pub/Sub em um tópico quando uma nova imagem é enviada.
- Definir as vinculações e contas necessárias do IAM. Ao contrário do Cloud Functions, em que tudo é automatizado, aqui a configuração é manual.
- Criamos uma assinatura para que nosso serviço do Cloud Run receba as mensagens do Pub/Sub.
- Sempre que uma nova imagem é enviada para o bucket, ela é redimensionada graças ao novo serviço do Cloud Run.
O que vimos
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub