lundi , 15 octobre 2018
Home » Tutoriel » Comment créer une application sans serveur pour le traitement d’image à la demande ?

Comment créer une application sans serveur pour le traitement d’image à la demande ?

Développer une application sans serveur à la demande qui met à jour l’échelle, la qualité et le format d’une image depuis votre navigateur.

Après avoir exploré les fonctions d’AWS Lambda pendant quelques mois, on commence à voir les énormes avantages de la création de petites applications sans serveur.

Tout d’abord, vous n’avez pas besoin d’un serveur – évidemment. Le problème avec la création d’applications côté serveur a toujours été le coût de maintenance d’un serveur qui déploie et héberge une application, et de payer pour les ressources inactives en attendant que quelqu’un interagisse avec elles.

Bien sûr, vous pouvez trouver des fournisseurs d’hébergement NodeJS gratuits sur le Web comme Heroku – mais vous serez confrontés à des restrictions de compte, vous ne pourrez pas déployer des centaines de petits micro-services et ne serez pas facturés uniquement pour un usage réel.

C’est là qu’AWS Lambda vient à la rescousse.

  1. Vous pouvez déployer autant de fonctions Lambda que vous le souhaitez, et essentiellement gratuitement en utilisant le compte tiers AWS.
  2. Vous êtes facturé uniquement pour l’utilisation réelle de vos fonctions – sans avoir besoin de maintenir et de payer pour un serveur entier

Un autre avantage intéressant est la vitesse. Les fonctions Lambda sont exécutées dans un temps incroyablement rapide – normalement entre 100 et 500 ms.

Contrairement à Docker, vous ne devrez jamais attendre que l’environnement virtuel soit amorcé pour que votre code soit exécuté. L’utilisation de fonctions Lambda est vraiment comme avoir une machine serveur très puissante toujours opérationnelle – mais sans payer le coût.

Avec le compte tiers gratuit AWS, vous bénéficiez gratuitement de 400 000 secondes d’exécution Lambda par mois. Basé sur une moyenne de 500ms par invocation lambda, cela signifie que vous pouvez invoquer vos fonctions 800.000 fois/mois complètement gratuit. Pas mal du tout.

Créer une application sans serveur pour l’optimisation de l’image

Cette fois, on va créer une application NodeJS chargée de fournir des images à une application client.

L’application doit être capable d’augmenter et de réduire automatiquement les images en fonction de la taille de l’écran du client. Ainsi, nous pouvons éviter de créer et de stocker plusieurs variantes de mêmes images requises pour les versions mobile, tablette et bureau. La qualité et le format de l’image doivent être changés à la demande – sans avoir besoin de stocker toutes les différentes images.

Cela semblait être un travail parfait pour utiliser les fonctions AWS Lambda.

La configuration de NodeJS

La première étape est de configurer NodeJS pour pouvoir tester notre code localement et ne pas avoir à redéployer notre code chaque fois qu’on voulait valider les modifications. Pour commencer, créez un nouveau dossier sur votre machine locale pour héberger le nouveau projet.

$ mkdir serverless-image-rendering && cd $_

Puis initialisez un nouveau projet npm et appuyez sur Entrée pour accepter les valeurs par défaut.

$ npm init

Maintenant, nous allons créer une ancienne application Express pour écouter sur votre port local 3000. Créez donc un nouveau fichier app.js et collez le code suivant à l’intérieur :

const app = require('express')();
const bodyParser = require('body-parser');
const PORT = 3000;
app.use(bodyParser.json());
const displayStatus = () => ({ status: `OK`, });
app.get('/status', (req, res) => { res.status(200).send(displayStatus());
});
const server = app.listen(PORT, () => console.log('Listening on ' + `http://localhost:${server.address().port}`));

Pour cette application, nous allons avoir besoin de 2 paquets npm Express et body-parser. Ces 2 paquets ne seront nécessaires que pour tester votre application localement, nous allons donc les installer dans vos dépendances de développement – cela évitera leur inclusion dans votre fonction Lambda.

$ npm i -D express body-parser

Normalement, on installe également nodemon globalement sur la machine – il surveille tous les changements de fichiers qui vont automatiquement redémarrer l’application.

$ npm i -g nodemon

Ensuite, vous pouvez démarrer votre application de serveur local :

$ nodemon app.js

Vous devriez maintenant pouvoir ouvrir votre navigateur sur http://localhost:3000/status et être capable de voir un message « status » : « OK« .

Comment récupérer vos images depuis S3 ?

On aime utiliser S3 pour stocker toutes les images, et avoir la fonction récupérer une image depuis le seau S3 pour le redimensionnement et la livraison à l’application cliente. Donc, on va utiliser AWS pour créer un seau S3 et le nommer image-bucket.

Ensuite, on aura besoin d’une classe d’image-fetcher pour ouvrir notre seau S3, trouver notre image cible et la renvoyer à notre application. Pour ce faire, créez simplement un image-fetcher.js dans un dossier src et collez le code suivant :

const AWS = require('aws-sdk');
const getS3 = (s3, bucketName, fileName) => new Promise((res, rej) => {s3.getObject({
    Bucket: bucketName,
    Key: fileName
  }, 
  (err, data) => {
    if (err) {
      return rej(err);
    }
const contentType = data.ContentType;
    const image = data.Body;
 return res({ image, contentType });
  });
});
class ImageFetcher {
  constructor(bucketName) {
    this.S3 = new AWS.S3();
    this.bucketName = bucketName;
  }
  fetchImage(fileName) {
    if (!fileName) {
      return Promise.reject('Filename not specified');
    }
  return Promise.resolve(
    getS3(this.S3, this.bucketName, fileName)));
  }
}
module.exports = ImageFetcher;

Cette classe ImageFetcher va tenter de lire un fichier stocké dans bucketName et retourner l’image si elle est trouvée.

Ok, maintenant nous pouvons définir notre fichier app.js pour consommer cette classe pour récupérer et livrer une image au navigateur. Alors, créons un point de terminaison /fetch-image !

// app.js
const ImageFetcher = require('./src/image-fetcher');
...
app.get('/fetch-image', (req, res) => {
  const imageFetcher = new ImageFetcher(process.env.BUCKET);
  const fileName = req.query && req.query.f;
return imageFetcher
    .fetchImage(fileName)
    .then(data => {
      const img = new Buffer(data.image.buffer, 'base64');
      res.writeHead(200, {
        'Content-Type': data.contentType
      });
      res.end(img);
    })
    .catch(error => {
      console.error(error);
      res.status(400).send(error.message || error);
    });
});

Vous devriez maintenant être capable de récupérer et d’afficher une image présente dans votre seau images-bucket S3 précédemment créé.

Notez que nous passons une variable process.env.BUCKET dans notre constructeur ImageFetcher. Cette variable est récupérée à partir de vos variables d’environnement système. Nous devons donc passer manuellement cette variable à notre application. A partir de maintenant, sur notre terminal, nous devrons lancer notre fichier app.js de cette manière :

$ BUCKET=images-bucket nodemon app.js

Cela assurera qu’une variable d’environnement BUCKET sera présente et définie sur notre nom de seau S3.

Maintenant, nous pouvons ouvrir un navigateur à notre nouveau point de terminaison appelé http://localhost:3000/fetch-image et passer un nom de fichier comme une chaîne de requête – bien que nous n’ayons aucune image dans notre seau pour le moment.

Téléchargez manuellement une nouvelle image appelée sample.jpg dans votre image-bucket et ouvrez votre navigateur sur http://localhost:3000/fetch-image?F=sample.jpg

Un message d’erreur doit apparaître sur l’écran. En effet, vous n’avez probablement pas l’accès à lire votre seau S3 pour le moment.

Créer un utilisateur AWS

Vous devez créer un nouvel utilisateur IAM dans AWS et configurer votre ordinateur local pour qu’il utilise ces informations d’identification pour accéder à votre seau S3.

Tout d’abord, créez une nouvelle référence à partir de votre tableau de bord AWS IAM sur https://console.aws.amazon.com/iam

Cliquez sur Utilisateurs, puis sur Créer un nouvel utilisateur

Créez un nouvel utilisateur appelé serverless-image-rendering et assurez-vous que l’option d’accès par programmation (Programmatic access) est sélectionnée – ceci sera nécessaire pour Lambda dans les étapes ultérieures.

Créez et nommez un Nouveau Groupe et vérifiez le « AdministratorAccess » à partir des stratégies répertoriées. Maintenant, tout ce que vous avez à faire est de créer un fichier d’identification (credentials) sous votre dossier ~/.aws, et collez vos informations IAM à l’intérieur en utilisant le format suivant :

[serverless-image-rendering] aws_access_key_id=YOUR_USER_ACCESS_KEY aws_secret_access_key=YOUR_USER_SECRET

Vous pouvez définir vos préférences locales pour utiliser ce profil en utilisant votre terminal avec la commande suivante :

export AWS_PROFILE=serverless-image-rendering

Maintenant, vous devriez être capable de créer votre application NodeJS, et ouvrez votre navigateur sur http://localhost:3000/fetch-image?F=sample.jpg, et vous pourriez voir votre image S3 apparaissant sur votre écran !

Créer la fonction pour le traitement d’image

L’élément fondamental de notre application est le processeur d’image responsable de la mise à l’échelle dynamique et de la modification de la qualité de votre image source.

Pour servir ce but, on va utiliser Sharp. L’implémentation est vraiment simple – c’est la classe que nous devons créée dans un nouveau fichier src/image-resizer.js

class ImageResizer {
  constructor(Sharp) {
    this.sharp = Sharp;
  }
  
  resize(image, size, quality) {
    if (!image) throw new Error('An Image must be specified');
    if (!size) throw new Error('Image size must be specified');
return new Promise((res, rej) => {
      this.sharp(new Buffer(image.buffer))
        .resize(size.w, size.h)
        .webp({quality: quality})
        .toBuffer()
        .then(data => {
          return res({
            image: data,
            contentType: 'image/webp',
          });
        })
        .catch(err => rej(err))
    });
  }
}
module.exports = ImageResizer;

La méthode de redimensionnement (resize) va recevoir un tampon d’image, un objet de taille contenant la valeur de largeur et de hauteur pour la nouvelle image et un attribut de qualité.

Tout d’abord, installons Sharp dans notre projet.

$ npm i -S sharp

Ensuite, créons un nouveau point de terminaison de resize-image dans notre application Express pour consommer ImageResizer.

// app.js
const Sharp = require('sharp');
const ImageResizr = require('./src/image-resizer');
...
app.get('/resize-image', (req, res) => {
  const imageFetcher = new ImageFetcher(process.env.BUCKET);
  const imageResizr = new ImageResizer(Sharp);
  const fileName = req.query && req.query.f;
  const quality = req.query && +req.query.q || 80;
  const size = {
    w: req && +req.query.w || 800,
    h: req && +req.query.h || null,
  };
  return imageFetcher
    .fetchImage(fileName)
    .then(data => imageResizr.resize(data.image, size, quality))
    .then(data => {
      const img = new Buffer(data.image.buffer, 'base64');
      res.writeHead(200, {
        'Content-Type': data.contentType
      });
      res.end(img);
    })
    .catch(error => {
      console.error('Error:', error);
      res.status(400).send(error.message || error);
    });
});

Cool ! Faisons un essai.

Amorcez une nouvelle fois votre application Node, et cette fois ouvrez votre navigateur sur http://localhost:3000/resize-image?F=sample.jpg

Par défaut, la taille de l’image va être 800px et la qualité à 80%. Cependant, nous pouvons maintenant changer la taille et la qualité en passant simplement une chaîne de requête à l’URL. Nous pouvons spécifier une largeur d’image à l’aide de la touche w, la hauteur avec la touche h, et définir une qualité personnalisée en utilisant la touche q.

http://localhost:3000/resize-image?f=sample.jpg&w=600&q=10

Nous pouvons maintenant afficher notre image redimensionnée à 600 pixels avec une qualité de 10% en collant simplement nos valeurs préférées en tant que paramètres dans la barre d’adresse.

Maintenant, commençons avec le sans serveur !

Nous venons de créer une application NodeJS normale – donc rien ne fonctionne encore sans serveur. Est-ce juste une faute de frappe dans le nom de l’article ? Bien sûr que non !

L’ajout de serverless est quelque chose que vous pouvez facilement faire en plus de votre application NodeJS conventionnelle. Tout ce dont nous avons besoin est un fichier de configuration sans serveur appelé serverless.yml que nous allons créer dans le répertoire racine de notre projet.

Pour ce projet spécifique, nous allons également installer deux plugins sans serveur appelés serverless-apigw-binary et serverless-apigy-binary. La structure sans serveur configurera automatiquement la passerelle API AWS pour servir la réponse au format application/json, mais nous devons fournir une image. Nous devons donc réécrire le document ContentType pour qu’il soit image/webp.

Commençons par installer tous les modules Node dont nous avons besoin pour cette dernière étape

$ npm i -S serverless-apigw-binary serverless-apigwy-binary

Maintenant, ouvrez votre nouveau fichier serverless.yml et collez la configuration suivante à l’intérieur :

service: serverless-image-rendering
custom:
  apigwBinary:
  types:
    - '*/*'
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: us-east-1
  timeout: 5 # optional, in seconds, default is 6
  role: ImageRenderingRole
environment:
  BUCKET: images-bucket
plugins:
  - serverless-apigw-binary
  - serverless-apigwy-binary
functions:
  resizeImage:
  handler: handler.resizeImage
  events:
    - http:
      path: resize-image
      method: get
      contentHandling: CONVERT_TO_BINARY
resources:
  Resources:
    ImageRenderingRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: ${self:service}-S3-ACCESS
        AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ${self:service}-s3-access
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "s3:GetObject"
                Resource:
                  - 'arn:aws:s3:::${self:provider.environment.BUCKET}/*'

Cette configuration va créer une fonction Lambda appelée « resizeImage », qui appelle une fonction resizeImage située dans un fichier handler.js

functions:
 resizeImage:
 handler: handler.resizeImage

Il va également configurer votre API Gateway pour appeler cette fonction sur n’importe quelle requête GET vers un chemin resize-image et retourner la réponse au format binaire.

events:
    - http:
      path: resize-image
      method: get
      contentHandling: CONVERT_TO_BINARY

Serverless créera également pour vous un nouveau rôle AWS IAM appelé « serverless-image-rendering-S3-ACCESS » pour permettre à la fonction Lambda de lire depuis votre Seau S3.

Bien que vous puissiez également créer tout cela manuellement à partir de votre tableau de bord AWS, le framework sans serveur vous fera gagner beaucoup de temps et de configuration manuelle.

Depuis Express à Lambda

Dans l’étape précédente, on a parlé d’un fichier handler.js – mais ce que nous avons maintenant c’est un fichier app.js. En effet, nous ne pouvons pas exécuter notre application Express sur Lambda, nous devons donc créer un nouveau fichier à télécharger sur AWS. Ce sera similaire à notre app.js précédent mais sans Express.

Alors, créons un nouveau fichier handler.js dans le dossier racine de votre projet. Nous pouvons simplement le coller à l’intérieur de la logique de resize-image précédente, et le convertir en code Lambda comme ceci :

const Sharp = require('sharp');
const ImageFetcher = require('./src/s3-image-fetcher');
const ImageResizer = require('./src/image-resizer');
module.exports.resizeImage = (event, context, callback) => {
  const imageFetcher = new ImageFetcher(process.env.BUCKET);
  const imageResizer = new ImageResizer(Sharp);
  const fileName = event.queryStringParameters && event.queryStringParameters.f;
  const quality = event.queryStringParameters && +event.queryStringParameters.q || 80;
  const size = {
    w: event && +event.queryStringParameters.w || 800,
    h: event && +event.queryStringParameters.h || null,
  };
  return imageFetcher.fetchImage(fileName)
    .then(data => 
      imageResizer.resize(data.image, size, quality))
    .then(data => {
      const contentType = data.contentType;
      const img = new Buffer(data.image.buffer, 'base64');
      callback(null, {
        statusCode: 200,
        headers: { 'Content-Type': contentType },
        body: img.toString('base64'),
        isBase64Encoded: true,
      });
    })
    .catch(error => {
      console.error('Error:', error);
      callback(null, error);
    });
};

C’est à peu près le même code que ce que nous avons écrit auparavant – mais nous devons spécifier « isBase64Encoded » pour que Lambda puisse lire correctement notre image.

Déploiement de code à l’aide de CLI Serverless

Ok, nous sommes maintenant prêts à déployer notre code en direct ! La première étape nécessite d’installer globalement Serverless sur votre machine avec la commande suivante :

$ npm i -g serverless

Maintenant, nous pouvons facilement déployer tout le code que nous avons créé :

$ serverless deploy

Cette opération prendra quelques minutes. Serverless va empaqueter votre application locale contenant toutes les dépendances de nœud dans un fichier zip et la télécharger dans un nouveau conteneur S3. Ensuite, il va créer un nouvel identifiant IAM, une API Gateway et une fonction Lambda.

Lorsque le processus de déploiement est terminé, votre nouveau terminal Lambda apparaît dans votre terminal. Vous pouvez également récupérer vos informations AWS à tout moment avec la commande suivante :

$ serverless info

Et vous verrez quelque chose comme ceci en réponse :

Service Information
service: serverless-image-rendering
stage: dev
region: us-east-1
stack: serverless-image-rendering-dev
api keys:
  None
endpoints:
  GET - https://xxx.us-east-1.amazonaws.com/dev/resize-image
functions:
  resizeImage: serverless-image-rendering-dev-resizeImage

Vous devriez maintenant pouvoir copier le point de terminaison GET et le coller dans votre navigateur. Vous pourrez voir votre nouvelle application sans serveur fonctionner en transmettant les mêmes paramètres que ceux utilisés précédemment dans votre application et votre environnement locaux.

For example:
https://xxx.us-east-1.amazonaws.com/dev/resize-image?f=sample.jpg&w=600&q=10

Problèmes de modules de nœud

Vous pourriez rencontrer un problème lors du déploiement de la fonction Lambda. Le problème est que vos modules de nœuds sont installés pour une configuration d’environnement incorrecte par rapport à AWS. Par conséquent, certains packages comme Sharp peuvent ne pas fonctionner dans votre fonction Lambda. Pour le résoudre, AWS a publié une image Docker appelée lambci/lambda que vous pouvez utiliser pour installer tous les modules de nœud avant de lancer le déploiement sans serveur.

Voici un dépôt GitHub où vous pouvez voir le code lié à cet article – n’hésitez pas à cloner et créer votre propre application de traitement d’image en utilisant une fonction Lambda. On est impatient de lire vos commentaires !

À lire aussi

Les compétences en machine learning que les ingénieurs logiciels doivent avoir

Vous n’avez pas besoin d’être un scientifique des données pour faire de machine learning (apprentissage …