Dans cet article nous allons revenir à des choses passionnantes comme Docker et ses outils, à savoir Compose ou Dockerfile. En effet, l'objectif ici va être de créer une application web NodeJs avec Redis en tant que Cache, d'abord en la construisant localement (build) puis en la "dockérisant". L'idée étant principalement d'obtenir une app super rapide en utilisant des technos modernes et souples, adaptables à Docker ou à Kubernetes, d'où la présence de Redis pour la gestion du cache.

Construire des applications super rapides dans Node.js en utilisant le cache Redis

Lorsque nous essayons d'optimiser nos applications, l'une des premières choses vers laquelle nous nous tournons est la mise en cache. La mise en cache implique le stockage de données dans un store hautes performances afin que ces données puissent être récupérées plus rapidement.

Redis est un store de données clé-valeur efficace qui est devenu très populaire pour la mise en cache. L'une des choses qui fait de Redis un bon choix est le nombre de structures de données prises en charge, telles que les chaînes, les hachages, les listes, les ensembles, etc. Cela nous donne une certaine flexibilité.

Dans ce tutoriel, nous allons construire une application simple dans Node.js et l’accélérer en utilisant Redis pour mettre en cache des données. Notre application sera utilisée pour récupérer les données de taux de change historiques d'une API et les afficher aux utilisateurs. Vous pouvez voir le code complet de ce tutoriel sur GitHub.

Convertisseur de devises : Webapp

Quelle est notre application ? comment et sur quoi est-elle bâtie ? Il s'agit d'un convertisseur de devises basé sur Node.js, qui utilise Redis pour la mise en cache.

Pour exécuter localement:

git clone https://github.com/gabrielsagnard/node-currency-converter.git

Conditions préalables

Pour suivre correctement, vous devez comprendre JavaScript (syntaxe ES6). Vous devrez également avoir installé Node.js et NPM sur votre machine, le tutoriel sera réalisé sur un MacBook (OS Mojave).

Vous pouvez trouver les instructions d'installation pour les deux ici. J'ai également installé le service Redis sur mon Mac depuis Homebrew avec :

Brew install redis

Mise en place

Nous allons garder notre structure de fichier très simple. Pour commencer, créez un dossier de projet et initialisez-y un nouveau projet avec cette commande:  

npm init -y
Astuce: L’indicateur -y permet de créer un fichier package.json avec des valeurs par défaut.

Nous allons utiliser le framework Express, nous devrons donc l’installer et le sauvegarder en tant que dépendance. Nous utiliserons également le client node.js redis pour communiquer avec Redis et Axios pour les appels API. Pour installer et sauvegarder les 3 sous forme de dépendances:  

npm install express redis axios 
Astuce: Si vous utilisez npm 5, vous n’avez pas besoin de spécifier l’indicateur -S ou --save pour l’enregistrer en tant que dépendance dans votre fichier package.json.

Ensuite, nous pouvons créer les fichiers nécessaires à notre application. Voici la structure de fichier que nous allons utiliser:

├── currency-converter-app    
├── views         
└── index.html    
├── package.json    
└── server.js

Notre vue de l'application sera située dans le dossier ./views, tandis que notre logique côté serveur résidera dans le fichier ./server.js.

Construire le backend de l'application

Pour commencer, dans notre fichier server.js, nous allons initialiser une application express et avoir besoin de tous nos modules nécessaires:

// ./server.js
const express = require('express');
const path = require('path');
const axios = require('axios');
const redis = require('redis');
const app = express();

Définir les itinéraires et les réponses

Ensuite, nous pouvons définir les itinéraires de base pour notre application:

// ./server.js
...
const API_URL = 'http://api.fixer.io';
app.get('/', (req, res) => {
  res.sendFile('index.html', {
    root: path.join(__dirname, 'views')
  });
});
app.get('/rate/:date', (req, res) => {
  const date = req.params.date;
  const url = `${API_URL}/${date}?base=USD`;
  axios.get(url).then(response => {
    return res.json({ rates: response.data.rates });
  }).catch(error => {
    console.log(error);
  });

});

De ce qui précède, nous pouvons voir que notre application a 2 routes principales:

  • / - l’itinéraire de base qui sert à la vue principale de notre application. Il affiche le fichier index.html du dossier views en utilisant sendFile.
  • / rate /: date - récupère le taux pour une date spécifiée, à l'aide de l'API de fixateur. Il renvoie la réponse de l'API en tant que JSON.

Pour démarrer l'application:

// ./server.js
...
const port = process.env.port || 5000;

app.listen(port, () => {
  console.log(`App listening on port ${port}!`)
});

Construire l'application

Nous pouvons maintenant créer une vue de base pour notre application. Nous allons importer Bulma (mon choix actuel de framework CSS) pour tirer parti de certains styles prédéfinis et améliorer notre vision:

<!-- ./views/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Currency Converter!</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">Currency💰Converter!</h1>
        <p class="subtitle">Get historical data about exchange rates quickly.</p>
      <div class="content">
        <blockquote>
          BASE RATE IS <strong>USD</strong>. 
          GET HISTORICAL RATES FOR ANY DAY SINCE 1999. 
          THANKS TO <a href="http://fixer.io/">FIXER.IO</a>
        </blockquote>
      </div>
      <div class="columns">
        <form id="rateForm">

          <div class="column">

            <div class="field">
              <div class="control">
                <input id="rateDate" name="date" class="input" type="date" required>
              </div>
            </div>
            <div class="field">
              <div class="control">
                <button class="button is-primary is-stretched">
                  Get rates
                </button>
              </div>
            </div>

          </div>

        </form>
      </div>
      <div class="notification is-link is-hidden" id="visits-count">
      </div>
      <!-- container for results -->
      <div class="columns is-multiline is-mobile" id="results">
      </div>
    </div>
  </section>
</body>
</html>

Le code ci-dessus contient un balisage de base pour notre application. Ensuite, nous écrirons le code JavaScript pour récupérer les taux de notre backend et les afficher. Nous importons à nouveau axios pour nos appels API:

<!-- ./views/index.html -->
...
<!-- importing axios for API calls -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  const datePicker = document.querySelector("#rateDate");
  const form = document.querySelector("#rateForm");
  const submitButton = document.querySelector("#rateForm button");
  form.onsubmit = e => {
    e.preventDefault()
    submitButton.classList.add("is-loading");

    const date = datePicker.value;
    const url = `/rate/${date}`;

    axios.get(url).then(response => {
      submitButton.classList.remove("is-loading");
      showRates(response.data.rates);
    }).catch(error => {
      console.log(error);
    });

  }
  function showRates(rates) {
    let html = '';

    for (rate in rates) {
      html += '<div class="column is-one-quarter"><div class="card"><div class="card-content">';
      html += `<p class="title">${rates[rate]}</p>
        <p class="subtitle">${rate}</p>`;
      html += '</div></div></div>';
    };

    document.querySelector("#results").innerHTML = html;
  }
  // IIFE - Executes on page load
  (function () {
    // set today's date as default date
    datePicker.valueAsDate = new Date();
  })();
</script>
</body>
</html>

Maintenant, nous pouvons tester notre application:

node server.js

Vous devriez voir ceci lorsque vous visitez le site http://localhost:5000.

Mise en cache de données à l'aide de Redis

Ensuite, nous allons nous connecter à un store Redis et commencer à mettre en cache les données d'historiques. Nous n’avons donc pas besoin de récupérer les données de l’API Fixer chaque fois qu’une nouvelle demande est faite pour la même date.  

Ainsi, par exemple, si une demande est faite pour les taux de change au 1er juin 2000 une fois par la Personne A, lorsque la Personne B demande les taux pour la même date, notre application extrait les données du cache au lieu de frapper l'API. Cela accélérera le temps de réponse de notre application.  Nous allons également enregistrer le nombre de demandes effectuées pour une date donnée dans notre store Redis et l'incrémenter à chaque nouvelle demande de tarif pour cette date.

Connexion à Redis

Nous communiquerons avec Redis en utilisant le client node_redis Node.js. Pour vous connecter à Redis:

// ./server.js
...
// connect to Redis
const REDIS_URL = process.env.REDIS_URL;
const client = redis.createClient(REDIS_URL);

client.on('connect', () => {
    console.log(`connected to redis`);
});
client.on('error', err => {
    console.log(`Error: ${err}`);
});
...

Nous définissons REDIS_URL obtenue à l'étape précédente comme une variable d'environnement et le transmettons à redis.createClient ().  

Remarque: Par défaut, redis.createClient () utilisera 127.0.0.1 comme nom d’hôte et 6379 comme port. Si votre configuration est différente, vous pouvez vous connecter à l'aide de l'URL redis ou en fournissant le nom d'hôte et le port. Pour obtenir une liste des options de connexion, vous pouvez consulter la documentation officielle ici.

Nous utilisons également les événements de connexion et d'erreur pendant le développement pour vérifier si une connexion a été établie et pour détecter les erreurs existantes. Pour une liste des événements disponibles, visitez ici.

Enregistrement et récupération des données de cache

Maintenant, nous pouvons stocker et récupérer des données de notre magasin Redis. Nous mettrons à jour la fonction qui récupère nos taux afin de pouvoir vérifier les données de notre cache avant de passer l'appel API, et incrémentons également le nombre de demandes de taux pour une date donnée.  

Remarque: Toutes les commandes Redis sont exposées en tant que fonctions sur l'objet client.

Mise à jour de la fonction qui récupère nos tarifs:

// ./server.js
...
app.get('/rate/:date', (req, res) => {
  const date = req.params.date;
  const url = `${API_URL}/${date}?base=USD`;
  const countKey = `USD:${date}:count`;
  const ratesKey = `USD:${date}:rates`;

  client.incr(countKey, (err, count) => {
    client.hgetall(ratesKey, function(err, rates) {
      if (rates) {
        return res.json({ rates, count });
      }
      axios.get(url).then(response => {
        // save the rates to the redis store
        client.hmset(
          ratesKey, response.data.rates, function(err, result) {
          if (err) console.log(err);
        });

        return res.json({ 
          count,
          rates: response.data.rates
        });
      })
      .catch(error => {
        return res.json(error.response.data)
      }));

    });
  });
});
...

Lancer l'application!

Pour exécuter l'application:  REDIS_URL = node YOUR_REDIS_URL server.js

Si vous exécutez Redis localement, vous pouvez simplement faire

node server js

Bilan de la première partie

Super nous avons désormais pu créer une application NodeJs fonctionnelle avec Redis comme moteur de cache, l'application tourne en local avec nodejs. Toutefois, il faut se poser rapidement de la question de la production.

Comment faire tourner cette application web en prod' avec efficacité, simplicité et souplesse tout en utilisant des technologies modernes ?

En utilisant Docker par exemple et les containers ! pour plus de fun nous créerons également un lien "Gitops" avec Github.

Docker-compose

Commençons en créant un fichier docker-compose.yml :

version: '3'
services:
  web:
    build: .
    volumes:
      - .:/usr/src/site
      - /usr/src/site/node_modules
    links:
      - redis
    environment:
      - REDIS_URL=redis://cache
    ports:
      - 5000:5000
  redis:
    image: redis
    container_name: cache
    expose:
    - 6379

Puis le Dockerfile pour builder l'application :

FROM node:8
 WORKDIR /usr/src/site/
COPY package*.json ./
RUN npm install
 COPY . .
 EXPOSE 5000
 CMD ["npm", "start"]

Une fois ceci fait nous allons tenter de faire fonctionner l'application localement à partir de Docker :

docker-compose up -d 

Ce qui va nous donner  :

Cela fonctionne parfaitement :)

Maintenant que tout cela fonctionne bien, sauvegardons nos travaux sur notre plate-forme préférée : Github.

Gitops = Github love

On va commencer par créer un repository Github de notre projet :

Puis dans notre terminal nous allons nous rendre dans le bon dossier de notre application.

git init

Après cela, créons les conditions nécessaires à un joli commit :

MBP-de-admin:node-currency-app admin$ echo "node_modules" > .gitignore
MBP-de-admin:node-currency-app admin$ git add .

Puis on commit :

MBP-de-admin:node-currency-app admin$ git commit -m "Initial commit"
[master (commit racine) 4ac8b0c] Initial commit

Puis on pushe :

MBP-de-admin:node-currency-app admin$ git remote add origin https://github.com/gabrielsagnard/node-currency-app.git
MBP-de-admin:node-currency-app admin$ git push -u origin master

Il ne reste désormais plus qu'à effectuer des modifs sur votre repo et synchroniser tout cela avec votre terminal local.

Alléger le poids de notre image Docker

Si nous utilisons la commande docker images, nous nous apercevons que l'image actuelle pèse un poids conséquent :

MBP-de-admin:node-currency-app admin$ docker images
REPOSITORY                                    TAG                 IMAGE ID            CREATED             SIZE
node-currency-app                             1.1                 33046e46541b        6 days ago          677MB

C'est assez logique puisque dans notre fichier yml de Docker-compose, nous utilisons une image générique de NodeJs basée sur Ubuntu par défaut. Or, si vous lisez ce blog en long, en large et en travers, vous saurez qu'il existe une alternative intéressante chez Docker pour alléger le fameux poids de nos containers :

Alpine Linux !!!

Cette solution est fantastique et simple à utiliser, il suffit de modifier nos images NodeJs et Redis utilisées dans notre fichier docker-compose comme ceci :

version: '3'
services:
  web:
    build: .
    volumes:
      - .:/usr/src/site
      - /usr/src/site/node_modules
    links:
      - redis
    environment:
      - REDIS_URL=redis://cache
    ports:
      - 5000:5000
  redis:
    image: redis:5-alpine
    container_name: cache
    expose:
    - 6379

Et dans notre Dockerfile :

FROM node:8-alpine
WORKDIR /usr/src/site/
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD ["npm", "start"]

En ajoutant le tag -alpine à nos images, nous allons considérablement alléger le poids du déploiement et des containers, tentons de la faire tourner localement désormais :

MBP-de-admin:node-currency-app admin$ sudo docker-compose up
Pulling redis (redis:5-alpine)...
5-alpine: Pulling from library/redis
4fe2ade4980c: Already exists
fb758dc2e038: Pull complete
989f7b0c858b: Pull complete
c5bb833489b0: Pull complete
2d61c5f31c80: Pull complete
22f79e14b074: Pull complete
Building web
Step 1/7 : FROM node:8-alpine
8-alpine: Pulling from library/node
4fe2ade4980c: Already exists
0ade4f5bae45: Pull complete
fd1dceaf77f3: Pull complete
Digest: sha256:fa979a27cee3c8664a689e27e778b766af72da52920454160421854027374f09
Status: Downloaded newer image for node:8-alpine
 ---> dc30b65a4cfe
Step 2/7 : WORKDIR /usr/src/site/
 ---> Running in 48315efbde9d
Removing intermediate container 48315efbde9d
 ---> 732bffaff241
Step 3/7 : COPY package*.json ./
 ---> 6c88ae448719
Step 4/7 : RUN npm install
 ---> Running in a81531e55d7b
npm WARN node-currency-converter@1.0.0 No repository field.

added 60 packages from 44 contributors and audited 131 packages in 3.537s
found 0 vulnerabilities

Removing intermediate container a81531e55d7b
 ---> 89d871629421
Step 5/7 : COPY . .
 ---> 39a28ea4d6cf
Step 6/7 : EXPOSE 5000
 ---> Running in bb177ecaca0e
Removing intermediate container bb177ecaca0e
 ---> 60be528fdeb9
Step 7/7 : CMD ["npm", "start"]
 ---> Running in 76042cea61c1
Removing intermediate container 76042cea61c1
 ---> b3152b5361e4
Successfully built b3152b5361e4
Successfully tagged node-currency-app_web:latest
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating cache ... done
Creating node-currency-app_web_1 ... done
Attaching to cache, node-currency-app_web_1
cache    | 1:C 10 Dec 2018 16:15:22.661 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
cache    | 1:C 10 Dec 2018 16:15:22.661 # Redis version=5.0.2, bits=64, commit=00000000, modified=0, pid=1, just started
cache    | 1:C 10 Dec 2018 16:15:22.661 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
cache    | 1:M 10 Dec 2018 16:15:22.662 * Running mode=standalone, port=6379.
cache    | 1:M 10 Dec 2018 16:15:22.662 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
cache    | 1:M 10 Dec 2018 16:15:22.662 # Server initialized
cache    | 1:M 10 Dec 2018 16:15:22.662 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
cache    | 1:M 10 Dec 2018 16:15:22.662 * Ready to accept connections
web_1    |
web_1    | > node-currency-converter@1.0.0 start /usr/src/site
web_1    | > node server.js
web_1    |
web_1    | App listening on port 5000!
web_1    | connected to redis
Killing node-currency-app_web_1  ... done
Killing cache                    ... done

Et en localhost :

avec alpine

C'est fonctionnel et rapide, un coup d'oeil sur le poids :

    REPOSITORY                                    TAG                 IMAGE ID            CREATED             SIZE
node-currency-app_web                         latest              b3152b5361e4        47 hours ago        73.4MB

73,4 MB plutôt que 677 MB !! quel gain !

Poussons tout cela vers Github histoire d'actualiser le repository.

Et voici le résultat depuis mon iPhone :

Gitops, second time

On va désormais actualiser notre repository en modifiant depuis le terminal de notre machine de développement, les éléments de notre repo pour mettre à jour des commandes ou des services de notre application.

Déploiement sur VPS

Comme prévu nous allons désormais déployer notre application qui fonctionne parfaitement en local vers un VPS Linux sur Ubuntu 16.04 avec Docker pour faire tourner les micro-services.

On commence par se connecter à notre VPS:

sudo ssh root@xxxxxxx

Une fois connecté, nous allons git cloner le repository à la racine du serveur :

git clone https://github.com/gabrielsagnard/node-currency-converter.git

Puis nous nous glissons dans le dossier avec :

cd node-currency-converter

Et enfin nous allons lancer la commande magique pour déployer les apps :

docker-compose up -d

Avec docker ps voici ce que vous pouvez constater :

5d2773ef93d0        node-currency-converter_web   "npm start"              19 minutes ago      Up 19 minutes       0.0.0.0:5000->5000/tcp                                                             node-currency-converter_web_1_883717da8519
1e07eab6db29        redis:5-alpine                "docker-entrypoint.s…"   19 minutes ago      Up 19 minutes       6379/tcp

Tout est fonctionnel, reste à tester en live depuis le navigateur :

Monitoring du service

Et enfin pour monitorer notre webapp NodeJS ainsi que le cache, sans oublier l'ensemble des composantes du serveur que nous souhaitons surveiller, nous allons déployer dockprom, déjà utilisée plusieurs fois sur ce blog.

Bibliographie :