Docker-compose et le COMPOSE_PROJECT_NAME

Rédigé par uTux - - 2 commentaires

Docker-compose est un outil permettant d'utiliser docker à l'aide d'un simple fichier .yml, facilitant ainsi grandement la vie et les interactions entre les containers d'un même service. Mais il n'est pas exempt de bugs ou de défauts de conception.

Le défaut dont je vais parler concerne le nommage des containers. Prenons par exemple deux projets docker : projet1 et projet2 avec l'arborescence suivante :

.
├── projet1
│   └── src
│       └── docker-compose.yml
└── projet2
    └── src
        └── docker-compose.yml

Nos docker-compose.yml sont identiques et commandent le lancement d'un simple container nginx :

# projet1/src/docker-compose.yml
version: '3'
services:

  nginx:
    image:
        nginx:latest

Et :

# projet2/src/docker-compose.yml
version: '3'
services:

  nginx:
    image:
        nginx:latest

Voyons ce qui arrive lorsque je démarre mon projet1 :

utux@docker:~/projet1/src$ docker-compose up -d
Creating src_nginx_1 ... 
Creating src_nginx_1 ... done

On voit que notre container a été nommé src_nginx_1 suivant la logique suivante : $dossier_$service_$numero. Très bien. Démarrons maintenant le projet2 :

utux@docker:~/projet2/src$ docker-compose up -d
src_nginx_1 is up-to-date

docker-compose dit que le container existe déjà alors que non ! En fait il applique le même raisonnement et veut nommer le container src_nginx_1 aussi alors qu'il existe déjà ! C'est un défaut de conception et c'est problématique car en cas de modification sur l'un des projets, docker-compose va recréer les containers, et donc écraser ceux de l'autre...

Pour palier à ce problème, plusieurs solutions sont possibles :

  • Nommer différemment votre dossier de travail. Ce n'est pas toujours possible car on peut avoir un environnement de dev et un environnement de prod avec les mêmes chemins.
  • Utiliser l'attribut container_name mais c'est un peu fastidieux car il faut le faire sur tous les services du docker-compose.yml
  • Utiliser la variable d'environnement COMPOSE_PROJECT_NAME. Malheureusement on ne peut pas la définir dans le docker-compose.yml malgré les demandes répétées des utilisateurs (ici et ) il faut la mettre dans un fichier .env dans votre projet.

Exemple d'utilisation du COMPOSE_PROJECT_NAME :

.
├── projet1
│   └── src
│       ├── docker-compose.yml
│       └── .env
└── projet2
    └── src
        ├── docker-compose.yml
        └── .env

Avec nos fichiers .env :

# projet1/src/.env
COMPOSE_PROJECT_NAME=projet1

Et :

# projet2/src/.env
COMPOSE_PROJECT_NAME=projet2

Démarrons maintenant notre projet1 puis notre projet2 :

utux@docker:~$ cd projet1/src/
utux@docker:~/projet1/src$ docker-compose up -d
Creating network "projet1_default" with the default driver
Creating projet1_nginx_1 ... 
Creating projet1_nginx_1 ... done
utux@docker:~/projet1/src$ cd ../../projet2/src/
utux@docker:~/projet2/src$ docker-compose up -d
Creating network "projet2_default" with the default driver
Creating projet2_nginx_1 ... 
Creating projet2_nginx_1 ... done

Cette fois les deux projets cohabitent bien et ne se marchent plus sur les pieds. La preuve :

utux@docker:~/projet2/src$ docker ps
CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS              PORTS                    NAMES
eacabeb7e961        nginx:latest                     "nginx -g 'daemon ..."   4 seconds ago       Up 2 seconds        80/tcp                   projet2_nginx_1
9477b31d2035        nginx:latest                     "nginx -g 'daemon ..."   16 seconds ago      Up 14 seconds       80/tcp                   projet1_nginx_1

En conclusion il ne faut pas avoir une confiance aveugle en docker-compose, qui est un outil bien pratique mais manifestement sujet à des défauts de conception. Imaginez ce genre de confusion de nommage, voire d'écrasement de containers en production. Utilisez COMPOSE_PROJECT_NAME et testez vos projets en pré-production avant de déployer sur la prod.

Docker, Ansible, NixOS : le savoir (re)faire

Rédigé par uTux - - 4 commentaires

L'informatique, comme tous les métiers, demande un savoir-faire, celui-ci vient avec l'expérience et la pratique. Mais s'il y a un autre point qui est important et souvent négligé, c'est de savoir refaire. Je vais expliquer.

Imaginez-vous administrateur système dans une entreprise, on vous charge d'installer un serveur web pour afficher une simple page html. Vous allez alors installer Debian + Apache puis placer le fichier html à la racine, rien de difficile jusque là.

Imaginez ensuite qu'au bout de 2 mois ce fichier html soit remplacé par un fichier php un peu plus avancé, vous allez alors installer libapache2-mod-php5 là encore c'est facile. Par la suite le fichier évolue encore et vous oblige à installer des modules, par exemple php5-gd et php5-curl.

Puis le serveur vit sa vie et au bout de 2 ans on vous demande d'en mettre en place un deuxième avec exactement le même rôle. Et là ça se complique car 2 ans c'est long et vous avez probablement oublié tout ce que vous et vos collègues avez fait pour configurer ce serveur au fur et à mesure. S'il est facile d'identifier que apache2 et php5 sont présents, en revanche les modules sont beaucoup moins évidents, surtout si vous avez utilisé des gestionnaires tiers tels que pecl, cpan, pip.

L'une des grandes problématiques est donc de trouver un moyen pour garder une trace et refaire rapidement toutes les étapes qui ont accompagné la vie du serveur. Alors bien sûr le grand classique est d'utiliser un wiki, mais qui vous garanti que ce qu'y s'y trouve représente bien ce qui est en production ? Personne n'est à l'abri d'une modification "urgente" en prod non rapportée sur le wiki.

Une solution que l'on retrouve souvent et qui est appliquée dans Ansible, Docker et NixOS, c'est d'éliminer toutes les manipulations sur la prod. On y touche plus, à la place on travaille sur un fichier de recette, que l'on va ensuite deployer et jouer. Sur Ansible ce sont les playbook, sur docker les Dockerfile, et sur NixOS le fichier configuration.nix.

Cela parait beaucoup plus propre et ça l'est, c'est une rigueur pas forcément naturelle qui paye sur le long terme. Néanmoins ce n'est pas toujours évident, une manipulation rapide à faire sur un serveur peut se transformer en plusieurs minutes voire heures de devops dans Ansible. Let's Encrypt est un exemple, son automatisation n'est pas simple car vous allez devoir gérer deux cas différents selon la présence ou non d'un certificat. En effet certbot peut soit utiliser le mode standalone et donc couper Nginx (car nécessité du port 80) ou alors utiliser le mode webroot et justement passer par Nginx + vhost de votre site, or ce dernier a justement besoin du certificat pour démarrer. Et quid du cas de Docker où vous devez créer un nouveau container et le relier à l'existant juste pour renouveler vos certificats.

En conclusion le boulot de sysadmin, développeur ou devops n'est pas seulement de faire ou réparer les choses mais également de prévoir l'avenir, s'imposer une rigueur même si elle parait contre-productive sur le moment. J'aurai probablement l'occasion de parler de plus en détail de Docker et de NixOS dans de prochains articles, restez branchés.

Configuration et déploiement avec Ansible

Rédigé par uTux - - 5 commentaires

Ansible est un logiciel d'automatisation et de déploiement, il permet de créer des listes de tâches qui peuvent ensuite être jouées sur un ou plusieurs serveurs.

Attention cet article n'est pas un tutoriel (pour cela je vous renvoie vers la documentation officielle), mon but est de faire un petit retour d'expérience et montrer un exemple de projet Ansible.

Pourquoi Ansible ?

Il existe de nombreuses solutions de ce type mais il y a selon moi deux points qui distinguent Ansible : il est simple à prendre en main (excellente documentation et syntaxe yaml humainement lisible) et il ne nécessite pas d'agent pour fonctionner. En effet sur vos cibles, vous avez uniquement besoin de Python et SSH, présents en standard sur quasiment toutes les distributions Linux. Ansible marche aussi avec Windows et FreeBSD.

Pourquoi pas un script Bash / Perl / Python ?

Ben oui, il y a beaucoup de gens qui se sont fait leur propre script afin de configurer rapidement leur serveur. Mais en vrai les scripts c'est pas idéal :

  • C'est long à développer et ce n'est pas forcément votre métier.
  • Plus le script est complexe moins il sera lisible.
  • Le script doit être copié et exécuté à la main sur chaque serveur.
  • Il se contente d'exécuter une série de commandes sans se soucier du résultat.
  • Bon courage pour gérer un inventaire centralisé et des variables dynamiques.
  • Même en documentant votre script, vous serez le seul à comprendre réellement comment il marche.
  • Qui n'a jamais eu à déboguer un script obscur vieux de 10 ans ?

Ansible est beaucoup plus propre car depuis une unique machine vous gérez votre inventaire de serveurs et vos playbooks dans un même projet. Vous ne vous souciez que du résultat, vous ne demandez pas à ansible de faire un apt-get install curl, vous lui dites juste que le paquet curl doit être présent, à lui de faire en sorte de l'installer si besoin .Et c'est important non seulement car Ansible sait ce qu'il fait, mais il sait aussi le faire plusieurs fois, c'est ce qu'on appelle l'idempotence (sous réserve de ne pas faire n'importe quoi avec les playbooks, bien entendu). Enfin, il y a une énorme communauté d'utilisateurs et beaucoup de modules tiers, on trouve donc toujours des solutions aux problèmes

Exemple d'utilisation

Avertissement : Il y a plusieurs moyens de structurer un projet Ansible, consultez la page Best Practises. L'organisation que j'ai choisi n'engage que moi, vous êtes libre de faire autrement si ça colle mieux à vos besoins !

Voici un exemple de projet Ansible qui utilise un inventaire, des playbooks et des roles. Il sera utilisé pour configurer les nouveaux serveurs fraîchement installés. Objectifs :

  • Installer une liste de paquets de base
  • Configurer le /etc/hostname avec le nom du serveur
  • Configurer la locale fr_FR.UTF-8.
  • Configurer la timezone Europe/Paris.
  • Installer Nginx et Php-fpm sur les serveurs web
  • Tout en étant compatible Debian 8 & Ubuntu 16.04 (les paquets php n'ont pas le même nom !)

Arborescence du projet :

├── deploy-newserver.yml
├── group_vars
│   └── all
├── host_vars
│   ├── server01.example.org
│   └── web01.example.org
├── production.ini
└── roles
    ├── configure-newserver
    │   └── tasks
    │       └── main.yml
    │   └── templates
    │       └── hostname.j2
    ├── install-nginx
    |   └── tasks
    |       └── main.yml
    └── install-php-fpm
        └── tasks
            └── main.yml

Détail des fichiers :

---
# deploy-newserver.yml

- hosts: all
  roles:
    - configure-newserver

- hosts: webservers
  roles:
    - install-nginx
    - install-php-fpm
---
# group_vars/all
ansible_user: root
---
# host_vars/server01.example.org

ansible_host: 172.16.42.190
---
# host_vars/web01.example.org

ansible_host: 172.16.42.180
# production.ini
server01.example.org

[webservers]
web01.example.org
---
# roles/configure-newserver/tasks/main.yml

  - name: set hostname (volatile)
    hostname:
      name: '{{ inventory_hostname }}'

  - name: set hostname (permanent)
    template:
      src: hostname.j2
      dest: /etc/hostname
      force: yes

  - name: generate locales
    locale_gen:
      name: '{{ locale }}'
      state: present
    with_items:
      - en_EN.UTF-8
      - fr_FR.UTF-8

  - name: set default locale
    lineinfile:
      dest: /etc/default/locale
      regexp: '^LANG=.*$'
      line: 'LANG=fr_FR.UTF-8'
      create: yes
      state: present

  - name: set timezone
    file:
      src: /usr/share/zoneinfo/Europe/Paris
      dest: /etc/localtime
      force: yes
      state: link

  - name: common packages
    apt:
      name: "{{ item }}"
      state: present
    with_items:
      - apt-transport-https
      - bsd-mailx
      - ca-certificates
      - htop
      - manpages
      - net-tools
      - openssl
      - pciutils
      - postfix
      - ntp
      - ntpdate
      - rsync
      - tree
      - tzdata
      - ufw
      - unzip
      - vim
# roles/configure-newserver/templates/hostname.j2
{{ inventory_hostname }}
---
# roles/install-nginx/tasks/main.yml

  - name: install
    apt:
      name: nginx
      state: present
---
# roles/install-php-fpm/tasks/main.yml

  - name: php5-fpm if Debian <= 8
    apt:
      name: '{{ item }}'
      state: present
    with_items:
      - php5-fpm
      - php5-gd
    when:
      - ansible_distribution == "Debian" and ansible_distribution_major_version <= '8'

  - name: php7.0-fpm if Ubuntu => 16
    apt:
      name: '{{ item }}'
      state: present
    with_items:
      - php7.0-fpm
      - php7.0-xml
      - php7.0-gd
    when:
      - ansible_distribution == "Ubuntu" and ansible_distribution_major_version >= '16'

Lancer en dry-run (ne modifie rien) :

$ ansible-playbook -i production.ini deploy-newserver.yml --check

Lancer le déploiement :

$ ansible-playbook -i production.ini deploy-newserver.yml

Lancer le déploiement sur web01.example.org uniquement :

$ ansible-playbook -i production.ini deploy-newserver.yml --limit web01.example.org

Et voilà.

Exécution du playbook.

Ansible et moi

J'utilise régulièrement Ansible et maintient des playbooks pour des projets persos et pro, et j'en suis très satisfait. La documentation m'a permis d'être autonome et à l'aise assez vite et m'a même poussé à automatiser plus de choses que nécessaire, comme le déploiement de sites web alors que je pensais me limiter à la stack nginx / php-fpm / letsencrypt.

Même si je suis pleinement convaincu et satisfait par Ansible, je rencontre quand même certaines limitations. Par exemple l'inventaire n'est pas idéal quand on a beaucoup de serveurs et de groupes, ça peut rapidement devenir illisible. Il y a aussi certains modules tels que authorized_key pour lesquels j'aimerai plus de fonctionnalités, en l’occurrence pouvoir utiliser le paramètre 'exclusive' avec plusieurs clés.

Petits conseils en vrac

  • Ansible évolue vite, utilisez une version récente, disponible dans les backports debian ou via pip.
  • Le state: present est souvent implicite, vous n'avez donc pas besoin de le mettre, cependant c'est mieux de le faire, pour rendre le playbook plus évident à comprendre.
  • Pensez à utiliser les variables, pour les URLs ou les numéros de version.
  • Ne mettez pas de mot de passe ou de credential dans les playbooks, utilisez des variables passées au moment de l'exécution, ou dans un fichier que vous prendrez soin d'exclure si vous utilisez un repo public.
  • Faites un dry-run pour tester vos playbooks, on ne sait jamais.
  • Attention au module file et plus particulièrement aux permissions, surtout quand vous travaillez sur des fichiers existants (tels que le resolv.conf).
  • La commande ansible hostname -m setup vous permet de récupérer tous les 'facts' (OS, version, ip...) d'une cible, ils peuvent être utilisés dans les playbooks.
  • Testez vos playbooks jusqu'au bout : redémarrez le serveur pour vérifier que les modifications ne sont pas volatiles.
  • Faites des choses propres et simples. Si vos besoins sont trop variés ou spécifiques, n'utilisez pas Ansible, ou limitez vous au minimum.
  • La machine sur laquelle vous exécutez Ansible est critique, car en général vous aurez l'accès root without-password à tous vos serveurs, c'est donc un point d'entrée que vous devez sécuriser au maximum.

Conclusion

Si vous souhaitez unifier, accélérer et automatiser vos déploiements ou tout simplement vous initier au devops, Ansible est un outil que je recommande fortement, l'essayer c'est l'adopter.

Fil RSS des articles de cette catégorie