Voici un nouvel article sur Docker qui fait suite à docker et macos . Cette fois-ci un usage réel, avec le développement d’un simple site web statique déployé dans un conteneur docker et publié par le serveur web caddy. J’utilise un poste de travail sous Linux/Fedora avec les packages golang et docker de la distribution.

Beego

Beego est un framework web en Golang . Golang est un langage que j’apprécie énormement, il fera peut être l’objet d’un article mais on peut le résumer en ces quelques termes : compilé, statique (binaire non dynamique), multiplateforme (windows,mac,linux), multiarchitecture (X86, ARM) , très performant, simple, efficace, concurrent ; go by example donne des exemples du langage et pour des exemples web https://gowebexamples.github.io/

Créé par Google en opensource, il a été pensé et conçu pour la programmation système, c’est à dire pour le développement de services côté serveur type serveur web, mais est aussi utilisé sur des systèmes embarqués type rasberry pi et IOT (https://gobot.io )

Golang founi de base le package net/http ainsi que le package html/template qui permettent en quelques lignes de code de développer un serveur web. Même s’ils suffisent pour un site web basique, certains ont développés des frameworks très complets afin de se rapprocher de ce qui se fait de mieux dans d’autres langages comme python, php ou ruby.

Revel par exemple est très proche de la phylosophie de Rails en ruby. D’autres framework comme Gorilla préfèrent proposer des boites à outils plutôt qu’un framework complet mais rigide. Le dernier à la mode est Echo , très minimaliste et rapide.

Pour ma part j’ai pour le moment choisi Beego un framework MVC qui inclut de nombreux outils (génération d’un squelette, ORM, pagination, API documentation, …).

Installation

go get github.com/astaxie/beego
go get github.com/beego/bee

Un binaire bee a été créé dans $GOPATH/bin/. Si ce chemin a été ajouté dans la variable $PATH, on peut le lancer :

➜  bee
Bee is a Fast and Flexible tool for managing your Beego Web Application.

USAGE
    bee command [arguments]

AVAILABLE COMMANDS

    new         Creates a Beego application
    run         Run the application by starting a local development server
    pack        Compresses a Beego application into a single file
    api         Creates a Beego API application
    hprose      Creates an RPC application based on Hprose and Beego frameworks
    bale        Transforms non-Go files to Go source files
    version     Prints the current Bee version
    generate    Source code generator
    migrate     Runs database migrations
    fix         Fixes your application by making it compatible with newer versions of Beego

Use bee help [command] for more information about a command.

ADDITIONAL HELP TOPICS


Use bee help [topic] for more information about that topic.

Développement

on se positionne dans $GOPATH/src

➜  cd $GOPATH/src
➜  bee new test_bee
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.6.2
2017/02/06 00:14:05 INFO     ▶ 0001 Creating application...
	create	 /home/fredix/Sync/code/golang/src/test_bee/
	create	 /home/fredix/Sync/code/golang/src/test_bee/conf/
	create	 /home/fredix/Sync/code/golang/src/test_bee/controllers/
	create	 /home/fredix/Sync/code/golang/src/test_bee/models/
	create	 /home/fredix/Sync/code/golang/src/test_bee/routers/
	create	 /home/fredix/Sync/code/golang/src/test_bee/tests/
	create	 /home/fredix/Sync/code/golang/src/test_bee/static/
	create	 /home/fredix/Sync/code/golang/src/test_bee/static/js/
	create	 /home/fredix/Sync/code/golang/src/test_bee/static/css/
	create	 /home/fredix/Sync/code/golang/src/test_bee/static/img/
	create	 /home/fredix/Sync/code/golang/src/test_bee/views/
	create	 /home/fredix/Sync/code/golang/src/test_bee/conf/app.conf
	create	 /home/fredix/Sync/code/golang/src/test_bee/controllers/default.go
	create	 /home/fredix/Sync/code/golang/src/test_bee/views/index.tpl
	create	 /home/fredix/Sync/code/golang/src/test_bee/routers/router.go
	create	 /home/fredix/Sync/code/golang/src/test_bee/tests/default_test.go
	create	 /home/fredix/Sync/code/golang/src/test_bee/main.go
2017/02/06 00:14:05 SUCCESS  ▶ 0002 New application successfully created!

Un squelette de code a été généré, on peut immédiatement tester :

➜  bee run    
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.6.2
2017/02/06 00:15:30 INFO     ▶ 0001 Using 'test_bee' as 'appname'
2017/02/06 00:15:30 INFO     ▶ 0002 Loading default configuration...
2017/02/06 00:15:30 INFO     ▶ 0003 Initializing watcher...
2017/02/06 00:15:30 INFO     ▶ 0004 Watching: /home/fredix/Sync/code/golang/src/test_bee/controllers
2017/02/06 00:15:30 INFO     ▶ 0005 Watching: /home/fredix/Sync/code/golang/src/test_bee
2017/02/06 00:15:30 INFO     ▶ 0006 Watching: /home/fredix/Sync/code/golang/src/test_bee/routers
2017/02/06 00:15:30 INFO     ▶ 0007 Watching: /home/fredix/Sync/code/golang/src/test_bee/tests
test_bee/controllers
test_bee/routers
test_bee
2017/02/06 00:15:32 SUCCESS  ▶ 0008 Built Successfully!
2017/02/06 00:15:32 INFO     ▶ 0009 Restarting 'test_bee'...
2017/02/06 00:15:32 SUCCESS  ▶ 0010 './test_bee' is running...
2017/02/06 00:15:32 [I] [asm_amd64.s:2086] http server Running on http://:8080

Il suffit d’ouvrir un navigateur vers http://localhost:8080 .

Dans le terminal on constate que chaque accès à la page web génère un log bien pratique

2017/02/06 00:16:33 [D] [server.go:2202] |      127.0.0.1| 200 |   6.585903ms|   match| GET      /     r:/
2017/02/06 00:17:39 [D] [server.go:2202] |      127.0.0.1| 200 |   5.996408ms|   match| GET      /     r:/

un ctrl/c puis un ls montre que beego a bien généré un binaire test_bee

➜  ls
conf  controllers  main.go  models  routers  static  test_bee  tests  views

il suffit de lancer ce binaire pour relancer le site web

➜  ./test_bee 
2017/02/06 00:21:41 [I] [asm_amd64.s:2086] http server Running on http://:8080

cependant pour développer, il vaut mieux utiliser bee run, en effet à chaque modification/sauvegarde de votre code golang, bee va le détecter et recompiler le binaire automatiquement, il n’y a qu’a reloader la page web du navigateur pour tester son nouveau code.

Pour le déploiement en production, il suffi de déployer le binaire et les répertoires views, static et conf. Nul besoin de déployer des centaines de fichiers php,ruby ou python et leurs multiples bibliothèques.

Docker

Pour “dockeriser” le site, il est nécessaire de créer un fichier Dockerfile dans le répertoire racine du projet

FROM golang:1.7.4

# Create the directory where the application will reside
RUN mkdir /app

# Copy the application files (needed for production)
ADD test_bee /app/test_bee
ADD views /app/views
ADD static /app/static
ADD conf /app/conf

# Set the working directory to the app directory
WORKDIR /app

# Expose the application on port 8080.
# This should be the same as in the app.conf file
EXPOSE 8080

# Set the entry point of the container to the application executable
ENTRYPOINT /app/test_bee

Ce Dockerfile utilise une image golang basé sur debian et qui contient le compilateur Go. Dans cet exemple il est inutile et n’importe quelle image Linux aurait fait l’affaire. Les directives ADD copient les repertoires et le binaire dans le répertoire de travail /app. On expose le service sur le port 8080 du conteneur enfin le point d’entré est le binaire lui même.

On peut ensuite construire l’image docker à partir de ce Dockerfile

➜  sudo docker build -t test_bee .
Sending build context to Docker daemon 11.27 MB
Step 1 : FROM golang:1.7.4
 ---> f3bdc5e851ce
Step 2 : RUN mkdir /app
 ---> Using cache
 ---> 340c95dce984
Step 3 : ADD test_bee /app/test_bee
 ---> 2309a364d30d
Removing intermediate container f81926095aa4
Step 4 : ADD views /app/views
 ---> 6e76c453a59e
Removing intermediate container 53462f22be29
Step 5 : ADD static /app/static
 ---> 0a509d902b81
Removing intermediate container dc17015a69cd
Step 6 : ADD conf /app/conf
 ---> 45ca0bd4797e
Removing intermediate container 5bc68100a7cb
Step 7 : WORKDIR /app
 ---> Running in 04c7d82795c7
 ---> f55cc98abbea
Removing intermediate container 04c7d82795c7
Step 8 : EXPOSE 8080
 ---> Running in bd0d6570f558
 ---> 5bd99ec3e7fe
Removing intermediate container bd0d6570f558
Step 9 : ENTRYPOINT /app/test_bee
 ---> Running in b6ba08bfc8b8
 ---> 379d95270e02
Removing intermediate container b6ba08bfc8b8
Successfully built 379d95270e02

on vérifie la présence de notre nouvelle image

➜  sudo docker images
REPOSITORY                     TAG                 IMAGE ID            CREATED              SIZE
test_bee                       latest              379d95270e02        About a minute ago   685.2 MB

enfin on démarre le conteneur basé sur notre image

➜  sudo docker run -it --rm --name test_bee -p 8080:8080 -v /app/test_bee:/go/src/test_bee -w /go/src/test_bee test_bee
2017/02/06 20:12:32 [I] [asm_amd64.s:2086] http server Running on http://:8080

en allant sur http://localhost:8080/ on consulte la page par défaut servie par le binaire Golang test_beestocké dans notre conteneur. Simple et rapide mais quel est l’intéret de lancer notre programme dans un conteneur docker par rapport à la version où on le lance via bee run ?

Pendant le développement pas grand chose sauf si le programme doit accéder à des services tiers types base de données, serveur clé/valeur (redis, ..) etc. En effet chacun de ces services devra être installé et configuré sur le poste de développement qui est lui certainement dans une version d’OS différent de la production et avec des paquets en version différente. Or chacun de ces services possèdent souvent une image docker qu’il suffira d’installer sur le poste de développement, ensuite notre image test_bee pourra communiquer avec ces autres instances docker. Cette configuration sera identique en production, ce qui garanti au développeur un comportement identique de son code en dev et prod. Exemple

docker run --name caddy -p 80:80 -p 443:443 -d -v caddy-data:/etc/ -v caddy-root:/root/.caddy --link gogs:gogs --link hugo --link wallabag --link keeweb abiosoft/caddy:latest

Ceci un docker run sur mon serveur personnel. Cette commande lance le conteneur caddy (un serveur web en Golang) qui est linké (–link) avec d’autres conteneurs. En effet chaque conteneur (gogs, hugo, wallabag) expose leur service vers un port non exposé sur Internet du serveur physique. Caddy, qui agit comme un proxy web, associe un domaine (fredix.xyz) vers le port 1313 du service web hugo :

docker run --name hugo -d -p 1313 -v hugo-data:/usr/share/blog/ fredix/hugo

Grâce au -link ,docker relie les interfaces réseaux les différents conteneurs, ce qui permet au conteneur caddy de communiquer avec le conteneur hugo. Cette configuration peut être totalement exécuté sur un poste de développement local et permet ainsi au codeur de valider son code et son architecture telle qu’elle le sera en production.

Revenons à notre simple conteneur test_bee. On est content il tourne en local, mais à présent nous souhaitons l’exécuter en production sur un serveur public. Il est nécessaire d’envoyer notre image dans un registre qui la stockera et permettra de la télécharger à volonté. Le pus simple et rapide est d’utiliser celui fourni par docker https://hub.docker.com/ . Il permet de stocker autant de conteneur publique que l’on veut et 1 seul conteneur privé dans la version gratuite.

Pour stocker un blog, un conteneur public fait bien l’affaire. Pour des conteneurs plus ‘corporate’ soit il suffit de payer sur hub.docker.com pour obtenir plus de dépôt privé, soit de monter sa propre registry.

Une fois son compte créé, il suffit de s’y connecter depuis son PC local

➜  sudo docker login 
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: fredix
Password: 
Login Succeeded

Avant d’envoyer l’image test_bee sur le registre docker, on modifie le conf/app.conf pour passer l’application en mode prod

➜  cat conf/app.conf 
appname = test_bee
httpport = 8080
runmode = prod

Enfin il faut renommer l’image en la préfixant par son login, on va donc la recréer

➜  sudo docker build -t fredix/test_bee .  

On la publie sur le registre

➜  sudo docker push fredix/test_bee                                                                                    
The push refers to a repository [docker.io/fredix/test_bee]
00f90a010461: Pushed 
12f4ee6b1258: Pushed 
1711061d538e: Pushed 
07e64af846fc: Pushed 
63a82295fe41: Mounted from fredix/nodecast.net 
fd3ac0159235: Mounted from fredix/nodecast.net 
784715688dd8: Mounted from fredix/nodecast.net 
7f8e95b7f6d7: Mounted from fredix/nodecast.net 
f4d2be23d596: Mounted from fredix/nodecast.net 
30339f20ced0: Mounted from fredix/nodecast.net 
0eb22bfb707d: Mounted from fredix/nodecast.net 
a2ae92ffcd29: Mounted from fredix/nodecast.net 
latest: digest: sha256:bdac1508df2d2e72c5c51804866646df5a345a5c2cb1fceac586bad117b6f6be size: 2833

Dans un navigateur web on peut consulter la présence de ce nouveau dépôt ( une fois connecté à https://hub.docker.com/ avec son compte bien entendu).

Cette image publique pourra être téléchargé par n’importe-qui, ce qui est le but si vous souhaitez diffuser un de vos logiciels opensource de manière simple à vos utilisateurs. Ainsi ils n’auront pas besoin d’installer et configurer eux-même tous les outils nécessaires au fonctionnement de votre service. Pour quelque chose de plus personnel comme un blog, il n’y a pas de mal mais pas d’intérêt pour un tiers à télécharger votre image, surtout si celle-ci ne contient qu’un binaire golang donc sans code source comme dans cet exemple.

Cependant il est tout à fait possible que le Dockerfile exécute un go get ou un git clone de votre code source hébergé chez github par exemple, le compile (si c’est du go l’image golang a alors tout son intérêt) puis l’exécute. C’est d’ailleurs sur ce principe que certains construisent et publient des images docker de logiciel opensource qui n’ont pas d’image docker faites par les développeurs du projet, ce qui devient des images non officielles.

Connectons nous en ssh sur notre serveur public. On va pouvoir télécharger notre image

docker pull fredix/test_bee

ou l’exécuter directement ce qui la téléchargera si elle n’est pas présente en local

docker run --name test_bee -d fredix/test_bee
Unable to find image 'fredix/test_bee:latest' locally
latest: Pulling from fredix/test_bee
5040bd298390: Already exists 
fce5728aad85: Already exists 
76610ec20bf5: Already exists 
86b681f75ff6: Already exists 
8553b52886d8: Already exists 
63c25ee63bd6: Already exists 
4268eec6f44b: Already exists 
d73944078585: Already exists 
6b99d012c4c6: Pull complete 
63b6323c4d21: Pull complete 
c1343e5441f0: Pull complete 
ae4a1f7364db: Pull complete 
Digest: sha256:bdac1508df2d2e72c5c51804866646df5a345a5c2cb1fceac586bad117b6f6be
Status: Downloaded newer image for fredix/test_bee:latest
fd09bb92f2b82d22399aab0ad020820920fd24d55cbfb759bdf86bc8b8860752

L’image et ses layers ont été téléchargés, puis elle a été aussitôt instancié.

docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED              STATUS              PORTS                                                NAMES
e70b96523cf1        fredix/test_bee         "/bin/sh -c /app/t..."   3 minutes ago        Up 3 minutes        8080/tcp                                             test_bee

On peut vérifier que tout fonctionne on consultant le log

docker logs test_bee
2017/02/06 21:40:07 [I] [asm_amd64.s:2086] http server Running on http://:8080

Mais aussi entrer dans le conteneur

docker exec -it test_bee bash
root@7e4e230fbca3:/app# ps ax
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 /bin/sh -c /app/test_bee
    6 ?        Sl     0:00 /app/test_bee
   18 ?        Ss     0:00 bash
   23 ?        R+     0:00 ps ax
root@7e4e230fbca3:/app# exit

Dans le docker run –name test_bee on aurait pu ajouter l’option -P ce qui aurait demandé à docker de mapper le port interne 8080 de l’application beego dans le conteneur vers un port aléatoire de l’hôte. Cela est inutile si le serveur web qui ferra reverse proxy est également dans un conteneur docker, s’il est hors de docker, alors vous pourrez vérifier avec un wget en utilisant le port du hôte relayé par docker vers le port 8080 du conteneur. En effet les ports des services dans les conteneurs ne sont pas accessible hors de ceux-ci, l’option –link de docker run va permettre de les rendre visible à l’extérieur de docker.

# On suppose que docker avec -P a alloué le port 32772 vers le port 8080 de test_bee
# sur l'hôte on peut alors faire 
wget localhost:32772
--2017-02-06 21:23:59--  http://localhost:32772/
Resolving localhost (localhost)... ::1, 127.0.0.1
Connecting to localhost (localhost)|::1|:32772... connected.
HTTP request sent, awaiting response... 200 OK
Length: 70111 (68K) [text/html]
Saving to: ‘index.html’

index.html                                                  100%[=========================================================================================================================================>]  68.47K  --.-KB/s    in 0s      

2017-02-06 21:23:59 (137 MB/s) - ‘index.html’ saved [70111/70111]

le contenu du index.html doit correspondre à la page d’accueil de test_bee. Il reste maintenant à exposer notre service sur Internet.

Caddy

Caddy est un serveur web en golang, qui en plus d’être simple à configurer a l’obligeance de générer et télécharger automatiquement des certificats Let’s encrypt ( https://caddyserver.com/docs/automatic-https ) . Ainsi chaque domaine configuré dans caddy sera exposé en https et avec un certificat valide \o/ J’ai pour ce test créé un sous domaine test.fredix.xyz qui renvoit vers l’ip de mon serveur.

Tout d’abord on créé des volumes dans docker afin de stocker les fichiers de configuration et les certificats de caddy. Cela permettra de supprimer pour mettre à jour l’image caddy sans supprimer ses données. Ce travail est d’ailleurs à faire sur chacun des conteneurs sauf s’ils n’ont pas de données à conserver ce qui est plutôt rare.

docker volume create --name caddy-root 
docker volume create --name caddy-data

on lance le conteneur

docker run --name caddy -p 80:80 -p 443:443 -d -v caddy-data:/etc/ -v caddy-root:/root/.caddy --link test_bee abiosoft/caddy:latest

On expose les ports 80 et 443 vers Internet puisque l’on souhaite qu’il agisse de server web pour nos autres conteneurs. On lui fourni 2 volumes dans lesquels il pourra stocker ses donnée persistantes (configuration et certificats) puis on le relie au conteneur test_bee afin qu’il puisse accèder à son port 8080 pour lui relayer les requetes HTTP en provenance d’Internet.

Entrons dans le conteneur caddy pour constater ce qu’à permis le –link

docker exec -it caddy sh
/srv # cat /etc/hosts
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.2	test_bee eb639a77249e
exit

On constate que docker a ajouté dans le /etc/hosts l’ip et le nom du conteneur test_bee. Cela permet de mettre dans la configuration Caddyfile test_bee qui pourra être résolu. On peut vérifier avec un wget

 docker exec -it caddy sh
cd /root
wget http://test_bee:8080
Connecting to test_bee:8080 (172.17.0.2:8080)
index.html           100%
cat index.html
exit

on peut éditer ensuite son fichier de configuration depuis le serveur hôte

cat /var/lib/docker/volumes/caddy-data/_data/Caddyfile

test.fredix.xyz {  
    proxy / test_bee:8080 {
	header_upstream Host {host}
	header_upstream X-Real-IP {remote}
	header_upstream X-Forwarded-Proto {scheme}
    }
    tls fredix@protonmail.com
}

Ces quelques lignes suffisent pour générer des certificats associés à mon email, et pour rediriger les requetes en http/https de test.fredix.xyz vers le conteneur test_bee sur le port 8080. On voit ici l’intérêt du –link test_bee : Docker autorise ainsi la connexion réseau entre les 2 conteneurs et nul besoin de connaitre le port affecté par docker sur l’hôte (32772), le nom du conteneur et le port interne suffise.

On peut relancer ensuite le conteneur

docker restart caddy

le log confirme la génération du certificat

docker logs caddy
Activating privacy features...2017/02/06 22:13:08 [INFO][test.fredix.xyz] acme: Obtaining bundled SAN certificate
2017/02/06 22:13:09 [INFO][test.fredix.xyz] acme: Authorization already valid; skipping challenge
2017/02/06 22:13:09 [INFO][test.fredix.xyz] acme: Validations succeeded; requesting certificates
2017/02/06 22:13:09 [INFO] acme: Requesting issuer cert from https://acme-v01.api.letsencrypt.org/acme/issuer-cert
2017/02/06 22:13:10 [INFO][test.fredix.xyz] Server responded with a certificate.
 done.
https://test.fredix.xyz
2017/02/06 22:13:10 https://test.fredix.xyz

que l’on peut trouver dans

ls -al /var/lib/docker/volumes/caddy-root/_data/acme/acme-v01.api.letsencrypt.org/sites/test.fredix.xyz/
total 20
drwx------ 2 root root 4096 Feb  6 22:13 .
drwx------ 8 root root 4096 Feb  6 22:13 ..
-rw------- 1 root root 3444 Feb  6 22:13 test.fredix.xyz.crt
-rw------- 1 root root  225 Feb  6 22:13 test.fredix.xyz.json
-rw------- 1 root root 1679 Feb  6 22:13 test.fredix.xyz.key

On peut à présent vérifier dans un navigateur web que tout fonctionne

caddy
caddy

Lorsque le développeur corrige des bugs, ajoute des fonctionnalités, il lui suffira de rebuilder son image, la republier sur le hub.docker.com, lancer un docker pull de celle-ci sur son serveur, stopper et supprimer le conteneur en prod et le relancer. Ces tâches sont bien sûr à automatiser avec par exemple ansible, à associer avec des outils d’intégration continue.

C’était un exemple très basique, vous trouverez un article plus complet du déploiement d’une application golang à cette adresse How To Deploy a Go Web Application with Docker