Pic-a-daily: Lab 2 - Creazione di miniature di immagini

1. Panoramica

In questo lab di codice, utilizzerai il lab precedente e aggiungerai un servizio di miniature. Il servizio di miniature è un contenitore web che scatta grandi foto e ne crea le miniature.

Quando l'immagine viene caricata in Cloud Storage, viene inviata una notifica tramite Cloud Pub/Sub a un container web Cloud Run, che quindi ridimensiona le immagini e le salva in un altro bucket in Cloud Storage.

31fa4f8a294d90df.png

Cosa imparerai a fare

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Configurazione e requisiti

Configurazione dell'ambiente da seguire in modo autonomo

  1. Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • Il Nome progetto è il nome visualizzato dei partecipanti del progetto. Si tratta di una stringa di caratteri non utilizzata dalle API di Google e può essere aggiornata in qualsiasi momento.
  • L'ID progetto deve essere univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo essere stato impostato). La console Cloud genera automaticamente una stringa univoca. di solito non ti importa cosa sia. Nella maggior parte dei codelab, devi fare riferimento all'ID progetto (che solitamente è identificato come PROJECT_ID), quindi, se non ti piace, generane un altro a caso oppure puoi fare un tentativo personalizzato e controllare se è disponibile. Poi c'è "congelato" dopo la creazione del progetto.
  • C'è un terzo valore, il numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, dovrai abilitare la fatturazione nella console Cloud per utilizzare le risorse/le API Cloud. Eseguire questo codelab non dovrebbe costare molto. Per arrestare le risorse in modo da non incorrere in fatturazione oltre questo tutorial, segui eventuali "pulizie" istruzioni riportate alla fine del codelab. I nuovi utenti di Google Cloud sono idonei al programma prova senza costi di 300$.

Avvia Cloud Shell

Anche se Google Cloud può essere utilizzato da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Dalla console di Google Cloud, fai clic sull'icona di Cloud Shell nella barra degli strumenti in alto a destra:

bce75f34b2c53987.png

Dovrebbe richiedere solo qualche istante per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere una schermata simile al seguente:

f6ef2b5f13479f3a.png

Questa macchina virtuale viene caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita su Google Cloud, migliorando notevolmente le prestazioni di rete e l'autenticazione. Tutto il lavoro in questo lab può essere svolto semplicemente con un browser.

3. Abilita API

In questo lab avrai bisogno di Cloud Build per creare le immagini container e di Cloud Run per il deployment del container.

Abilita entrambe le API da Cloud Shell:

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

L'operazione dovrebbe essere completata correttamente:

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

4. Crea un altro bucket

Le miniature delle immagini caricate verranno archiviate in un altro bucket. Usiamo gsutil per creare il secondo bucket.

In Cloud Shell, imposta una variabile per il nome univoco del bucket. In Cloud Shell è già impostato GOOGLE_CLOUD_PROJECT sul tuo ID progetto univoco. Puoi aggiungerlo al nome del bucket. Quindi, crea un bucket pubblico multiregionale in Europa con accesso a livello 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

Alla fine, dovresti avere un nuovo bucket pubblico:

8e75c8099938e972.png

5. Clona il codice

Clona il codice e vai alla directory contenente il servizio:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
cd serverless-photosharing-workshop/services/thumbnails/nodejs

Per il servizio avrai il seguente layout di file:

services
 |
 ├── thumbnails
      |
      ├── nodejs
           |
           ├── Dockerfile
           ├── index.js
           ├── package.json

La cartella thumbnails/nodejs ha tre file:

  • index.js contiene il codice Node.js
  • package.json definisce le dipendenze della libreria
  • Dockerfile definisce l'immagine container

6. Esplora il codice

Per esplorare il codice, puoi usare l'editor di testo integrato facendo clic sul pulsante Open Editor in alto nella finestra di Cloud Shell:

3d145fe299dd8b3e.png

Puoi anche aprire l'editor in una finestra del browser dedicata per avere più spazio sullo schermo.

Dipendenze

Il file package.json definisce le dipendenze di libreria necessarie:

{
  "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"
  }
}

La libreria Cloud Storage viene utilizzata per leggere e salvare file immagine all'interno di Cloud Storage. Firestore per aggiornare i metadati dell'immagine. Express è un framework web JavaScript / Nodo. Il modulo body-parser viene utilizzato per analizzare facilmente le richieste in entrata. Bluebird viene utilizzato per gestire le promesse, mentre Imagemagick è una libreria per la manipolazione di immagini.

Dockerfile

Dockerfile definisce l'immagine container per l'applicazione:

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" ]

L'immagine di base è il nodo 14 e la libreria imagemagick viene utilizzata per la manipolazione delle immagini. Vengono create alcune directory temporanee per conservare i file delle immagini originali e in miniatura. Quindi, i moduli di Gestione dei partner di rete richiesti dal nostro codice vengono installati prima di iniziare il codice con npm start.

index.js

Analizziamo il codice in più parti, in modo da comprendere meglio l'obiettivo del programma.

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

Innanzitutto, richiediamo le dipendenze necessarie, quindi creiamo la nostra applicazione web Express, oltre a indicare che vogliamo utilizzare l'analizzatore sintattico del corpo JSON, poiché le richieste in entrata sono in realtà solo payload JSON inviati tramite una richiesta POST alla nostra applicazione.

app.post('/', async (req, res) => {
    try {
        // ...
    } catch (err) {
        console.log(`Error: creating the thumbnail: ${err}`);
        console.error(err);
        res.status(500).send(err);
    }
});

Riceviamo i payload in arrivo sull'URL / di base e stiamo avvolgendo il codice con una gestione logica di errore, per avere informazioni migliori sul motivo per cui qualcosa potrebbe non funzionare nel nostro codice esaminando i log che saranno visibili dall'interfaccia di Stackdriver Logging nella console web di 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}`);

Sulla piattaforma Cloud Run, i messaggi Pub/Sub vengono inviati tramite richieste POST HTTP, come payload JSON del modulo:

{
  "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"
}

Ma ciò che è davvero interessante in questo documento JSON è in realtà ciò che è contenuto nell'attributo message.data, che è solo una stringa che codifica il payload effettivo in Base 64. Ecco perché il codice riportato sopra sta decodificando i contenuti di Base 64 di questo attributo. L'attributo data una volta decodificato contiene un altro documento JSON che rappresenta i dettagli dell'evento Cloud Storage e che, tra gli altri metadati, indica il nome del file e il nome del 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="
}

Ci interessano i nomi delle immagini e dei bucket, in quanto il nostro codice recupererà l'immagine dal bucket per la visualizzazione delle miniature:

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

Stiamo recuperando il nome del bucket di archiviazione di output da una variabile di ambiente.

Abbiamo il bucket di origine la cui creazione del file ha attivato il nostro servizio Cloud Run e il bucket di destinazione in cui archivieremo l'immagine risultante. Stiamo usando l'API integrata path per gestire i file locali, perché la libreria imagemagick creerà la miniatura localmente nella directory temporanea /tmp. await in una chiamata asincrona per scaricare il file immagine caricato.

const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
        srcPath: originalFile,
        dstPath: thumbFile,
        width: 400,
        height: 400         
});
console.log(`Created local thumbnail in ${thumbFile}`);

Il modulo imagemagick non è particolarmente adatto a async / await, quindi lo stiamo racchiudendo in una promessa JavaScript (fornita dal modulo Bluebird). Quindi richiamiamo la funzione di ridimensionamento / taglio asincrono che abbiamo creato con i parametri per i file di origine e di destinazione, nonché le dimensioni della miniatura che vogliamo creare.

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

Una volta caricato il file delle miniature su Cloud Storage, aggiorneremo anche i metadati in Cloud Firestore per aggiungere un flag booleano che indica che la miniatura dell'immagine è stata effettivamente generata:

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

Una volta terminata la richiesta, rispondiamo alla richiesta HTTP POST per confermare che il file è stato elaborato correttamente.

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
    console.log(`Started thumbnail generator on port ${PORT}`);
});

Alla fine del nostro file sorgente, abbiamo le istruzioni per fare in modo che Express avvii la nostra applicazione web sulla porta predefinita 8080.

7. Esegui test in locale

Testa il codice in locale per assicurarti che funzioni prima del deployment nel cloud.

All'interno della cartella thumbnails/nodejs, installa le dipendenze npm e avvia il server:

npm install; npm start

Se tutto è andato a buon fine, il server dovrebbe essere avviato sulla porta 8080:

Started thumbnail generator on port 8080

Usa CTRL-C per uscire.

8. Crea e pubblica l'immagine container

Cloud Run esegue i container, ma prima devi creare l'immagine container (definita in Dockerfile). Google Cloud Build può essere utilizzato per creare immagini container e quindi ospitare in Google Container Registry.

All'interno della cartella thumbnails/nodejs in cui si trova Dockerfile, esegui il comando seguente per creare l'immagine container:

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

Dopo un paio di minuti, la build dovrebbe riuscire:

b354b3a9a3631097.png

La "cronologia" di Cloud Build dovrebbe mostrare anche la build riuscita:

df00f198dd2bf6bf.png

Fai clic sull'ID build per ottenere la visualizzazione dei dettagli nella sezione "Artefatti build" dovresti vedere che l'immagine container è stata caricata in Cloud Registry (GCR):

a4577ce0744f73e2.png

Se vuoi, puoi verificare che l'immagine container venga eseguita localmente in Cloud Shell:

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

Il server dovrebbe essere avviato sulla porta 8080 nel container:

Started thumbnail generator on port 8080

Usa CTRL-C per uscire.

9. Esegui il deployment in Cloud Run

Prima di eseguire il deployment in Cloud Run, imposta la regione di Cloud Run su una delle regioni e della piattaforma supportate su managed:

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

Puoi verificare che la configurazione sia impostata:

gcloud config list

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

Esegui questo comando per eseguire il deployment dell'immagine container su 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

Nota il flag --no-allow-unauthenticated. Questo rende il servizio Cloud Run un servizio interno che viene attivato solo da account di servizio specifici.

Se il deployment viene eseguito correttamente, dovresti vedere l'output seguente:

c0f28e7d6de0024.png

Se vai all'interfaccia utente di Cloud Console, dovresti anche vedere che il deployment del servizio è stato eseguito correttamente:

9bfe48e3c8b597e5.png

10. Eventi di Cloud Storage in Cloud Run tramite Pub/Sub

Il servizio è pronto, ma devi ancora creare eventi Cloud Storage per il servizio Cloud Run appena creato. Cloud Storage può inviare eventi di creazione di file tramite Cloud Pub/Sub, ma per eseguire questa operazione sono necessari alcuni passaggi.

Crea un argomento Pub/Sub come pipeline di comunicazione:

TOPIC_NAME=cloudstorage-cloudrun-topic
gcloud pubsub topics create $TOPIC_NAME

Crea notifiche Pub/Sub quando i file vengono archiviati nel bucket:

BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT
gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES

Crea un account di servizio per la sottoscrizione Pub/Sub che creeremo in seguito:

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

Concedi all'account di servizio l'autorizzazione per richiamare un servizio 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 hai abilitato l'account di servizio Pub/Sub entro l'8 aprile 2021, concedi il ruolo iam.serviceAccountTokenCreator all'account di servizio 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

La propagazione delle modifiche IAM può richiedere alcuni minuti.

Infine, crea un abbonamento Pub/Sub con l'account di servizio:

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

Puoi verificare se è stata creata una sottoscrizione. Vai a Pub/Sub nella console, seleziona l'argomento gcs-events e in basso dovresti vedere la sottoscrizione:

e8ab86dccb8d890.png

11. Prova il servizio

Per verificare se la configurazione funziona, carica una nuova immagine nel bucket uploaded-pictures e controlla nel bucket thumbnails se le nuove immagini ridimensionate vengono visualizzate come previsto.

Puoi anche controllare i log per visualizzare i messaggi di logging, man mano che vengono eseguiti i vari passaggi del servizio Cloud Run:

42c025e2d7d6ca3a.png

12. Libera spazio (facoltativo)

Se non intendi continuare con gli altri lab della serie, puoi eseguire la pulizia delle risorse per risparmiare sui costi ed essere nel complesso un buon cittadino del cloud. Puoi eseguire la pulizia delle risorse singolarmente come descritto di seguito.

Elimina il bucket:

gsutil rb gs://$BUCKET_THUMBNAILS

Elimina il servizio:

gcloud run services delete $SERVICE_NAME -q

Elimina l'argomento Pub/Sub:

gcloud pubsub topics delete $TOPIC_NAME

In alternativa, puoi eliminare l'intero progetto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. Complimenti!

Ora è tutto a posto:

  • È stata creata una notifica in Cloud Storage che invia messaggi Pub/Sub su un argomento, quando viene caricata una nuova immagine.
  • Sono stati definiti le associazioni e gli account IAM richiesti (a differenza di Cloud Functions, in cui è tutto automatizzato, viene configurato manualmente qui).
  • È stato creato un abbonamento in modo che il servizio Cloud Run riceva i messaggi Pub/Sub.
  • Ogni volta che viene caricata una nuova immagine nel bucket, questa viene ridimensionata grazie al nuovo servizio Cloud Run.

Argomenti trattati

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Passaggi successivi