Foto do dia: Laboratório 2: crie miniaturas de fotos

Foto do dia:
Laboratório 2:
crie miniaturas de fotos

Sobre este codelab

subjectÚltimo nov. 14, 2021 atualizado
account_circleEscrito por Guillaume Laforge, Mete Atamel

1. Visão geral

Neste codelab, você usará o laboratório anterior e adicionará um serviço de miniaturas. O serviço de miniaturas é um contêiner da Web que tira fotos grandes e cria miniaturas com elas.

Quando a imagem é carregada no 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 em outro bucket no Cloud Storage.

31fa4f8a294d90df.png

O que você vai aprender

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Configuração e requisitos

Configuração de ambiente autoguiada

  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.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.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.

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:

bce75f34b2c53987.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:

f6ef2b5f13479f3a.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.

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

Você verá que a operação será concluída com sucesso:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. Criar outro bucket

Você 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 seu ID do projeto exclusivo. É possível anexar isso ao nome do bucket. Em seguida, crie um bucket público multirregional na Europa com acesso de nível uniforme:

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:

8e75c8099938e972.png

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, você tem três arquivos:

  • index.js contém o código Node.js.
  • package.json define as dependências da biblioteca.
  • Dockerfile define 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 superior da janela do Cloud Shell:

3d145fe299dd8b3e.png

Para ter mais espaço na tela, você também pode abrir o editor em uma janela dedicada do navegador.

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. Express é um framework da Web JavaScript / Node. O módulo do analisador de corpo é usado para analisar facilmente as solicitações recebidas. O Bluebird é usado para lidar com 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 imagem. Alguns diretórios temporários são criados para manter arquivos de imagens originais e de miniatura. Em seguida, os módulos do 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 o programa faz.

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 o aplicativo da Web Express, além de indicar que queremos usar o analisador de corpo JSON, já que as solicitações de entrada são apenas payloads JSON enviados por uma solicitação POST para o 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 os payloads de entrada no URL base / e estamos encapsulando nosso código com um tratamento de lógica de erros para entender melhor por que algo pode estar falhando no código. Para isso, basta analisar os registros 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 do 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 neste documento JSON é, na verdade, o que está contido no atributo message.data, que é apenas uma string, mas que codifica o payload real em Base 64. É por isso que nosso código acima está decodificando o conteúdo em Base 64 desse atributo. Depois de decodificado, o atributo data contém outro documento JSON que representa os detalhes do evento do Cloud Storage que, entre outros metadados, indicam 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 na imagem e nos nomes de bucket, já que nosso código buscará essa imagem do bucket para o tratamento de miniaturas:

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 em que a criação do arquivo acionou o serviço do Cloud Run e o bucket de destino em que armazenaremos a imagem resultante. Estamos usando a API integrada path para processar arquivos locais, já que a biblioteca imagemagick criará a miniatura localmente no diretório temporário /tmp. 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, ele está sendo finalizado em uma promessa do JavaScript (fornecida pelo módulo Bluebird). Em seguida, chamamos a função assíncrona de redimensionamento e corte que criamos com os parâmetros dos arquivos de origem e de destino, além das dimensões da miniatura que queremos criar.

await thumbBucket.upload(thumbFile);
console
.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);

Assim que o arquivo de miniatura for enviado para o Cloud Storage, também atualizaremos os metadados no Cloud Firestore para adicionar uma sinalização booleana indicando que a miniatura da imagem foi de fato 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 nossa solicitação é encerrada, respondemos à solicitação POST HTTP informando 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 nosso arquivo de origem, temos as instruções para que o Express inicie nosso aplicativo da Web na porta padrão 8080.

7. Testar localmente

Teste o código localmente para garantir que ele funciona antes de implantá-lo na nuvem.

Dentro da pasta thumbnails/nodejs, instale as dependências de npm e inicie o servidor:

npm install; npm start

Se tudo tiver dado certo, ele deverá iniciar o servidor 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 você precisa criar a imagem do contêiner (definida em Dockerfile). O Google Cloud Build pode ser usado para criar imagens de contêiner e hospedá-lo no Google Container Registry.

Na pasta thumbnails/nodejs, em que Dockerfile está, emita o seguinte comando para criar a imagem do contêiner:

gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

Após um ou dois minutos, a compilação será bem-sucedida:

b354b3a9a3631097.png

O "histórico" do Cloud Build também deve mostrar o build bem-sucedido:

df00f198dd2bf6bf.png

Clicar no ID do build para acessar a visualização de detalhes na seção "artefatos da versão" Você verá que a imagem do contêiner foi enviada para o registro do Cloud (GCR):

a4577ce0744f73e2.png

Se quiser, verifique novamente se a imagem do contêiner é executada localmente no Cloud Shell:

docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

Ele deve iniciar o servidor na porta 8080 do contêiner:

Started thumbnail generator on port 8080

Use CTRL-C para sair.

9. Implantar no Cloud Run

Antes de implantar no Cloud Run, defina a região do Cloud Run como uma das regiões e plataformas compatíveis como managed:

gcloud config set run/region europe-west1
gcloud config set run/platform managed

Verifique se a configuração foi definida:

gcloud config list

...
[run]
platform = managed
region = europe-west1

Execute este comando 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 sinalização --no-allow-unauthenticated. Isso faz 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ê verá a seguinte saída:

c0f28e7d6de0024.png

Se você acessar a IU do Console do Cloud, também verá que o serviço foi implantado com sucesso:

9bfe48e3c8b597e5.png

10. Eventos do Cloud Storage para o Cloud Run via Pub/Sub

O serviço está pronto, mas você ainda precisa criar eventos do Cloud Storage para o serviço recém-criado do Cloud Run. O Cloud Storage pode enviar eventos de criação de arquivos pelo Cloud Pub/Sub, mas há algumas etapas para fazer isso.

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 será criada mais tarde:

SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
     --display-name "Cloud Run Pub/Sub Invoker"

Conceda permissão à conta de serviç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

A propagação das alterações do IAM pode levar alguns minutos.

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

É possível verificar se uma assinatura foi criada. Acesse o Pub/Sub no console, selecione o tópico gcs-events e, na parte inferior, você verá a assinatura:

e8ab86dccb8d890.png

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 no bucket thumbnails se as novas imagens redimensionadas aparecem conforme o esperado.

Também é possível verificar os registros para conferir se as mensagens de registro aparecem durante as várias etapas do serviço do Cloud Run:

42c025e2d7d6ca3a.png

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 cidadão da nuvem. É possível limpar 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 tudo está no lugar:

  • Criou uma notificação no Cloud Storage que envia mensagens do Pub/Sub sobre um tópico quando é feito o upload de uma nova imagem.
  • Definimos as vinculações e contas do IAM necessárias (ao contrário do Cloud Functions, em que ele é totalmente automatizado, mas é configurado manualmente aqui).
  • Criou uma assinatura para que nosso serviço do Cloud Run receba as mensagens do Pub/Sub.
  • Sempre que uma nova imagem é enviada ao bucket, ela é redimensionada graças ao novo serviço do Cloud Run.

O que vimos

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Próximas etapas