Loupe

Héberger une application ASP.NET Core 2 sur un Raspberry Pi

Comme vous le savez certainement, .NET Core est supporté sur de nombreuses plateformes : Windows, macOS, et de nombreuses variantes d'UNIX/Linux, aussi bien sur des architectures x86/x64 que sur ARM. Ce qui ouvre la voie à des scénarios intéressants... Une très petite machine comme un Raspberry Pi, avec son processeur ARM de faible performance et sa quantité de RAM limitée (1 Go sur mon RPi 2 Model B), est-elle suffisante pour héberger une application web ASP.NET Core ? Et bien la réponse est oui, du moins si vous n'avez pas besoin de supporter une charge très importante ! Thomas l'avait déjà mentionné il y quelques mois, sans rentrer dans le détail. Voyons donc en pratique comment déployer et exposer une application ASP.NET Core sur un Raspberry Pi.

Créer l'application

On va partir d'un template basique d'application MVC ASP.NET Core 2.0 :

dotnet new mvc

Même pas la peine d'ouvrir le projet pour l'instant, on va le compiler et le publier tel quel pour le Raspberry Pi :

dotnet publish -c Release -r linux-arm

Pré-requis

On va travailler sur un Raspberry Pi équipé de Raspbian, la distribution Linux officielle pour Raspberry Pi, qui est dérivée de Debian. Pour faire tourner une application .NET Core 2.0 sous Raspbian, assurez-vous d'avoir la version Jessie ou supérieure de l'OS (j'ai testé avec Raspbian Stretch Lite).

Bien que l'application soit self-contained, il y a quand même quelques dépendances à installer sur le Raspberry Pi ; elles sont détaillées ici. On utilise apt-get pour les installer :

sudo apt-get update
sudo apt-get install curl libunwind8 gettext apt-transport-https

Déployer et lancer l'application

On copie tous les fichiers du dossier bin\Release\netcoreapp2.0\linux-arm\publish sur le Raspberry Pi, et on rend exécutable le binaire de l'application (remplacez MyWebApp par le nom de votre application) :

chmod 755 ./MyWebApp

On lance l'application :

./MyWebApp

Si tout se passe bien, l'application devrait se lancer et écouter sur le port 5000. Par contre, vu qu'elle écoute par défaut sur localhost, elle n'est accessible que depuis le Raspberry Pi lui-même...

Exposer l'application sur le réseau

Il y a plusieurs façons de remédier à cela. Par exemple, on peut définir la variable d'environnement ASPNETCORE_URLS à une valeur comme http://*:5000/ pour écouter sur toutes les adresses. Mais ce n'est pas forcément une bonne idée : en effet, le serveur Kestrel utilisé par défaut par ASP.NET Core n'est pas prévu pour être exposé directement sur l'extérieur. Il est mal protégé contre les attaques et pas adapté à supporter une charge importante. Il est donc recommandé de le placer derrière un reverse proxy comme nginx. Voyons donc comment faire !

Tout d'abord, il faut installer nginx s'il n'est pas déjà présent sur le Raspberry Pi, à l'aide de cette commande :

sudo apt-get install nginx

Et le démarrer :

sudo service nginx start

On va maintenant le configurer pour que les requêtes arrivant sur le port 80 du Raspberry Pi soient passées à notre application sur le port 5000. Pour cela, on va éditer le fichier de configuration /etc/nginx/sites-available/default. La configuration par défaut ne comporte qu'un serveur, qui écoute sur le port 80. Dans la définition de ce serveur, cherchez la section qui commence par location / : c'est la section qui correspond au chemin racine sur ce serveur. Remplacez son contenu par les paramètres suivants :

location / {
        proxy_pass http://localhost:5000/;
        proxy_http_version 1.1;
        proxy_set_header Connection keep-alive;
}

Attention à bien conserver le slash final dans l'URL de destination.

Cette configuration est volontairement minimaliste, on va l'enrichir un peu par la suite.

Une fois la configuration effectuée, exécuter la commande suivante pour que nginx recharge sa configuration :

sudo nginx -s reload

Depuis votre PC, essayez maintenant d'accéder à l'application sur le Raspberry Pi en entrant son adresse IP dans le navigateur. Si tout s'est bien passé, vous devriez voir s'afficher la page d'accueil du template d'application ASP.NET Core !

Notez qu'il faut faire preuve d'un peu de patience : le premier chargement des vues peut prendre un peu de temps étant donné la faible puissance du RPi. En effet la précompilation des vues Razor n'est pas encore disponible en ASP.NET Core 2.0 pour les applications self-contained ; ce problème sera réglé dans la version 2.1, actuellement en preview. Il y a donc actuellement 3 options :

  • être patient et accepter le délai lors du premier affichage des pages
  • migrer vers ASP.NET Core 2.1 (pas encore stable), comme expliqué ici
  • faire un déploiement non self-contained, c'est-à-dire qui nécessite l'installation de .NET Core sur le Raspberry Pi.

Dans le cadre de cet article, j'ai opté pour la première option, afin de ne pas compliquer les choses.

En-têtes du proxy

On pourrait s'arrêter là, mais en l'état il y a certaines choses qui ne vont pas marcher correctement... En effet, notre application ne sait rien du fait qu'elle se trouve derrière un reverse proxy, et croit écouter uniquement des requêtes sur localhost sur le port 5000... Elle ne sait pas, par exemple :

  • quelle est l'adresse IP réelle du client (la requête semble venir de l'adresse locale)
  • quel est le protocole utilisé par le client (HTTP ou HTTPS)
  • quel est le nom d'hôte spécifié par le client

Il faut donc que le reverse proxy fournisse ces informations, et que l'application les prenne en compte. On va tout d'abord modifier la configuration de nginx pour qu'il renseigne 3 en-têtes, non standards mais généralement utilisés par les proxies :

    proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host   $http_host;
    proxy_set_header X-Forwarded-Proto  http;

(N'oubliez pas de recharger la configuration de nginx)

Et au tout début de la méthode Configure de la classe Startup, on va utiliser le middleware qui gère ces en-têtes :

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.All
});

(pour plus d'infos sur les middleware ASP.NET Core, voir l'article de William)

Ce middleware va lire les headers qu'on a configurés dans nginx, et les utiliser pour modifier :

  • le Host et le Scheme (protocole) de la requête
  • le Connection.RemoteIpAddress qui contient l'adresse du client

De cette façon, l'application aura l'impression de recevoir la requête directement depuis le client.

Exposer l'application à un chemin spécifique

Actuellement notre application est accessible à l'adresse http://<adresse-ip>/, c'est à dire à la racine du serveur. Mais si on veut héberger plusieurs applications sur le RPi, ça va poser un problème... On pourrait les mettre sur des ports différents, mais ce n'est pas forcément pratique. Ce qui serait plus pratique, c'est que le premier segment du chemin indique à quelle application on veut accéder, par exemple avec une adresse comme http://<adresse-ip>/MyWebApp/.

C'est très facile à faire avec nginx. Modifions à nouveau la configuration, et remplaçons location / par location /MyWebApp/ (attention, le slash final est important). Rechargeons la configuration, et essayons d'accéder à notre application à sa nouvelle adresse... la page se charge, mais nos CSS et scripts JS ne chargent plus : erreur 404. De plus les liens sont faux, et pointent par exemple sur http://<adresse-ip>/Home/About au lieu de http://<adresse-ip>/MyWebApp/Home/About. Mais que se passe-t-il ?

Tout simplement, notre application ne sait pas qu'elle n'est pas à la racine du serveur, et génère tous ses liens comme si elle y était... Pour corriger ça, on va demander à nginx de passer encore un autre en-tête à notre application :

proxy_set_header X-Forwarded-Path   /MyWebApp;

Notez que cet en-tête X-Forwarded-Path est encore moins standard que les précédents, je l'ai inventé pour l'occasion... Du coup, il n'existe pas de middleware ASP.NET Core capable de le prendre en compte, on va donc devoir le créer. Heureusement, c'est très simple : il faut juste que l'application utilise le chemin indiqué dans cet en-tête comme chemin racine. Dans notre Startup.Configure, on va juste ajouter ceci à la suite du UseForwardedHeaders :

// Patch path base with forwarded path
app.Use(async (context, next) =>
{
    var forwardedPath = context.Request.Headers["X-Forwarded-Path"].FirstOrDefault();
    if (!string.IsNullOrEmpty(forwardedPath))
    {
        context.Request.PathBase = forwardedPath;
    }

    await next();
});

On redéploie et relance l'application, on recharge la configuration nginx, et on teste à nouveau : ça marche !

Configurer l'application comme un service

Si on souhaite que notre application tourne en permanence, on ne va pas s'amuser à aller la relancer à chaque fois qu'elle aura planté ou que le Raspberry Pi aura redémarré... On veut qu'elle se lance au démarrage, et qu'elle se relance toute seule en cas de crash. Pour cela, on va utiliser systemd, qui permet de gérer les services dans la majorité des distributions Linux, dont Raspbian.

On va créer un fichier nommé MyWebApp.service dans le répertoire /lib/systemd/system/, et y mettre le contenu suivant :

[Unit]
Description=My ASP.NET Core Web App
After=nginx.service

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/apps/MyWebApp
ExecStart=/home/pi/apps/MyWebApp/MyWebApp
Restart=always

[Install]
WantedBy=multi-user.target

(remplacez bien sûr le nom et les chemins par ceux de votre application)

Il suffit ensuite d'exécuter la commande suivante pour activer le service :

sudo systemctl enable MyWebApp

Puis celle-ci pour le démarrer (les nouveaux services ne sont pas automatiquement démarrés) :

sudo systemctl start MyWebApp

Et voilà, notre application est maintenant gérée par systemd, qui s'assure qu'elle est lancée au démarrage et relancée en cas de crash !

Conclusion

On a donc vu qu'il était assez facile de faire tourner une application ASP.NET Core 2.0 sur un Raspberry Pi, moyennant quelques bricolages pour le reverse proxy. Maintenant il n'y a plus qu'à donner libre cours à votre imagination !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus