docker

Industrialiser WordPress avec docker

Détaillons une procédure d’industrialisation de wordpress via docker.

Je bascule l’ensemble de mon infrastructure sur docker. Le but est d’avoir des serveurs hosts minimalistes. L’ensemble du SI est réparti dans des containers indépendants. Ils sont plus faciles à déplacer, upgrader, …  Le nombre réduit de packages installés sur le système hôte améliore également sa maintenance.

Ma «dockerisation» comprend entre autre une douzaine de blogs.

Architecture initiale

docker-wordpress.001

Des raisons historiques expliquent la présence des deux serveurs web. À l’époque de l’installation de cette machine, Apache était la norme. Depuis j’utilise nginx, moins gourmant en mémoire et plus facile à paramétrer. Apache est encore présent pour la gestion de mes blogs. Le passage de wordpress sur docker doit le faire disparaître.

Les blogs sont différenciés par :

  • un répertoire propre contenant tous les fichiers php, les uploads,…
  • une instance de base mysql.

Le moteur de base de donnée mysql est partagé par toutes les instances.

Pour chaque blog, il est nécessaire de paramétrer le reverse proxy apache et la connexion à la base de données.

Architecture cible

docker-wordpress.002

Le serveur apache a disparu.

Le serveur Nginx frontal reste sur le host pour gérer le reverse proxy chacun des containers. Il pourrait dans une prochaine étape être également dockérisé.

Chaque container docker contient un blog autonome (nginx, php, mysql).

Installation de Wordpress

Première étape lors de la dockérisation d’un composant: chercher une image docker la plus proche de nos besoins.

Dans notre cas, https://github.com/eugeneware/docker-wordpress-nginx semble prometteur. 

L’essentiel de ce container est réparti dans 2 fichiers: le Dockerfile et le fichier de lancement (start.sh).

docker-wordpress.003

 

Le build de l’image du container installe tous les composants (essentiellement via des apt-get) et les configure. L’installation de wordpress est réalisée durant la phase de build de l’image par le téléchargement du dernier tar de wordpress et son extraction.

Le démarrage de l’image lance le script start.sh qui finalise l’installation :

  • si premier démarrage, création de la base de donnée,
  • lancement de supervisor qui lui même démarre php5-fpm, nginx, mysqld.

En suivant les instructions de démarrage, on démarre en 5 minutes (le temps de tout télécharger) un nouveau wordpress.

Capture d’écran 2014-10-08 à 13.05.55

Un petit pstree nous confirme les processus démarrés.

 

Capture d’écran 2014-10-08 à 13.09.15

Cette image est conçue uniquement pour des nouveaux blogs. Il n’est pas question ici de migrer des blogs existants. De plus rien n’est prévu pour la gestion des sauvegardes. Nous allons donc modifier cette image pour répondre à nos besoins.

Restauration d’un blog existant

Lors de la reprise d’un blog existant, tous les fichiers php du blog vont être réutilisés. Il n’est désormais plus nécessaire de télécharger la dernière version de wordpress.

Nous allons donc déplacer l’installation de wordpress de la phase de build vers celle de démarrage. Nous pourrons alors vérifier s’il s’agit d’une nouvelle installation et limiter le téléchargement à cet unique cas.

Dans le fichier Dockerfile, nous supprimons les lignes:

# Install WordPress
ADD http://wordpress.org/latest.tar.gz /usr/share/nginx/latest.tar.gz
RUN cd /usr/share/nginx/ && tar xvf latest.tar.gz && rm latest.tar.gz
RUN mv /usr/share/nginx/html/5* /usr/share/nginx/wordpress
RUN rm -rf /usr/share/nginx/www
RUN mv /usr/share/nginx/wordpress /usr/share/nginx/www
RUN chown -R www-data:www-data /usr/share/nginx/www

pour les déplacer dans une fonction de notre fichier start.sh:

downLoadAndInstallWP(){
 curl -L -o /usr/share/nginx/latest.tar.gz http://wordpress.org/latest.tar.gz
 cd /usr/share/nginx/ && tar xvf latest.tar.gz && rm latest.tar.gz
 mv /usr/share/nginx/html/5* /usr/share/nginx/wordpress
 rm -rf /www
 mv /usr/share/nginx/wordpress /www
 chown -R www-data:www-data /www
}

Un code de test nous permet de vérifier la configuration du blog et d’agir en conséquence.

if [ -e /restore/www/wp-config.php ]; then
  restoreFiles
  restoreDatabase
else
  downLoadAndInstallWP
  createDatabase
fi
chown www-data:www-data /www/wp-config.php

stopDb

# start all services
/usr/local/bin/supervisord -n

La restauration d’un blog existant est déclenchée par la présence du fichier de paramétrage wp-config.php dans le répertoire restore. On modifie alors le comportement initial en copiant les fichiers depuis le répertoire /restore/www et en restaurant le dump de la base de données.

On profite de cette étape de restauration pour normaliser le nom de notre base. Initialement les bases de tous les blogs étaient contenues dans une seule instance de mysql. Chaque blog se différenciait par ses paramètres de connexion. Désormais, un container ne contient qu’un seul blog et une seule base. On peut donc utiliser un nom de base unique (wordpress). Cette opération est réalisée par une commande sed  qui positionne les  paramètres DB_NAME, DB_USER à ‘wordpress‘.

restoreFiles(){
  echo Restore content from /restore/www
  cp -r /restore/www /
  sed -i.bak "s/'DB_NAME', '.*'/'DB_NAME', 'wordpress'/
  s/'DB_USER', '.*'/'DB_USER', 'wordpress'/" /www/wp-config.php
  mv /usr/share/nginx/html/5* /www
  chown -R www-data:www-data /www
  echo End of copy
}

La base de données est créée avec un utilisateur wordpress. Le mot de passe est lu dans le fichier de paramétrage.

restoreDatabase(){
  echo Restauration de la base depuis /restore/wordpress.dmp
  MYSQL_PASSWORD=`grep DB_PASSWORD /www/wp-config.php | awk -F\' '{print $4}'`
  echo mysql root password: $MYSQL_PASSWORD
  mysqladmin -u root password $MYSQL_PASSWORD
  mysql -uroot -p$MYSQL_PASSWORD -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' WITH GRANT OPTION; FLUSH PRIVILEGES;"
  mysql -uroot -p$MYSQL_PASSWORD -e "CREATE DATABASE wordpress; GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost' IDENTIFIED BY '$MYSQL_PASSWORD'; FLUSH PRIVILEGES;"
  mysql -uroot -p$MYSQL_PASSWORD wordpress < /restore/wordpress.dmp
}

Gestion des sauvegardes

Je suis content.

J’arrive désormais à reprendre les sauvegardes de mes blogs pour les intégrer dans un container docker. Pour être complètement détendu je dois disposer d’un mécanisme de sauvegarde de mes blogs.

Je souhaite tout d’abord effectuer une sauvegarde lors de l’arrêt de mon container docker. Ces deux lignes vont faire l’affaire:

#when receiving sigterm backup www and database
trap "source /backup.sh" SIGTERM
# start all services
/usr/local/bin/supervisord -n &

Lorsque SIGTERM est envoyé à mon processus, il démarre le script backup.sh.

Attention à l’astuce de démarrage de supervisord. Sans l’ajout du & cela ne fonctionne pas. En effet, le processus supervisord doit être en arrière plan pour que le bash reçoive le signal d’arrêt.

Étant paranoïaque, je veux également des sauvegardes régulières.

Ajoutons un cron pour effectuer des sauvegardes régulières. Le fichier /crontab.root ajouté lors du build de l’image contient ceci.

MAILTO=""
MIN * * * * /backup.sh

La première ligne sert à désactiver l’envoi des mails et ainsi éviter l’installation de postfix dans le container. La deuxième ligne est l’appel de mon script de sauvegarde chaque heure. MIN est remplacé par une valeur aléatoire au premier lancement de la machine:

# Load the crontab
# one backup at a random minute each hour
sed -i.bak "s/MIN/`echo $[ $[ RANDOM % 59 ]]`/" /crontab.root
sleep 10; crontab /crontab.root

Il s’agit d’éviter que tous les blogs ne démarrent leur sauvegarde en même temps. Sans le sleep 10, la crontab n’était pas prise en compte. Le daemon n’était sans doute pas encore prêt.

C’est tout pour l’image de notre container. Nous avons désormais un blog qui démarre, est capable de restaurer une version précédente, et qui effectue des sauvegardes. Voyons maintenant le script de lancement de notre blog.

Démarrage et mise à jour automatique du reverse proxy

Le fichier startBlog.sh effectue quelques vérifications et paramètre le démarrage du blog.

Il vérifie la cohérence des fichiers dans les répertoires backups et restore. Il s’agit de lancer une restauration uniquement lors de la présence conjointe du dump de la base de données et des fichiers wordpress.

Le cas échéant, nous mettons à jour les fichiers de restauration par une version du backup plus récente.

checkRepo restore
docker stop $1
docker rm $1
checkRepo backups
updateRestoreIfBackupsYounger

Le container est ensuite démarré avec la commande :

DOCKER_CONTAINER=$(docker run -d \
  --name=$1 \
  -P \
  -v /etc/localtime:/etc/localtime:ro \
  -v $FROM/$1/restore:/restore \
  -v $FROM/$1/backups:/backups \
  gzoritchak/wp-nginx)

Le volume /etc/localtime permet de synchroniser l’heure du container sur celle du serveur hôte. Les deux autres volumes nous garantissent un accès simplifié aux répertoires restore et backups depuis le serveur hôte.

Les ports ne sont pas figés. Nous laissons à docker le soin de choisir un port disponible pour accéder au serveur nginx de notre container.

Nous récupérons ensuite ce port en inspectant notre container pour mettre à jour le fichier de configuration de notre nginx frontal.

PORT=$(docker inspect --format='{{(index (index .NetworkSettings.Ports "80/tcp") 0).HostPort}}' $DOCKER_CONTAINER)
echo Port http :: $PORT
sed -e "s/servername/$1/
s/port/$PORT/" nginx.conf > $1

sudo ln -fs `pwd`/$1 /etc/nginx/sites-enabled/$1
sudo service nginx reload

Voilà, c’est fait. J’ai désormais pour chacun de mes blogs un répertoire nommé comme le domaine du blog. Il contient un fichier de configuration et les répertoires restore et backups.

Pour lancer ou redémarrer un blog je n’ai plus qu’à tapper:

./startBlog.sh example-de-domaine.com

L’ensemble des sources est disponible sur github : github.com/gzoritchak/docker-wordpress-nginx

Si vous avez des avis divergents, des questions, n’hésitez pas.

6 Comments

  1. bel article, bien expliqué, bien didactique, bon support par des images pour bien comprendre !

    Je vois un risque quand meme dans ta strategie de sauvegarde :
    – 1/ tu ecrases forcement avec la derniere sauvegarde au demarrage du container
    2/ tu crees une nouvelle sauvegarde uniquement lorsque le container s’arrête proprement.

    Donc si j’ai bien compris, dans n’importe quelle situation où le container s’arrête brutalement (crash server, docker kill au lieu de docker stop, service docker restart si docker kill est utilise au lieu de docker stop dans le script d’upstart), alors au redemarrage, pouf plutôt que de conserver l’état courant (qui n’est generalement pas perdu s’il y a eu just un crash de serveur / kill), tu vas recharger le dernier backup, qui si le container était up depuis des semaines, date un peu beaucoup !?!

    1. Merci.

      Oui et non.

      Par défaut je crée effectivement un nouveau container au démarrage.

      Dans le cas d’un arrêt «normal», je charge le backup qui a été généré au moment de l’arrêt.

      Dans de kill ou de crash du container. Si je démarre sans me poser de question, je récupère mon dernier backup (une heure dans mon cas). Mais On est dans une procédure anormale qui nécessite une intervention. Si c’est nécessaire, je peux toujours lancer ma commande docker run manuellement.

      On verra à l’usage mais jusqu’à maintenant jusqu’à maintenant mes autres containers sont stables. Je n’ai pas connu de crash. J’ai plusieurs containers sur lesquels je ne suis pas intervenu et qui sont restés démarré plus de 6 semaines.

      Par ailleurs, mon service docker ne fait pas un kill mais bien un stop de mes instances. Il ne doit pas y avoir de problème dans ce cas.

      1. Oui, je reconnais bien là ton optimisme, qui est avant tout une énorme qualité chez toi … En revanche je ne sais pas si c’en est une quand on endosse la casquette de sysadmin :-)

        Il y a dans le dernier docker un nouveau flag pour que le daemon redémarre automatiquement une instance si elle s’arrête, on l’utilise, c’est notamment très utile après un redémarrage serveur. Mais du coup dans ton cas il vaut mieux ne pas l’utiliser !

        Moi je suis de plus en plus adepte du « crash only software » (cf google), qui considère que puisqu’on ne peut éviter des arrêts inopinés de nos serveurs/applis à certains instants, autant embrasser la contrainte et développer le plus possible avec ce style. Les applis ios notamment sont développées dans cet état d’esprit.

        Les gros acteurs comme netflix ou autre qui déploient des armées de gorilles pour couper tous les jours régulièrement des services brutalement afin de vérifier que le système de continuité/reprise fonctionne bien adoptent aussi ce style d’architecture système il me semble.

        On fait aussi des backups toutes les heures en déportant sur un autre serveur, mais on ne redémarre pas automatiquement sur un backup.

        1. ;-)

          Disons que j’adapte le niveau d’automatisation et de traitement au risque. Dans le cas des blogs, si exceptionnellement je dois les démarrer manuellement, ce n’est pas très grave. Pareil, si je perds un maximum d’une heure de données. Ce n’est pas fondamental pour un blog. Entre bosser encore quelques heures pour peaufiner ces scripts et éventuellement perdre ces données mon choix est fait. Si l’avenir me donnait tort, je reverrai mon jugement et ajusterai mes scripts.

          Pour le démarrage des containers, le seul flag de redémarrage me semble trop léger (notamment pour la gestion des dépendances entre containers).

      2. En pratique, nos applis sont aussi très stables, et on n’a jamais eu à intervenir sur un serveur ou un container pour redémarrer quelque chose qui se serait éteint tout seul : une fois que c’est lancé, ça tourne, c’est comme Linux.
        Et quand on doit intervenir, c’est à cause de notre soft (besoin de reboot pour contourner une limitation de prise en compte à chaud de tel ou tel paramètre, etc.), pas à cause de Docker.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>