Optimisation des temps de build
Les temps de build (compilation) sont du temps perdu surtout si on est dans des phases de développement où on relance souvent les mêmes commandes d’installation. Je vais prendre comme exemple le temps de déploiement d’Ansible dans les différents outils que j’utilise dans mon Home Lab Devops.
Ansible est écrit en python qui est soit livré en
package, soit via le repository pypi. Mais voilà pour les packages, ils
sont souvent très en retard sur les versions et pour le repository pypi
seules les codes sources en tar.gz
sont mis à disposition. Il en est de
même pour les dépendances dont openssl
, cffi
et cryptography
.
Temps de build d’une image ansible-lint sans optimisation
Je vais commencer par prendre un temps de build de l’image de container ansible-lint qui intègre Ansible dans ses dépendances à partir d’une image alpine.
Son code source :
FROM alpine:3.15 as builderWORKDIR /COPY Pipfile ./RUN apk updateRUN apk add --no-cache bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev py3-pipRUN pip3 install ansible-lint==6.0.0
Allez on build en prenant soin de purger le cache de docker :
docker system prune -atime docker build -t test:0.1 .docker build -t test-build:0.1 . 0.02s user 0.03s system 0% cpu 3:02.40 total
Le build a pris 3 min. Si on prends le temps de regarder les traces on voit
bien que le temps est pris pour compiler des librairies avec gcc
et rust
.
Pourquoi pas précompiler ces packages python au format wheel (stockage des
binaires) et les stocker dans un repository Pypi dans Nexus?
Création des Repository Pypi dans Nexus.
Connectez-vous à Nexus avec le compte admin. Allez dans le menu repository, cliquez sur [create repository].
Choisissez pypi (hosted) donnez un nom et autorisez le redeploy.
Pour éviter de toujours télécharger sur internet tous les packages python,
nous allons aussi déclarer un repo de type (proxy). Donnez-lui un nom et
saisissez l’url https://pypi.org
Pour ne déclarer qu’un seul repo dans la configuration de pip nous allons regrouper les deux repos précédents dans un repository pypi de type (group). Donnez-lui un nom et sélectionnez les deux repo créez précédemment.
Pour séparer les droits admin et en lecture, Nous allons Créer deux
rôles nx-python-admin et nx-python-read avec respectivement
les droits nx-repository-view-pypi-*-*
et nx-repository-view-pypi-*-read
.
Maintenant nous allons créer deux users : un python-admin et un
python-read avec leur rôle respectif. Allez dans le menu security/users
et
cliquez sur [Create Local User].
Compilation et stockage des packages au format Wheel
Je vais utiliser docker pour compiler et stocker les packages whl
et les
envoyer dans Nexus :
FROM alpine:3.15.1RUN apk update && apk add bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev py3-pipRUN pip3 install setuptools setuptools_rust pip wheel twine pipenv --upgradeWORKDIR /srcCOPY requirements.txt .RUN pip3 wheel -r requirements.txtRUN mkdir /crtCOPY rootCA.pem /crt/RUN twine upload -u python-admin -p passwd --cert /crt/rootCA.pem --repository-url https://artefacts.robert.local/repository/mypackages/ *.whl
Par la suite on pourra ajouter dans le fichier requirements.txt
tous les
packages python que nous utilisons dans tous nos projets.
ansible==5.5.0ansible-lint==6.0.0ansible-builder==1.0.1ansible-runner==2.1.2ansible-bender==0.9.0
On peut allez voir le résultat dans Nexus. Browse > mypackages
Modification de l’image ansible-lint pour utiliser Nexus comme cache
Commençons par écrire la configuration pip
que nous allons injecter dans
l’image.
[global]index = https://python-read:passwd@artefacts.robert.local/repository/pypi-all/pypiindex-url = https://python-read:passwd@artefacts.robert.local/repository/pypi-all/simple/timeout = 10trusted-host = artefacts.robert.local
On modifie le Dockerfile pour l’utiliser :
FROM alpine:3.15 as builderWORKDIR /RUN mkdir -p /root/.config/pipCOPY Pipfile ./COPY pip.conf /root/.config/pip/RUN apk updateRUN apk add --no-cache bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev py3-pipRUN pip3 install ansible-lint==6.0.0
On clean le cache de docker et on relance :
docker builder prune -atime docker build -t test-build:0.1 .docker build -t test-build:0.1 . 0.02s user 0.03s system 0% cpu 44.692 total
On divise le temps par 6 le temps de build. Cool non ?
Ecriture du projet de construction des packages wheel
Voici donc le projet qui va se charger de remplir le cache Nexus. Je vais
utiliser la parallélisation des jobs dans le CI Gitlab avec matrix
. Je l’ai
documenté dans ce billet
Je vais utiliser deux variables qui vont construire le nom de l’image :
- VERSION : pour la version de python
- DISTRIBUTION : pour mixer les distributions.
Pourquoi plusieurs distributions ? Parce que pour chacun le nom des packages wheel propres à chacune.
stages: - build
build: tags: - myrunner image: python:${VERSION}-${DISTRIBUTION} stage: build script: - ./install-package.sh - cp pip.conf /etc - pip install setuptools setuptools_rust pip twine --upgrade - pip wheel -r requirements.txt - twine upload -u python-admin -p ${PASSWORD} --cert ${CERT} --repository-url https://artefacts.robert.local/repository/mypackages/ *.whl parallel: matrix: - DISTRIBUTION: alpine3.16 VERSION: ["3.10", "3.9"] - DISTRIBUTION: slim-buster VERSION: ["3.10", "3.9"]
J’injecte ma configuration pip.conf
pour charger les packages depuis Nexus.
Donc si les packages wheel sont déjà compilés, ils ne le seront pas à
nouveaux.
Pour installer les packages j’utilise un script qui en fonction de la distribution utilise les bonnes commandes :
!#/bin/shif type lsb_release >/dev/null 2>&1 ; then distro=$(lsb_release -i -s)elif [ -e /etc/os-release ] ; then distro=$(awk -F= '$1 == "ID" {print $2}' /etc/os-release)elif [ -e /etc/some-other-release-file ] ; then distro=$(ihavenfihowtohandleotherhypotheticalreleasefiles)fi
# convert to lowercasedistro=$(printf '%s\n' "$distro" | LC_ALL=C tr '[:upper:]' '[:lower:]')case "$distro" in debian*) apt update && apt install build-essential python3-dev rustc cargo libssl-dev libffi-dev -y;; alpine*) apk update && apk add bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev ;; *) echo "unknown distro: '$distro'" ; exit 1 ;;esac
Plus loin
On voit que désormais on va passer moins de temps à attendre la compilation des packages python, si on prend soin de stocker les binaires dans Nexus. Il ne faudra pas oublier de déclarer partout l’utilisation de ce repository pypi, par exemple dans le déploiement de rundeck.
On peut reprendre ce principe pour les autres langages et aussi pour stocker des packages apt, yum, …