Date Catégorie posts Étiquettes DevOps

CI (continous integration)

Depuis des années nous utilisons MkDOcs pour générer notre documentation utilisateur.

Deuis des années nous utilisions le fichier .gitlab-ci.yml suivant

# A partir de l'image ubuntu bionic https://hub.docker.com/_/ubuntu
image: ubuntu:bionic

# Avant toute chpse
before_script:
  # mettre à jour les repo
  - apt-get update
  # installer ssh-agent si cela est nécessaire https://linux.die.net/man/1/ssh-agent
  - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
  # ouvrir un shell de commandes sur la sortie standard
  - eval $(ssh-agent -s)
  # ajouter la clé privée stockée dans les secrets de la ci sur gitlab via https://gitlab.isima.fr/cri/doc-user/-/settings/ci_cd > expand > collapse
  - ssh-add <(echo "$SSH_PRIVATE_KEY")
  # créer un dossier .ssh dans le home directory de l'utilisateur courant
  - mkdir -p ~/.ssh
  # forcer l'acceptation des clés de l'ihôte distant
  - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'

# déclarer 2 étapes
stages:
  # une étape de construction de l'html
  - build
  # une étape de déploiement de l'html généré
  - deploy

# nommer l'étape de déploiement build_job
build_job:
  # associer build_job à l'étape de build décllarée plus haut
  stage: build
  script:
    # exporter dses variables d'environnement pour obtenir la bonne langue lors de la génération du html
    - export LC_ALL=C.UTF-8
    - export LANG=C.UTF-8
    # installer tout le nécessaire pour python et pip
    - apt-get install -y --force-yes build-essential python3-dev python3-pip
    # installer tous les paquets pip mentionnées dans le fichier requirements.txt de notre repo (notez que tout les fichier de notre repo sont accessibles depuis notre conteneur)
    - pip3 install -r requirements.txt
    # hum je crois que cette ligne ne servait à rien dans la mesure ou nous n'utilisaon pas d'envirionnement virtuel python ...
    - source bin/activate
    # avec mkdocs généré l'html statiques à partir des fichier markdown du répertoire docs de notre repo
    - mkdocs build
  # l'html est généré dans le répertoire ./site
  artifacts:
    # cette directive permet de conserver le répertoire généré pour l'étape suivante
    untracked: true

# nommer l'étape de déploiement deploy_job
deploy_job:
  # associer deploy_job à l'étape de build décllarée plus haut
  stage: deploy
  # rendre deploy_job dépendant de l'étape build_job i.e. deploy_job ne s'exécute que si build_job c'est exécuté sans erreur
  dependencies:
    - build_job
  script:
    # installer rsync
    - apt-get install -y --force-yes rsync
    # synchroniser les fichiers générés dans site/ avec les fichiers du serveur distant (username est également un secret de CI)
    - rsync -az -e ssh site/ $SSH_USERNAME@doc.isima.fr:/var/www/doc.isima.fr/web

Sans être très complexe, cette approche est coûteuse à différents points de vue:

  • télécharger une image système entier pour utiliser une commande est probablement surdimensionné
  • la mise à jour des repos est longue, et couteuse en requếtes réseaux
  • l'installation des différents paquets est couteuse en temps
  • la politique des gitlab runners étaient jusqu'à il y a peu de toujours télécharger l'image afin d'avoir la denière version. La politique de docker hub est de limité le nombre de téléchargement d'image (docker pull) à 200 par jour pour un utilisateur non authentifié ... or tous les giltab-runners sortent avec la même IP, donc toutes les CI git gitlab isima et gitlab limos sont décomptés pour un utilisateur

Tous ces coûts se répétent à chaque exécution de la CI, donc à chaque commit sur notre doc ... que nous mettons souvent à jour.

De plus l'utilisation du conteneur est circonscrite au seul contexte de la CI. Si nous pouvions utiliser docker également en local nous pourrions bénéficier d'une meilleure reproducitbilité, en garantissant les mêmes versions de toutes les dépendances. En effet s'il est possible de spécifier les versions des paquets pip dans requirements.txt rien ne nous permet, en l'état, de spécifier la version de python qui va exécuter mkdocs, ni même la version de pip qui va installer les paquets.

Vous pouvez parcourir la documentation officielle pour en savoir plus sur la CI Gitlab

Choisir son image

Si partir d'un système que l'on connaît n'est pas une mauvaise approche, c'est souvent une bonne idée de regarder ce qui existe déjà dans le monde docker sur https://hub.docker.com/.

Chaque image possède un namespace sou la forme <registry>/<image>/<name>.

Si <registry> n'est pas spécifié il s'agit implicitement du registry dockerhub.

Dans les candidats:

Lors de la sélection des images il est souvent instructif de chercher le repo github associé afin de voir ce que fait le Dockerfile utilisé pour builder l'image.

Il est également intéressant de regarder la pulse du projet: date de dernière mise à jour, combien de contributeurs sur le projet, fréquence des mises à jours, nombre d'issues ouvertes et fermées ...

Après avoir tirer toutes ces images on peut se faire une idée de leur taille respective avec un docker image ls:

REPOSITORY                         TAG        IMAGE ID       CREATED         SIZE
squidfunk/mkdocs-material          latest     05fb07a7cc1a   38 hours ago    99.6MB
python                             3.7-slim   3e12d0db6381   3 weeks ago     120MB
ubuntu                             focal      ba6acccedd29   5 weeks ago     72.8MB
ubuntu                             bionic     5a214d77f5d7   7 weeks ago     63.1MB
ubuntu                             xenial     b6f507652425   2 months ago    135MB

Construire un image

Pour construire une image il nous faut un fichier Dockerfile.

Docker met à disposition un ensemble de commandes utilisables dans les Dockerfile

Nous allons commencer par tester un build minimaliste avec ubuntu/focal, en utilisant la commande FROM qui permet de spécifier une image existante sur laquelle nous allons travailler

Contenu du Dockerfile

FROM ubuntu:focal

Construire l'image mkdocs à partir de Dockerfile

$ docker build -t mkdocs .
Sending build context to Docker daemon   1.45GB
Step 1/1 : FROM ubuntu:focal
 ---> ba6acccedd29
Successfully built ba6acccedd29
Successfully tagged mkdocs:latest

un docker image ls donne:

mkdocs                             latest     ba6acccedd29   5 weeks ago     72.8MB
ubuntu                             focal      ba6acccedd29   5 weeks ago     72.8MB

En vérité nous avons juste copier l'image ubuntu en une image nommée mkdocs. Notez qu'elle a automatiquement été taggé latest par docker

Exécuter des commandes à la construction d'une image

La commande RUN permet d'exécuter des commandes systèmes pendant la construction d'une image, c'est le bon endroit pour installer nos dépendances. Commençons par les dépendances python.

Contenu du Dockerfile

FROM ubuntu:focal
RUN apt-get update
RUN apt-get install -y --allow build-essential python3-dev python3-pip
$ docker build -t mkdocs .
Sending build context to Docker daemon   1.45GB
Step 1/3 : FROM ubuntu:focal
 ---> ba6acccedd29
Step 2/3 : RUN apt-get update
 ---> Running in 2ef776c9893a
Get:1 http://archive.ubuntu.com/ubuntu focal InRelease [265 kB]
..
Fetched 19.8 MB in 1s (16.2 MB/s)
Reading package lists...
Removing intermediate container 2ef776c9893a
 ---> 4f947ee6c4fd
Step 3/3 : RUN apt-get install -y --force-yes build-essential python3-dev python3-pip
 ---> Running in 2227f0637319
Reading package lists...
Building dependency tree...
Reading state information...
The following additional packages will be installed:
  binutils binutils-common binutils-x86-64-linux-gnu ca-certificates cpp cpp-9
  ...
Updating certificates in /etc/ssl/certs...
0 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
W: --force-yes is deprecated, use one of the options starting with --allow instead.
Removing intermediate container 2227f0637319
 ---> 6e3956e6d628
Successfully built 6e3956e6d628
Successfully tagged mkdocs:latest

Notez les différentes étapes (Step). Docker créée un "calque" (layer) à chaque commande du Dockerfile. Un calque peut être vu comme un snapshot du système associé à la commande exécutrée (ici RUN). Cette gestion a beaucoup d'avantages: - lors de la construction d'une image, seuls les calques impactés par la modification du Dockerfile seront mis à jour - lors du téléchargement d'une image (docker pull) seuls les calques à jour seront téléchargés - il est aussi possible d'écrire d'un calque dans un autre avec la commande COPY

un docker image ls donne:

mkdocs                             latest     6e3956e6d628   12 seconds ago   412MB
ubuntu                             focal      ba6acccedd29   5 weeks ago     72.8MB

Notre image à pris du poids! Nous verrons comment l'alléger un peu plus tard en jouant avec ces fameux calques

Exécuter un conteneur

Que peut on attendre de notre image mkdocs en l'état ... probablement pas grand chose.

Testons la commande docker run

$ docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

docker run mkdocs ne produit pas grand chose. En effet une image docker si elle n'exécute pas un service en avant plan, rend la main ... C'est pourquoi un docker ps n'affichera rien.

Alors que peut on faire avec cette image?

Observoncs le Dockerfile qui a servi à construire l'image de laquelle nous sommes partis

FROM scratch
ADD ubuntu-focal-oci-amd64-root.tar.gz /
CMD ["bash"]

1 - on apprend qu'il est possible de partir d'une image vierge avec FROM scratch. Rien de surprenant à retrouver cette commande pour une image officelle d'OS 2 - la commande CMD est la commande par défaut exécuter par notre container. il est donc possible de passer des paramètres à cette commande (ici bash) à la fin de la commande docker run.

Essayons

$ docker run mkdocs pip
Usage:   
  pip <command> [options]

Commands:
  install                     Install packages.
  download                    Download packages.
  uninstall                   Uninstall packages.
  freeze                      Output installed packages in requirements format.
  list                        List installed packages.
  show                        Show information about installed packages.
  check                       Verify installed packages have compatible dependencies.
  config                      Manage local and global configuration.
  search                      Search PyPI for packages.
  wheel                       Build wheels from your requirements.
  hash                        Compute hashes of package archives.
  completion                  A helper command used for command completion.
  debug                       Show information useful for debugging.
  help                        Show help for commands.

General Options:
  ...

Il semblerait que notre image a bien pip d'installer \o/

Reste à installer toutes nos dépendances, et à regrouper le tout afin de ne pas créer de calques inutiles.

FROM ubuntu:focal

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        openssh-client \
        python3-dev \
        python3-pip \
        rsync

COPY requirements.txt .

RUN pip install --user -r requirements.txt

notez la commande COPY qui permet de copier le fichier requirements.txt sur notre système, vers . du container.

Docker dispose également d'une commande ADD légèrement différente de COPY.

voir aussi: Docker Tip #2: The Difference between COPY and ADD in a Dockerfile

$ docker build -t mkdocs .

un docker image ls donne:

mkdocs                             latest     6e3956e6d628   12 seconds ago   482MB
ubuntu                             focal      ba6acccedd29   5 weeks ago     72.8MB

Sans surprise notre image a encore enflée.

Parcontre elle ne fait toujours pas grand chose, et se termine toujours aussitôt lancée

Lançons notre conteneur et passons lui un sleep pour le maintenir articiellement up pour 5 minutes

docker run mkdocs sleep 300

voyons voir si notre conteneur tourne

$ docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
5254e7534930   mkdocs    "sleep 300"   36 seconds ago   Up 34 seconds             practical_cori

Notez que docker a nommé aléatoirement ce conteneur en exécution.

Nous pouvons donner un nom avec la commande docker run --name mkdocs_container mkdocs sleep 300

$ docker ps
CONTAINER ID   IMAGE                                     COMMAND                  CREATED          STATUS          PORTS                    NAMES
b7f6126c4b13   mkdocs                                    "sleep 300"              9 seconds ago    Up 8 seconds                             mkdocs_container

Si vous relancez ce conteneur avec ce nom il vous faudra alors le supprimer et pour cela l'arrêter

$ docker stop mkdocs_container 
$ docker rm mkdocs_container 

Explorer un container en cours d'exécution

La commande docker exec permet d'exécuter des commandes sur un conteneur en cours d'exécution.

Lançons maintenant un shell interactif sur notre conteneur

$ docker exec -it mkdocs_container /bin/bash
root@781021be46e5:/# /root/.local/bin/mkdocs 
Usage: mkdocs [OPTIONS] COMMAND [ARGS]...

  MkDocs - Project documentation with Markdown.

Options:
  -V, --version  Show the version and exit.
  -q, --quiet    Silence warnings
  -v, --verbose  Enable verbose output
  -h, --help     Show this message and exit.

Commands:
  build      Build the MkDocs documentation
  gh-deploy  Deploy your documentation to GitHub Pages
  new        Create a new MkDocs project
  serve      Run the builtin development server
root@781021be46e5:/# /root/.local/bin/mkdocs  build

Config file '/mkdocs.yml' does not exist.

la commande mkdocs est disponible dans notre conteneur \o/

En revanche notre code source n'est pas disponible à l'intérieur du conteneur.

On pourrait le copier au moment du build avec la commade COPY mais cela risque d'allourdir notre image et surtout il faudrait reconstruire l'image à chaque modification de notre source.

Exposer un volume

Il est possible d'accéder à notre code source via un volume.

C'est la commande VOLUME qui va nous permettre d'exposer les fichiers présents sur notre système dans ..

Dans notre Dockerfile

FROM ubuntu:focal

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        openssh-client \
        python3-dev \
        python3-pip \
        rsync

COPY requirements.txt .

RUN pip install --user -r requirements.txt

WORKDIR /srv/mkdocs
RUN mkdir site
VOLUME ["/srv/mkdocs/"]

Ces 3 dernières lignes vont nous permettre de retrouver les fichiers présents sur le système dans . dans le répertoire /srv/mkdocs du conteneur.

Elles sont interprétées à l'exécution, elles ne créent pas de calques supplémentaires, donc il n'y a pas besoin de reconstruire l'image.

$ docker stop mkdocs_container 
$ docker rm mkdocs_container 
$ docker run --volume $(pwd):/srv/mkdocs --name mkdocs_container mkdocs sleep 300
$ docker exec -it mkdocs_container /bin/bash
root@cc24c9e0c0bf:/# cd /srv/
root@cc24c9e0c0bf:/srv# /root/.local/bin/mkdocs build
INFO    -  Cleaning site directory 
INFO    -  Building documentation to directory: /srv/site 
INFO    -  The following pages exist in the docs directory, but are not included in the "nav" configuration:
  - clouds/onedrive/presentation.md
  - lecteurs_reseaux/glouglou.md
  - mail/isima.md
  - mail/webmail.md
  - mail/sogo/99-add-mail-account.md
  - pedagogie/remote_access/linux.md
  - pedagogie/remote_access/windows.md
  - sauvegardes/windows.md
  - temp_doc/change_domain.md
  - visio/te50.md
  - vpn/connection.md
  - vpn/faq.md
  - vpn/installation.md 
INFO    -  Documentation built in 6.94 seconds 

On dirait bien qu'on a construit notre documentation \o/.

en listant notre répertoire courant

ls -al
total 76
drwxrwxr-x  8 mazenovi mazenovi 4096 nov.  10 11:46 .
drwxrwxr-x  5 mazenovi mazenovi 4096 sept.  2 17:16 ..
drwxrwxr-x  2 mazenovi mazenovi 4096 nov.   9 09:36 bin
drwxrwxr-x  6 mazenovi mazenovi 4096 oct.   6 10:40 custom_theme
-rw-rw-r--  1 mazenovi mazenovi  268 nov.  25 10:07 Dockerfile
-rw-rw-r--  1 mazenovi mazenovi  793 nov.  23 10:39 Dockerfile.prod
drwxrwxr-x 35 mazenovi mazenovi 4096 sept.  7 14:14 docs
drwxrwxr-x  9 mazenovi mazenovi 4096 nov.  10 11:46 .git
-rw-rw-r--  1 mazenovi mazenovi   50 août  30 13:13 .gitignore
-rw-rw-r--  1 mazenovi mazenovi  510 nov.  10 11:46 .gitlab-ci.yml
-rw-rw-r--  1 mazenovi mazenovi 1622 août  30 13:13 LICENCE.md
-rw-rw-r--  1 mazenovi mazenovi  747 nov.  10 10:12 Makefile
-rw-rw-r--  1 mazenovi mazenovi 5959 nov.   9 14:17 mkdocs.yml
-rw-rw-r--  1 mazenovi mazenovi  202 août  30 13:13 pyvenv.cfg
-rw-rw-r--  1 mazenovi mazenovi  766 nov.  10 10:13 README.md
-rw-rw-r--  1 mazenovi mazenovi  121 nov.   9 14:17 requirements.txt
drwxr-xr-x 41 root     root     4096 nov.  25 11:39 site
drwxrwxr-x  4 mazenovi mazenovi 4096 août  30 14:32 .venv

On s'aperçoit qu'un répertoire site contenant tout le html généré a été créé.

En fait nous avons créé ce répertoire vide à partir du docker file et la commande mkdocs build a écrit les fihgiers html générés ensuite.

Notez que le propriétaire du répertoire est root. Nous y reviendrons plus tard.

Exposer une commande

Docker dispose d'une command CMD qui permet au conteneur d'exposer directement une commande.

FROM ubuntu:focal

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        openssh-client \
        python3-dev \
        python3-pip \
        rsync

COPY requirements.txt .

RUN pip install --user -r requirements.txt

WORKDIR /srv/mkdocs
RUN mkdir site
VOLUME ["/srv/mkdocs/"]

CMD ["/root/.local/bin/mkdocs","build"]

on reconstruit l'image

$ docker build -t mkdocs .
$ docker stop mkdocs_container && docker rm mkdocs_container
mkdocs_container
mkdocs_container
$ docker run --volume $(pwd):/srv/mkdocs/ --name mkdocs_container mkdocs
INFO    -  Cleaning site directory 
INFO    -  Building documentation to directory: /srv/mkdocs/site 
INFO    -  The following pages exist in the docs directory, but are not included in the "nav" configuration:
  - clouds/onedrive/presentation.md
  - lecteurs_reseaux/glouglou.md
  - mail/isima.md
  - mail/webmail.md
  - mail/sogo/99-add-mail-account.md
  - pedagogie/remote_access/linux.md
  - pedagogie/remote_access/windows.md
  - sauvegardes/windows.md
  - temp_doc/change_domain.md
  - visio/te50.md
  - vpn/connection.md
  - vpn/faq.md
  - vpn/installation.md 
INFO    -  Documentation built in 7.21 seconds 

Nous avons construit une image docker capable de générés les pages statiques avec mkdocs à partir de nos source sans avoir mkdocs installé sur notre machine ;)

Exposer un port

Makefile

Choisir son registre

Choisir son image

Modifier la CI