Docker, c'est la grande mode du moment. Ce projet très récent permet de cloisonner facilement n'importe quel processus avec une installation simplifiée. La gestion exceptionnelle du chainage entre les containers tout en restant isolé de l'hôte ouvre des possibilités assez hors-norme. La documentation officielle et de nombreux articles traitent déjà la bonne manière pour installer le moteur Docker, et comment démarrer un nouveau service à partir de rien.

La problématique pour les vieux sysadmins est légèrement différente, puisque nous avons déjà un nombre conséquent de services qui tournent sur nos serveurs LAMP traditionnels. Des services qui ne demandent qu'à être aussi dockerizé. Ensuite, vient s'ajouter la question du backup des données statiques, engendrées par l'utilisation de l'application. La plupart des articles de blog traitent l'installation de services standards, sauf qu'ici nous allons voir comment exploiter le moteur Docker hors des sentiers battus...

Faites place au porte-container !

Avant même d'attaquer les opérations, vous pouvez ajouter un disque dur supplémentaire et créer un nouveau Volume Logique LVM de 1024Gio pour /var/lib/docker/. C'est le répertoire où seront stocké les images, les containers d'application, et surtout les containers de données qui contiennent toutes vos données statiques. Pour ma part j'estime que 1Tio sera suffisant.

# lvcreate -n lv24 -L 1024G vg_lancaster /dev/sdb1
# mkfs.ext4 /dev/mapper/vg_lancaster-lv24

Dans la première commande, je crée le Volume Logique uniquement sur le disque dur initialisé en Volume Physique /dev/sdb1. En effet, lorsque je concentre toutes mes données dans un seul volume, j'ai pour habitude de répliquer ce volume sur d'autres disques dur. Ce volume sera ultérieurement répliqué sur /dev/sdc1, mais je ne le fais pas maintenant car ce disque est déjà occupé par les données statiques de mon serveur LAMP traditionnel.

# systemctl stop docker
# mount /dev/mapper/vg_lancaster-lv24 /mnt/lv1
# mv /var/lib/docker/* /mnt/lv1/
<< Ajouter le point de montage /var/lib/docker dans fstab >>
# umount /mnt/lv1
# mount -a
# systemctl start docker

Nous voilà à présent avec suffisament de place pour procéder à la suite des opérations.

Construction de containers de données

Commençons par le plus facile, c'est à dire commençons par les containers de données statique. Ces containers ne contiendrons que les données générées par l'application elle-même. Son fonctionnement est guère différent des services non-dockerisés sur serveur. Vous avez l'habitude de mettre sur un volume à part et de backuper /var/lib/mysql/ ou /var/www/mon_super_site/, et bien ce container ne contiendra que ce type de donnée. Il servira de backend au container faisant tourner le processus lui-même.

Pour dockerizer ce blog par exemple, je vais récupérer mes données dans les répertoires /var/lib/mysql et /var/www/dotclear1. Avant de construire ces containers, il est nécessaire de stopper les services httpd et mariadb, pour éviter de se retrouver avec une base de données corrompue dans le container.

Placer les Dockerfiles dans leurs répertoires de travail respectifs. Il faut créer une arborescence :

docker
├── data
│   ├── Dockerfile
│   └── truc
│       └── trunk
│           └── trac
├── dotclear1
│   └── Dockerfile
├── mariadb
│   ├── Dockerfile
│   └── Dockerfile~

Dockerfile pour les données Mariadb :

FROM scratch
ADD var/lib/mysql /var/lib/mysql
# Execution environement
VOLUME /var/lib/mysql
CMD ''
MAINTAINER <insert_mail_address_here>

Dockerfile les données Apache :

FROM scratch
ADD var/www/dotclear1 /var/www/html
# Execution environement
VOLUME /var/www/html
CMD ''
MAINTAINER <insert_mail_address_here>

Il faut aussi copier les données statiques dans les répertoires de travail :

# systemctl stop httpd
# systemctl stop mariadb
# mkdir -p /home/casper/docker/dotclear1/var/www/
# cp -a /var/www/dotclear1 /home/casper/docker/dotclear1/var/www/
# mkdir -p /home/casper/docker/mariadb/var/lib/
# cp -a /var/lib/mysql /home/casper/docker/mariadb/var/lib/
# systemctl start mariadb
# systemctl start httpd

Puis on construit les images docker :

# cd /home/casper/docker/dotclear1
# docker build -t web-blog-data:20150926 .
# cd /home/casper/docker/mariadb
# docker build -t database-blog-data:20150926 .

Nos images de données applicatives sont prêtes...

De l'arrière vers l'avant (ou du plus bas vers le haut)

Au niveau applicatif, c'est à dire au niveau des programmes sur lesquels reposent les données statiques que l'on a cloisonné précédemment, il y a une certaine logique à respecter. Avant de dockerizer le serveur web qui fait tourner notre application en PHP, il faut dockeriser le serveur de base de donnée sur lequel s'appuie l'appli PHP. En fait, l'idée est de construire un système, et comme pour tout système il faut commencer par la base, la basse couche invisible pour l'utilisateur. Des serveurs de backend il y en a des tonnes, si vous voulez construire un système avec des bases solides, je vous recommande de commencer par les backends.

Dans le cas de mon petit blog par exemple, mariadb est le serveur le plus en arrière car inaccessible depuis Internet.

Pour le lancement du processus mariadb cloisonné en container, il n'y a pas de conflit de port d'écoute avec le mariadb sur le système hôte à craindre. En effet, Docker va faire en sorte que son port d'écoute soit injoignable depuis l'interface réseau de l'hôte, et seul les containers que j'aurais spécialement configuré pour communiquer avec le container mariadb, pourront accèder à son port d'écoute. Cet avantage vient du fait que Docker possède sa propre interface réseau, pour gérer les communications entre les containers et l'hôte.

Attention à la différence de version entre le programme sur le système hôte, et le programme cloisonné. La base de donnée fonctionnait avec mariadb-10.0.20-1.fc21.x86_64 livrée avec le RPM, il faut donc veiller à réutiliser la même version du mariadb dockerizé soit la 10.0.20.

# docker run --name db-blog-static database-blog-data:20150926
# docker run --name mariadb-blog -d --restart always --volumes-from db-blog-static docker.io/mariadb:10.0.20

Mariadb est désormais opérationnel, et il est en attente de tout nouveau container pour se plugger dessus. En l'occurence le container apache+php.

Voici quelques explications sur les deux commandes 'docker run' à rallonge, et qui peuvent parraître déroutantes. Le container mariadb-blog expose par nature son répertoire /var/lib/mysql, ce qui nous offre la possibilité d'assigner ce répertoire à un répertoire particulier sur l'hôte ou bien de l'assigner au répertoire exposé d'un autre container.

Comme vous le voyez avec l'option --volumes-from, son répertoire exposé va être assigné au répertoire exposé du container db-blog-static et le programme pourra accèder aux données que l'on a cloisonné précédemment. L'option --restart always indique que le container du programme doit être démarré si celui-ci crashe ou si l'hôte redémarre. Enfin, l'option -d indique de lancer le container en arrière-plan, comme un démon ou service traditionnel.

Images sur mesure

Nous venons de voir comment migrer un service en docker en réutilisant une image toute prête faite. Maintenant, voyons comment adapter une image pré-existante à nos besoins. On ne change rien sur l'idée d'avoir un container de données statiques pour les backups à froid, sauf que l'on peut se permettre de customiser l'image contenant l'application, lorsqu'on en trouve aucune sur la registry Docker répondant à nos besoin. Toujours sur le cas mon petit blog, je cherche une image d'un serveur web (Apache) pouvant exécuter du code en PHP, avec les dépendances PHP spécifiques au moteur de blog Dotclear. Autant vous dire qu'il y en a aucune sur la registry Docker officielle. Mais on peut reprendre une images apache simple et lui ajouter les dépendances PHP.

Une image ou un container Docker n'est pas une boite noire où l'on peut rien entrevoir à l'intérieur, les sources des images sont libres et accessibles, on peut les lire, les copier, les modifier et les redistribuer.

Je vais donc reprendre les sources de l'image fedora/apache et ajouter la liste des paquets PHP présents sur mon serveur. (Entre parenthèses, mon serveur ne fait tourner qu'une seule application PHP : Dotclear. S'il y avait eu plusieurs applis, ça aurait vite compliqué la tâche...).

$ git clone https://github.com/fedora-cloud/Fedora-Dockerfiles.git
$ cp -a Fedora-Dockerfiles/apache apache-php-dotclear/
<< Éditer le Dockerfile >>

Dockerfile :

FROM fedora:21
MAINTAINER http://fedoraproject.org/wiki/Cloud

RUN yum -y update && yum clean all
RUN yum -y install httpd php-php-gettext php-mysqlnd \
                   php             \
                   php-pdo         \
                   php-imap        \
                   php-simplepie   \
                   php-mbstring    \
                   php-pear        \
                   php-mcrypt      \
                   php-domxml-php4-php5 \
                   php-cli         \
                   php-snmp        \
                   php-ldap        \
                   php-pgsql       \
                   php-process     \
                   php-IDNA_Convert \
                   php-xml         \
                   php-pecl-jsonc  \
                   php-common      \
                   php-gd          \
&& yum clean all

EXPOSE 80
# Simple startup script to avoid some issues observed with container restart
ADD run-apache.sh /run-apache.sh
RUN chmod -v +x /run-apache.sh
VOLUME /var/www/html
CMD ["/run-apache.sh"]

Puis on reconstruit l'image :

# cd /home/casper/docker/apache-php-dotclear/
# docker build -t apache-php-dotclear:2.4.16 .

Notre image Apache + PHP est prête. Attention à ne pas tomber dans l'excès, une image doit rester minimaliste, d'ailleurs il est inconcevable d'avoir Linux + Apache + MySQL + PHP dans la même image. Chaque container représente un peu une brique LEGO, et c'est de la façon dont on les empile que dépend la fiabilité du système.

# docker run --name web-blog-static web-blog-data:20150926
# docker run --name apache-php-blog -d -p 8082:80 --restart always --link mariadb-blog:mysql --volumes-from web-blog-static apache-php-dotclear:2.4.16

L'Apache de mon blog est désormais opérationnel...

« Euh mais attend Casper, ton Apache il écoute sur le port 8082 là, tu t'es pas un peu planté ?... »

Un peu de réseau

Revenons en arrière, imaginons que je n'ai pas ajouté l'option -p 8082:80. Docker aurait isolé le container et seul un autre container pourrait spécifiquement se connecter au port 80 du container apache-php-blog. C'est insuffisant. Maintenant imaginons que j'ajoute l'option -p 80:80. L'hôte pourrait se connecter au container en passant par le port 80, sauf que le port 80 est déjà pris sur l'hôte !

Imaginons maintenant que le port 80 est disponible sur l'hôte et que j'associe ce port, au port 80 du container. Si j'ai un second site web avec son container apache+php et son container de données statiques, comment vais-je faire pour que ce nouveau container apache-php-truc écoute lui aussi sur le port 80 ?

La solution est de relèguer les containers Apache au rang de serveurs backend : ils ne sont là que pour exécuter du code (PHP/Python/whatever). Le container peut continuer à écouter sur le port 80, ce n'est pas un soucis, mais le port de l'hôte associé à ce port sera différent. Ce qui ne pose aucun problème également, puisque Docker permet une gestion très affinée des linkages de port d'écoute.

Si le container apache-php-blog est un backend, quel serveur HTTP est le frontend ? Il y a deux solutions possible dans ce cas de figure, soit on démarre un nouveau container Apache qui écoute sur les ports 80 et 443 de l'hôte, et qui est linké pour pouvoir se connecter aux containers de backend (apache-php-blog et apache-php-truc), soit on utilise l'Apache du système de l'hôte (qui pourra se connecter au container à travers le port 8082 exposé par Docker).

Le première solution est la meilleure, mais surtout c'est celle qui est mise en place en tout dernier, après avoir dockerizé tous les backends du Apache de l'hôte sans exception. Patience, donc...

L'option --link mariadb-blog:mysql entraine deux choses distinctes : Docker autorise la connexion du container apache-php-dotclear vers le port 3306 (mysql) du container mariadb-blog, ce qui est plutôt utile pour que l'application puisse se connecter à sa base de données. Mais surtout Docker ajoute dynamiquement une entrée au DNS du container apache.

Quelque soit l'adresse IP du container mariadb, son adresse IP sera automatiquement associée au nom de domaine "mysql". Il faudra penser à mettre à jour le fichier de configuration de l'application et remplacer "localhost" par "mysql" pour joindre le serveur de base de données. À réaliser avant ou après la migration, ça n'a pas d'importance.

Un ptit coup de grep :

# docker exec -ti apache-php-blog /bin/bash
bash-4.3# grep -r localhost /var/www/html/*
/var/www/html/inc/config.php.in:// Database hostname (usually "localhost")
/var/www/html/inc/prepend.php:                  ,(DC_DBHOST != '' ? DC_DBHOST : 'localhost')
/var/www/html/inc/config.php:// Database hostname (usually "localhost")
/var/www/html/inc/config.php:define('DC_DBHOST','localhost');

Comme prévu, il n'y a qu'un fichier de config à modifier à chaud :

bash-4.3# sed -i s/localhost/mysql/ /var/www/html/inc/config.php

Pas besoin de relancer le container, l'entrée DNS est déjà là sauf que maintenant l'application pointe vers le bon serveur de base de données. Elle peut donc fonctionner normallement.

Troubleshooting

Il se peut que votre application en PHP ne parvienne pas à se connecter à la base de données, vous devriez voir dans le journal du container mariadb-blog une ligne du genre :

# docker logs mariadb-blog
151004 17:57:48 [Warning] Access denied for user 'dotclear1user'@'172.17.0.18' (using password: YES)

En effet, lorsque j'ai initialisé la base de données, j'ai configuré les privilèges pour l'utilisateur 'dotclear1user'@'localhost'. Évidemment avec un nom d'hôte différent, ça marche moins bien. La solution la plus simple pour résoudre ce problème est de se connecter au serveur de base de données de l'hôte puis de reconstruire un container de données statiques :

Se connecter en root :

# mysql -u root -h localhost -p

Ajouter un mot de passe à l'utilisateur :

MariaDB [(none)]> set password for 'dotclear1user'@'%'=password('monsupermotdepasse');

Ajouter les bons privilèges :

MariaDB [(none)]> grant all on dotclear1.* to 'dotclear1user'@'%';

Pour finir, il faut reconstruire l'image de données statiques et le problème aura disparu.

Rebuild the system

Okay, on a 2 serveurs de backend cloisonnés... il ne reste plus qu'à configurer l'Apache de l'hôte en mode reverse proxy pointant vers localhost:8082... Okay, on a cloisonné 2 services... Regardons ce qui se passe si l'on essaye de cloisonner d'autres sortes de logiciels.

Casper, tu penses à quoi là ?

Je pense à cloisonner des applications bureautique comme Firefox et Thunderbird, je pense à cloisonner mon client Jabber, je pense à cloisonner mes routeurs Tor, mais aussi générer des images de backup de mes données personnelles !

À suivre...