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

1. Panoramica

In questo codelab, continuerai a lavorare sul lab precedente e aggiungerai un servizio di generazione di miniature. Il servizio di generazione di miniature è un contenitore web che prende le immagini di grandi dimensioni e 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 ridimensiona le immagini e le salva in un altro bucket in Cloud Storage.

31fa4f8a294d90df.png

Obiettivi didattici

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Configurazione e requisiti

Configurazione dell'ambiente autonomo

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

96a9c957bc475304.pngb9a10ebdf5b5a448.pnga1e3c01a38fa61c2.png

  • Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google e puoi aggiornarla in qualsiasi momento.
  • L'ID progetto deve essere univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo l'impostazione). La console Cloud genera automaticamente una stringa univoca; in genere non ti interessa quale sia. Nella maggior parte dei codelab, dovrai fare riferimento all'ID progetto (in genere identificato come PROJECT_ID), quindi, se non ti piace, generane un altro casuale oppure puoi provare il tuo e verificare se è disponibile. Dopo la creazione del progetto, l'ID progetto è "bloccato".
  • Esiste un terzo valore, un numero di progetto , utilizzato da alcune API. Scopri di più su tutti e tre i valori nella documentazione.
  1. Dopodiché, dovrai abilitare la fatturazione nella console Cloud per utilizzare le risorse/API Cloud. L'esecuzione di questo codelab non dovrebbe costare molto, se non nulla. Per arrestare le risorse in modo da non incorrere in addebiti di fatturazione oltre questo tutorial, segui le istruzioni di "pulizia" alla fine del codelab. I nuovi utenti di Google Cloud possono beneficiare del programma di prova senza costi di 300$.

Avvia Cloud Shell

Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

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

bce75f34b2c53987.png

Bastano pochi istanti per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere qualcosa di simile a questo:

f6ef2b5f13479f3a.png

Questa macchina virtuale è caricata con tutti gli strumenti di sviluppo di cui avrai bisogno. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. 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 immagini container e di Cloud Run per eseguire il deployment del container.

Abilita entrambe le API da Cloud Shell:

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

Dovresti vedere l'operazione completata correttamente:

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

4. Crea un altro bucket

Memorizzerai le miniature delle immagini caricate in un altro bucket. Utilizziamo gsutil per creare il secondo bucket.

In Cloud Shell, imposta una variabile per il nome univoco del bucket. Cloud Shell ha già GOOGLE_CLOUD_PROJECT impostato sull'ID progetto univoco. Puoi aggiungerlo al nome del bucket. Quindi, crea un bucket pubblico su più regioni 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

Avrai il seguente layout di file per il servizio:

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

Nella cartella thumbnails/nodejs sono presenti 3 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 utilizzare l'editor di testo integrato facendo clic sul pulsante Open Editor nella parte superiore della 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 della 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 i file immagine in Cloud Storage. Firestore per aggiornare i metadati dell'immagine. Express è un framework web JavaScript / Node. Il modulo body-parser viene utilizzato per analizzare facilmente le richieste in entrata. Bluebird viene utilizzato per la gestione delle promesse e Imagemagick è una libreria per la manipolazione delle 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 è Node 14 e la libreria imagemagick viene utilizzata per la manipolazione delle immagini. Vengono create alcune directory temporanee per contenere i file immagine originali e delle miniature. Prima di avviare il codice con npm start, vengono installati i moduli NPM necessari per il nostro codice.

index.js

Esploriamo il codice in parti, in modo da capire meglio cosa fa questo 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 e creiamo la nostra applicazione web Express, oltre a indicare che vogliamo utilizzare il parser 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 questi payload in entrata sull'URL di base / e racchiudiamo il nostro codice con una logica di gestione degli errori, 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 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 HTTP POST, 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, ma che codifica il payload effettivo in Base64. Ecco perché il nostro codice sopra decodifica il contenuto Base64 di questo attributo. L'attributo data, una volta decodificato, contiene un altro documento JSON che rappresenta i dettagli dell'evento Cloud Storage, 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 dell'immagine e del bucket, poiché il nostro codice recupererà l'immagine dal bucket per il trattamento 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 di file ha attivato il nostro servizio Cloud Run e il bucket di destinazione in cui memorizzeremo l'immagine risultante. Stiamo utilizzando l'API integrata path per la gestione dei file locali, poiché la libreria imagemagick creerà la miniatura localmente nella directory temporanea /tmp. Attendiamo await 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 è molto compatibile con async / await, quindi lo racchiudiamo in una promessa JavaScript (fornita dal modulo Bluebird). Quindi chiamiamo la funzione di ridimensionamento / ritaglio asincrona 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 della miniatura in Cloud Storage, aggiorneremo anche i metadati in Cloud Firestore per aggiungere un flag booleano che indica che la miniatura per questa 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`);

Al termine della richiesta, rispondiamo alla richiesta HTTP POST 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 file di origine, abbiamo le istruzioni per avviare effettivamente l'applicazione web Express sulla porta predefinita 8080.

7. Prova localmente

Prova il codice localmente per assicurarti che funzioni prima di eseguire il deployment nel cloud.

Nella 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

Utilizza 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 poi ospitarle in Google Container Registry.

Nella 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 minuto o due, la build dovrebbe riuscire:

b354b3a9a3631097.png

Anche la sezione "Cronologia" di Cloud Build dovrebbe mostrare la build riuscita:

df00f198dd2bf6bf.png

Se fai clic sull'ID build per visualizzare i dettagli, nella scheda "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

Utilizza CTRL-C per uscire.

9. Esegui il deployment in Cloud Run

Prima di eseguire il deployment in Cloud Run, imposta la regione Cloud Run su una delle regioni supportate e la piattaforma 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 il comando seguente per eseguire il deployment dell'immagine container in 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

Prendi nota del flag --no-allow-unauthenticated. In questo modo, il servizio Cloud Run diventa un servizio interno che verrà attivato solo da account di servizio specifici.

Se il deployment è andato a buon fine, dovresti vedere l'output seguente:

c0f28e7d6de0024.png

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

9bfe48e3c8b597e5.png

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

Il servizio è pronto, ma devi comunque fare in modo che gli eventi Cloud Storage vengano inviati al servizio Cloud Run appena creato. Cloud Storage può inviare eventi di creazione di file tramite Cloud Pub/Sub, ma sono necessari alcuni passaggi per far funzionare questa funzionalità.

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 un secondo momento:

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

Concedi al service account 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 attivato il service account Pub/Sub l'8 aprile 2021 o in una data precedente, concedi il ruolo iam.serviceAccountTokenCreator al service account 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 potrebbe richiedere alcuni minuti.

Infine, crea una sottoscrizione Pub/Sub con il service account:

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 che sia 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. Verifica il servizio

Per verificare se la configurazione funziona, carica una nuova immagine nel bucket uploaded-pictures e controlla nel bucket thumbnails che le nuove immagini ridimensionate vengano 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. Liberare spazio (facoltativo)

Se non intendi continuare con gli altri lab della serie, puoi liberare spazio dalle risorse per ridurre i costi e per essere un buon cittadino del cloud. Puoi liberare spazio dalle risorse singolarmente come segue.

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:

  • Hai creato una notifica in Cloud Storage che invia messaggi Pub/Sub a un argomento quando viene caricata una nuova immagine.
  • Hai definito i binding e gli account IAM richiesti (a differenza di Cloud Functions, dove tutto è automatizzato, qui la configurazione è manuale).
  • Hai creato una sottoscrizione in modo che il nostro servizio Cloud Run riceva i messaggi Pub/Sub.
  • Ogni volta che viene caricata una nuova immagine nel bucket, l'immagine viene ridimensionata grazie al nuovo servizio Cloud Run.

Argomenti trattati

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Passaggi successivi