Pic-à-quoi : Atelier 2 : Créer des vignettes de photos

1. Présentation

Dans cet atelier de programmation, vous allez vous appuyer sur l'atelier précédent et ajouter un service de vignettes. Le service de vignettes est un conteneur Web qui utilise de grandes images pour créer des vignettes.

Lorsque l'image est importée dans Cloud Storage, une notification est envoyée via Cloud Pub/Sub à un conteneur Web Cloud Run, qui redimensionne ensuite les images et les enregistre dans un autre bucket Cloud Storage.

31fa4f8a294d90df.png

Points abordés

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Préparation

Configuration de l'environnement au rythme de chacun

  1. Connectez-vous à la console Google Cloud, puis créez un projet ou réutilisez un projet existant. (Si vous ne possédez pas encore de compte Gmail ou Google Workspace, vous devez en créer un.)

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • Le nom du projet est le nom à afficher pour les participants au projet. Il s'agit d'une chaîne de caractères qui n'est pas utilisée par les API Google, et que vous pouvez modifier à tout moment.
  • L'ID du projet doit être unique sur l'ensemble des projets Google Cloud et doit être immuable (vous ne pouvez pas le modifier une fois que vous l'avez défini). Cloud Console génère automatiquement une chaîne unique dont la composition importe peu, en général. Dans la plupart des ateliers de programmation, vous devrez référencer l'ID du projet (généralement identifié comme PROJECT_ID), donc s'il ne vous convient pas, générez-en un autre au hasard ou définissez le vôtre, puis vérifiez s'il est disponible. Il est ensuite "gelé" une fois le projet créé.
  • La troisième valeur est le numéro de projet, utilisé par certaines API. Pour en savoir plus sur ces trois valeurs, consultez la documentation.
  1. Vous devez ensuite activer la facturation dans Cloud Console afin d'utiliser les ressources/API Cloud. L'exécution de cet atelier de programmation est très peu coûteuse, voire sans frais. Pour arrêter les ressources afin d'éviter qu'elles ne vous soient facturées après ce tutoriel, suivez les instructions de nettoyage indiquées à la fin de l'atelier. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai gratuit pour bénéficier d'un crédit de 300 $.

Démarrer Cloud Shell

Bien que Google Cloud puisse être utilisé à distance depuis votre ordinateur portable, nous allons nous servir de Google Cloud Shell pour cet atelier de programmation, un environnement de ligne de commande exécuté dans le cloud.

Depuis la console GCP, cliquez sur l'icône Cloud Shell de la barre d'outils située dans l'angle supérieur droit :

bce75f34b2c53987.png

Le provisionnement et la connexion à l'environnement prennent quelques instants seulement. Une fois l'opération terminée, le résultat devrait ressembler à ceci :

f6ef2b5f13479f3a.png

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser toutes les activités de cet atelier dans un simple navigateur.

3. Activer les API

Dans cet atelier, vous allez avoir besoin de Cloud Build pour créer des images de conteneur et de Cloud Run pour déployer le conteneur.

Activez les deux API dans Cloud Shell:

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

L'opération doit s'afficher pour se terminer correctement:

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

4. Créer un autre bucket

Vous allez stocker les vignettes des photos importées dans un autre bucket. Utilisons gsutil pour créer le deuxième bucket.

Dans Cloud Shell, définissez une variable pour le nom unique du bucket. GOOGLE_CLOUD_PROJECT est déjà défini sur votre ID de projet unique dans Cloud Shell. Vous pouvez l'ajouter au nom du bucket. Ensuite, créez un bucket public multirégional en Europe avec un accès de niveau 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

Au final, vous devriez avoir un nouveau bucket public:

8e75c8099938e972.png

5. Cloner le code

Clonez le code et accédez au répertoire contenant le service:

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

La mise en page des fichiers du service est la suivante:

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

Le dossier thumbnails/nodejs contient trois fichiers:

  • index.js contient le code Node.js
  • package.json définit les dépendances de la bibliothèque.
  • Dockerfile définit l'image de conteneur.

6. Explorer le code

Pour explorer le code, vous pouvez utiliser l'éditeur de texte intégré en cliquant sur le bouton Open Editor en haut de la fenêtre Cloud Shell:

3d145fe299dd8b3e.png

Vous pouvez également ouvrir l'éditeur dans une fenêtre de navigateur dédiée pour bénéficier de plus d'espace à l'écran.

Dépendances

Le fichier package.json définit les dépendances de bibliothèque nécessaires:

{
  "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 bibliothèque Cloud Storage permet de lire et d'enregistrer des fichiers image dans Cloud Storage. Firestore pour mettre à jour les métadonnées de l'image. Express est un framework Web JavaScript / Node. Le module d'analyse du corps permet d'analyser facilement les requêtes entrantes. Bluebird est utilisé pour traiter les promesses, tandis qu'Imagemagick est une bibliothèque dédiée à la manipulation des images.

Dockerfile

Dockerfile définit l'image de conteneur pour l'application:

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'image de base est Node 14 et la bibliothèque imagemagick est utilisée pour la manipulation des images. Des répertoires temporaires sont créés pour stocker les fichiers d'origine et les vignettes. Les modules NPM nécessaires à notre code sont ensuite installés avant de commencer le code par npm start.

index.js

Explorons le code en plusieurs parties afin de mieux comprendre ce que fait ce programme.

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

Tout d'abord, nous avons besoin des dépendances nécessaires et nous créons notre application Web Express, tout en indiquant que nous souhaitons utiliser l'analyseur de corps JSON, car les requêtes entrantes ne sont en fait que des charges utiles JSON envoyées à notre application via une requête POST.

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

Nous recevons ces charges utiles entrantes sur l'URL / et nous encapsulons notre code dans une logique d'erreur. Pour mieux comprendre pourquoi une défaillance peut se produire dans notre code, examinez les journaux qui seront visibles dans l'interface Stackdriver Logging de la 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}`);

Sur la plate-forme Cloud Run, les messages Pub/Sub sont envoyés via des requêtes HTTP POST, sous la forme de charges utiles JSON au format suivant:

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

Mais ce qui est vraiment intéressant dans ce document JSON, c'est le contenu de l'attribut message.data, qui n'est qu'une chaîne, mais qui encode la charge utile réelle en base 64. C'est pourquoi le code ci-dessus décoder le contenu en base 64 de cet attribut. Une fois décodé, cet attribut data contient un autre document JSON qui représente les détails de l'événement Cloud Storage, qui, entre autres métadonnées, indiquent le nom du fichier et le nom du 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="
}

Nous nous intéressons aux noms des images et des buckets, car notre code va extraire cette image du bucket pour son traitement des vignettes:

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

Nous récupérons le nom du bucket de stockage de sortie à partir d'une variable d'environnement.

Nous avons le bucket d'origine dont la création de fichier a déclenché notre service Cloud Run, et le bucket de destination, dans lequel nous stockerons l'image obtenue. Nous utilisons l'API intégrée path pour gérer les fichiers locaux, car la bibliothèque imagemagick créera la vignette en local dans le répertoire temporaire /tmp. Nous await pour un appel asynchrone visant à télécharger le fichier image importé.

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

Le module imagemagick n'est pas très compatible avec async / await. Nous allons donc le résumer dans une promesse JavaScript (fournie par le module Bluebird). Ensuite, nous appelons la fonction de redimensionnement / recadrage asynchrone que nous avons créée avec les paramètres des fichiers source et de destination, ainsi que les dimensions de la vignette que nous souhaitons créer.

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

Une fois le fichier de vignette importé dans Cloud Storage, nous mettrons également à jour les métadonnées dans Cloud Firestore pour ajouter un indicateur booléen indiquant que la vignette de cette image a bien été générée:

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

Une fois la requête terminée, nous répondons à la requête HTTP POST indiquant que le fichier a été correctement traité.

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

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

À la fin de notre fichier source, nous avons les instructions pour qu'Express démarre notre application Web sur le port 8080 par défaut.

7. Tester en local

Testez le code localement pour vous assurer qu'il fonctionne avant de le déployer dans le cloud.

Dans le dossier thumbnails/nodejs, installez les dépendances npm et démarrez le serveur:

npm install; npm start

Si tout s'est déroulé comme prévu, le serveur doit démarrer sur le port 8080:

Started thumbnail generator on port 8080

Appuyez sur CTRL-C pour quitter.

8. Créer et publier l'image de conteneur

Cloud Run exécute les conteneurs, mais vous devez d'abord créer l'image de conteneur (définie dans Dockerfile). Google Cloud Build permet de créer des images de conteneurs, puis de les héberger dans Google Container Registry.

Dans le dossier thumbnails/nodejs, où se trouve Dockerfile, exécutez la commande suivante pour créer l'image de conteneur:

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

Après une minute ou deux, la compilation devrait aboutir:

b354b3a9a3631097.png

"Historique" de Cloud Build doit également montrer que la compilation a réussi:

df00f198dd2bf6bf.png

Cliquez sur l'ID de compilation pour afficher les détails de la section "Artefacts de compilation". vous devriez constater que l'image de conteneur a été importée dans Cloud Registry (GCR):

a4577ce0744f73e2.png

Si vous le souhaitez, vous pouvez vérifier que l'image de conteneur s'exécute localement dans Cloud Shell:

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

Il doit démarrer le serveur sur le port 8080 du conteneur:

Started thumbnail generator on port 8080

Appuyez sur CTRL-C pour quitter.

9. Déployer dans Cloud Run

Avant le déploiement sur Cloud Run, définissez la région Cloud Run sur l'une des régions et la plate-forme compatibles sur managed:

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

Vous pouvez vérifier que la configuration est définie:

gcloud config list

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

Exécutez la commande suivante pour déployer l'image de conteneur sur 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

Notez l'indicateur --no-allow-unauthenticated. Le service Cloud Run devient ainsi un service interne qui ne sera déclenché que par des comptes de service spécifiques.

Si le déploiement réussit, le résultat suivant doit s'afficher:

c0f28e7d6de0024.png

Si vous accédez à l'interface utilisateur de la console Cloud, vous devriez également constater que le service a bien été déployé:

9bfe48e3c8b597e5.png

10. Événements Cloud Storage vers Cloud Run via Pub/Sub

Le service est prêt, mais vous devez encore créer des événements Cloud Storage sur le service Cloud Run que vous venez de créer. Cloud Storage peut envoyer des événements de création de fichiers via Cloud Pub/Sub, mais vous devez suivre quelques étapes pour que cela fonctionne.

Créez un sujet Pub/Sub en tant que pipeline de communication:

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

Créez des notifications Pub/Sub lorsque des fichiers sont stockés dans le bucket:

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

Créez un compte de service pour l'abonnement Pub/Sub que nous créerons ultérieurement:

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

Accordez au compte de service l'autorisation d'appeler un service 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

Si vous avez activé le compte de service Pub/Sub au plus tard le 8 avril 2021, accordez le rôle iam.serviceAccountTokenCreator au compte de service 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 propagation des modifications IAM peut prendre quelques minutes.

Enfin, créez un abonnement Pub/Sub avec le compte de service:

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

Vous pouvez vérifier qu'un abonnement a bien été créé. Accédez à Pub/Sub dans la console, sélectionnez le sujet gcs-events. L'abonnement doit s'afficher en bas de l'écran:

e8ab86dccb8d890.png

11. Tester le service

Pour vérifier que la configuration fonctionne, importez une nouvelle image dans le bucket uploaded-pictures et vérifiez dans le bucket thumbnails que les nouvelles images redimensionnées s'affichent comme prévu.

Vous pouvez également vérifier les journaux pour voir s'afficher les messages de journalisation au fur et à mesure de la réalisation des différentes étapes du service Cloud Run:

42c025e2d7d6ca3a.png

12. Nettoyer (facultatif)

Si vous n'avez pas l'intention de suivre les autres ateliers de la série, vous pouvez nettoyer les ressources pour réduire les coûts et utiliser globalement le cloud comme il se doit. Vous pouvez nettoyer les ressources individuellement comme suit.

Supprimez le bucket :

gsutil rb gs://$BUCKET_THUMBNAILS

Supprimez le service :

gcloud run services delete $SERVICE_NAME -q

Supprimez le sujet Pub/Sub :

gcloud pubsub topics delete $TOPIC_NAME

Vous pouvez également supprimer l'intégralité du projet:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. Félicitations !

À présent, tout est en place:

  • Création d'une notification dans Cloud Storage qui envoie des messages Pub/Sub sur un sujet lorsqu'une nouvelle image est importée.
  • Définition des liaisons et comptes IAM requis (contrairement à Cloud Functions où tout est automatisé, il est configuré ici manuellement)
  • créé un abonnement pour que notre service Cloud Run reçoive les messages Pub/Sub ;
  • Chaque fois qu'une nouvelle image est importée dans le bucket, elle est redimensionnée grâce au nouveau service Cloud Run.

Points abordés

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Étapes suivantes