URL: https://linuxfr.org/users/nokomprendo-3/journaux/deployer-une-application-web-c-sur-heroku-avec-docker-et-nix Title: Déployer une application web C++ sur Heroku avec Docker et Nix Authors: nokomprendo Date: 2018年11月15日T01:55:43+01:00 License: CC By-SA Tags: nix, c++, développement_web, docker, debian et mastodon Score: 17 Les services de plateforme ([PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service)) comme [Heroku](https://www.heroku.com/) permettent de déployer des applications web écrites dans des langages comme PHP, Ruby, Java... Cependant, déployer des applications C++ est plus compliqué (portabilité de l'interface binaire ABI, gestion des dépendances...). Ce post présente plusieurs solutions pour déployer des applications web C++ sur Heroku, en utilisant des images Docker et le [gestionnaire de paquet Nix](https://nixos.org/nix/). Voir aussi : [code source](https://framagit.org/nokomprendo/tuto_fonctionnel/tree/master/posts/tuto_fonctionnel_30/) - [vidéo youtube](https://youtu.be/CC7sEojuguM) - [vidéo peertube](https://peertube.mastodon.host/videos/watch/9eb86322-a032-43b6-9f93-b09bf89fa4f4) # Exemple d'application web C++ avec le framework Wt [Wt](https://www.webtoolkit.eu/wt) est un framework web basé widget. Il permet de définir les composants de l'interface et leurs interactions, de façon similaire aux APIs d'interface graphique de bureau comme Qt ou Gtkmm. Wt produit des applications web client-serveur mais ceci est transparent pour le développeur. Pour illustrer ce post, prenons une application simple qui repète le texte entré par l'utilisateur : ![appli](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_30/images/tuto30-repeat.png) Cette application peut être implémentée avec le code suivant (`myrepeat.cpp`) : ```cpp #include #include #include #include #include using namespace std; using namespace Wt; // définit une application web struct App : WApplication { App(const WEnvironment& env) : WApplication(env) { // ajoute des widgets auto myEdit = root()->addWidget(make_unique()); root()->addWidget(make_unique()); auto myText = root()->addWidget(make_unique()); // connecte les widgets aux fonctions de rappel auto editFunc = [=]{ myText->setText(myEdit->text()); }; myEdit->textInput().connect(editFunc); } }; // lance l'application web int main(int argc, char **argv) { auto mkApp = [](const WEnvironment& env) { return make_unique(env); }; return WRun(argc, argv, mkApp); } ``` Ce code peut être compilé et exécuté localement, avec les commandes suivantes : ```sh g++ -O2 -o myrepeat myrepeat.cpp -lwthttp -lwt ./myrepeat --docroot . --http-address 0.0.0.0 --http-port 3000 ``` Cependant, on ne peut pas déployer directement le binaire généré sur un service comme Heroku car le système distant peut être différent du système local. Une solution classique consiste à construire une image Docker contenant un système autonome. C'est ce que font les quatre solutions présentées ci-dessous. # Solution 1 : Dockerfile simple Un `Dockerfile` permet de définir un système complet. On part d'une image de base, ici une Debian 9, on installe les dépendances et on construit notre application à partir de son code source. Ici, on installe Wt manuellement car Debian fournit la version 3 et on a besoin de la version 4. ```dockerfile # configure l'image de base FROM debian:stretch-slim RUN apt-get update RUN apt-get install -y --no-install-recommends \ ca-cacert \ cmake \ build-essential \ libboost-all-dev \ libssl-dev \ wget \ zlib1g-dev # installe Wt4 WORKDIR /root RUN wget https://github.com/emweb/wt/archive/4.0.4.tar.gz RUN tar zxf 4.0.4.tar.gz WORKDIR /root/wt-4.0.4/build RUN cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF .. RUN make -j2 install RUN ldconfig # compile notre application puis configure la commande de lancement WORKDIR /root/myrepeat ADD . /root/myrepeat RUN g++ -O2 -o myrepeat myrepeat.cpp -lwthttp -lwt CMD /root/myrepeat/myrepeat --docroot . --http-address 0.0.0.0 --http-port $PORT ``` On note la variable d'environnement `PORT` dans la commande de lancement, qui sera définie par Heroku lors du déploiement. On peut ensuite construire et lancer localement l'image : ```sh docker build -t myrepeat:v1 . docker run --rm -it -e PORT=3000 -p 3000:3000 myrepeat:v1 ``` L'application est alors accessible à partir d'un navigateur web, à l'adresse http://localhost:3000. L'interface console d'Heroku permet de déployer des images Docker très facilement. Ceci nécessite, bien évidemment, un compte sur Heroku (voir [Heroku for free](https://www.heroku.com/free)). Par exemple, pour déployer une image Docker dans une application `myrepeat`, à partir du `Dockerfile` précédent : ```sh heroku container:login heroku create myrepeat heroku container:push web --app myrepeat heroku container:release web --app myrepeat ``` L'application déployée est alors accessible à l'adresse http://myrepeat.herokuapp.com/. Cependant, l'image Docker générée est lourde (876 Mo) car elle contient tous les paquets de développement et les produits de compilation de Wt. # Solution 2 : Dockerfile multi-stage Pour réduire la taille de l'image Docker, on peut compiler notre application dans un système dédié puis récupérer, dans le système final, le binaire généré et ses dépendances. ```dockerfile # configure une image pour construire notre application FROM debian:stretch-slim as builder RUN apt-get update RUN apt-get install -y --no-install-recommends \ ca-cacert \ cmake \ build-essential \ libboost-all-dev \ libssl-dev \ wget \ zlib1g-dev # installe Wt4 WORKDIR /root RUN wget https://github.com/emweb/wt/archive/4.0.4.tar.gz RUN tar zxf 4.0.4.tar.gz WORKDIR /root/wt-4.0.4/build RUN cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF -DSHARED_LIBS=OFF .. RUN make -j2 install # construit notre application, avec liaison statique WORKDIR /root/myrepeat ADD . /root/myrepeat RUN g++ -static -O2 -o myrepeat myrepeat.cpp -pthread -lwthttp -lwt \ -lboost_system -lboost_thread -lboost_filesystem -lboost_program_options \ -lz -lssl -lcrypto -ldl # crée l'image finale, contenant notre application FROM debian:stretch-slim RUN apt-get update WORKDIR /root COPY --from=builder /root/myrepeat/myrepeat /root/ CMD /root/myrepeat --docroot . --http-address 0.0.0.0 --http-port $PORT ``` On peut construire, exécuter et déployer une image de la même façon que précédemment mais l'image obtenue est beaucoup plus légère (83 Mo). # Solution 3 : configuration Nix simple Avec Nix, il est très facile de configurer un projet. Pour cela, on définit une dérivation, dans un fichier `default.nix` : ```nix { pkgs ? import {}, wt ? pkgs.wt }: pkgs.stdenv.mkDerivation { name = "myrepeat"; src = ./.; buildInputs = [ wt ]; buildPhase = "g++ -O2 -o myrepeat myrepeat.cpp -lwthttp -lwt"; installPhase = '' mkdir -p $out/bin cp myrepeat $out/bin/ ''; } ``` On peut alors construire notre application avec la commande `nix-build` puis exécuter le binaire obtenu : ```sh nix-build ./result/bin/myrepeat --docroot . --http-address 0.0.0.0 --http-port 3000 ``` Nix peut également construire des images Docker. Ceci est documenté dans le [manuel Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) et dans le [wiki Nix](https://nixos.wiki/wiki/Docker). À la place du `Dockerfile`, on écrit un fichier Nix (par exemple `docker.nix`), qui décrit l'image Docker à construire : ```nix { pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/18.09.tar.gz") {} }: let # importe la configuration de notre application myapp = import ./default.nix { inherit pkgs; }; # script pour lancer notre application, dans l'image Docker entrypoint = pkgs.writeScript "entrypoint.sh" '' #!${pkgs.stdenv.shell} $@ --docroot . --http-address 0.0.0.0 --http-port $PORT ''; in # construit l'image Docker, avec notre application pkgs.dockerTools.buildImage { name = "myrepeat"; tag = "v3"; config = { Entrypoint = [ entrypoint ]; Cmd = [ "${myapp}/bin/myrepeat" ]; }; } ``` À partir de ce fichier `docker.nix`, on peut construire une image Docker et la charger dans le registre Docker local : ```sh nix-build docker.nix && docker load < result ``` On peut alors exécuter l'image Docker localement comme avec les solutions précédentes. Pour le déploiement, on définit un tag vers le registre Docker d'Heroku et on y charge notre image : ```sh heroku container:login heroku create myrepeat docker tag myrepeat:v3 registry.heroku.com/myrepeat/web docker push registry.heroku.com/myrepeat/web heroku container:release web --app myrepeat ``` L'image Docker obtenue est assez lourde (579 Mo) car elle est construite à partir des paquets Nix standards, qui sont génériques. # Solution 4 : configuration Nix optimisée Pour réduire la taille de l'image Docker générée, on peut adapter les options des paquets Nix à notre application. Pour cela, on peut redéfinir les options des dérivations ou écrire nos propres dérivations. Par exemple, on peut réécrire la dérivation Wt de la façon suivante (fichier `wt.nix`) : ```nix { stdenv, fetchFromGitHub, cmake, boost, openssl, zlib }: stdenv.mkDerivation { name = "wt"; src = fetchFromGitHub { owner = "emweb"; repo = "wt"; rev = "4.0.4"; sha256 = "17kq9fxc0xqx7q7kyryiph3mg0d3hnd3jw0rl55zvzfsdd71220w"; }; enableParallelBuilding = true; buildInputs = [ cmake boost openssl zlib ]; cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" "-DBUILD_TESTS=OFF" "-DBUILD_EXAMPLES=OFF" ]; } ``` On modifie ensuite le fichier `docker.nix` de façon à prendre en compte notre version de Wt : ```nix { pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/18.09.tar.gz") {} }: let # importe un paquet de Wt optimisé pour notre application mywt = pkgs.callPackage ./wt.nix {}; # importe la configuration de notre application, en utilisant notre version de Wt myapp = import ./default.nix { inherit pkgs; wt = mywt; }; entrypoint = pkgs.writeScript "entrypoint.sh" '' #!${pkgs.stdenv.shell} $@ --docroot . --http-address 0.0.0.0 --http-port $PORT ''; in pkgs.dockerTools.buildImage { name = "myrepeat"; tag = "v4"; config = { Entrypoint = [ entrypoint ]; Cmd = [ "${myapp}/bin/myrepeat" ]; }; } ``` On peut alors construire et déployer une image Docker de la même façon qu'avec la solution précédente. L'image Docker générée ici fait 105 Mo. # Conclusion Sans être aussi riche que Node.js ou PHP, C++ possède également des frameworks web intéressants. Wt, notamment, permet de développer des applications client-serveur avec une API très proche des frameworks d'interface de bureau, comme Qt et Gtkmm. Si les PaaS comme Heroku permettent facilement de déployer des applications dans les « langages web classiques », il est également souvent possible de déployer des images Docker, et donc des applications C++. Les Dockerfile permettent de construire des images Docker relativement facilement. Cependant construire une image optimisée demande un peu plus de travail (image multi-stage, compilation statique...), notamment pour éviter d'inclure un inutile environnement de compilation dans l'image à déployer. Enfin, le gestionnaire de paquet Nix permet également de construire des images Docker, avec les dockerTools. Ces outils s'intègrent au système de gestion de paquet de Nix, ce qui permet de profiter de ses avantages (fichiers Nix, composition, reproductibilité, isolation...).

AltStyle によって変換されたページ (->オリジナル) /