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.

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.