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 conteneurs 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, et qui ne demandent qu'à être aussi dockerizé. À cette problématique vient s'ajouter celle du backup des données statiques, engendrées par l'utilisation de l'application. La plupart des articles de blogs traitent l'installation des services standards, ici nous allons voir comment exploiter le moteur Docker hors des sentiers battus, parce qu'on est des oufs.

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/, le répertoire où seront stocké les images, les conteneurs d'application, et surtout les conteneurs 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, je ne le fais pas maintenant car ce dd est déjà rempli 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 des containeurs de données

Commençons par le plus facile, les containeurs de données statique. Ces conteneurs ne contiendrons que les données générées par l'application dockerisée. 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 containeur ne contiendra que ce type de donnée. Il servira de backend au containeur 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 corrompu dans le container.

Placer les Dockerfiles dans leurs répertoires de travail respectifs :
docker
├── data
│   ├── Dockerfile
│   └── truc
│       └── trunk
│           └── trac
├── dotclear1
│   └── Dockerfile
├── mariadb
│   ├── Dockerfile
│   └── Dockerfile~


Dockerfile :
FROM scratch

ADD var/lib/mysql /var/lib/mysql

# Execution environement
VOLUME /var/lib/mysql
CMD ''

MAINTAINER fantom at fedoraproject.org


Dockerfile :
FROM scratch

ADD var/www/dotclear1 /var/www/html

# Execution environement
VOLUME /var/www/html
CMD ''

MAINTAINER fantom at fedoraproject.org


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.

# 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 statiques sont prêtes...

De l'arrière vers l'avant

Au niveau applicatif, c'est à dire au niveau des programmes sur lesquels reposent les données statiques que l'on a empaqueté 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 tout Internet.

Pour le démarrage du mariadb cloisonné en conteneur, il n'y a pas de conflits de port d'écoute avec le mariadb sur mon système à craindre. En effet Docker va faire en sorte que son port d'écoute soit injoignable depuis l'interface réseau, et seul les containeurs que j'aurais spécialement configuré pour communiquer avec le containeur 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 containeurs et l'hôte.

Attention à la différence de version entre le programme sur le système et le programme cloisonné. La base de donnée fonctionnait avec mariadb-10.0.20-1.fc21.x86_64, 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 attend tout nouveau container pour se plugger dessus. En l'occurence le containeur apache+php.
Voici queques explications sur les deux commandes 'docker run' à rallonge 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 empaqueté 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 images Apache + PHP est prête. Attention à ne pas tomber dans l'excès, une image ***doit*** rester minimaliste, il est inconcevable d'avoir Linux + Apache + MySQL + PHP dans la même image. Chaque contener 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 gourré ?... »

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 ! Maintenant imaginons que le port 80 est disponible sur l'hôte et que j'associe ce port au port 80 du container. 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 rand 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 un port différent, ce qui ne pose aucun soucis é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 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 hôte, qui doit pouvoir 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.

L'option --link mariadb-blog:mysql entraine deux choses distinctes : Docker autorise la connexion du container apache-php-dotclear au port 3306 (mysql) du container mariadb-blog, 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. Quelle que 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 avant ou après la migration ça n'a pas d'importance.

Un ptit coup de sed :
# 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 re-runner 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.

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 tout de suite moins bien. La solution la plus simple pour fixer ce problème est de se connecter au serveur 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'@'%';

C'est tout !

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 maintenant ce qui se passe si on cloisonne d'autres types de programmes.

Casper, tu penses à quoi là ?

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

À suivre...