Docker – création de CD d’installation debian avec preseed

Pour pouvoir installer un système Debian automatiquement je devais créer un CD personnalisé avec un preseed. Comme à mon habitude j’aime mettre sous IC toute phase de mise au point. Cela permet une fois la mise sous IC effectuée de pouvoir modifier et reconstruire automatiquement l’image du CD.

Construction d’une ISO custom avec preseed

Rapatriement de l’ISO

Le processus général indiqué dans mon billet précédent commence par la rapatriement de l’image ISO officielle de Debian que nous allons appelé vanilla. Ce rapatriement se fait avec jigdo, outils de téléchargement promu par Debian qui permet de récupérer l’ISO par morceaux depuis différents serveurs. Il est évident depuis que nous allons utiliser un version dockerisé de l’outil afin de ne pas être dépendant d’une installation de l’outil sur l’hôte. Finalement le seul type de gitlab-runner qui nous intéresse désormais c’est le type docker !

Construction d’image jigdo

Un principe de conception qu’il faut appliquer au paradigme des conteneur est celui de la modularité. Comme en programmation objet, chaque classe doit avoir un objectif simple. Les objectifs plus complexes seront le résultat de l’agrégation de classes simples. Avec les conteneurs c’est la même chose. Il faut définir l’objectif de chaque conteneur et composer les conteneurs entre eux. Dans le cas du téléchargement de l’image Debian, l’image ne doit contenir que le strict minimum pour effectuer cette tâche.

En java on hérite de java.lang.Object, en docker on hérite de scratch.

Mais en java on peut hériter de classe abstraite, en docker on peut hériter d’image plus riche comme debian:stable-slim

Le chois de l’image de base est la première étape du travail. Il faut choisir la plus petite possible. Idéalement on devrait partir de scratch puis construire l’image en y ajoutant (copiant) tous les éléments nécessaires à l’exécution du service. On voit alors que partir d’un installation minimale de Debian est compromis judicieux. Le fichier Dockerfile est alors très simple car il suffit d’installer les paquets jigdo. Notez le -y pour que l’installation se fassee sans poser de question.

FROM debian:stable-slim
RUN apt-get update RUN apt-get install -y jigdo-file  

Il suffit ensuite de mettre sous IC le projet jigdo avec un fichier comme suit.

image: docker:latest
 before_script:
 docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" 
 build:
   stage: build
   tags:
      - docker
      - infra
      - regular
   script:
      - docker pull "$CI_REGISTRY_IMAGE:latest" || true
      - docker build --cache-from "$CI_REGISTRY_IMAGE:latest" --tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
      - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
 test:
   stage: test
   variables:
     # For non-Kubernetes executors, we use tcp://docker:2375/
     DOCKER_HOST: tcp://docker:2375/
     DOCKER_TLS_CERTDIR: ""
     # When using dind, it's wise to use the overlayfs driver for
     # improved performance.
     DOCKER_DRIVER: overlay2
 services:
     - docker:dind  
   tags:
       - dind
       - infra
       - regular
   script:
      - docker run "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" /bin/bash -c 'which jigdo-lite'
 deploy-master:
   stage: deploy
   tags:
      - docker
      - infra
      - regular
 script:
     - docker pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" || true
     - docker build --cache-from "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" --pull -t "$CI_REGISTRY_IMAGE" .
     - docker push "$CI_REGISTRY_IMAGE"
 only:
     - master
 deploy:
   stage: deploy
   tags:
      - docker
      - infra
      - regular
 script:
     - docker pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" || true
     - docker build --cache-from "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
     - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
   except:
     - master

Les éléments à retenir sont:

  1. l’étape de build se fait dans un docker natif (pas docker in docker) pour plus de rapidité d’exécution. En contrepartie comme on ne part pas de rien, il se peut que la version de debian slim utilisée ne soit pas la dernière car dans le cache du deamon docker (point à vérifier). Ce n’est pas grave pour l’image jigdo car le plus important c’est d’avoir jigdo qui fonctionne et pas la version de Debian qui le fait fonctionner
  2. Le test se fait en Docker in docker (DIND) car on veut vraiment tester que l’image produite correspond aux attentes sans effet de bord, donc on isole son exécution au maximum.
  3. Le contenu du test consiste à vérifier que la commande jigdo-lite existe bien dans le conteneur. Trivial…

Utilisation de l’image jigdo et persistance

En première approche il suffit dans un job de l’IC de dire que l’on utilise l’image que l’on vient de créer. Comme ceci:

do_update_vanilla:
 stage: update_vanilla
 image: registry.bressure.net/docker/services/jigdo

Ainsi dans la section script on peut appeler jigdo-lite…mais lors des exécutions du job, un nouveau conteneur est utilisé et le téléchargement complet de l’image Debian stable est effectué. Une ISO netinst pèse 350 Mo ce qui n’est pas neutre avec une liaison ADSL ! Donc il faut avoir un mécanisme de cache. Or dans ma version de gitlab je ne peux pas spécifier de volume persistants dans la définition du job (une demande d’amélioration est ouverte depuis un an auprès de Gitlab pour cette fonctionnalité), je dois donc lancer manuellement le conteneur jigdo. Finalement l’image que j’utilise pour le job est une simple docker:latest comme ceci:

do_update_vanilla:
   stage: update_vanilla
   image: docker:latest 

Puis dans la section script j’invoquerais une commande comme cela:

docker run --rm -v cache_root:/root  registry.bressure.net/docker/services/jigdo

Le plus important à retenir c’est l’utilisation du volume nommé afin d’avoir une persistance du répertoire /root en guise de cache et que docker ne le supprime pas lors de la suppression du conteneur en fin d’exécution.

Copie des sources

Lorsque l’on est dans la section script, le gitlab-runner a déjà fait un checkout correspondant au commit et on est dans à la racine de l’arborescence des sources. En clair on accès à tous notre contexte de construction comme les fichiers sources et autres scriptes. Cela a son importance dans la conception modulaire des scriptes de constructions. Ainsi dans mon cas la mise à jour de l’ISO de Debian est faite dans un scripte update_vanilla_iso.sh, le voici:

!/bin/bash
 scripte qui met à jour l'iso originale de debian
 1) détermine le dernier cd stable
 2) télécharge l'iso
 cd $1
 echo "find last stable iso…"
 LAST_RELEASE=wget -O - https://cdimage.debian.org/debian-cd/current/amd64/jigdo-cd/ | grep -E -o 'debian-[[:digit:]]*\.[[:digit:]]*\.[[:digit:]]*-amd64-netinst\.jigdo' | sort -n -r | head -n 1
 LAST_ISO=basename $LAST_RELEASE .jigdo.iso
 echo $LAST_ISO
 if [ -f $LAST_ISO ]
 then
   echo "ISO file already exists, do not download"
 else
   jigdo-lite https://cdimage.debian.org/debian-cd/current/amd64/jigdo-cd/$LAST_RELEASE
 fi
 ln -s -f $LAST_ISO debian_vanilla.iso

Il est plus simple et lisible d’invoquer ce script dans le job que de traduire ce script bash en commandes gitlab-ci. Le rapatriement n’a pas à savoir qu’il est lancé par une IC ! Or quand on lance le conteneur jigdo manuellement, les sources ne sont pas dans le conteneur. Il faut alors copier dans le conteneur ce dont on a besoin. Ici il s’agit de mon script update_vanilla_iso.sh. Voici la version finale du job de mise à jour de l’ISO vanilla:

do_update_vanilla:
   stage: update_vanilla
   image: docker:latest
   tags:
     - docker
     - infra
     - regular
 before_script:
     - mkdir -p ./target/vanilla
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
     - echo "pull jigdo container because latest dependency"
     - docker pull registry.bressure.net/docker/services/jigdo
     - echo "launch jigdo container on cache volume of already downlaoded iso"
     - docker run --rm -v cache_root:/root -d --cidfile ./jigdo.cid -t registry.bressure.net/docker/services/jigdo
     - JIGDO_ID=cat ./jigdo.cid
 script:
     - echo "retrive last vanilla iso"
     - echo "copying scripts into jigdo container"
     - docker cp "$(pwd)/update_vanilla_iso.sh"  ${JIGDO_ID}:/tmp
     - echo "launch script into jigdo container"
     - docker exec $JIGDO_ID bash ./tmp/update_vanilla_iso.sh /root
     - echo "copying result from jigdo container into build target"
     - docker cp ${JIGDO_ID}:/root/.  ./target/vanilla/
   after_script:
     - echo "do some cleanup"
     - docker stop cat ./jigdo.cid

Les points importants sont:

  1. Création du conteneur en détaché juste pour avoir le volume de cache et en autoremove pour que docker le supprime quand on va l’arrêter
  2. On sauve l’identifiant du conteneur pour pouvoir y revenir
  3. On copie le scripte du répertoire courant dans le conteneur
  4. On lance le scripte dans le conteneur et ce scripte aura bien accès à la commande jigdo-file
  5. On copie le résultat du scripte depuis le conteneur vers le contexte du build. Cette dernière commande est utile si on veut passer le résultat du job en artéfact pour le job suivant de construction de l’iso custom.
  6. Enfin on arrête le conteneur et docker va le supprimer automatiquement

Passage d’artéfact d’une étape à l’autre…

Une fois l’image vanilla obtenu il faut la personnaliser. Le job suivant de personnalisation prend donc entrée l’image vanilla. La méthode de gitlab est de passé par les artéfacts de construction. Ainsi dans le job do_update_vanilla il faut ajouter à la fin les lignes suivantes:

artifacts:
 paths:
 - target/vanilla/
 expire_in: 1 day

Et dans le job suivant do_build_custom:

dependencies:
     - do_update_vanilla  

Gitlab va alors en fin du job do_update_vanilla mettre le répertoire /target/vanilla en tant qu’artéfact. Cela engendre un transfert http depuis le runner vers gitlab: soit 350 Mo. Puis lors de l’exécution du job suivant do_build_custom, l’artéfact est copié dans le répertoire de build : 350 Mo de transfert. Les performances du pipeline s’en ressentent.

… par volume docker

Comme le job de construction va se faire également de manière dockerisée, alors pourquoi pas lancer manuellement l’image servant à la construction et réutiliser le volume de cache. On évite le surcout des transferts réseaux avec une petite entorse à la correction du build: puisque rien ne garantie que l’image iso dans le volume persistant provienne bien de l’exécution du job précédent dans le même pipeline. Heureusement dans ce cas précis l’ISO Debian n’est pas le résultat d’une construction (compilation) à partir d’un commit de source mais le résultat de l’exécution d’un script de rapatriement (sans compilation). Au pire 2 commits différents et dont les pipeline se chevauchent pourraient rapatrier des fichiers différents dans le répertoire de cache. Cela arrive si le script de rapatriement n’est pas encore au point où que la version de l’ISO de Debian a changé entre les 2 commits. Peu probable et le risque d’avoir une construction incorrecte perdurer et passer inaperçue est mitigé par le build quotidien.

Construction d’une image docker pour construire l’iso

Le script bash qui permet de construire l’ISO custom à partir de l’ISO vanilla et du fichier preseed.cfg est le suivant:

!/bin/bash
 Create a custom iso given the vanilla iso and the preseed.cfg
 ISOFILE=$1
 echo "using vanilla $ISOFILE"
 rm -rf ./target/custom/debian_custom.iso
 rm -rf ./target/isofiles/
 mkdir ./target/isofiles
 cd ./target
 bsdtar -C isofiles -xf  ${ISOFILE}
 chmod +w -R isofiles/
 gunzip isofiles/install.amd/initrd.gz
 echo ../preseed.cfg | cpio -H newc -o -A -F isofiles/install.amd/initrd
 gzip isofiles/install.amd/initrd
 auto install in txt mode
 sed -i "s/timeout 0/timeout 1/" isofiles/isolinux/isolinux.cfg
 sed -i "s/menu default//" isofiles/isolinux/gtk.cfg
 echo "menu default"  >> isofiles/isolinux/txt.cfg
 cd isofiles/
 md5sum find -follow -type f > md5sum.txt
 cd ..
 genisoimage -r -J -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -o ./custom/debian_custom.iso isofiles

Le script prend en argument l’image ISO vanilla. En gras se trouvent les commandes essentielles et nécessaires pour la création de l’ISO personnalisée. Comme pour jigdo je suis parti d’une image de base debian:stable-slim. Le script utilsait au début 7z au lieu de bsdtar mais dans l’image Debian de base, 7zr l’équivalant de 7z sous Debian n’arrivait pas extraire l’ISO alors que sous Ubuntu cela fonctionnait bien. J’ai donc opté pour bsdtar qui lui fait très bien le travail.

Le fichier Dockerfile de l’image customiso est alors :

FROM debian:stable-slim
RUN apt-get update
RUN apt-get install -y bsdtar cpio genisoimage

Le fichier de lC est similaire à celui de l’image jigdo en adaptant le job de test pour vérifier que les commandes essentielles sont bien présente dans l’image:

image: docker:latest
 before_script:
 docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" 
 build:
   stage: build
   tags:
      - docker
      - infra
      - regular
   script:
      - docker pull "$CI_REGISTRY_IMAGE:latest" || true
      - docker build --cache-from "$CI_REGISTRY_IMAGE:latest" --tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
      - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
 test:
   stage: test
   variables:
     # For non-Kubernetes executors, we use tcp://docker:2375/
     DOCKER_HOST: tcp://docker:2375/
     DOCKER_TLS_CERTDIR: ""
     # When using dind, it's wise to use the overlayfs driver for
     # improved performance.
     DOCKER_DRIVER: overlay2
 services:
     - docker:dind  
   tags:
       - dind
       - infra
       - regular
   script:
      - docker run "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" /bin/bash -c 'which bsdtar'
      - docker run "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" /bin/bash -c 'which genisoimage'
      - docker run "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" /bin/bash -c 'which cpio'
 deploy-master:
   stage: deploy
   tags:
      - docker
      - infra
      - regular
 script:
     - docker pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" || true
     - docker build --cache-from "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" --pull -t "$CI_REGISTRY_IMAGE" .
     - docker push "$CI_REGISTRY_IMAGE"
 only:
     - master
 deploy:
   stage: deploy
   tags:
      - docker
      - infra
      - regular
 script:
     - docker pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" || true
     - docker build --cache-from "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
     - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
   except:
     - master

Utilisation de l’image customiso

Disposant d’un image pour construire l’ISO, il suffit dans le projet de création du l’iso debian custom, dans un job faisant suite au rapatriement d’uiliser une image docker:stable juste pour pouvoir lancer l’image customiso en prenant soin de bien recopier les sources dans le conteneur. Cela donne:

do_build_custom:
   stage: build
   image: docker:latest
   tags:
     - docker
     - infra
     - regular
   image: docker:latest
 before_script:
     - mkdir -p ./target/custom
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
     - echo "pull registry.bressure.net/docker/services/customiso container because latest dependency"
     - docker pull registry.bressure.net/docker/services/customiso
     - echo "launch debian:stable-slim container on cache volume of already downlaoded iso"
     - docker run --rm -v cache_root:/root -d --cidfile ./customiso.cid -t registry.bressure.net/docker/services/customiso
     - CUSTOMISO_ID=cat ./customiso.cid
 script:
     - echo "build custom iso with preseed"
     - echo "copying scripts into jigdo container"
     - docker cp "$(pwd)/."  ${CUSTOMISO_ID}:/tmp
     - echo "listing of /tmp in container"
     - docker exec -w /tmp $CUSTOMISO_ID  ls -l
     - echo "listing of /root in container"
     - docker exec -w /root $CUSTOMISO_ID  ls -l
     - echo "launch script into debian container"
     - docker exec -w /tmp $CUSTOMISO_ID bash ./build_custom_iso.sh /root/debian_vanilla.iso
     - echo "copying result from debian container into build target"
     - docker cp ${CUSTOMISO_ID}:/tmp/target/custom/.  ./target/custom/
 after_script:
     - echo "do some cleanup"
     - docker stop cat ./customiso.cid
 dependencies:
     - do_update_vanilla  

Les points importants sont:

  1. le lancement du conteneur avec le volume de cache monté sur /root
  2. la copie du répertoire courant dans le répertorie /tmp de conteneur
  3. on lance le script dans le répertoire /tmp

Ce billet un peu long montre bien les écueils rencontrés et les solutions trouvées. Je suis seulement au milieu du chemin car il faut maintenant tester l’ISO dans l’IC c’estèà-dire lancerune VM sur l’ISO custom et vérifier qu’elle répond aux attentes.

La suite au prochain billet.

Docker – VM modèle ou installation automatique

Je voulais initialement mettre au point une VM modèle que je pourrais cloner pour servir de base à une machine paramétrable avec Ansible. La mise au point de la VM passse par l’installation de l’OS et cette phase vait aussi pour une machine physique. Comme l’informatique est la science de ceux qui n’aiment pas se répéter, je me lance donc sur le sujet d’automatisation d’une installation.

Preseed

Le processus d’installation d’un système Debian peut peut-être automatisé par un fichier preseed.cfg qui contient les réponses aux questions de l’installateur. La mise au point de ce fichier merite l’utilisation de l’intégration continue qui consistera à :

  1. rappatrier une ISO de debian en version stable
  2. extraire l’ISO
  3. ajouter le preseed
  4. recréer l’ISO
  5. demarrer une VM sur l’ISO
  6. vérifier que le système installé répond à l’attente
  7. déployer l’ISO pour utilisation future.

Gitlab-runner et libvirt

Le principe est séduisant mais il faut que je trouve comment depuis un gitlab-runner lancer une VM. Cette VM devra être créée from scrach pour bien reproduire le cas d’utilisation de l’ISO custom. J’entrevois déjà des questions nouvelles dans la coneption de la solution: idealement le test devrait se faire dans un conteneur mais la mise sous docker sera impossible car le conteneur partage les librairies systèmes (noyau) de l’hôte. Donc le seul moyen d’isoler le test création/installation de VM serait de le faire sur un hôte dédié: une VM avec gitlab-runner et libvirt afin de créer des VM !

Affaire à suivre….

Docker – vers la scalabilité

Feuille de route du 26/05/2019

La migration a pour objectif de:

  • Réduire le nombre de VM (moins d’administration etc)
  • Réduire l’empreinte mémoire des services (blog divers, owncloud etc)
  • Répondre au besoin de sauvegarde comme à l’heure actuel
  • Répondre au besoin de monitoring comme à l’heure actuel
  • Ajouter la possibilité d’avoir une préproduction complète

En option:

  • Permettre la scalabilité des services
  • L’exécution des services dans le cloud

Objectifs atteints

Si j’ai atteints facilement les objectifs principaux, la mis à l’échelle et la capacité à mettre mes services dans le cloud c’est-à-dire de faire héberger ou bien mes conteneurs ou bien la VM hôte dans le cloud, sont encore à faire.

Scalabilité

J’ai commencé avec une VM de 4 Go pour progressivement monter à 7 Go. Cela se fait en modifiant la VM et ajouter du swap pour les raisons que j’évoquais dans le billet sur l’over-commit. L’objectif était de voir combien de RAM je pouvais économiser c’est-à-dire combien j’en avais gaspillé avec le modèle tout VM où un service c’est une VM.

Rappel de mon infra sur serveur mutalisé de VM à conteneur

Cette méthode nécessite de l’administration de la VM et atteindra sa limite de toute façon avec le nombre maximum de processus par OS et la résistance à la panne. Si la VM plante alors tous les services sont impactés. C’est le Single Point Of Failure (SPOF) à éviter.

Augmentation de l’enveloppe dal VM pour s’adapter au nombre de conteneur

Outils possibles

Le principe est d’avoir plusieurs dockers sur des machines différente (physique ou VM) afin de répartir les conteneurs. C’est ce que swarm semble promettre. Un autre outil serait le bazooka Kubernetes. Le choix de l’outil dépendra des bénéfices apportés par rapport au temps de mise en œuvre.

Solution cible

Quelle que soit la solution technique qui permettra de panacher les conteneurs sur différents docker, il faudra que je revoie le reverse proxy apache pour l’instant natif sur l’hôte. Je pense à le dockerisé ou à utiliser une solution comme traefik.

Automatisation de la création de l’hôte

Dans mon infra j’ai réussi à rendre transparent le déploiement des conteneurs via le processus d’intégration continue. Cela nécessite la création préalable de l’hôte du moteur docker et du gitlab-runner natif. Si docker s’exécute dans une VM comme dans mon cas il faut encore avoir créer la VM et donc installer un OS. Tout cela est encore manuel chez moi même si cela reste simple: une débian minimaliste, docker, docker-compose et gitlab-runner natif.

Clonage de VM

Afin d’avoir une préprod et une prod identique au niveau OS/Docker, j’ai procédé par clonage de VM. Une fois la VM de préprod prête, j’ai cloné celle-ci. Ce mécanisme permet d’ajouter autant de VM que voulu à partir d’une image creuse prête à l’emploi ne contenant que l’OS, le moteur docker et le gitlab-runner natif. Cependant comment ajouter une machine physique à mon infra ? Il faut être capable de créer et configurer une machine physique également:

  • Aussi bien pour accueillir un moteur docker directement
  • Que pour accueillir des VM à conteneurs

Outils de configuration

La création de machine physique prête à rentrer dans mon cluster de moteur docker peut se faire avec un outils de configuration comme Ansible. Le principe est d’appliquer une configuration (installation, paramétrage) sur une machine disposant d’un accès SSH. Dès lors il me suffit d’être capable d’installer rapidement une machine physique avec les caractéristiques suivantes:

  • Débian minimale
  • Accès SSH pour Ansible

Dans le cas d’un machine virtuelle, il me suffira d’avoir une VM modèle avec ces mêmes caractéristiques minimales afin de pouvoir utiliser Ansible pour la configurer plus en avant. Cette configuration consistera à:

  • mettre à jours le système
  • installer le moteur docker
  • installer docker compose
  • installer le gitlab-runner natif ?

Gitlab-runner

J’utilise jusque là l’intégration continue pour faire les déploiements via gitlab-runner. Cette façon de faire m’évite d’avoir à mettre à jours une copie locale des fichiers de configuration des application par un git pull, le runner faisant tout ça automatiquement. Dans le cadre des conteneurs en cluster gérés par swarm ou kubernetes, le gitlab-runnner sera peut-être remis en cause.

La prochaine étape sera la création d’une VM modèle. La suite au prochain billet !

Docker – conteneuriser c’est mutualiser efficacement

Le chantier de passage de mes VM en conteneur docker commencé en mai touche à sa fin. Il ne me reste qu’une seule VM à migrer. Pour rappel, je me contrains à réellement migrer en conteneur et je n’utilise pas d’outils de conversion VM vers conteneur afin d’avoir réellement des images minimales offrant uniquement le service attendu. Le résultat obtenu début juillet était déjà probant. Dans ce billet j’enfonce le clou graphique munin à l’appui.

Réduction de l’enveloppe mémoire de VM et de la mémoire alloué sur l’hôte

La ligne bleu montre l’enveloppe mémoire des VM de ma machine hôye

Les 15 VM occupaient initialement 18-19 Go de RAM avant que ne me lance dans la migration avec uniquement un production. Au début de la migration en conteneur docker, l’enveloppe mémoire avait fait un bon puisque j’ajoutais 2 VM : une production et un pré-production puis au fil des migrations l’enveloppe mémoire sur l’hôte s’était mise à chuter rapidement. Actuellement j’ai doublé le nombre de services (une prod et une pré-prod) et réduit l’enveloppe mémoire à 15 Go. 30 Services pour 15 Go là où la virtualisation prenait 18-19 Go pour 15 services !

La ligne verte montre la mémoire commitée

Mutualisation efficace par over-commit

Du point de vu de l’hôte la mémoire alloué à sensiblement diminuée passant de 45 Go à 33 Go. Tout cela doit être vu avec la perspective du doublement du nombre de service. Cela est rendu possible par la mutualisation au sein d’une même VM (un seul OS) de tous les services. L’OS a alors le choix d’allouer intelligemment les ressources limitées par l’enveloppe de la VM. Dans mon cas ma VM de prod occupe 7 Go de RAM. Idem pour la pré-pod. Pour ce faire le noyaux linux à recours à la surréservation (over-commit). Ici la machine de production:

La ligne verte montre la réservation mémoire : on est bien en over-commit

La machine de pré-production certes moins sollicitée est également en over-commit:

Bien que la sur-réservation mémoire est normale car par empirisme les applications allouent la mémoire sans vraiment en avoir besoin tout le temps ce qui a conduit Linux à prendre en comportement par défaut l’over-commit. Cependant si jamais toutes les applications veulent accéder en même temps à la mémoire qui leur avait été promise, le système va planter sauf si on met suffisamment de fichier d’échange pour amortir le choc. Dans mon cas je me fixe comme limite de commit la quantité de mémoire virtuelle disponible (RAM 7 Go + SWAP 7 Go = 14 Go). Quand l’over-commit s’approche de cette limite, une alerte munin est rémontée !

Je ne triche pas avec le Swap

On pourrait croire que le swap est un moyen de tricher. Et non, il n’est là que pour amortir le cas improbable où tous les services voudraient leur mémoire promise. D’ailleurs on voit bien sur les graphiques que les VM ne « swappent » pas.

Overcommit sans swap = jusque là tout va bien

Et l’usage disque supplémentaire pour le swap ? C’était déjà la cas avec chacune des 15 VM du départ. Chacune ayant entre 1 Go et 4 Go de swap. Avec docker j’ai 15 services qui tiennent avec 7 Go de swap.

Docker (ou une solution de conteneurisation) est vraiment une avancée dans l’usage raisonné des ressources. Il offre en plus l’avantage de garantie d’une exécution dans des environnements à l’identique en terme de dépendance. Ces 2 points me permettent d’avoir une pré-prod représentative de ma prod aussi bien en terme d’installation (dépendances) et de taille (sans que cela ne coûte trop cher)

kvm – ajouter de l’espace disque swap à chaud avec LVM

L’ajout d’espace d’une VM peut être augmenté à chaud pourvu que l’on utilise LVM. Cela permet de se sortir de situation où on voit la saturation du disque mettre en péril la continuité de service. L’article précédent traite de ce sujet: https://blog.bressure.net/2019/07/05/kvm-ajouter-de-lespace-disque-a-chaud/

Cas du swap

Quand on doit augmenter la taille du swap, on procède de la même manière en ajoutant un disque à la VM puis ajoutant le disque au groupe de volume utilisé par le volume logique du swap et enfin en étendant la volume logique correspondant au swap à la totalité de l’espace disponible dans le groupe de volume précédemment agrandi avec le nouveau disque.

# vgextend staging-vg /dev/vdc
# lvm lvextend -l +100%FREE /dev/staging-vg/swap_1

A ce moment l’espace du volume logique a bien augmenté. Pour cela lancer la commande fdisk -l

En revanche le swap disponible au niveau de l’OS n’a pas encore bougé. Lancer la commande free -h pour se convaincre.

La prise en compte de la nouvelle capacité de l’espace de swap, il faut désactiver le swap puis formater le volume logique et enfin réactiver le swap en utilisant bien l’argument -v. Utiliser la commande fdisk -l pour avoir le chemin du volume logique du disque swap

# swapoff -va
# mkswap /dev/mapper/staging--vg-swap_1
# swapon -va

Cas où le swap est utilisé

Si jamais le système utilise déjà de l’espace swap et que la quantité de RAM ne peut pas prendre en charge ce qui est alloué dans le swap, alors il faudra créer un autre volume logique puis créer le swap avec mkswap et enfin activer le swap.

kvm – ajouter de l’espace disque à chaud

En train d’importer un blog avec 36000 entrées à partir d’un fichier de WRX de 54 Mo, je voyais l’espace disque se remplir à vue d’oeil ! à 92% munin commençait à m’envoyer des mails d’alerte.

Pas de panique, comme j’utilise kvm et lvm, l’ajout d’espace disque peut se faire à chaud. La difficulté était que je devais tout faire en shell distant.

J’ai ainsi réussi à sauver mon processus d’import en ajoutant un disque et augmenter le volume logique mais le disque virtuelle était au mauvais format: 20 Go virtuel et 20 Go sur l’hôte même si il était vide.

Finalement voici la méthode pour ajouter à chaud de l’espace que une VM en train de staurer.

Créer un nouveau disque

Se mettre sur l’hôte et en tant que root en remplaçant staging-2.qcow par c3 qu2 vous voulez:

qemu-img create -f qcow2 /var/lib/libvirt/images/staging-2.qcow2 20G

Ne pas utiliser les option de preallocation

Attacher le disque à la VM

C’est là que réside la subtilité pour que la VM croire avoir à faire à un disque de 20G même si le fichier ne fait que quelques centaines de kilos. La commande qemu-img info permet de voir les propriétés du disque virtuel.

/var/lib/libvirt/images# qemu-img info staging-2.qcow2
image: test.qcow2
file format: qcow2
virtual size: 20G (21474836480 bytes)
disk size: 196K
cluster_size: 65536
Format specific information:
compat: 1.1
lazy refcounts: false
refcount bits: 16
corrupt: false



On va attacher le disque à la VM en précisant bien le driver qcow2

# virsh attach-disk staging /var/lib/libvirt/images/staging-2.qcow2 vdb --driver qemu --subdriver qcow2 --targetbus virtio --persistent --config --live

Remplacer vdb par un identifiant de device disponible dans la VM qui s’appelle ici staging.

En tant que root sur le VM, vérifier la présence du disque et qu’il a la bonne taille

# fdisk -l

Disque /dev/vdb : 20 GiB, 21474836480 octets, 41943040 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets

Ajouter le disque /dev/vdb dans le volume group à étendre., ici staging-vg

# vgextend staging-vg /dev/vdb

Ajouter tout l’espace au volume logique, ici /dev/staging-vg/root

# lvm lvextend -l +100%FREE /dev/staging-vg/root

Redimensionner le système de fichier qui se trouve ici dans /dev/mapper/staging--vg-root

resize2fs -p /dev/mapper/staging--vg-root

Docker – résultat de migration VM vers conteneurs

Les premiers resultats se confirment: le passage aux conteneurs docker me permet de faire de economies drastiques de ressources !

Avec 15 VM (soit 15 applications) qui occupaient 17 Go de RAM mon systeme hôte montrait des signes de saturation. J’ai déjà migré 7 applications et cela se traduit par un du nombre de service (conteneur docker) consequent car une application est constituée par exemple d’un service wordpress et d’un service de base de données. Actuellement pour 7 applications j’ai 17 conteneurs mais qui tiennent dans 4 Go de RAM.

Ceci me permet d’avoir une prod et une preprod. J’ai 14 applications dans 2 VM pour 8 Go de RAM totale.

Réduction du nombre de VM

Si au démarrage du projet j’ai ajouté 2 VM pour accueillir la production et la préproduction. La transformation progressive des anciennes VM de prod en conteneur à porté ses fruits.

La capture suivante montre bien l’empreinte mémoire des VM diminuer au fur et à mesure de leur disparition. Actuellement les VM occupent 14 Go contre 17 Go avant. Le gain semble faible en réalité il est très important…. car j’ai aujourd’hui le double d’application: une prod et une preprod. Je suis donc virtuellement passé de 34 Go à 14 Go !

Multiplication des conteneurs

Quand on observe le contenu de la VM de production, on voit que la transformation de 7 application qui tenaient dans 8 VM à cause d’une VM dédié à Elasricsearch pour un dzq blog, a donné naissance à 16 conteneurs. Il faut ajouter le gitlab-runner pour effectuer les déploiements.

On remarque que les conteneurs consomment à peine 300 Mo chacun sauf celui d’Elasticsearch. On comprend alors la gabegie d’avoir une VM dédiée par application.

Charge mémoire de l’hôte

La gestion de la mémoire des VM est telle que une VM ne rend jamais à l’hôte la mémoire allouée. On peut par une action manuelle diminuer la taille de la mémoire mais cela ne rend pas la mémoire au système. Donc ce qui est alloué au bénéfice de l’overcommit finira par générer du swap. L’extinction des VM au fur et à mesure montre bien la diminution de la charge mémoire.

Charge mémoire de la VM docker

La VM de prod avec 4 Go exécute sans broncher tous les conteneurs au prix d’un overcommit important. Toutefois puisque les services ne sont pas utilisés en même temps, le swap n’a pas encore eu lieu mais le système est à sa limite. Je ne vais pas pouvoir décemment rester avec 4 Go de RAM par VM.

Docker – premier résultats de migration VM vers conteneurs

Le 23 mai dernier je me lançais dans la transformation de mes VM en conteneurs docker. Cette migration est un changement de paradigme et j’ai voulu de plus mettre en oeuvre de l’integration et du deploiement continu comme fondations de ma nouvelle infrastructure.

Ces prérequis ont un peu retardé les resultats visibles que cette transformation avait comme promesse:

  1. moins de VM
  2. moins d’empreinte mémoire

Souvenez-vous je partais de 15 VM qui offraient differents services (blog….) consommant 17Go de RAM en total et un système hôte qui était en overcommit avec du swap.

Nombre de VM

J’ai ajouté 2 VM: une production et une production. A ce jour j’ai dockerisé 5 VM donc le nombre total de VM est en baisse: -3. Mais il faut bien voir que je gagne également en terme de « robustesse » car j’ai une preprod qui n’existait pss avant. Dès la 2e VM dockerisée j’étais à l’équilibre.

Pour comparer réellement c’était comme si j’avais 15×2 VM (prod et prepod) et que maintenant j’ai ajouté 2 VM et retiré 5×2 VM. Donc au final j’ai virtuellement fait -8 en nombre de VM. Avec une préprod avant j’aurais été à l’équilibre dès la 1er VM dockerisée.

Empreinte memoire

Les 2 VM de prod et preprod consomment chacune 4 Go de RAM tandis que les 5 VM supprimées utilisaient au total 8 Go. Donc je suis à l’équilibre.

Je dispose, avec la même quantité de mémoire, d’une préprod pour mes 5 services conteneurisés.

On remarque que l’overcommit du système hôte a baissé dès que les 8Go de VM ont été enlevés. En revanche dans la VM de production qui héberge les 5 applications (ex-VM) sous forme de services dockers, les 4 Go sont bien remplis et semblent être juste

On voit de overcommit mais pas vraiment de swap. Ce résultat est encourageant et montre bien que la conteneurisation permet de mieux partager les ressources sans le gaspillage lié à la multiplication des OS dans le paradigme du tout virtualisé.

Je vais donc poursuivre l’expérience de mise sous docker et voir quand la VM de prod va craquer.

Docker – ma premiere VM dockerisée

Ce billet est le premier de mon blog édité dans sa version dockerisé. Enfin ! Depuis plus d’un mois je me lançai dans la migration de mes VM en conteneur docker. Je suis passé par l’étape préliminaire de la mise en place d’un outillage d’intégration continue et déploiement continue. La solution que j’ai trouvée est basée sur Gitlab et me permet d’avoir une grade grande agilité dans cette phase de migration.

Ce matin mon blog professionnel est passé de la version VM turnkey linux à une version docker. J’ai fais l’impasse sur la création d’une images personnalisée afin d’avoir rapidement un résultat qui fonctionne.

Je vais maintenant migrer rapidement toutes mes VM wordpress en conteneur docker et voir le gain obtenu en terme de ressources utilisées sur mon systeme hôtes.

Docker – Mise a jour du gitlab-runner dockerisé

Dans mon infra j’utilise gitlab-runner dockerisé pour déployer. Comme j’entrevois de devoir modifier souvent la configuration du service gitlab-runner, il me faut la mettre sous intégration continue au moins et voire en déploiement continue dans un second temps.

Un runner qui s’arrête lui-même

En intégrant le fichier de configuration standard de mon projet blog, le déploiement en environnement staging commence par arrêter le service et là:

docker-compose version 1.24.0, build 0aa59064
docker-py version: 3.7.2
CPython version: 3.6.8
OpenSSL version: OpenSSL 1.1.0j 20 Nov 2018
$ echo "deploy to staging"
deploy to staging
$ docker-compose down
Stopping runner_docker_1 ...
ERROR: Job failed (system failure): aborted: terminated

En effet le job s’execute dans un runner géré par le démon (service) gitlab-runner. En arrêtant le service, on arrête le runner et donc le job.

Un gitlab-runner bootstrap

Le gitlab-runner dockerisé sert à déployer les services. Si lui-même est un service déployable comme un autre, il faut donc un gitlab-runner de démarrage dédié. Ce dernier n’a pas intérêt à être dockerisé car on ne pourrait pas le mettre à jour comme les autres services.

Il faut donc avoir un gitlab-runner dit de bootstrap qui sera une installation système native. Ce dernier enregistera un runner marqué bootstrap qui communiquera avec le docker engine via le lancement d’un conteneur docker-compose.

Mise à jour du service gitlab conteneurisé

La mise à jour du service gitlab promettait de poser le même problème. Si un runner l’arrête, la constructions va s’arrêter. Voilà que je risquaitns d’atteindre la limite de mon infra à base de gitlab pour déployer !

Il en est rien ! Le runner peut stoper le service gitlab via docker-compose. A ce moment gitlab ne répond plus mais le runner continue de s’exécuter et redémarre le service gitlab. Gitlab est de nouveau disponible et reçoit le résultat du job.

Donc je peux mettre a jour mon service Gitlab via gitlab-runner. Pour cela j’utilise le gitlab-runner conteneurisé mais avec un runner docker utilisant la socket de l’hôte. Dans le schéma plus haut on voit que le gitlab-runner dockerisé gère 2 runner:

  1. l’un utilisant Dind pour l’intégration continue
  2. l’autre par socket binding pour le déploiement automatique du service gitlab conteneurisé

Tag de job

J’utilise les tag pour sectionnelle le bon runner en fonction du stage du job. Chaque runner est caractérisé par une combinaisons de tag qui le définit de manière unique.

Voici le fichier docker-compose.yml du service GitLab:

image:     
    name: docker/compose:1.24.0
    entrypoint: [""]
  
before_script:
    - docker info
    - docker-compose version

stages:
    - build
    - test
    - staging
    - production

run_gitlab:
    stage: test
    tags:
        - dind
        - infra
        - regular
        
    variables:
        # For non-Kubernetes executors, we use tcp://docker:2375/
        DOCKER_HOST: tcp://docker:2375/
        # When using dind, it's wise to use the overlayfs driver for
        # improved performance.
        DOCKER_DRIVER: overlay2
    services:
        - docker:dind
    script:
        - echo "run blog for testing"
        - docker-compose up -d
        - docker-compose ps

deploy_to_staging:
    stage: staging
    tags:
        - staging
        - docker
        - regular
    script:
        - echo "deploy to staging"
        - docker-compose down
        - docker-compose up -d
    only:
        - master


deploy_to_infra:
    stage: production
    tags: 
        - infra
        - docker
        - regular
    script: 
        - echo "deploy to dev infra"
        - docker-compose down
        - docker-compose up -d   
    only:
        - master
    when: manual

Dans le cas où le service est le gitlab-runner conteneurisé lui-même, il faut que le job utilise le gitlab-runner natif (que je qualifie de bootstrap). Voici le fichier docker-compose.yml du service gitlab-runner:

image:     
    name: docker/compose:1.24.0
    entrypoint: [""]
  
before_script:
    - docker info
    - docker-compose version

stages:
    - build
    - test
    - staging
    - production

run_runner:
    stage: test
    tags:
        - dind
        - infra
        - regular
        
    variables:
        # For non-Kubernetes executors, we use tcp://docker:2375/
        DOCKER_HOST: tcp://docker:2375/
        # When using dind, it's wise to use the overlayfs driver for
        # improved performance.
        DOCKER_DRIVER: overlay2
    services:
        - docker:dind
    script:
        - echo "run blog for testing"
        - docker-compose up -d
        - docker-compose ps

deploy_to_staging:
    stage: staging
    tags:
        - staging
        - docker
        - bootstrap
    script:
        - echo "deploy to staging"
        - docker-compose down
        - docker-compose up -d
    only:
        - master

deploy_to_production:
    stage: production
    tags: 
        - production
        - docker
        - bootstrap
    script: 
        - echo "deploy to production"
        - docker-compose down
        - docker-compose up -d   
    only:
        - master
    when: manual

deploy_to_infra:
    stage: production
    tags: 
        - infra
        - docker
        - bootstrap
    script: 
        - echo "deploy to dev infra"
        - docker-compose down
        - docker-compose up -d   
    only:
        - master
    when: manual