Sobre este codelab
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.
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
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:
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:
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:
O "histórico" do Cloud Build também deve mostrar o build bem-sucedido:
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):
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:
Se você acessar a IU do Console do Cloud, também 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 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:
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:
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