Crawler un grand volume de pages web

Crawler et scraper les données présentes sur le net est une tâche amusante. C’est relativement facile et gratifiant puisque vous obtenez très vite des résultats concrêts. Cependant, passer d’un crawler basique (écrit sous forme de petit script Python par exemple) à un crawler rapide permettant d’acquérir un grand volume de données, n’est pas quelque chose de simple. Je vais tenter de vous décrire deux ou trois défis que vous rencontrerez lorsque vous vous lancerez dans ce type de projet.

Concurrence

Introduire de la concurrence est absolument central dans la plupart des applications modernes, et c’est particulièrement vrai en ce qui concerne les applications exigeant un grand nombre d’accès réseaux tels que les crawlers. En effet, puisque chaque requête HTTP que vous déclenchez prend un temps monstre avant de répondre, vous avez tout intérêt à les lancer en parallèle plutôt qu’en séquentiel. En gros cela signifie que si vous crawlez 10 pages prenant 1 seconde chacune, cela vous prendra approximativement 1 seconde au total plutôt que 10 secondes.

La concurrence est donc critique, mais comment la mettre en place ?

L’approche naïve, qui fonctionne parfaitement pour une petite application, est de coder vous-même le déclenchement des jobs en parallèle, attendre les résultats, et les traiter. Typiquement en Python vous lanceriez plusieurs processus, et en Go (qui se prête mieux à ce type de travail) vous devriez créer des goroutines. Mais gérer tout cela manuellement peut vite devenir compliqué : comme vos ressources en RAM et CPU sont limitées, vous ne pouvez pas indéfiniment lancer vos jobs en parallèle, donc comment gérer les queues, et comment gérer les retries lorsque des jobs échouent (et vous pouvez être certain que cela arrivera) ou lorsque votre serveur s’arrête pour différentes raisons ?

L’approche la plus robuste est de mettre à profit un système de messaging tel que RabbitMQ. Chaque nouvelle URL récupérée lors du parsing peut désormais être mise en queue dans RabbitMQ, et toute nouvelle page que votre application souhaite crawler doit être prise dans la queue de RabbitMQ. Le nombre de requêtes concurrentes que vous souhaitez atteindre est simplement un paramètre à déclarer dans RabbitMQ.

Bien entendu, même lorsque l’on utilise un système de messaging, le choix du langage de programmation reste important : déclencher 100 jobs en parallèle en Go vous coûtera beaucoup moins de ressources qu’en Python par exemple (raison en partie pour laquelle j’apprécie beaucoup Go !).

Scalabilité

A un certain point, peu importe le degré d’optimisation que votre crawler aura atteint, vous serez limité par des ressources hardware.

La première solution est d’upgrader les performances de votre serveur (ce que l’on appelle “scalabilité verticale”). C’est facile à faire, mais une fois que vous aurez atteint un certain niveau de RAM ou CPU, cela s’avérera moins coûteux de favoriser la “scalabilité horizontale”.

Le principe de la scalabilité horizontale est d’ajouter plusieurs serveurs de taille modeste à votre infrastructure, plutôt que de transformer votre serveur en supercalculateur. Réaliser cela est plus difficile sur le plan technique car vos serveurs auront certainement besoin de communiquer à propos d’un état commun à tous, et un refactoring de votre application peut s’avérer nécessaire. La bonne nouvelle est qu’un crawler peut facilement devenir “stateless” : plusieurs instances de votre application peuvent tourner en parallèle et les informations à partager seront certainement situées dans votre outil de messaging et/ou votre base de données. Il est alors facile d’augmenter/diminuer le nombre de serveurs en fonction de la vitesse que vous souhaitez atteindre. Chaque serveur doit gérer un certain nombre de requêtes concurrentes consommées depuis le serveur de messaging. C’est votre rôle de définir combien de requêtes concurrentes chaque serveur peut absorber en fonction de ses ressources en RAM/CPU.

Les orchestrateurs de conteneurs tels que Kubernetes rendent la scalabilité horizontale plus aisée. Il est plus simple d’accroître le nombre d’instances en quelques clics, et Kubernetes peut même se charger de scaler les instances automatiquement pour vous (en revanche mettez toujours en place des limites pour éviter aux dépenses de déraper).

Si vous souhaitez avoir une connaissance plus pointue des challenge liés à la scalabilité, je vous recommande de lire cet excellent ouvrage de Martin Kleppmann: Data Intensive Applications.

Data Intensive Applications book

Rapporter les erreurs intelligemment

Des tonnes de vilaines choses peuvent arriver au cours d’un crawl : des problèmes de connectivité (côté client comme côté serveur), des congestions réseaux, une page HTML trop lourde, une limite de mémoire…

Il est crucial de gérer ces erreurs correctement et de les rapporter avec discernement afin de ne pas crouler sous les erreurs.

Une bonne pratique est de centraliser toutes les erreurs dans Sentry. Certaines erreurs ne sont jamais envoyées à Sentry car nous ne les considérons pas comme critiques. Par exemple nous souhaitons être alerté lorsqu’une instance connaît un problème de mémoire, mais nous ne voulons pas être prévenu lorsqu’une page ne peut pas être téléchargée à cause d’un site en timeout (ce type d’erreur fait partie du fonctionnement normal d’un crawler). Décidez donc intelligement de quelles erreurs valent le coup d’être rapportées, et lesquelles ne le valent pas.

File descriptors et utilisation mémoire

Lorsque l’on touche au sujet des crawlers, il est intéressant de se familiariser avec le concept de file descriptors. Chaque requête HTTP que vous lancez ouvre un file descriptor, et ce dernier consomme de la mémoire.

Sur les systèmes Linux, le nombre maximal de file descriptors ouverts en parallèle est plafonné par l’OS afin de ne pas mettre le système à genoux. Une fois que cette limite est atteinte, il n’est plus possible d’ouvrir une seule nouvelle page web.

Vous pouvez décider d’augmenter cette limite mais procédez avec attention car cela peut mener à une consommation mémoire excessive.

Eviter certains pièges

Voici 2 pièges typiques dans lesquels il faut éviter de tomber si vous souhaiter améliorer la performance de votre crawler :

  • stopper la requête si la page à télécharger est trop lourde : c’est important non seulement pour des questions de stabilité (vous n’avez certainement pas envie de remplir tout votre disque) mais aussi pour des questions d’efficacité.
  • paramétrer les timeouts avec attention : une requête web peut être en timeout pour plusieurs raisons et il est important de comprendre les concepts qu’il y a là-dessous afin d’adopter différent niveaux de timeouts. Jetez un oeil à cet excellent article de Cloudflare pour plus de détails. En Go vous pouvez mettre en place un timeout lorsque vous créez un client net/http, mais une façon plus idiomatique (et peut-être plus moderne) de faire est d’utiliser les contexts.

DNS

Lorsque l’on crawl des millions de pages web, le serveur DNS que l’on utilise par défaut a des chances de finir par rejeter les requêtes. Il devient alors intéressant de commencer à utiliser un serveur DNS plus robuste comme celui de Google ou de Cloudflare, ou même d’effectuer une rotation sur ces différents serveurs.

Rafraichir les données

Crawler des données une seule fois est souvent de peu d’intérêt. Les données doivent être rafraichies de façon asynchrone régulièrement en utilisant des tâches périodique, ou de façon synchrone à chaque requête utilisateur.

Une application sur laquelle j’ai travaillée récemment rafraichissait les données de façon asynchrone. Chaque fois que l’on crawlait un domaine, la date du crawl était stockée en base de données, et par la suite chaque jour une tâche périodique scannait en base tous les domaines nécessitant un update. Comme Cron est très limité, nous utilisions cet lib permettant de faire du Cron plus avancé dans notre application Go: https://github.com/robfig/cron.

Etre juste

Crawler le web doit être fait respectueusement. Cela signifie en gros 2 choses :

  • ne pas crawler une page si le robots.txt l’interdit
  • ne pas harceler de requêtes un seul serveur : baissez drastiquement votre niveau de concurrence lorsque vous crawlez plusieurs page d’un même domaine, et faites des pauses entre chaque requête.

Conclusion

Mettre en place un crawler permettant de crawler un grand volume de pages web est une aventure fascinante qui demande des réflexions avancées à la fois sur le code et sur l’infrastructure.

Dans ce poste je me contente d’aborder les bases mais j’espère malgré tout que ces concepts vous aideront lors de votre prochain projet ! Si vous avec des remarques ou des questions n’hésitez pas, ce sera avec plaisir.

Also available in English
Créer une PWA avec notifications push grâce à Vue.js et Django

Mettre en place une Progressive Web App (PWA) est simple comme bonjour grâce à Vue.js, en particulier grâce au CLI Vue v3. Toutefois la mise en place des notifications push peut s’avérer piégeuse.

Vue.js sera utilisé pour la partie frontend, Python/Django et Django Rest Framework pour le backend, et Google Firebase Messaging (FCM) comme intermédiaire de messaging. FCM est nécessaire puisqu’il fera office de tierce partie en charge de pousser les notifications vers l’appareil utilisateur. Je sais, c’est assez décevant de devoir ajouter ce service externe à notre architecture, mais il n’y a malheureusement pas d’autre choix. Bien entendu il existe des alternatives à Firebase, telles que Pusher par exemple.

Firebase devra être intégré à plusieurs endroits dans votre code :

  • côté frontend afin que le navigateur écoute les nouvelles notifications en provenance de Firebase
  • côté frontend aussi sur la page où vous voulez demander à l’utilisateur s’il souhaite autoriser les notifications et, s’il accepte, obtenir un token de notification de la part de Firebase à envoyer à notre backend pour le stocker en base de données. Si un utilisateur utilise plusieurs navigateurs (ex : Chromium mobile sur smartphone, et Firefox desktop sur PC), plusieurs tokens lui seront associés en base de données, et les notifications seront reçues à différents endroits au même moment.
  • côté backend afin d’envoyer les notifications push à l’utilisateur via l’envoi d’un message à l’API Firebase. Firebase se chargera de récupérer votre message et de le router vers les bons navigateurs associés.

Veuillez garder à l’esprit que le standard PWA est encore en pleine évolution et pas encore intégré de la même façon par tous les navigateurs sur toutes les plateformes. Par exemple les notifications push ne sont pas encore acceptées par iOS à l’heure où j’écris cet article !

PWA Vue.js

Installez le CLI Vue.js via la commande npm suivante (installez NPM en premier lieu si nécessaire) :

npm i -g @vue/cli

Créez un nouveau projet PWA :

vue create <My Project Name>

Sélectionnez l’option “Manually select features” puis sélectionnez “Progressive Web App (PWA) support” :

Vue CLI v3

Sélectionnez toutes les autres options dont vous avez besoin et patientez le temps que le CLI Vue crée votre projet. Veuillez noter que le CLI crée automatiquement un registerServiceWorker.js dans le répertoire src et l’importe en haut de votre main.js. Ce fichier prendra en charge la génération automatique d’un fichier service-worker.js à la racine de votre projet au cours du build de production. Ce fichier est nécessaire pour permettre au navigateur de reconnaître votre site comme PWA.

Dans votre répertoire public, créez un fichier manifest.json qui décrit votre PWA : le nom de votre app, les icônes de l’app pour différentes tailles d’écrans, les couleurs… Les éléments importants sont start_url qui est l’url à ouvrir par défaut au lancement de votre PWA par l’utilisateur sur son smartphone, et gcm_sender_id qui est l’ID que toutes les web apps utilisant Firebase doivent utiliser (donc ne le changez pas). Vous pouvez spécifier beaucoup plus d’information dans ce fichier, pour cela jetez un oeil à la doc. Au final votre fichier devrait ressembler à ça :

{
  "name": "My App Name",
  "short_name": "My App Short Name",
  "icons": [{
      "src": "./img/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./img/icons/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-60x60.png",
      "sizes": "60x60",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-76x76.png",
      "sizes": "76x76",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-120x120.png",
      "sizes": "120x120",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-180x180.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "./img/icons/favicon-16x16.png",
      "sizes": "16x16",
      "type": "image/png"
    },
    {
      "src": "./img/icons/favicon-32x32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "./img/icons/msapplication-icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "./img/icons/mstile-150x150.png",
      "sizes": "150x150",
      "type": "image/png"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#210499",
  "gcm_sender_id": "103953800507"
}

Veuillez noter que votre site doit absolument être en HTTPS afin que le navigateur accepte de lire le manifest.json et ainsi se comporter comme une PWA.

Si tout se passe correctement, la PWA est maintenant installable sur votre smartphone. Rendez-vous sur votre site via un navigateur mobile moderne comme Chrome. Si le navigateur détecte le manifest.json il vous propose normalement d’installer la PWA sur votre smartphone comme une application (ce n’est pas encore supporté par tous les navigateurs à l’heure où j’écris cet article).

Mise en place de Firebase

Afin que votre PWA supporte les notifications push, il faut que vous intégriez un service externe comme Firebase Cloud Messaging (FCM). Veuillez noter que FCM n’est qu’une petite partie de Firebase mais vous n’aurez pas besoin des autres fonctionnalités de Firebase (comme les bases de données, l’hébergement, …).

Veuillez donc créer un compte Firebase, rendez-vous sur votre console Firebase, créez un projet pour votre site, et récupérez les informations suivantes depuis les réglages de votre projet (attention, ces infos ne sont pas forcément faciles à identifier; pensez à ouvrir les différents onglets) :

  • Project ID
  • Web API Key
  • Messaging Sender ID
  • Server Key
  • créez un certificat web push et récupérez la Public Vapid Key générée

Backend Django

Je pars du principe ici que vous connaissez Django Rest Framework.

Dans Django, utilisez l’app tierce FCM Django afin de faciliter l’intégration de FCM (cette app prendra soin d’automatiquement sauvegarder et supprimer les tokens de notifications en BDD, et vous fournira des fonctions toutes prêtes pour envoyer les notifications à FCM).

Installez l’application grâce à pip install fcm-django, ajoutez-la à vos apps Django, et configurez-la (n’hésitez pas à adapter les réglages ci-dessous, le seul requis est FCM_SERVER_KEY pour l’authentification auprès de FCM) :

INSTALLED_APPS = (
        ...
        "fcm_django"
)

FCM_DJANGO_SETTINGS = {
        # authentication to Firebase
        "FCM_SERVER_KEY": "<Server Key>",
        # true if you want to have only one active device per registered user at a time
        # default: False
        "ONE_DEVICE_PER_USER": False,
        # devices to which notifications cannot be sent,
        # are deleted upon receiving error response from FCM
        # default: False
        "DELETE_INACTIVE_DEVICES": True,
}

Ajoutez une route dans urls.py vers le point d’accès FCM Django qui se chargera de recevoir le token de notification et de le stocker en BDD.

from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet

urlpatterns = [
  path('register-notif-token/',
    FCMDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_fcm_device'),
]

Désormais, lorsque vous souhaitez envoyer une notification push à un utilisateur faites ce qui suit (il y a des chances pour que ce soit dans votre views.py) :

from fcm_django.models import FCMDevice

user = <Retrieve the user>
fcm_devices = FCMDevice.objects.filter(user=user)
fcm_devices.send_message(
  title="<My Title>", body="<My Body>", time_to_live=604800,
  click_action="<URL of the page that opens when clicking the notification>")

C’est à vous d’adapter la requête vers la BDD pour définir précisément à qui vous souhaitez envoyer la notification push. Ici j’envoie des notifications push vers tous les navigateurs d’un utilisateur, mais je pourrais aussi bien décider de n’envoyer les notifications qu’à un navigateur spécifique (appelé “device” dans la terminologie Django FCM).

Il existe d’autres paramètres disponibles à passer à la méthode send_message, n’hésitez pas à jeter un oeil à la doc mais aussi à la doc du projet Python sous-jacent sur lequel cette lib est basée.

Déclarer un time_to_live était nécessaire dans mon cas : Firebase dit qu’il existe un TTL par défaut mais il s’est avéré que ce n’était pas le cas me concernant (bug ?), ce qui fait que les notifications envoyées lorsque le device de l’utilisateur était coupé n’arrivaient jamais lorsque le device était rallumé.

Mettre en place les notifications push dans Vue.js

Créez un fichier firebase-messaging-sw.js dans votre répertoire public et mettez ce qui suit dedans :

importScripts('https://www.gstatic.com/firebasejs/5.5.6/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.5.6/firebase-messaging.js');

var config = {
    apiKey: "<Web API Key>",
    authDomain: "<Project ID>.firebaseapp.com",
    databaseURL: "https://<Project ID>.firebaseio.com",
    projectId: "<Project ID>",
    storageBucket: "<Project ID>.appspot.com",
    messagingSenderId: "<Messenging Sender ID>"
};

firebase.initializeApp(config);

const messaging = firebase.messaging();

Vous disposez maintenant d’un service worker valide qui se chargera d’écouter Firebase en arrière plan en attente de nouvelles notifications push.

Il est désormais temps de demander sa permission à l’utilisateur afin de lui envoyer des notifications et, s’il est d’accord, récupérer un token de notification de FCM et le stocker en BDD côté backend. Votre backend utilisera ce token pour envoyer des notifications push via FCM. C’est à vous de décider sur quelle page de votre app vous souhaitez recueillir la permission de l’utilisateur. Par exemple vous pourriez mettre en place cela sur la page d’accueil de votre application une fois l’utilisateur loggué. Vous pourriez faire quelque chose dans ce style :

import firebase from 'firebase/app'
import 'firebase/app'
import 'firebase/messaging'

export default {
  methods: {
    saveNotificationToken(token) {
      const registerNotifTokenURL = '/register-notif-token/'
      const payload = {
        registration_id: token,
        type: 'web'
      }
      axios.post(registerNotifTokenURL, payload)
        .then((response) => {
          console.log('Successfully saved notification token!')
          console.log(response.data)
        })
        .catch((error) => {
          console.log('Error: could not save notification token')
          if (error.response) {
            console.log(error.response.status)
            // Most of the time a "this field must be unique" error will be returned,
            // meaning that the token already exists in db, which is good.
            if (error.response.data.registration_id) {
              for (let err of error.response.data.registration_id) {
                console.log(err)
              }
            } else {
              console.log('No reason returned by backend')
            }
            // If the request could not be sent because of a network error for example
          } else if (error.request) {
            console.log('A network error occurred.')
            // For any other kind of error
          } else {
            console.log(error.message)
          }
        })
      },
    },
  mounted() {
    var config = {
      apiKey: "<Web API Key>",
      authDomain: "<Project ID>.firebaseapp.com",
      databaseURL: "https://<Project ID>.firebaseio.com",
      projectId: "<Project ID>",
      storageBucket: "<Project ID>.appspot.com",
      messagingSenderId: "<Messenging Sender ID>"
    }
    firebase.initializeApp(config)

    const messaging = firebase.messaging()

    messaging.usePublicVapidKey("<Public Vapid Key>")

    messaging.requestPermission().then(() => {
      console.log('Notification permission granted.')
      messaging.getToken().then((token) => {
        console.log('New token created: ', token)
        this.saveNotificationToken(token)
      })
    }).catch((err) => {
      console.log('Unable to get permission to notify.', err)
    })

    messaging.onTokenRefresh(function () {
      messaging.getToken().then(function (newToken) {
        console.log('Token refreshed: ', newToken)
        this.saveNotificationToken(newToken)
      }).catch(function (err) {
        console.log('Unable to retrieve refreshed token ', err)
      })
    })
  }
}

Conclusion

Mettre en place les notifications push dans une PWA n’est clairement PAS évident ! De nombreuses briques de votre application doivent être impliquées et vous devez comprendre comment fonctionne le service tiers que vous avez choisi (ici Firebase).

Veuillez garder en tête que la PWA est une techno encore jeune en pleine évolution. Plus important : ne basez pas la communication d’informations critiques uniquement sur les notifications push car elles sont moins fiables que d’autres systèmes tels que les SMS ou les emails…

Par ailleurs, n’oubliez pas d’utiliser les notifications push avec circonspection car un excès de notifications peut vite devenir fort agaçant pour l’utilisateur !

J’espère que vous avez apprécié ce tuto. N’hésitez pas à me faire un retour ou à partager vos idées via les commentaires !

Also available in English
Tirer parti de Django Rest Framework et des vues génériques pour accélérer le développement d'API

En tant que développeur expérimenté, vous êtes certainement à la recherche d’outils permettant de supprimer les tâches répétitives et d’accélérer le développement. En tant que novice, vous cherchez peut-être un moyen de respecter les standards REST sans trop vous poser de questions.

Dans les deux cas, Django Rest Framework (DRF) est une excellente solution. DRF est un framework d’API très utilisé, respectant les standards, et regorgeant de fonctionnalités, qui va non seulement vous faire gagner beaucoup de temps mais aussi vous montrer la bonne direction pour développer vos API RESTful. Plus particulièrement, DRF propose des vues génériques, c’est à dire des points d’accès à votre API déjà pré-construits.

Vous pourrez trouver le code ci-dessous dans un petit projet Django fonctionnel à cette adresse.

Concept

Les vues génériques de DRF sont parfaites pour les API simples qui font du CRUD (create, read, update, delete) basique sur la base de données sans trop de retraitement. Par exemple, disons que vous avez une table produit qui contient tous les produits de votre boutique et que vous souhaitez exposer ces produits tels quels à vos clients via une API. Alors c’est un cas idéal pour utiliser ListAPIView (cf. plus bas).

A partir de maintenant je vais supposer que vous avez installé Python, Django, DRF, et que vous maîtrisez les bases de Django.

Exemple de base 1 : lire les données

Créons un point d’accès API exposant tous les produits aux utilisateurs authentifiés seulement. Dans votre views.py faites ce qui suit :

from rest_framework import generics
from .serializers import ProductsSerializer

class GetProducts(generics.ListAPIView):
    """Return all products."""
    serializer_class = ProductsSerializer

ProductsSerializer est le serializer qui va convertir vos données issues de la base de données en données au format API. Ce serializer doit être écrit dans le fichier serializers.py et sera en charge de la récupération des données du model Product et de les transformer :

from rest_framework import serializers
from .models import Product

class ProductsSerializer(serializers.ModelSerializer):
    """Serialize products."""

    class Meta:
        model = Product
        fields = ("__all__")

Maintenant dans votre urls.py créez la route vers votre point d’accès :

from django.urls import path
from .views import GetProducts

urlpatterns = [
    path('get-products/', GetProducts.as_view(), name='get_products'),
]

Comme vous pouvez le voir, c’est simple comme bonjour puisque DRF se charge de presque tout tout seul. Vous avez désormais un point d’accès (/get-products/) que vous pouvez consommer via des requêtes HTTP get, et qui affiche vos produits au format API (habituellement json mais cela dépend, là aussi, de vos settings).

Exemple basique 2 : supprimer des données

Maintenant créons un point d’accès dédié à la suppression de produits par les utilisateurs authentifiés seulement. C’est encore plus simple puisque cela ne nécessite pas de sérialiser les données (en effet une fois le produit supprimé aucune donnée ne peut plus être retournée à l’utilisateur).

Dans views.py :

from rest_framework import generics

class DeleteProduct(generics.DestroyAPIView):
    """Remove product"""
    permission_classes = (permissions.IsAuthenticated,) # Limit to authenticated users only

Dans urls.py

from django.urls import path
from .views import DeleteProduct

urlpatterns = [
    path('delete-product/', DeleteProduct.as_view(), name='delete_product'),
]

Vous avez maintenant un point d’accès /delete-product/ qui permet de supprimer les produits un par un en utilisant les requêtes HTTP delete, et qui accepte seulement les requêtes authentifiées (le mécanisme d’authentification dépend de vos settings).

Personnaliser le comportement des vues génériques

Chaque vue générique peut être affinée via l’écriture d’une méthode get_queryset(). Par exemple disons que vous souhaitez uniquement afficher les produits ayant un flag active à True en base de données. Vous pouvez faire comme suit :

from rest_framework import generics
from .serializers import ProductsSerializer
from .model import Product

class GetActiveProducts(generics.ListAPIView):
    """Return all active products."""
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = ProductsSerializer

    def get_queryset(self):
        """Filter active products."""
        return Product.objects.filter(active=True)

get_queryset() est une méthode commune à toutes les vues génériques. Certaines vues génériques ont aussi leurs propres méthodes afin de contrôler plus précisément le comportement du point d’accès. Par exemple, disons que vous ne souhaitez pas vraiment supprimer les produits mais juste les marquer comme inactifs. Vous pourriez utiliser la méthode destroy() :

from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework import status

class DisableProduct(generics.DestroyAPIView):
    """Remove product"""
    permission_classes = (permissions.IsAuthenticated,)

    def destroy(self, request, pk):
        """
        By default, DestroyAPIView deletes the product from db.
        Here we only want to flag it as inactive.
        """
        product = get_object_or_404(self.get_queryset(), pk=pk)
        product.active = False
        product.save()
        return Response(status=status.HTTP_204_NO_CONTENT)

Dans l’exemple ci-dessus nous essayons en premier de rechercher le produit que l’utilisateur veut supprimer. Si nous ne parvenons pas à le trouver, nous retournons un code 404 à l’utilisateur. Si le produit est marqué comme inactif avec succès, nous retournons un code 204 à l’utilisateur signifiant que le produit a été supprimé avec succès.

Les vues génériques sont parfaites pour les cas simples, et il est parfois plus sage d’utiliser les classiques APIView pour les cas spéciaux. Par exemple, disons que vous souhaitez non seulement retourner les produits à l’utilisateur mais aussi les enrichir avec des données ne se trouvant pas dans le model Product (ex : les commandes liées à ce produit, le fournisseur du produit, etc.). Dans ce cas, si vous vouliez utiliser les vues génériques, il vous faudrait définir de nouveaux champs dans le serializer grâce à des méthodes get_new_field() additionnelles qui peuvent facilement rendre votre serializer inutilement complexe…

Conclusion

Comme vous l’avez vu, les vues génériques de DRF rendent le développement d’API extrêmement simple grâce à un peu de magie. Toutefois gardez à l’esprit que les vues génériques ne peuvent pas s’appliquer à tous les cas d’usage et que parfois essayer à tout prix d’adapter les vue génériques à vos besoins sera plus difficile que de re-développer les choses de zéro par vous-même !

J’espère que vous avez aimé ce petit tuto. J’adorerais avoir vos retours !

Also available in English
Sécurité d'un site web en Go (Golang)

Dans le monde Go il n’est pas très courant d’utiliser des frameworks web comme on pourrait le voir dans d’autres langages. Cela donne certes de la liberté, mais un avantage majeur des frameworks web est qu’ils imposent certaines bonnes pratiques en terme de sécurité, notamment pour les néophytes.

Si, comme moi, vous développez des sites ou API en Go sans framework, voyons ici quels éléments de sécurité vous devez garder à l’esprit.

CSRF

Les attaques de type Cross-site Requests Forgery (CSRF) concernent les parties du site protégées par mot de passe faisant l’usage de formulaires. Les utilisateurs authentifiés (via un cookie de session dans leur navigateur) risquent, s’ils se rendent sur un site malicieux, de poster des informations sur un formulaire protégé à leur insu. Pour éviter ce risque, il faut que chaque formulaire intègre un champ caché contenant un token CSRF grâce auquel le serveur pourra vérifier l’authenticité de la requête.

Utilisons Gorilla Toolkit pour cela. Commencez par intégrer le middleware CSRF. Vous pouvez le faire soit pour tout le site:

package main

import (
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()

    http.ListenAndServe(":8000",
        csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}

Soit pour certaines pages seulement:

package main

import (
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))

    protectedPageRouter := r.PathPrefix("/protected-page").Subrouter()
    protectedPageRouter.Use(csrfMiddleware)
    protectedPageRouter.HandleFunc("", protectedPage).Methods("POST")

    http.ListenAndServe("8080", r)
}

Puis passez le token CSRF à votre template:

func protectedPage(w http.ResponseWriter, r *http.Request) {
    var tmplData = ContactTmplData{CSRFField: csrf.TemplateField(r)}
    tmpl.Execute(w, tmplData)
}

Et enfin intégrez le champs caché {{.CSRFField}} à votre template.

CORS

Les attaques de type Cross-origin Resource Sharing (CORS) consistent à envoyer des informations vers un site malicieux en se rendant sur un site sain. Pour éviter cela le site sain doit empêcher ses utilisateurs d’émettre des requêtes asynchrones (XHR) vers un autre site que lui. Bonne nouvelle : c’est le comportement par défaut appliqué par tous les navigateurs modernes ! Mauvaise nouvelle : cela peut créer un faux positif qui fait que si vous souhaitez, depuis vote page web, consommer une API se trouvant sur un autre domaine que le domaine d’origine, ou sur un port différent, les requêtes seront bloquées par le navigateur. C’est souvent un casse-tête pour les débutants en développement d’API.

Afin de mettre certains domaines sur liste blanche et ainsi éviter le problème ci-dessus, vous pouvez utiliser la bibliothèque github.com/rs/cors comme suit :

package main

import (
    "net/http"

    "github.com/rs/cors"
)

func main() {
    c := cors.New(cors.Options{
        AllowedOrigins: []string{"http://my-whitelisted-domain"},
    })
    handler = c.Handler(handler)

    http.ListenAndServe(":8080", handler)
}

HTTPS

Passer le site en HTTPS est un élément de sécurité incontournable. Je considère ici que vous utilisez le serveur HTTP built-in de Go. Si ce n’est pas le cas (parce que vous utilisez par exemple Nginx ou Apache) vous pouvez sauter cette section.

Obtenir un A sur SSLLabs.com

Afin d’obtenir la meilleure note de sécurité sur SSLLabs (signe que le certificat est parfaitement configuré et ainsi éviter potentiellement les erreurs de sécurité sur certains clients web), il faut proscrire l’utilisation de SSL et utiliser TLS 1.0 comme version minimum. Pour cela on utilise la bibliothèque crypto/tls, et pour servir les requêtes on utilise http.ListenAndServeTLS:

package main

import (
    "crypto/tls"
    "net/http"
)

func main() {
    config := &tls.Config{MinVersion: tls.VersionTLS10}
    server := &http.Server{Addr: ":443", Handler: r, TLSConfig: config}
    server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)
}

Rediriger HTTP vers HTTPS

C’est une bonne pratique de forcer les requêtes HTTP vers HTTPS. Il suffit de les rediriger comme suit via une goroutine dédiée :

package main

import (
    "crypto/tls"
    "net/http"
)

// httpsRedirect redirects http requests to https
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(
        w, r,
        "https://"+r.Host+r.URL.String(),
        http.StatusMovedPermanently,
    )
}

func main() {
    go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))

    config := &tls.Config{MinVersion: tls.VersionTLS10}
    server := &http.Server{Addr: ":443", Handler: r, TLSConfig: config}
    server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)
}

Renouvellement des certificats Let’s Encrypt

Let’s Encrypt est aujourd’hui la façon la plus répandue de provisionner les certificats TLS (car gratuite) mais pas nécessairement la plus facile. Une fois Let’s Encrypt installé et les premiers certificats provisionnés, se pose la question du renouvellement des certificats avec Certbot. Puisque Certbot ne s’intègre pas automatiquement au server HTTP de Go, il est nécessaire d’utiliser la version standard de certbot (par exemple celle-là pour Ubuntu 18.04), puis de couper brièvement (quelques secondes) le serveur de production pendant le renouvellement des certificats (afin d’éviter un conflit sur les ports 80 ou 443). Cela peut se faire en modifiant la commande de renouvellement lancée par le cron Certbot (dans /etc/cron.d/certbot). Sur Ubuntu Certbot utilise aussi le timer systemd (en priorité par rapport au cron) donc modifier le fichier de configuration /lib/systemd/system/certbot.service est préférable :

[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://letsencrypt.readthedocs.io/en/latest/
[Service]
Type=oneshot
# Proper command to stop server before renewal and restart server afterwards
ExecStart=/usr/bin/certbot -q renew --pre-hook "command to stop go server" --post-hook "command to start go server"
PrivateTmp=true

Une alternative est d’utiliser une bibliothèque Go dédiée au renouvellement des certificats au sein même de votre programme appelée x/crypto/acme/autocert. Personnellement j’aime moins cette option car, même si elle ne crée aucune interruption de service contrairement à ma solution ci-dessus, elle oblige votre code à être fortement couplé à un type de renouvellement de certificats particulier (ACME).

XSS

Les attaques de type Cross-site scripting (XSS) consistent à exécuter du code Javascript dans le navigateur d’un utilisateur se rendant sur votre site, alors que ça ne devrait pas être le cas. Par exemple dans le cas d’un forum, si un utilisateur poste un message contenant du Javascript, et que vous affichez ce message à tous les utilisateurs sans filtre, ces derniers verront tous ce script s’exécuter dans leur propre navigateur. Pour éviter cela il faut “échapper” les chaînes de caractères avant de les afficher à l’utilisateur afin de les rendre inoffensives.

Bonne nouvelle : lorsque vous utilisez les templates via la bibliothèque html/template, Go sécurise les chaînes de caractères automatiquement. Attention à ne pas utiliser text/template qui elle n’échappe pas les caractères !

Injections SQL

Les attaques de type injection SQL consistent pour un utilisateur malicieux à entrer du code SQL dans un formulaire du site. Au moment de l’enregistrement ou de l’affichage des informations en base de donnée, ce code malicieux peut faire des dégâts.

Encore une fois, bonne nouvelle : cette attaque se contourne naturellement en utilisant correctement les bibliothèques SQL classiques. Par exemple en utilisant database/sql les injections SQL sont automatiquement échappées lorsque vous utilisez les mots clés $ ou ?. Voici le cas d’une requête SQL pour PostgreSQL bien écrite :

db.Exec("INSERT INTO users(name, email) VALUES($1, $2)",
  "Julien Salinas", "julien@salinas.com")

Personnellement j’utilise l’ORM optimisé pour PostgreSQL github.com/go-pg/pg. Une requête bien écrite dans ce cas serait :

user := &User{
    name:       "Julien Salinas",
    email:      "julien@salinas.com",
}
err = db.Insert(user)

Directory Listing

Le directory listing est le fait de pouvoir afficher tout le contenu d’un répertoire statique sur le site. Cela peut révéler des documents que vous ne souhaitez pas afficher à l’utilisateur s’ils n’ont pas l’url exacte. Le directory listing est activé par défaut si vous utilisez la bibliothèque standard http.FileServer. J’explique dans cet article comment le neutraliser.

Conclusion

Voici un petit aperçu des éléments essentiels de sécurité pour votre site en Go.

Il peut être une bonne idée d’utiliser un outil permettant de paramétrer facilement différents éléments essentiels relatifs à la sécurité. Je trouve que github.com/unrolled/secure est une excellente bibliothèque à cet égard. Elle permet de facilement paramétrer les redirections HTTPS, gérer CORS, mais aussi le filtrage d’hôtes autorisés et pas mal de choses encore plus pointues.

J’espère que ces bases seront utiles à certains !

Also available in English
Développer et déployer un site entier en Go (Golang)

À mon avis Go (Golang) est un excellent choix de langage pour le développement web :

  • il permet les requêtes non bloquantes et rend la concurrence facile
  • il facilite le test du code et le déploiement puisqu’il ne demande d’installer aucun environnement d’exécution ou de dépendances
  • il n’exige pas d’installer un serveur HTTP front-end tel qu’Apache ou Nginx puisqu’il en embarque déjà un excellent dans sa bibliothèque standard
  • il ne vous force pas à utiliser un framework web puisque tous les outils requis pour le développement web sont déjà disponibles dans la bibliothèque standard

Il y a encore quelques années, le manque de bibliothèques et tutoriels autour de Go étaient un problème, mais aujourd’hui ce n’est plus le cas. Je vais vous montrer étape par étape comment construire un site web en Go et le déployer sur votre serveur Linux de A à Z.

Les bases

Imaginons que vous souhaitiez développer une page HTML basique appelée love-mountains. Comme vous le savez peut-être déjà, la génération de love-mountains se fait dans une fonction, et il vous faut lancer le serveur web avec une route pointant vers cette fonction. C’est une bonne pratique que d’utiliser des templates HTML lorsque l’on fait du développement web alors générons notre page via un template basique ici. C’est aussi une bonne pratique que d’importer des paramètres tels que le chemin vers le dossier de template depuis des variables d’environnement pour une meilleure flexibilité.

Voici votre code Go :

package main

import (
    "html/template"
    "net/http"
)

// Get path to template directory from env variable
var templatesDir = os.Getenv("TEMPLATES_DIR")

// loveMountains renders the love-mountains page after passing some data to the HTML template
func loveMountains(w http.ResponseWriter, r *http.Request) {
    // Build path to template
    tmplPath := filepath.Join(templatesDir, "love-mountains.html")
    // Load template from disk
    tmpl := template.Must(template.ParseFiles(tmplPath))
    // Inject data into template
    data := "La Chartreuse"
    tmpl.Execute(w, data)
}

func main() {
    // Create route to love-mountains web page
    http.HandleFunc("/love-mountains", loveMountains)
    // Launch web server on port 80
    http.ListenAndServe(":80", nil)
}

La récupération de données dynamiques depuis un template se fait facilement via {{.}} ici. Voici votre template love-mountains.html:

<h1>I Love Mountains<h1>
<p>The mountain I prefer is {{.}}</p>

HTTPS

De nos jours, mettre en place l’HTTPS sur votre site est devenu quasiment obligatoire. Comment passer votre site Go HTTPS ?

Lier les certificats TLS

Avant tout, générez votre certificat et votre clé privée au format .pem. Vous pouvez parfaitement les générer vous-même avec openssl (mais vous allez vous retrouver avec un certificat auto-signé qui déclenchera une alerte de sécurité dans le navigateur), ou bien vous pouvez commander un certificat auprès d’une tierce partie de confiance comme Let’s Encrypt. Personnellement, j’utilise Let’s Encrypt et Certbot afin de générer les certificats et les renouveler automatiquement sur mes serveurs. Plus d’infos sur l’utilisation de Certbot ici.

Il vous faut alors dire à Go où se trouve votre certificat et votre clé privée. J’importe ces chemins depuis des variables d’environnement.

On utilise désormais la fonctionListenAndServeTLS au lieu de ListenAndServe :

[...]

// Load TLS cert info from env variables
var tlsCertPath = os.Getenv("TLS_CERT_PATH")
var tlsKeyPath = os.Getenv("TLS_KEY_PATH")

[...]

func main() {
    [...]
    // Serve HTTPS on port 443
    http.ListenAndServeTLS(":443", tlsCertPath, tlsKeyPath, nil)
}

Forcer les redirections HTTPS

Pour le moment nous avons un site web qui écoute sur les ports 80 et 443 à la fois. Il serait bien de rediriger automatiquement les utilisateurs du port 80 vers 443 avec une redirection 301. Il nous faut pour cela créer une nouvelle goroutine dédiée à la redirection de http:// vers https:// (même principe que ce que vous feriez dans la configuration d’un serveur front-end comme Nginx). Voici ce qu’il faut faire :

[...]

// httpsRedirect redirects HTTP requests to HTTPS
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(
        w, r,
        "https://"+r.Host+r.URL.String(),
        http.StatusMovedPermanently,
    )
}

func main() {
    [...]
    // Catch potential HTTP requests and redirect them to HTTPS
    go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))
    // Serve HTTPS on port 443
    http.ListenAndServeTLS(":443", tlsCertPath, tlsKeyPath, nil)
}

Les assets statiques

Servir les assets statiques (comme les images, les vidéos, les fichiers Javascript, les fichiers CSS,…) stockés sur le disque est relativement facile mais désactiver le directory listing relève un peu plus de la bidouille.

Servir les fichiers statiques depuis le disque

En Go, la façon la plus sécurisée de servir des fichiers depuis le disque est d’utiliser http.FileServer. Par exemple, disons que nous stockons les fichiers statiques dans un dossier sur le disque appelé static, et que nous voulons les servir à cette adresse : https://my-website/static. Voici comment il faut s’y prendre :

[...]
http.Handle("/", http.FileServer(http.Dir("static")))
[...]

Empêcher le directory listing

Par défaut,http.FileServer permet un directory listing sans limitation, ce qui signifie que https://my-website/static affichera tous vos fichiers statiques. Ce n’est pas ce que nous voulons pour des questions à la fois de sécurité et de propriété intellectuelle.

Désactiver le directory listing requiert la création d’un FileSystem custom. Créons une struct qui implémente l’interface http.FileSystem. Cette struct doit avoir une méthode Open() afin de satisfaire l’interface. Cette méthode Open() vérifie en premier si le chemin vers le fichier ou le répertoire existe, et si c’est le cas elle détermine s’il s’agit d’un fichier ou d’un répertoire. Si le chemin est un répertoire alors nous retournons une erreur file does not exist qui sera convertie en une page d’erreur HTTP 404 pour l’utilisateur final. De cette façon l’utilisateur ne peut pas savoir s’il a atteint un dossier qui existe vraiment ou non.

Une fois de plus, importons le chemin vers le dossier de fichiers statiques depuis une variable d’environnement.

[...]

// Get path to static assets directory from env variable
var staticAssetsDir = os.Getenv("STATIC_ASSETS_DIR")

// neuteredFileSystem is used to prevent directory listing of static assets
type neuteredFileSystem struct {
    fs http.FileSystem
}

func (nfs neuteredFileSystem) Open(path string) (http.File, error) {
    // Check if path exists
    f, err := nfs.fs.Open(path)
    if err != nil {
        return nil, err
    }

    // If path exists, check if is a file or a directory.
    // If is a directory, stop here with an error saying that file
    // does not exist. So user will get a 404 error code for a file or directory
    // that does not exist, and for directories that exist.
    s, err := f.Stat()
    if err != nil {
        return nil, err
    }
    if s.IsDir() {
        return nil, os.ErrNotExist
    }

    // If file exists and the path is not a directory, let's return the file
    return f, nil
}

func main() {
    [...]
    // Serve static files while preventing directory listing
    mux := http.NewServeMux()
    fs := http.FileServer(neuteredFileSystem{http.Dir(staticAssetsDir)})
    mux.Handle("/", fs)
    [...]
}

Exemple complet

Au final, voici à quoi ressemblerait votre site en entier :

package main

import (
    "html/template"
    "net/http"
    "os"
    "path/filepath"
)

var staticAssetsDir = os.Getenv("STATIC_ASSETS_DIR")
var templatesDir = os.Getenv("TEMPLATES_DIR")
var tlsCertPath = os.Getenv("TLS_CERT_PATH")
var tlsKeyPath = os.Getenv("TLS_KEY_PATH")

// neuteredFileSystem is used to prevent directory listing of static assets
type neuteredFileSystem struct {
    fs http.FileSystem
}

func (nfs neuteredFileSystem) Open(path string) (http.File, error) {
    // Check if path exists
    f, err := nfs.fs.Open(path)
    if err != nil {
        return nil, err
    }

    // If path exists, check if is a file or a directory.
    // If is a directory, stop here with an error saying that file
    // does not exist. So user will get a 404 error code for a file/directory
    // that does not exist, and for directories that exist.
    s, err := f.Stat()
    if err != nil {
        return nil, err
    }
    if s.IsDir() {
        return nil, os.ErrNotExist
    }

    // If file exists and the path is not a directory, let's return the file
    return f, nil
}

// loveMountains renders love-mountains page after passing some data to the HTML template
func loveMountains(w http.ResponseWriter, r *http.Request) {
    // Load template from disk
    tmpl := template.Must(template.ParseFiles("love-mountains.html"))
    // Inject data into template
    data := "Any dynamic data"
    tmpl.Execute(w, data)
}

// httpsRedirect redirects http requests to https
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(
        w, r,
        "https://"+r.Host+r.URL.String(),
        http.StatusMovedPermanently,
    )
}

func main() {
    // http to https redirection
    go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))

    // Serve static files while preventing directory listing
    mux := http.NewServeMux()
    fs := http.FileServer(neuteredFileSystem{http.Dir(staticAssetsDir)})
    mux.Handle("/", fs)

    // Serve one page site dynamic pages
    mux.HandleFunc("/love-mountains", loveMountains)

    // Launch TLS server
    log.Fatal(http.ListenAndServeTLS(":443", tlsCertPath, tlsKeyPath, mux))
}

Plus votre template love-mountains.html :

<h1>I Love Mountains<h1>
<p>The mountain I prefer is {{.}}</p>

Tester, déployer, et créer un démon avec systemd

Bâtir un process de test et de déploiement robuste et facile à la fois est très important pour l’efficacité d’un projet, et Go est vraiment d’une grande aide à cet égard. Go compile tout au sein d’un seul et unique exécutable, incluant toutes les dépendances (excepté les templates mais ces derniers ne sont pas réellement des dépendances et il est de toute façon plus sage de les garder à part pour une meilleure flexibilité). Go embarque aussi son propre serveur HTTP front-end, il n’est donc pas besoin d’installer un Nginx ou un Apache. Il est ainsi relativement facile de tester votre application en local et de vous assurer que cette dernière est équivalente à votre site web en production sur le serveur (ici nous n’abordons pas la question de la persistance des données bien entendu…). Ainsi nul besoin de mettre en place un système de conteneur comme Docker dans votre workflow pour compiler et déployer !

Tester

Pour tester votre application localement, compilez votre binaire Go et lancez-le avec les bonnes variables d’environnement de cette façon :

TEMPLATES_DIR=/local/path/to/templates/dir \
STATIC_ASSETS_DIR=/local/path/to/static/dir \
TLS_CERT_PATH=/local/path/to/cert.pem \
TLS_KEY_PATH=/local/path/to/privkey.pem \
./my_go_website

Et voilà ! Votre site tourne désormais dans votre navigateur à l’adresse https://127.0.0.1.

Déployer

Le déploiement consiste juste à copier votre binaire Go sur le serveur (ainsi que vos templates, assets statiques et certificats, le cas échéant). Un simple outil tel que scp est parfait pour cela. Vous pouvez aussi utiliser rsync en cas de besoin plus poussé.

Transformer votre app en démon avec systemd

Vous pourriez lancer votre site web sur le serveur en vous contentant de la commande ci-dessus, mais il est bien mieux de lancer votre site sous forme de service (démon) afin que votre système Linux le lance automatiquement au démarrage (en cas de redémarrage serveur) et essaie aussi de le redémarrer en cas de crash de l’application. Sur les distributions Linux modernes, la meilleure façon de s’y prendre est d’utiliser systemd, qui est l’outil par défaut dédié à la gestion des services système. Rien à installer donc !

Supposons que vous mettiez votre binaire Go dans /var/www sur votre serveur. Créez un nouveau fichier qui décrira votre service dans le répertoire systemd : /etc/systemd/system/my_go_website.service. Mettez maintenant le contenu suivant à l’intérieur :

[Unit]
Description=my_go_website
After=network.target auditd.service

[Service]
EnvironmentFile=/var/www/env
ExecStart=/var/www/my_go_website
ExecReload=/var/www/my_go_website
KillMode=process
Restart=always
RestartPreventExitStatus=255
Type=simple

[Install]
WantedBy=multi-user.target

La directive EnvironmentFile pointe vers un fichier env dans lequel vous pouvez mettre toutes vos variables d’environnement. systemd s’occuper de charger ces variables et de les passer à votre programme. J’ai placé le fichier dans /var/www mais n’hésitez pas à le placer autre part. Voici à quoi votre fichier env devrait ressembler :

TEMPLATES_DIR=/remote/path/to/templates/dir
STATIC_ASSETS_DIR=/remote/path/to/static/dir
TLS_CERT_PATH=/remote/path/to/cert.pem
TLS_KEY_PATH=/remote/path/to/privkey.pem

N’hésitez pas à vous documenter sur systemd pour plus de détails sur la configuration ci-dessus.

À présent :

  • lancez ce qui suit pour lier votre app à systemd : systemctl enable my_go_website
  • lancez ce qui suit pour démarrer votre site : systemctl start my_go_website
  • redémarrez avec : systemctl restart my_go_website
  • arrêtez avec : systemctl stop my_go_website

Remplacer Javascript par WebAssembly (Wasm)

Voici une petite section bonus pour ceux qui se sentent l’âme d’un aventurier !

Depuis la version 1.11 de Go vous pouvez désormais compiler Go vers WebAssembly (Wasm). Plus de détails ici. C’est vraiment cool puisque Wasm peut servir de substitut à JavaScript. En d’autres mots vous pouvez théoriquement remplacer JavaScript par Go à travers Wasm.

Wasm est supporté par les navigateurs modernes mais reste malgré tout relativement expérimental. Personnellement, je ne l’utiliserais que pour une preuve de concept pour le moment, mais à moyen terme cela pourrait devenir une excellente façon de développer en Go sur l’intégralité de votre stack. Patience donc !

Conclusion

Vous savez désormais comment développer un site web entier en Go, et le déployer sur un serveur Linux. Pas de serveur front-end à installer, pas de cauchemar à cause des dépendances, et des performances excellentes… C’était plutôt facile pas vrai ?

Si vous voulez apprendre à développer une Single Page App (SPA) avec Go et Vue.js, jetez un coup d’œil à mon autre post sur le sujet ici.

Also available in English