Docker est habituellement utilisé dans le cadre d’architectures orientées micro-services car les conteneurs sont légers (comparés aux VMs tout du moins), faciles à configurer, faciles à faire communiquer, et peuvent être déployés très rapidement. Cependant Docker peut parfaitement être utilisé si vous cherchez à Dockeriser un serveur physique ou un VPS entier au sein d’un seul et unique conteneur. Je vais tenter de vous montrer comment et dans quel but.
Contexte
J’ai récemment eu à travailler sur un projet développé par plusieurs des personnes ayant quitté l’entreprise avant mon arrivée. Je n’ai jamais eu l’opportunité de les rencontrer et les contacter était difficile. Malheureusement une grande partie du projet n’était pas documentée. Et en plus de tout cela, seulement certaines briques du projet étaient gérées via un outil de contrôle de version (Git ici). Bien entendu il n’existait aucun serveur de dev ou de staging : tout était directement déployé sur le serveur de production… Est-ce que vous commencez à sentir les soucis arriver ?
Il s’agissait d’un projet de web scraping accomplissant pas mal de tâches complexes. La stack technique du serveur de prod était en gros la suivante :
- Ubuntu 16.04
- Nginx
- Postgresql 9.3
- Python/Django
- Python virtualenvs
- Gunicorn
- Celery
- RabbitMQ
- Scrapy/Scrapyd
Première tentative : échec
Je me suis arraché les cheveux à essayer de faire la rétro-ingénierie de ce serveur de production. Mon but ultime était d’isoler chaque application, la Dockeriser, et faire communiquer entre eux les différents conteneurs.
Mais ça a été un échec !
J’ai réussi à Dockeriser Nginx, l’application Django, et le système de gestion des tâches asynchrones (Celery), mais impossible de faire fonctionner Scrapy et Scrapyd correctement. Je pense que cela venait principalement du fait que des développements spécifiques avaient été apportés directement aux fichiers sources des bibliothèques Scrapy et Scrapyd par les précédents développeurs (c’est à dire au sein du répertoire python/site-package
lui-même !) et le tout bien entendu sans aucune doc. Par ailleurs certaines bibliothèques Python utilisées à l’époque ne sont plus disponibles aujourd’hui, ou bien sont disponibles mais plus dans la bonne version (vous pouvez oublier pip freeze
et pip install -r requirements.txt
ici).
Deuxième tentative : échec
J’ai en fin de compte abandonné l’idée d’une architecture de type micro-service trop compliquée à monter ici. Mais il n’empêche que j’avais un besoin urgent de sécuriser le serveur de production avant qu’il ne rencontre des problèmes. La base de donnée était sauvegardée à intervalles réguliers mais rien d’autre sur le serveur n’était sauvegardé.
J’ai pensé à créer une image système de tout le serveur en utilisant un outil comme CloneZilla ou une simple commande rsync
comme celle-là. Mais un tel backup ne m’aurait pas permis de continuer à faire évoluer facilement le projet en y intégrant de nouvelles fonctionnalités.
C’est pourquoi j’ai aussi étudié la possibilité de convertir le serveur physique en machine virtuelle VMWare en utilisant leur outil VMware vCenter Converter mais le lien de téléchargement était cassé et si peu de personnes mentionnaient cet outil que j’ai pris peur et abandonné.
Enfin, j’ai essayé cette solution de Dockerisation basée sur Blueprint mais sans réussir à vraiment la faire complètement fonctionner, et Blueprint semblait par ailleurs un projet à l’arrêt.
Troisième tentative : réussie
En réalité la solution était relativement simple : j’ai décidé de Dockeriser l’intégralité du serveur de prod par moi-même - à l’exception des données Postgresql - de façon à avoir une sauvegarde du serveur sous forme d’image Docker et à pouvoir ajouter de nouvelles fonctionnalités à mon conteneur sous forme de commits successifs vers de nouvelles images. Le tout sans avoir peur de casser le serveur pour toujours. Voici comment je m’y suis pris :
1. Installez et paramétrez Docker sur le serveur
- Installez Docker en suivant ce guide.
- Loguez-vous à votre compte Docker Hub:
docker login
- Créez un réseau Docker (si besoin):
docker network create --subnet=172.20.0.0/16 my_network
2. Créez une image Docker de votre serveur
Rendez-vous à la racine de votre serveur :
cd /
Créez le fichier Dockerfile
suivant basé sur Ubuntu 16.04 LTS (sans la partie dédiée à Nginx et Rabbitmq bien sûr) :
FROM ubuntu:xenial
# Copy the whole system except what is specified in .dockerignore
COPY / /
# Reinstall nginx and rabbitmq because of permissions issues in Docker
RUN apt remove -y nginx
RUN apt install -y nginx
RUN apt remove -y rabbitmq-server
RUN apt install -y rabbitmq-server
# Launch all services
COPY startup.sh /
RUN chmod 777 /startup.sh
CMD ["bash","/startup.sh"]
Créez le fichier .dockerignore
qui mentionnera tous les fichiers ou répertoires que vous souhaitez exclure de la commande COPY
ci-dessus. C’est là qu’il vous faut utiliser votre intuition. Excluez autant de fichiers que possible de façon à ce que l’image Docker ne soit pas trop volumineuse, mais n’excluez pas les fichiers vitaux pour votre application. Voici mon fichier mais bien entendu à vous de l’adapter à votre propre serveur :
# Remove folders mentioned here:
# https://wiki.archlinux.org/index.php/Rsync#As_a_backup_utility
/dev
/proc
/sys
/tmp
/run
/mnt
/media
/lost+found
# Remove database's data
/var/lib/postgresql
# Remove useless heavy files like /var/lib/scrapyd/reports.old
**/*.old
**/*.log
**/*.bak
# Remove docker
/var/lib/lxcfs
/var/lib/docker
/etc/docker
/root/.docker
/etc/init/docker.conf
# Remove the current program
/.dockerignore
/Dockerfile
Créez un script startup.sh
afin de lancer tous les services au démarrage du conteneur et de mettre en place les redirections vers la base de donnée. Ici mon script va être radicalement différent du vôtre bien entendu :
# Redirect all traffic from 127.0.0.1:5432 to 172.20.0.1:5432
# so any connection to Postgresql keeps working without any other modification.
# Requires the --privileged flag when creating container:
sysctl -w net.ipv4.conf.all.route_localnet=1
iptables -t nat -A OUTPUT -p tcp -s 127.0.0.1 --dport 5432 -j DNAT --to-destination 172.20.0.1:5432
iptables -t nat -A POSTROUTING -j MASQUERADE
# Start RabbitMQ.
rabbitmq-server -detached
# Start Nginx.
service nginx start
# Start Scrapyd
/root/.virtualenvs/my_project_2/bin/python /root/.virtualenvs/my_project_2/bin/scrapyd >> /var/log/scrapyd/scrapyd.log 2>&1 &
# Use Python virtualenvwrapper
source /root/.profile
# Start virtualenv and start Django/Gunicorn
workon my_project_1
cd /home/my_project_1
export DJANGO_SETTINGS_MODULE='my_project_1.settings.prod'
gunicorn -c my_project_1/gunicorn.py -p /tmp/gunicorn.pid my_project_1.wsgi &
# Start Celery
export C_FORCE_ROOT=True
celery -A my_project_1 beat &
celery -A my_project_1 worker -l info -Q queue1,queue2 -P gevent -c 1000 &
# Little hack to keep the container running in foreground
tail -f /dev/null
Comme vous pouvez le voir, j’utilise des redirections iptables de façon à ce que toutes les connections vers la base de données Postgresql (port 5432) continuent de fonctionner sans modification supplémentaire des fichiers de configuration. En effet ma base de données était initialement située sur le localhost, mais elle est désormais située sur l’hôte Docker dont l’ip est 172.20.0.1 (j’ai tout copié vers le conteneur Docker à l’exception de la base de données). Les redirections au niveau du kernel sont assez pratiques lorsque vous ne savez pas où sont situés tous vos fichiers de configuration, et elles sont indispensables si vous n’êtes pas du tout en mesure de modifier ces fichiers de config (comme dans le cas d’une application compilée dont vous n’avez pas le code source).
Maintenant lancez la création de l’image et patientez… Dans mon cas, l’image pesait environ 3Go et a été créée en 5 minutes. Assurez-vous d’avoir suffisamment d’espace disque à disposition sur votre serveur avant de lancer la commande.
docker build -t your_repo/your_project:your_tag .
Si vous n’avez obtenu aucune erreur ici, alors bravo vous avez fait le plus dur ! A présent testez votre image et voyez si tout fonctionne normalement. Si ce n’est pas le cas il vous faut certainement adapter un des 3 fichiers ci-dessus.
3. Sauvegardez votre nouvelle image Docker
Uploadez simplement votre image sur le Hub Docker :
docker build -t your_repo/your_project:your_tag .
4. Intégrez de nouvelles fonctionnalités à votre image serveur
Désormais, si vous avez besoin de travailler sur cette image serveur, vous pouvez procéder comme suit :
- créez un conteneur basé sur cette image avec
docker run
(n’oubliez pas de spécifier le nom du réseau, l’adresse ip, le forwarding de ports, et d’ajouter l’option--privileged
afin que la commandesysctl
présente dansstartup.sh
fonctionne) - travaillez dans votre conteneur
- faites un commit des changements effectués vers une nouvelle image Docker avec
docker commit
- uploadez votre nouvelle image sur le Docker Hub avec
docker push
et déployez-la en staging ou en production
Conclusion
Cette solution m’a vraiment sauvé la mise et il me semble qu’elle prouve bien que Docker est un excellent outil, pas uniquement pour les architectures micro-service mais aussi dans le cadre d’une conteneurisation d’un serveur entier. Dockeriser un serveur entier peut être une excellente solution si vous avez besoin de sécuriser un serveur de prod existant tel que le mien ici sans documentation, sans dépôt GitHub, et sans les développeurs historiques.
La première image à créer peut être assez lourde, mais par la suite chaque commit n’est pas si gros grâce à l’architecture en couches de Docker.
S’agit-il d’un hack ? Peut-être bien mais qui fonctionne à la perfection alors !
J’aimerais beaucoup entendre l’avis d’autre devs là-dessus.