Catégories
Astuces et Design

Création de votre premier service sans serveur avec les fonctions AWS Lambda

De nombreux développeurs connaissent au moins marginalement les fonctions AWS Lambda. Leur configuration est assez simple, mais le vaste paysage AWS peut rendre difficile la vision globale. Avec autant de pièces différentes, il peut être intimidant et frustrant de voir comment elles s'intègrent parfaitement dans une application Web normale.

Le framework Serverless est d'une grande aide ici. Il rationalise la création, le déploiement et, plus important encore, l'intégration des fonctions Lambda dans une application Web. Pour être clair, cela fait beaucoup, beaucoup plus que cela, mais ce sont les éléments sur lesquels je vais me concentrer. Espérons que ce message suscite votre intérêt et vous encourage à découvrir les nombreuses autres fonctionnalités prises en charge par Serverless. Si vous êtes complètement nouveau sur Lambda, vous voudrez peut-être d'abord consulter cette intro AWS.

Il n'y a aucun moyen que je puisse couvrir l'installation initiale et la configuration mieux que le guide de démarrage rapide, alors commencez par là pour être opérationnel. En supposant que vous disposez déjà d'un compte AWS, vous pourriez être opérationnel en 5 à 10 minutes; et si vous ne le faites pas, le guide couvre également cela.

Votre premier service sans serveur

Avant de commencer à travailler sur des éléments tels que les téléchargements de fichiers et les compartiments S3, créons une fonction Lambda de base, connectez-la à un point de terminaison HTTP et appelez-la à partir d'une application Web existante. La Lambda ne fera rien d’utile ou d’intéressant, mais cela nous donnera une belle occasion de voir à quel point il est agréable de travailler avec Serverless.

Commençons par créer notre service. Ouvrez n'importe quelle application web nouvelle ou existante que vous pourriez avoir (create-react-app est un excellent moyen d'en créer rapidement une nouvelle) et trouvez un endroit pour créer nos services. Pour moi, c'est mon lambda dossier. Quel que soit le répertoire que vous choisissez, cd dans le terminal et exécutez la commande suivante:

sls create -t aws-nodejs --path hello-world

Cela crée un nouveau répertoire appelé hello-world. Ouvrons-le et voyons ce qu'il y a dedans.

Si vous regardez dans handler.js, vous devriez voir une fonction asynchrone qui renvoie un message. Nous pourrions frapper sls deploy dans notre terminal en ce moment, et déployer cette fonction Lambda, qui pourrait ensuite être invoquée. Mais avant de le faire, rendons-le accessible sur le Web.

En travaillant avec AWS manuellement, nous aurions normalement besoin d'accéder à la passerelle API AWS, de créer un point de terminaison, puis de créer une étape et de lui indiquer de proxy pour notre Lambda. Avec sans serveur, tout ce dont nous avons besoin est un peu de configuration.

Toujours dans le hello-world annuaire? Ouvrez le fichier serverless.yaml qui y a été créé.

Le fichier de configuration est en fait livré avec passe-partout pour les configurations les plus courantes. Décommentons le http entrées, et ajoutez un chemin plus sensible. Quelque chose comme ça:

functions:
  hello:
    handler: handler.hello
#   The following are a few example events you can configure
#   NOTE: Please make sure to change your handler code to work with those events
#   Check the event documentation for details
    events:
      - http:
        path: msg
        method: get

C'est ça. Sans serveur fait tout le travail de grognement décrit ci-dessus.

Configuration CORS

Idéalement, nous voulons appeler cela à partir du code JavaScript frontal avec l'API Fetch, mais cela signifie malheureusement que nous avons besoin de CORS pour être configuré. Cette section vous guidera à travers cela.

Sous la configuration ci-dessus, ajoutez cors: true, comme ça

functions:
  hello:
    handler: handler.hello
    events:
      - http:
        path: msg
        method: get
        cors: true

C’est la section! CORS est maintenant configuré sur notre point de terminaison API, permettant une communication entre origines.

CORS Lambda tweak

Bien que notre point de terminaison HTTP soit configuré pour CORS, c'est à notre Lambda de renvoyer les bons en-têtes. C’est ainsi que CORS fonctionne. Automatisons cela en revenant dans handler.js et en ajoutant cette fonction:

const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});

Avant de revenir du Lambda, nous enverrons la valeur de retour via cette fonction. Voici l'intégralité de handler.js avec tout ce que nous avons fait jusqu'à présent:

'use strict';
const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});


module.exports.hello = async event => {
  return CorsResponse("HELLO, WORLD!");
};

Lançons-le. Type sls deploy dans votre terminal depuis le hello-world dossier.

Lorsque cela s'exécute, nous avons déployé notre fonction Lambda sur un point de terminaison HTTP que nous pouvons appeler via Fetch. Mais… est-ce? nous pourrait ouvrez notre console AWS, trouvez l'API de passerelle créée sans serveur pour nous, puis trouvez l'URL d'invocation. Cela ressemblerait à quelque chose comme ça.

La console AWS affichant l'onglet Paramètres qui inclut les paramètres de cache. Au-dessus se trouve un avis bleu contenant l'URL d'invocation.

Heureusement, il existe un moyen plus simple, qui consiste à taper sls info dans notre terminal:

Juste comme ça, nous pouvons voir que notre fonction Lambda est disponible au chemin suivant:

https://6xpmc3g0ch.execute-api.us-east-1.amazonaws.com/dev/ms

Woot, appelons-le maintenant!

Ouvrons maintenant une application Web et essayons de la récupérer. Voici à quoi ressemblera notre Fetch:

fetch("https://6xpmc3g0ch.execute-api.us-east-1.amazonaws.com/dev/msg")
  .then(resp => resp.json())
  .then(resp => {
    console.log(resp);
  });

Nous devrions voir notre message dans la console de développement.

Sortie console montrant Hello World.

Maintenant que nous avons les pieds mouillés, répétons ce processus. Cette fois, cependant, faisons un service plus intéressant et utile. Plus précisément, faisons le Lambda canonique «redimensionner une image», mais au lieu d'être déclenché par un nouveau téléchargement de bucket S3, laissez l'utilisateur télécharger une image directement sur notre Lambda. Cela supprimera la nécessité de regrouper tout type de aws-sdk ressources dans notre offre côté client.

Construire un utile Lambda

D'accord, dès le début! Cette Lambda particulière prendra une image, la redimensionnera, puis la téléchargera dans un compartiment S3. Commençons par créer un nouveau service. Je l'appelle cover-art mais cela pourrait certainement être autre chose.

sls create -t aws-nodejs --path cover-art

Comme précédemment, nous allons ajouter un chemin d'accès à notre point de terminaison HTTP (qui dans ce cas sera un POST, au lieu de GET, car nous envoyons le fichier au lieu de le recevoir) et activer CORS:

// Same as before
  events:
    - http:
      path: upload
      method: post
      cors: true

Ensuite, accordons à notre Lambda l'accès aux compartiments S3 que nous allons utiliser pour le téléchargement. Regardez dans votre fichier YAML – il devrait y avoir un iamRoleStatements section contenant le code passe-partout qui a été mis en commentaire. Nous pouvons en tirer parti en le décommentant. Voici la configuration que nous utiliserons pour activer les compartiments S3 que nous voulons:

iamRoleStatements:
 - Effect: "Allow"
   Action:
     - "s3:*"
   Resource: ("arn:aws:s3:::your-bucket-name/*")

Noter la /* a la fin. Nous ne répertorions pas les noms de compartiments spécifiques isolément, mais plutôt les chemins d'accès aux ressources; dans ce cas, ce sont toutes les ressources qui existent à l'intérieur your-bucket-name.

Étant donné que nous voulons télécharger des fichiers directement sur notre Lambda, nous devons effectuer un ajustement supplémentaire. Plus précisément, nous devons configurer le point de terminaison API pour accepter multipart/form-data comme type de support binaire. Localisez le provider dans le fichier YAML:

provider:
  name: aws
  runtime: nodejs12.x

… Et le modifier pour:

provider:
  name: aws
  runtime: nodejs12.x
  apiGateway:
    binaryMediaTypes:
      - 'multipart/form-data'

Pour faire bonne mesure, donnons à notre fonction un nom intelligent. Remplacer handler: handler.hello avec handler: handler.upload, puis changez module.exports.hello à module.exports.upload dans handler.js.

Maintenant, nous pouvons écrire du code

D'abord, prenons quelques aides.

npm i jimp uuid lambda-multipart-parser

Attendez, c'est quoi Jimp? C'est la bibliothèque que j'utilise pour redimensionner les images téléchargées. uuid servira à créer de nouveaux noms de fichiers uniques des ressources dimensionnées, avant de les télécharger sur S3. Oh et lambda-multipart-parser? C’est pour analyser les informations du fichier dans notre Lambda.

Ensuite, créons une aide pratique pour le téléchargement S3:

const uploadToS3 = (fileName, body) => {
  const s3 = new S3({});
  const  params = { Bucket: "your-bucket-name", Key: `/${fileName}`, Body: body };


  return new Promise(res => {
    s3.upload(params, function(err, data) {
      if (err) {
        return res(CorsResponse({ error: true, message: err }));
      }
      res(CorsResponse({ 
        success: true, 
        url: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}` 
      }));
    });
  });
};

Enfin, nous allons brancher du code qui lit les fichiers de téléchargement, les redimensionne avec Jimp (si nécessaire) et télécharge le résultat sur S3. Le résultat final est ci-dessous.

'use strict';
const AWS = require("aws-sdk");
const { S3 } = AWS;
const path = require("path");
const Jimp = require("jimp");
const uuid = require("uuid/v4");
const awsMultiPartParser = require("lambda-multipart-parser");


const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});


const uploadToS3 = (fileName, body) => {
  const s3 = new S3({});
  var params = { Bucket: "your-bucket-name", Key: `/${fileName}`, Body: body };
  return new Promise(res => {
    s3.upload(params, function(err, data) {
      if (err) {
        return res(CorsResponse({ error: true, message: err }));
      }
      res(CorsResponse({ 
        success: true, 
        url: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}` 
      }));
    });
  });
};


module.exports.upload = async event => {
  const formPayload = await awsMultiPartParser.parse(event);
  const MAX_WIDTH = 50;
  return new Promise(res => {
    Jimp.read(formPayload.files(0).content, function(err, image) {
      if (err || !image) {
        return res(CorsResponse({ error: true, message: err }));
      }
      const newName = `${uuid()}${path.extname(formPayload.files(0).filename)}`;
      if (image.bitmap.width > MAX_WIDTH) {
        image.resize(MAX_WIDTH, Jimp.AUTO);
        image.getBuffer(image.getMIME(), (err, body) => {
          if (err) {
            return res(CorsResponse({ error: true, message: err }));
          }
          return res(uploadToS3(newName, body));
        });
      } else {
        image.getBuffer(image.getMIME(), (err, body) => {
          if (err) {
            return res(CorsResponse({ error: true, message: err }));
          }
          return res(uploadToS3(newName, body));
        });
      }
    });
  });
};

Je suis désolé de vider autant de code sur vous, mais – ceci étant un article sur Amazon Lambda et sans serveur – je préfère ne pas collaborer au travail de grognement dans la fonction sans serveur. Bien sûr, la vôtre pourrait être complètement différente si vous utilisez une bibliothèque d'images autre que Jimp.

Exécutons-le en téléchargeant un fichier de notre client. J'utilise la bibliothèque react-dropzone, donc mon JSX ressemble à ceci:

 onDrop(files)}
  multiple={false}
>
  
Click or drag to upload a new cover

le onDrop la fonction ressemble à ceci:

const onDrop = files => {
  let request = new FormData();
  request.append("fileUploaded", files(0));


  fetch("https://yb1ihnzpy8.execute-api.us-east-1.amazonaws.com/dev/upload", {
    method: "POST",
    mode: "cors",
    body: request
    })
  .then(resp => resp.json())
  .then(res => {
    if (res.error) {
      // handle errors
    } else {
      // success - woo hoo - update state as needed
    }
  });
};

Et juste comme ça, nous pouvons télécharger un fichier et le voir apparaître dans notre bucket S3!

Capture d'écran de l'interface AWS pour les compartiments montrant un fichier téléchargé dans un compartiment provenant de la fonction Lambda.

Un détour optionnel: le bundling

Nous pouvons apporter une amélioration facultative à notre configuration. À l'heure actuelle, lorsque nous déployons notre service, Serverless zippe l'intégralité du dossier des services et l'envoie tout à notre Lambda. Le contenu pèse actuellement à 10 Mo, car tous nos node_modules sont entraînés pour la balade. Nous pouvons utiliser un bundler pour réduire considérablement cette taille. Non seulement cela, mais un bundler réduira le temps de déploiement, l'utilisation des données, les performances de démarrage à froid, etc. En d'autres termes, c'est une bonne chose à avoir.

Heureusement pour nous, il existe un plugin qui intègre facilement webpack dans le processus de construction sans serveur. Installons-le avec:

npm i serverless-webpack --save-dev

… Et ajoutez-le via notre fichier de configuration YAML. Nous pouvons déposer cela à la toute fin:

// Same as before
plugins:
  - serverless-webpack

Naturellement, nous avons besoin d'un fichier webpack.config.js, alors ajoutons-le au mix:

const path = require("path");
module.exports = {
  entry: "./handler.js",
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '.webpack'),
    filename: 'handler.js',
  },
  target: "node",
  mode: "production",
  externals: ("aws-sdk"),
  resolve: {
    mainFields: ("main")
  }
};

Notez que nous définissons target: node afin que les actifs spécifiques aux nœuds soient traités correctement. Notez également que vous devrez peut-être définir le nom du fichier de sortie sur handler.js. J'ajoute aussi aws-sdk au tableau externe afin que webpack ne le regroupe pas du tout; au lieu de cela, il laissera l'appel à const AWS = require("aws-sdk"); seul, ce qui lui permet d'être géré par notre Lamdba, lors de l'exécution. C'est OK puisque Lambdas a déjà le aws-sdk disponible implicitement, ce qui signifie qu'il n'est pas nécessaire que nous l'envoyions par câble. Finalement, le mainFields: ("main") est de dire à webpack d'ignorer tout ESM module des champs. Cela est nécessaire pour résoudre certains problèmes avec la bibliothèque Jimp.

Redéployons maintenant, et nous espérons que le pack Web sera en cours d'exécution.

Maintenant, notre code est joliment regroupé dans un seul fichier qui est 935K, qui zippe plus loin à un simple 337K. C’est beaucoup d’économies!

Bouts

Si vous vous demandez comment envoyer d'autres données à la Lambda, vous devez ajouter ce que vous voulez à l'objet de demande, de type FormData, D'avant. Par exemple:

request.append("xyz", "Hi there");

… Puis lisez formPayload.xyz dans la Lambda. Cela peut être utile si vous devez envoyer un jeton de sécurité ou d'autres informations sur le fichier.

Si vous vous demandez comment configurer les variables env pour votre Lambda, vous avez peut-être deviné maintenant que c'est aussi simple que d'ajouter des champs à votre fichier serverless.yaml. Il prend même en charge la lecture des valeurs à partir d'un fichier externe (probablement pas engagé dans git). Ce billet de blog de Philipp Müns le couvre bien.

Emballer

Sans serveur est un cadre incroyable. Je vous promets, nous avons à peine gratté la surface. J'espère que ce post vous a montré son potentiel et vous a motivé à le découvrir encore plus.

Si vous souhaitez en savoir plus, je recommanderais le matériel pédagogique de David Wells, ingénieur chez Netlify et ancien membre de l'équipe sans serveur, ainsi que le manuel sans serveur de Swizec Teller

Laisser un commentaire

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