Sommaire
- Exemple d'application web C++ avec le framework Wt
- Solution 1 : Dockerfile simple
- Solution 2 : Dockerfile multi-stage
- Solution 3 : configuration Nix simple
- Solution 4 : configuration Nix optimisée
- Conclusion
Les services de plateforme (PaaS) comme Heroku 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.
Voir aussi : code source - vidéo youtube - vidéo peertube
Exemple d'application web C++ avec le framework Wt
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 :
Cette application peut être implémentée avec le code suivant (myrepeat.cpp
) :
#include <Wt/WApplication.h>
#include <Wt/WBreak.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WLineEdit.h>
#include <Wt/WText.h>
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<WLineEdit>());
root()->addWidget(make_unique<WBreak>());
auto myText = root()->addWidget(make_unique<WText>());
// 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<App>(env); };
return WRun(argc, argv, mkApp);
}
Ce code peut être compilé et exécuté localement, avec les commandes suivantes :
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.
# 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 :
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). Par exemple, pour déployer une image Docker dans une application myrepeat
, à partir du Dockerfile
précédent :
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.
# 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
:
{ pkgs ? import <nixpkgs> {}, 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 :
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 et dans le wiki Nix. À la place du Dockerfile
, on écrit un fichier Nix (par exemple docker.nix
), qui décrit l'image Docker à construire :
{ 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 :
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 :
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
) :
{ 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 :
{ 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…).
# Heroku et Docker
Posté par CrEv (site web personnel) . Évalué à 7.
Avec tout de même un bémol dont il faut être conscient: Heroku n'execute pas d'images docker. Les layers sont extraits et ça tourne sous LXC.
Et ça a quelques incidences à prendre en compte.
Expose
De base en docker une image expose un ou plusieurs ports et ça permet de savoir quoi mapper et qui écoute. Pour Heroku ça ne fonctionne pas, il faut passer
$PORT
.Entrypoint
L'entrypoint des images est surchargé par
/bin/sh -c
s'il n'est pas définit.Par exemple si on utilise distroless pour faire une image Go, l'entrypoint est
null
et la commande est le binaire Go. Et ça fonctionne bien sous Docker. Mais sous heroku c'est/bin/sh -c <binaire>
qui est executé.En mettant le binaire dans l'entrypoint et la commande à
""
ça fonctionne.HealthChecks
Les healthchecks ne sont pas supportés, le Dyno manager fait automatiquement ses propres checks.
Pour avoir une idée des autres limites, voir Unsupported Dockerfile commands.
Sinon j'ai jamais essayé avec Nix, mais les images multi-stage say bon mangez-en !
# Beast
Posté par David Demelier (site web personnel) . Évalué à 1.
Moi j'ai utilisé Boost.Beast pour faire un petit simili pastebin.
C'est très bas niveau, à peu près comme flask en python. On gère directement les requête HTTP et les réponses. La bibliothèque est assez récente et un peu jeune mais ça fonctionne plutôt bien.
Seul problèmes :
Pour le premier, c'est dans la TODO de l'auteur. À voir quand il l'implémentera.
Pour avoir testé Wt, je n'aime pas trop le HTML généré, il est difficile de le personnaliser et nécessite pas mal de javascript. J'aime bien avoir accès total et complet au rendu généré.
git is great because linus did it, mercurial is better because he didn't
# Proposé en dépêche
Posté par ZeroHeure . Évalué à 4. Dernière modification le 15 novembre 2018 à 19:27.
Les modérateurs éblouis par ce déploiement, ont préparé une dépêche basée sur le journal et le commentaire de CrEv. Elle est dans la file de modération.
"La liberté est à l'homme ce que les ailes sont à l'oiseau" Jean-Pierre Rosnay
[^] # Re: Proposé en dépêche
Posté par nokomprendo (site web personnel) . Évalué à 1.
Ok. Merci pour la dépêche et pour le commentaire de CrEv, qui est effectivement très intéressant.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.