XinCTO/premier lab docker 1/2

Ecrit le Thu, 07 December 2017 11:56:13 +0000
1936 mots 10 minutes

premier lab docker 1/2

Comme demandé par un de mes lecteurs, j’entame ici une petite série d’articles sur docker, les containers et quelques usages. Je vous les propose sous la forme de lab afin de ne pas rester théorique, c’est plus simple en manipulant un peu pour s’approprier les concepts et sortir du théorique et de la discussion de machine à café.

La suite de ce premmier lab creusera plus sur des sujets d’héritage, d’intégration et de devops.

De mon point de vue les principes de containérisation vont très largement modifier le paysage des systèmes d’information. Beaucoup n’ont pas encore pris conscience de l’ampleur du phénomène, je le compare à ce qui s’est passé avec la virtualisation de serveur il y a une dizaine d’années. Alors sortons du théorique et un peu de pratique pour se mettre en jambes.

Container

La notion de container, bien qu’imagée, est assez concrète : un ensemble autonome et celé de composants logiciels qui peuvent s’exécuter indépendamment du système hôte. Le niveau de granularité est donc positionné à l’application ou au composant applicatif, contrairement à la VM qui se positionne plutôt au niveau du système d’exploitation.

Le container est donc plus léger, plus modulaire, indépendant du système mais en revanche utilise des niveaux assez bas dans le noyau et par conséquent est efficace ; il ne s’agit pas ici d’interprétation comme on pourrait l’avoir dans java par exemple.

Image et héritage

Le principe de container celé est important à intégrer :

  • on ne peut pas modifier le contenu d’un container (d’une image)
  • les modifications apportées pendant l’exécution du container seront perdues à son arrêt
  • à chaque instanciation d’un container on repart donc d’une situation stable et bien connue

Un principe d’héritage permet de construire un container, via une définition qui peut hérité d’une autre. Contrairement à une VM, on peut donc ajouter progressivement des fonctionnalités, des fichiers, à un container afin d’en faire un nouveau plus riche ou plus spécifique. C’est probablement le principe le plus intéressant de cette technologie et surtout celui qui ouvre le plus de perspectives.

Par exemple, nous pouvons avoir l’arbre d’héritage suivant :

  • jenkins
  • openjdk
  • buildpack-deps (stretch-scm)
  • buildpack-deps (stretch-curl)
  • debian
  • scratch

Dans le cas de l’image du container jenkins, les sous jacents principaux sont donc openjdk pour la partie java et debian pour la partie sous-système. On pourrait donc avoir un autre outil basé sur java qui utilise une grande partie de ce qui a déjà été fait pour jenkins en repiquant l’image openjdk comme source.

S’il est possible de construire un container docker sans se baser sur une image existante, cela représente un travail et une technicité assez importante. Autant démarrer d’un existant qui aura le mérite d’être maintenu et rendra bien plus simple l’implémentation de votre container applicatif. Donc si vous souhaitez mettre en container un applicatif java, autant partir de l’image openjdk, idem pour du python à partir de l’image python ou flask à partir de python.

Fonctionnement

On sépare 2 étapes que sont la construction de l’image du container et son exécution. La première étape dans le cas de docker est descriptive, on construit un fichier Dockerfile explicitant le contenu du container et la façon dont il sera utilisé. La seconde étape peut être répétée autant de fois que souhaité, l’exécution peut indifféremment se faire sur un poste en local, sur un cluster de plusieurs noeuds (type swarm ou kubernetes par exemple) ou sur un cloud comme amazon ECS.

Mise en place du labo

Assez de théorie, on s’y met : tout d’abord il vous faut un environnement pour travailler, je vous propose :

Validation de docker

Vous devez disposer de docker avec les bons droits :

$ docker -v
Docker version 17.09.0-ce, build afdb6d4

Image hello-world

Testons avec l’image hello-world

$ docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
ca4f61b1923c: Pull complete
Digest: sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905c
Status: Downloaded newer image for hello-world:latest

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              f2a91732366c        2 weeks ago         1.85kB

Execution de hello-world

$ docker run --rm hello-world
Hello from Docker!
[...]

premier container

C’est parti, vous avez les outils, maintenant on passe à la pratique. J’ai préparé un petit code simple de vérification de nombre premier, présenté sous la forme d’un web service et implémenté en python. Les sources sont disponibles sur mon git : docker-lab

Vous pouvez cloner le repo, nous allons détailler son contenu :

git clone https://github.com/achauvinhameau/docker-lab.git

On utilise ici le module d’exposition flask qui se base directement sur python et permet de gérer toute la partie d’exposition des webservices, le routage des url d’appel, la prise en charge des paramètres et l’encapsulation des réponses.

Container de base

Notre premier container va donc s’appuyer sur une image python et l’enrichir avec flask et le code. La description du container se prépare dans le fichier Dockerfile. On spécifie avec la commande FROM l’image de référence sous la forme d’un produit et d’une version, ici, python est le produit et on prend la dernière version, il s’agit d’un alias pour la version 3.6.3 à l’écriture de ce billet. En utilisant latest on est sûr d’avoir toujours une version à jour, parfois il est nécessaire de spécifier une version particulière afin de conserver un niveau de qualité constant.

# source image, python 3
FROM python:latest

Construction de notre permier container :

$ docker build -t flask .

Le fichier Dockerfile du répertoire courant va être utilisé afin de construire notre container, nommé flask. Suite à la construction du container, qui va entraîner la récupération de l’image python et de toutes celles héritées, vous le trouverez dans la liste des images en local.

$ docker images
REPOSITORY   TAG     IMAGE ID       CREATED       SIZE
flask        latest  79e1dc9af1c1   4 weeks ago   691MB
python       latest  79e1dc9af1c1   4 weeks ago   691MB

Ici on peut constater que nos 2 images (flask et python) sont identiques, elles partagent le même identifiant, la même date de création et la même taille. Ceci est assez logique puisque nous n’avons pas surchargé l’image avec un paramétrage propre à notre application.

Container avec code

# copy reqs for python to be installed by pip
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# copy all python files to the container
COPY *py ./

Les lignes sont executées lors de la construction du container dans l’ordre d’apparition. On spécifie trois actions :

  • la copie du fichier requirements.txt dans le container, ce fichier est utilisé par python afin d’installer des dépendances. Dans le cas présent, le fichier contient uniquement la dépendance flask.
  • exectution (commande RUN) de l’installation de l’ensemble des dépendances spécifiées dans le fichier requirements
  • copie des fichiers source de notre application dans le container

Création du container avec notre code et les dépendances :

$ docker build -t prime .
Sending build context to Docker daemon  1.715MB
Step 1/4 : FROM python:latest
 ---> 79e1dc9af1c1
Step 2/4 : COPY requirements.txt ./
 ---> 720f7c7358b9
Step 3/4 : RUN pip install --no-cache-dir -r requirements.txt
 ---> Running in a261e1318622
Collecting flask (from -r requirements.txt (line 1))
  Downloading Flask-0.12.2-py2.py3-none-any.whl (83kB)
Collecting click>=2.0 (from flask->-r requirements.txt (line 1))
  Downloading click-6.7-py2.py3-none-any.whl (71kB)
Collecting Werkzeug>=0.7 (from flask->-r requirements.txt (line 1))
  Downloading Werkzeug-0.13-py2.py3-none-any.whl (311kB)
Collecting itsdangerous>=0.21 (from flask->-r requirements.txt (line 1))
  Downloading itsdangerous-0.24.tar.gz (46kB)
Collecting Jinja2>=2.4 (from flask->-r requirements.txt (line 1))
  Downloading Jinja2-2.10-py2.py3-none-any.whl (126kB)
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask->-r requirements.txt (line 1))
  Downloading MarkupSafe-1.0.tar.gz
Installing collected packages: click, Werkzeug, itsdangerous, MarkupSafe, Jinja2, flask
  Running setup.py install for itsdangerous: started
    Running setup.py install for itsdangerous: finished with status 'done'
  Running setup.py install for MarkupSafe: started
    Running setup.py install for MarkupSafe: finished with status 'done'
Successfully installed Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.13 click-6.7 flask-0.12.2 itsdangerous-0.24
 ---> 2edef0a3ea8f
Removing intermediate container a261e1318622
Step 4/4 : COPY *py ./
 ---> b7b777d858bb
Successfully built b7b777d858bb
Successfully tagged prime:latest

Que c’est il passé ici ? Dans la progression de la construction du container, noté par les steps, on peut voir les actions et le résultat de celles-ci. La construction du container s’effectue en executant les commandes dans un environnement spécifique, on peut imaginer que le container est pendant cette phase ouvert tout en étant étanche par rapport à l’hôte.

A la fin de chaque étape, un identifiant intermédiaire de container est présenté, certains seront supprimé, le dernier identifiant correspondant à la dernière action sera conservé comme identifiant du container permettant de le manipuler (ici b7b777d858bb, en plus de son nom (ici prime:latest).

On peut constater que les date de création et taille différent de l’image de base python et donc de celle précédemment construite (flask).

Pour regarder un peu l’intérieur, je vous propose d’utiliser le container dockviz. Celui-ci permet de représenter la chaine des containers en remontant les héritages, si vous avez graphviz sur votre serveur vous pouvez également construire une version graphique, c’est celle que je vous propose ici pour plus de clarté.

$ docker pull nate/dockviz
$ docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz images -t

On retrouve nos différentes étapes avec les numéros de container associés :

  • la base python = 79e1dc9af1c1
  • la copie du fichier requirements.txt = 720f7c7358b9
  • l’installation des dépendances via pip = 2edef0a3ea8f
  • la copie de nos fichiers source = b7b777d858bb

Container et execution

Nous avons construit le container, mais pas encore donné les commandes nécessaires à son execution. Dans le cas de notre web service, nous avons besoin de présenter un port TCP d’exposition et une commande de démarrage de notre service.

EXPOSE 5000
CMD [ "python", "./srv.py", "-l", "INFO" ]

La première partie permet d’exposer à l’extérieur du container le port TCP 5000, celui-ci est par défaut utilisé par flask pour présenter ses web services. La seconde partie indique comment démarrer le contenu du container.

La construction est similaire à celle de l’étape précédente :

$ docker build -t prime .
Sending build context to Docker daemon  1.716MB
Step 1/6 : FROM python:latest
 ---> 79e1dc9af1c1
Step 2/6 : COPY requirements.txt ./
 ---> Using cache ---> 720f7c7358b9
Step 3/6 : RUN pip install --no-cache-dir -r requirements.txt
 ---> Using cache ---> 2edef0a3ea8f
Step 4/6 : COPY *py ./
 ---> Using cache
 ---> b7b777d858bb
Step 5/6 : EXPOSE 5000
 ---> Running in 1c8ebc06dc4a
 ---> 19e57781322a
Removing intermediate container 1c8ebc06dc4a
Step 6/6 : CMD python ./srv.py -l INFO
 ---> Running in 40cfb89402f3
 ---> 5c361f868c3c
Removing intermediate container 40cfb89402f3
Successfully built 5c361f868c3c
Successfully tagged prime:latest

Execution

Maintenant que vous maitrisez la construction de container avec docker, je vous laisse fabriquer celui du lab issu du git, il se trouve dans le répertoire lab-01.

A l’instanciation de notre container, il nous faut juste accrocher le port 5000 exposé à un port de notre hôte, pour l’exemple, j’ai choisi le 5001. En prime, on limite l’usage de la mémoire à 64 MO.

docker run --rm -p 5001:5000 -m 64m prime

Le web service est actif, vous pouvez l’interroger sur 2 services : info et check, via curl ou Postman par exemple :

$ curl http://host:5001/info
{
  "name": "prime checker",
  "status": "OK"
}

$ curl http://host:5001/check/13
{
  "result": true,
  "status": "OK"
}

$ curl http://host/check/14
{
  "result": false,
  "status": "OK"
}

En démarrant le container avec l’argument -d, celui-ci devient daemon, vous pouvez alors inspecter son fonctionnement et ses logs respectivement avec les commandes :

  • docker stats
  • docker logs

Enfin, la commande docker ps -a vous donnera des informations similaires à celles proposée par la commande ps d’un unix.

Dans la suite de ce lab, nous regarderons comment se servir de la technologie pour équiper notre chaine d’intégration continue.


Photo from Erwan Hesry