URL: https://linuxfr.org/news/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 ZeroHeure, Davy Defaud, palm123 et Nÿco Date: 2018年11月15日T12:35:04+01:00 License: CC By-SA Tags: heroku, docker, nix et debian Score: 30 Les services de plate‐forme ([PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service "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, etc.). Cette article 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/). ---- [Journal à l’origine de la dépêche](https://linuxfr.org/users/nokomprendo-3/journaux/deployer-une-application-web-c-sur-heroku-avec-docker-et-nix) [Code source](https://framagit.org/nokomprendo/tuto_fonctionnel/tree/master/posts/tuto_fonctionnel_30/) [La vidéo sur YouTube](https://youtu.be/CC7sEojuguM) [La vidéo sur PeerTube](https://peertube.mastodon.host/videos/watch/9eb86322-a032-43b6-9f93-b09bf89fa4f4) [Nix 2.0 sur LinuxFr.org](https://linuxfr.org/news/le-gestionnaire-de-paquets-nix-en-version-2-0) ---- # Exemple d’application Web C++ avec le cadriciel Wt [Wt](https://www.webtoolkit.eu/wt) est un cadriciel Web basé _widget_. Il permet de définir les composants de l’interface et leurs interactions, de façon similaire aux API d’interfaces graphiques de bureau comme Qt ou gtkmm. Wt produit des applications Web client‐serveur, mais ceci est transparent pour le développeur. Pour illustrer cet article, prenons une application simple qui répè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 l’on construit notre application à partir de son code source. Ici, on installe Wt manuellement car Debian fournit la version 3 et l’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 . 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 . Cependant, l’image Docker générée est lourde (876 Mio) 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 Mio). # 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 une étiquette 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 Mio) 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 Mio. # Conclusion Sans être aussi riche que Node.js ou PHP, C++ possède également des cadriciels Web intéressants. Notamment Wt, qui permet de développer des applications client‐serveur avec une API très proche des cadriciels 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 fichiers 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 paquets Nix permet également de construire des images Docker, avec les Docker Tools. Ces outils s’intègrent au système de gestion de paquets de Nix, ce qui permet de profiter de ses avantages (fichiers Nix, composition, reproductibilité, isolation...). # Bémol Cependant, n’oubliez pas que Heroku n’exécute pas d’images Docker. Les _layers_ sont extraits et ça tourne sous LXC. Il y a quelques incidences à prendre en compte : ## Expose De base, en Docker, une image expose un ou plusieurs ports, et ça permet de savoir quoi mettre en correspondance et qui écoute. Pour Heroku, ça ne fonctionne pas, il faut passer `$PORT` : ``` CMD /root/myrepeat --docroot . --http-address 0.0.0.0 --http-port $PORT ``` ## Point d’entrée Le point d’entrée (_entrypoint_) des images est surchargé par `/bin/sh -c` s’il n’est pas défini. Par exemple, si l’on utilise [distroless](https://github.com/GoogleContainerTools/distroless) pour faire une image Go, le point d’entrée est `null` et la commande est le binaire Go. Et ça fonctionne bien sous Docker. Mais sous Heroku, c’est `/bin/sh -c ` qui est executé. En mettant le binaire dans le point d’entrée et la commande à `""`, ça fonctionne. ## HealthChecks Les bilans de santé (_healthchecks_) ne sont pas pris en charge, le _Dyno manager_ fait automatiquement ses propres _checks_. Pour avoir une idée des autres limites, voir [_Unsupported Dockerfile commands_](https://devcenter.heroku.com/articles/container-registry-and-runtime#unsupported-dockerfile-commands). --- _N. D. M. : Ce bémol n’est pas de l’auteur, mais de_ [CrEv](https://linuxfr.org/users/crev).

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