Il y a 3 mois -

Temps de lecture 11 minutes

Google Cloud : Démarrer une VM depuis une Cloud Function pour décompresser des fichiers

Introduction

Dans cet article nous allons développer pas à pas une Cloud Function pour décompresser des archives. Elle sera déclenchée lors d’un événement de dépôt de fichier dans GCS et démarrera une VM pour réaliser l’extraction des fichiers.

Pour donner un peu de contexte, dans notre projet nous recevons des fichiers compressés que nous devons extraire. Nous avons immédiatement pensé à utiliser les Cloud Functions pour répondre à ce besoin: lorsqu’un fichier tar est déposé, Cloud Storage déclenche une Cloud Function qui extrait les fichiers et les dépose dans le bucket.

Malheureusement ce fonctionnement atteint ses limites lorsque l’on reçoit des fichiers supérieurs à environs 50 Mo. La Cloud Function atteint alors son délai maximum d’exécution (9 minutes au moment de l’écriture de cet article) sans avoir pu finir l’extraction des fichiers !

Nous avons alors décidé de creuser une solution hybride pour résoudre ce problème. La Cloud Function démarre une VM et délègue à celle-ci l’extraction des fichiers. Puis, une fois la tâche terminée, la VM s’éteint automatiquement.

Le schéma ci-dessous illustre le fonctionnement global de la solution:

Démarrer une VM avec l’API Node.js

Nous allons utiliser l’environnement de développement accessible depuis Cloud Shell pour développer notre code. Cet environnement est implicitement authentifié pour utiliser les services GCP, nous pourrons coder facilement dans de nombreux langages : idéal donc pour rapidement “POCer” notre idée.

Dans la console Google Cloud nous allons sur le projet GCP qui va héberger notre VM et Cloud Function, nous démarrons Cloud Shell puis nous ouvrons l’éditeur de code intégré:

Voilà !

Depuis la console de Cloud Shell nous nous créons un projet Node.js

mkdir poc_vm
cd poc_vm
npm init

Nous désirons utiliser l’API Compute Engine nous installons donc les packages nécessaires:

npm install --.save @google-cloud/compute

Puis dans un fichier createSimpleVM.js nous copions le code suivant (voir la documentation de l’API Compute):

const Compute = require('@google-cloud/compute');
const compute = new Compute();
(async () => {
   try {
       const zone = compute.zone('europe-west1-b');
       const name = 'my-ubuntu-vm';
       const data = await zone.createVM(name, {os: 'ubuntu'});

       const vm = data[0];
       const operation = data[1]; 
      
       console.log('\nLa demande de création de la VM est faite');
       console.log('\nLes metadatas de la VM en cours de création:');
       console.log(JSON.stringify(await vm.getMetadata()));

       await operation.promise();
      
       console.log('\nLa VM est créée');

       console.log('\nLes metadatas de la VM créée:');
       console.log(JSON.stringify(await vm.getMetadata()));
       console.log(JSON.stringify(await operation.getMetadata()));
   } catch(err) {
       console.error(err);
   }
})();

Les deux grandes étapes de ce code consistent à :

  • demander la création d’une VM :
await zone.createVM(name, {os: 'ubuntu'});
  • attendre que la création de la VM soit terminée :
await operation.promise();

Pour exécuter ce script Node.js nous exécutons la commande suivante:

node createSimpleVM.js

Un petit tour dans la console sur la partie Compute VM nous permet de vérifier que la VM est bien présente:

La capture d’écran suivante montre les différences dans les données « metadatas » loggées avant et après la création de la VM. Certains attributs sont en plus tel que l’adresse IP.

Cela clôt cette première étape, nous savons comment créer une VM avec l’API Node.js.

Etre notifié du dépôt d’un fichier dans Cloud Storage

D’abord nous créons notre bucket qui recevra nos fichiers:

gsutil mb gs://decompress-from-bucket-2019

Puis nous créons un nouveau fichier index.js qui contiendra le code de la Cloud Function.

C’est cette Cloud Function qui sera appelée par les notifications provenant de Cloud Storage.

/**
* Generic background Cloud Function to be triggered by Cloud Storage.
*
* @param {object} data The event payload.
* @param {object} context The event metadata.
*/
exports.helloGCSGeneric = (data, context) => {
 const file = data;
 console.log(`  Event ${context.eventId}`);
 console.log(`  Event Type: ${context.eventType}`);
 console.log(`  Bucket: ${file.bucket}`);
 console.log(`  File: ${file.name}`);
 console.log(`  Metageneration: ${file.metageneration}`);
 console.log(`  Created: ${file.timeCreated}`);
 console.log(`  Updated: ${file.updated}`);
};

(Voir la documentation GCP https://cloud.google.com/functions/docs/tutorials/storage#functions-update-install-gcloud-node8)

Déployons notre Cloud Function en la déclarant comme étant déclenchée par notre bucket précédemment créé:

gcloud functions deploy helloGCSGeneric --runtime nodejs8 --trigger-bucket decompress-from-bucket-2019

Dans la console GCP nous pouvons constater qu’elle est bien déployée et déclenchée par les notifications du bucket

Finalement pour tester il suffit de déposer un fichier dans notre bucket et vérifier les logs:

Nous avons donc le code pour créer une VM et une Cloud Function qui se déclenche sur les événements d’un bucket.

La prochaine étape est donc de créer la VM depuis notre Cloud Function sur un événement déclenché par Cloud Storage.

Créer une VM depuis un Cloud Function

Le code suivant fusionne les deux étapes précédentes:

const Compute = require('@google-cloud/compute');

const compute = new Compute();

/**
* Generic background Cloud Function to be triggered by Cloud Storage.
*
* @param {object} data The event payload.
* @param {object} context The event metadata.
*/
exports.spawnVM = async (file, context) => {   
   console.log(`  Event ${context.eventId}`);
   console.log(`  Event Type: ${context.eventType}`);
   console.log(`  Bucket: ${file.bucket}`);
   console.log(`  File: ${file.name}`);
   console.log(`  Metageneration: ${file.metageneration}`);
   console.log(`  Created: ${file.timeCreated}`);
   console.log(`  Updated: ${file.updated}`);

   const zone = compute.zone('europe-west1-b');
   const name = 'my-ubuntu-vm';
   const data = await zone.createVM(name, {os: 'ubuntu'});

   const vm = data[0];
   const operation = data[1]; 

   console.log('\nLa demande de création de la VM est faite');

   console.log(JSON.stringify(await vm.getMetadata()));

   await operation.promise();

   console.log('\nLa VM est créée');
};

Nous déployons:

gcloud functions deploy spawnVM --runtime nodejs8 --trigger-resource decompress-from-bucket-2019 --trigger-event google.storage.object.finalize

A noter que la commande de déploiement est légèrement modifiée par rapport à la fois précédente : nous ne déclenchons la Cloud Function que sur la création d’un objet dans le bucket car nous avons ajouté l’option –trigger-event google.storage.object.finalize

Nous déposons un fichier dans le bucket et nous pouvons constater qu’une VM a bien été démarrée. Nous la supprimons via la console afin d’éviter de payer inutilement pour celle-ci.

Décompresser le fichier reçu

Maintenant que nous avons notre VM démarrée, il nous faut:

  • télécharger sur la VM le fichier compressé,
  • le décompresser
  • transférer les fichiers décompressés dans notre bucket Cloud Storage

Nous allons fournir un script shell à notre VM, celui-ci sera automatiquement exécuté lorsque celle-ci est prête (https://cloud.google.com/compute/docs/startupscript).

Nous modifions le code précédent avec les éléments suivants:

exports.spawnVM = async (file, context) => {   
   const zone = compute.zone('europe-west1-b');

   if ((!file.name.toLowerCase().endsWith('.tar.gz'))) {
       console.log('This is not a tar.gz file');
       return;
   }

   const bucketFile = `gs://${file.bucket}/${file.name}`

   const bashScript = `#! /bin/bash
                       function log_debug() {
                           gcloud logging write uncompress_tar_gz_logs "$1" --severity=DEBUG
                       }
                      
                       function log_error() {
                           gcloud logging write uncompress_tar_gz_logs "$1" --severity=ERROR
                       }

                       log_debug "starting startup script"

                       if ! gsutil cp gs://${file.bucket}/${file.name} . &> /dev/null; then
                           log_error "cannot download from gs://${file.bucket}/${file.name}"                           
                       fi
                    
                       if ! tar -zxvf ${file.name} &> /dev/null; then
                           log_error "cannot decompress $filename"
                       fi

                       if ! gsutil cp *.csv gs://${file.bucket} &> /dev/null; then
                           log_error "cannot upload to gs://${file.bucket}"                           
                       fi

                       log_debug "ending startup script"
                       `;

   const config = {
       os: 'ubuntu',   
       http: true,
       metadata: {
           items: [
               {
                   value: bashScript,
                   key: 'startup-script'
               },
           ],
       },
       serviceAccounts: [
           {
               email: '360728966861-compute@developer.gserviceaccount.com',
               scopes: [
               'https://www.googleapis.com/auth/cloud-platform'
               ]
           }
       ],         
   };

   const name = 'my-ubuntu-vm';
   const data = await zone.createVM(name, config);

   const vm = data[0];
   const operation = data[1]; 

   console.log('\nLa demande de création de la VM est faite');

   console.log(JSON.stringify(await vm.getMetadata()));

   await operation.promise();

   console.log('\nLa VM est créée');
};

Dans le script bash nous appelons l’API de logging de gcloud (gcloud logging write …) ce qui nous permet d’avoir des infos sur ce qui se passe dans notre VM lors de la décompression.

Quelques petites explications sont nécessaires pour l’attribut “serviceAccounts” que nous avons ajouté dans la configuration passée à la création de notre VM:

Nous avons spécifié que la VM serait identifiée comme utilisant le “service account” par défaut pour une instance compute engine dans notre projet. En l’occurrence pour notre projet le service account en question porte le nom 360728966861-compute@developer.gserviceaccount.com

La gestion des authorizations avec l’utilisation des service account mériterait un article à elle seule sur le sujet. Pour ce cas concret nous spécifions à la VM que les interactions qu’elle aura avec les autres ressources GCP se fera en tant qu’utilisateur technique ayant pour identifiant 360728966861-compute@developer.gserviceaccount.com

En l’occurrence nous souhaitons avoir des interactions en lecture et en écriture sur notre bucket “decompress-from-bucket-2019”.

Nous devons donc ajouter au service account le rôle “roles/storage.objectAdmin” (https://cloud.google.com/storage/docs/access-control/iam-roles).

Pour cela nous exécutons la commande gsutil suivante :

gsutil iam ch serviceAccount:360728966861-compute@developer.gserviceaccount.com:objectAdmin gs://decompress-from-bucket-2019

Finalement nous déployons ensuite cette nouvelle version de la Cloud Function:

gcloud functions deploy spawnVM --runtime nodejs8 --trigger-resource decompress-from-bucket-2019 --trigger-event google.storage.object.finalize

Nous déposons un fichier tar.gz dans le bucket, la VM démarre et quelques secondes plus tard les fichiers csv apparaissent dans le bucket !

Quelques améliorations nécessaires

Il nous reste un petit souci, notre VM reste allumée après avoir décompressé le fichier, ça va vite coûter cher cette histoire !

Pour limiter au maximum les coûts il serait peut-être possible d’utiliser des instances préemptives (https://cloud.google.com/compute/docs/instances/preemptible) (en quelques mots, une instance préemptive est moins chère mais on ne peut la conserver plus de 24h).

Dans la configuration transmise à l’API pour créer la VM, nous ajoutons une option pour utiliser une instance préemptive :

   const config = {
       os: 'ubuntu',   
       http: true,
       scheduling: {
           preemptible: true
       },
       metadata: {
           items: [
               {
                   value: bashScript,
                   key: 'startup-script'
               },
           ],
       },
       serviceAccounts: [
           {
               email: '360728966861-compute@developer.gserviceaccount.com',
               scopes: [
               'https://www.googleapis.com/auth/cloud-platform'
               ]
           }
       ],         
   };

Nous ajoutons le code suivant dans le script bash afin que notre VM se “suicide” à la fin de la décompression:

const bashScript = `#! /bin/bash
                   function log_debug() {
                       gcloud logging write uncompress_tar_gz_logs "$1" --severity=DEBUG
                   }
                  
                   function log_error() {
                       gcloud logging write uncompress_tar_gz_logs "$1" --severity=ERROR
                   }

                   function shutdownVM {                     
                       VMNAME=$(curl -H Metadata-Flavor:Google http://metadata/computeMetadata/v1/instance/hostname | cut -d. -f1) &&
                       ZONE=$(curl -H Metadata-Flavor:Google http://metadata/computeMetadata/v1/instance/zone | cut -d/ -f4) &&
                       log_debug "shutting down VM $VMNAME"
                       gcloud compute instances delete $VMNAME --zone $ZONE --quiet                           
                       exit
                   }

                   log_debug "starting startup script"

                   if ! gsutil cp gs://${file.bucket}/${file.name} . &> /dev/null; then
                       log_error "cannot download from gs://${file.bucket}/${file.name}"                           
                   fi
                  
                   if ! tar -zxvf ${file.name} &> /dev/null; then
                       log_error "cannot decompress $filename"
                   fi

                   if ! gsutil cp *.csv gs://${file.bucket} &> /dev/null; then
                       log_error "cannot upload to gs://${file.bucket}"                           
                   fi

                   log_debug "ending startup script"

                   shutdownVM
                   `;

Nous déployons la Cloud Function puis déposons un fichier de test dans le bucket.

Les fichiers sont bien décompressés et la VM disparaît une fois la tâche terminée !

Conclusion

La mise en oeuvre est plutôt simple et permet de contourner les quotas (temps, mémoire, puissance CPU, …) imposés par Cloud Functions afin de décompresser des fichiers de taille conséquentes.

Si vous vous inspirez de ce qui est fait ici il conviendra d’y ajouter du monitoring afin de déclencher des alertes sur des problèmes qui pourraient (inévitablement) se produire (fichier corrompu, …) en utilisant par exemple les logs, produits par le script bash, avec Stackdriver.

 

 

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.