1. Présentation
Dans cet atelier de programmation, vous allez vous appuyer sur l'atelier précédent et ajouter un service de miniatures. Le service de vignettes est un conteneur Web qui prend des images volumineuses et en crée 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 de Cloud Storage.

Points abordés
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub
2. Préparation
Configuration de l'environnement au rythme de chacun
- 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.)



- 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.
- 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 sans frais 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 :

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

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 aurez besoin de Cloud Build pour créer des images de conteneurs et de Cloud Run pour déployer le conteneur.
Activez les deux API depuis Cloud Shell :
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
L'opération devrait se terminer correctement :
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Créer un autre bucket
Vous stockerez les miniatures 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. Cloud Shell a déjà défini GOOGLE_CLOUD_PROJECT sur votre ID de projet unique. Vous pouvez l'ajouter au nom du bucket. Ensuite, créez un bucket multirégional public en Europe avec un accès 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
À la fin, vous devriez avoir un nouveau bucket public :

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
Votre service utilisera la structure de fichiers suivante :
services
|
├── thumbnails
|
├── nodejs
|
├── Dockerfile
├── index.js
├── package.json
Le dossier thumbnails/nodejs contient trois fichiers :
index.jscontient le code Node.js.package.jsondéfinit les dépendances de la bibliothèque.Dockerfiledéfinit l'image du 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 :

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 est utilisée pour lire et 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 body-parser est utilisé pour analyser facilement les requêtes entrantes. Bluebird est utilisé pour gérer les promesses, et Imagemagick est une bibliothèque permettant de manipuler 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 d'images. Des répertoires temporaires sont créés pour stocker les fichiers d'image d'origine et de miniatures. Les modules NPM nécessaires à notre code sont ensuite installés avant de démarrer le code avec npm start.
index.js
Explorons le code par morceaux pour 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());
Nous commençons par exiger les dépendances nécessaires et créons notre application Web Express. Nous indiquons également 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 via une requête POST à notre application.
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 de base "/" et nous encapsulons notre code avec une logique de gestion des erreurs pour mieux comprendre pourquoi quelque chose peut échouer dans notre code en examinant 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 forme de charges utiles JSON du type 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 ce qui est contenu dans l'attribut message.data, qui n'est qu'une chaîne, mais qui encode la charge utile réelle en base64. C'est pourquoi notre code ci-dessus décode le contenu Base64 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 indique, entre autres métadonnées, 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 de l'image et du bucket, car notre code va extraire cette image du bucket pour le traitement de la 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}`);
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 résultante. Nous utilisons l'API intégrée path pour gérer les fichiers locaux, car la bibliothèque ImageMagick créera la miniature localement dans le répertoire temporaire /tmp. Nous await pour un appel asynchrone afin de 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 l'encapsuler dans une promesse JavaScript (fournie par le module Bluebird). Nous appelons ensuite 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 miniature 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 miniature importé dans Cloud Storage, nous mettrons également à jour les métadonnées dans Cloud Firestore pour ajouter un indicateur booléen indiquant que la miniature 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 notre requête terminée, nous répondons à la requête HTTP POST 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 permettant à Express de démarrer notre application Web sur le port 8080 par défaut.
7. Tester en local
Testez le code en local 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 bien passé, le serveur devrait démarrer sur le port 8080 :
Started thumbnail generator on port 8080
Utilisez CTRL-C pour quitter.
8. Créer et publier l'image de conteneur
Cloud Run exécute des conteneurs, mais vous devez d'abord créer l'image de conteneur (définie dans Dockerfile). Google Cloud Build peut être utilisé pour créer des images de conteneurs, puis 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
Au bout d'une ou deux minutes, la compilation devrait réussir :

La section "Historique" de Cloud Build devrait également afficher la compilation réussie :

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

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
Le serveur devrait démarrer sur le port 8080 du conteneur :
Started thumbnail generator on port 8080
Utilisez CTRL-C pour quitter.
9. Déployer dans Cloud Run
Avant de déployer sur Cloud Run, définissez la région Cloud Run sur l'une des régions compatibles et la plate-forme 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 alors 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 s'affiche :

Si vous accédez à l'interface utilisateur de la console Cloud, vous devriez également voir que le service a été déployé avec succès :

10. Événements Cloud Storage vers Cloud Run via Pub/Sub
Le service est prêt, mais vous devez toujours envoyer les événements Cloud Storage au 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 il y a quelques étapes à suivre pour que cela fonctionne.
Créez un sujet Pub/Sub comme canal 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 plus tard :
SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name "Cloud Run Pub/Sub Invoker"
Autorisez le compte de service à 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 le 8 avril 2021 ou avant cette date, attribuez 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 diffusion 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 été créé. Accédez à Pub/Sub dans la console, sélectionnez le sujet gcs-events, puis l'abonnement devrait s'afficher en bas de la page :

11. Tester le service
Pour tester si la configuration fonctionne, importez une nouvelle image dans le bucket uploaded-pictures et vérifiez que de nouvelles images redimensionnées apparaissent comme prévu dans le bucket thumbnails.
Vous pouvez également vérifier les journaux pour voir les messages de journalisation s'afficher au fur et à mesure des différentes étapes du service Cloud Run :

12. Nettoyer (facultatif)
Si vous n'avez pas l'intention de continuer à suivre les autres ateliers de cette série, vous pouvez nettoyer les ressources pour limiter vos dépenses et utiliser le cloud de manière raisonnée. 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 !
Tout est désormais en place :
- Vous avez créé une notification dans Cloud Storage qui envoie des messages Pub/Sub sur un sujet lorsqu'une nouvelle photo est importée.
- Définissez les liaisons et les comptes IAM requis (contrairement à Cloud Functions où tout est automatisé, ils sont configurés manuellement ici).
- Nous avons créé un abonnement pour que notre service Cloud Run reçoive les messages Pub/Sub.
- Chaque fois qu'une nouvelle photo 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