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

1. Panoramica

In questo codelab, ti baserai sul lab precedente e aggiungerai un servizio di generazione di miniature. Il servizio di generazione delle miniature è un container web che prende immagini di grandi dimensioni e crea miniature.

Man mano che 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 di nuovo 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 ancora un account Gmail o Google Workspace, devi crearne uno.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.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 essere stato impostato). Cloud Console genera automaticamente una stringa univoca, di solito non ti interessa di cosa si tratta. Nella maggior parte dei codelab, devi fare riferimento all'ID progetto (che in genere è identificato come PROJECT_ID), quindi, se non ti piace, generane un altro casuale oppure puoi provare il tuo e vedere se è disponibile. Viene "congelato" dopo la creazione del progetto.
  • Esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, devi abilitare la fatturazione in Cloud Console 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 costi di fatturazione al termine di questo tutorial, segui le istruzioni di "pulizia" riportate alla fine del codelab. I nuovi utenti di Google Cloud possono beneficiare del programma 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 GCP, 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 un risultato simile a questo:

f6ef2b5f13479f3a.png

Questa macchina virtuale è caricata con tutti gli strumenti per sviluppatori di cui avrai bisogno. Offre una home directory permanente da 5 GB e viene eseguita su Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Tutto il lavoro di 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 che l'operazione è stata 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à impostato GOOGLE_CLOUD_PROJECT sul tuo ID progetto univoco. Puoi aggiungerlo al nome del bucket. Quindi, crea un bucket multiregionale pubblico 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 dei file:

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

All'interno della cartella thumbnails/nodejs, hai 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 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 delle immagini originali e delle miniature. Poi vengono installati i moduli NPM necessari al nostro codice prima di avviare il codice con npm start.

index.js

Esaminiamo 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, indicando 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 nell'URL di base / e inseriamo il nostro codice in 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 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 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 è ciò che è contenuto nell'attributo message.data, che è solo una stringa, ma che codifica il payload effettivo in Base 64. Ecco perché il nostro codice riportato sopra decodifica i contenuti in 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 delle immagini e dei bucket, perché 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}`);

Recuperiamo 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 archivieremo l'immagine risultante. Utilizziamo l'API integrata path per la gestione dei file locali, poiché la libreria imagemagick creerà la miniatura localmente nella directory temporanea /tmp. await per 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 stiamo inserendo in una promessa JavaScript (fornita dal modulo Bluebird). Poi chiamiamo la funzione di ridimensionamento / ritaglio 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 della miniatura su 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 POST HTTP 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 fare in modo che Express avvii effettivamente la nostra applicazione web sulla porta predefinita 8080.

7. Testare localmente

Testa il codice localmente 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 avviarsi 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 poi ospitarle 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 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 la visualizzazione dei 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

Dovrebbe avviare il server 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 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 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

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

Se il deployment è riuscito, dovresti visualizzare l'output seguente:

c0f28e7d6de0024.png

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

9bfe48e3c8b597e5.png

10. Eventi Cloud Storage a 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 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 service account 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 l'account di servizio Pub/Sub il giorno 8 aprile 2021 o in una data precedente, 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 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 stato creato un abbonamento. Vai a Pub/Sub nella console, seleziona l'argomento gcs-events e in basso dovresti vedere l'abbonamento:

e8ab86dccb8d890.png

11. Testare 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. Pulizia (facoltativo)

Se non intendi continuare con gli altri lab della serie, puoi eseguire la pulizia delle risorse per risparmiare sui costi e per essere un buon cittadino del cloud. Puoi ripulire le risorse singolarmente nel seguente modo.

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 pronto:

  • È stata creata una notifica in Cloud Storage che invia messaggi Pub/Sub su un argomento quando viene caricata una nuova immagine.
  • Definisci gli account e i binding IAM richiesti (a differenza di Cloud Functions, dove tutto è automatizzato, qui la configurazione è manuale).
  • È stato creato un abbonamento in modo che il nostro servizio Cloud Run riceva i messaggi Pub/Sub.
  • Ogni volta che viene caricata una nuova immagine nel bucket, le dimensioni vengono modificate grazie al nuovo servizio Cloud Run.

Argomenti trattati

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Passaggi successivi