Aller au contenu principal

Optimisation des temps de build

· 6 minutes de lecture
Stéphane ROBERT
Consultant DevOps

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 builder
WORKDIR /
COPY Pipfile ./
RUN apk update
RUN apk add --no-cache bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev py3-pip
RUN pip3 install ansible-lint==6.0.0

Allez on build en prenant soin de purger le cache de docker :

docker system prune -a
time 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.1
RUN apk update && apk add bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev py3-pip
RUN pip3 install setuptools setuptools_rust pip wheel twine pipenv --upgrade
WORKDIR /src
COPY requirements.txt .
RUN pip3 wheel -r requirements.txt
RUN mkdir /crt
COPY 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.0
ansible-lint==6.0.0
ansible-builder==1.0.1
ansible-runner==2.1.2
ansible-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/pypi
index-url = https://python-read:passwd@artefacts.robert.local/repository/pypi-all/simple/
timeout = 10
trusted-host = artefacts.robert.local

On modifie le Dockerfile pour l'utiliser :

FROM alpine:3.15 as builder
WORKDIR /
RUN mkdir -p /root/.config/pip
COPY Pipfile ./
COPY pip.conf /root/.config/pip/
RUN apk update
RUN apk add --no-cache bc cargo gcc libffi-dev musl-dev openssl-dev rust python3-dev py3-pip
RUN pip3 install ansible-lint==6.0.0

On clean le cache de docker et on relance :

docker builder prune -a
time 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/sh
if 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 lowercase
distro=$(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, ...