Le blog d'Archiloque

Fichiers de configuration

Vous écrivez un programme, et vous décidez de mettre en place un système basé sur un fichier de configuration pour le paramétrer.

Si vous ajoutez des fonctionnalités à votre programme, la configuration va se complexifier peu à peu, mais partons du début.

Le début

La première étape est en général de définir une série de clés-valeurs, pour remplacer une trop longue série de variables d’environnement ou de paramètres en ligne de commandes.

property_1=value_1
property_2=value_2

Jusque là tout va bien.

Il peut être nécessaire de pouvoir utiliser des = dans les valeurs, ce qui signifie permettre des échappements.

property_1="val=ue1"
property_2="va=lu\"e_2"

Ensuite, on peut vouloir que certaines valeurs ne soient pas seulement des chaînes de caractères, mais aussi parfois des listes ou des dictionnaires (c’est-à-dire que les valeurs puissent elles-mêmes contenir un ensemble de clés-valeurs).

property_1=["val=ue1", "val=ue11"]
property_2={"va=lu\"e_2": "a", "va=lu\"e_22": "b"}

Quand le nombre d’entrées devient trop grand, et pour pouvoir réutiliser les noms de clés, on peut vouloir organiser le contenu du fichier en définissant des sections.

[section 1]
property_1=["val=ue1", "val=ue11"]
property_2=value3

[section 2]
property_1=🐰
property_2={"va=lu\"e_2": "a", "va=lu\"e_22": "b"}

La limite sans fichier de configuration

On a ici dépassé ce qu’on peut faire confortablement avec des paramètres ou des variables d’environnements. Il reste possible de passer des listes dans des chaînes de caractères et d’utiliser des préfixes pour simuler les sections, le résultat est moins lisible, surtout s’il y a de nombreuses entrées.

La factorisation ou la compilation pour les fichiers de configuration

Quand le fichier de configuration s’allonge et que le contenu se répète, le réflexe quand on travaille dans le développement est de vouloir faire de la factorisation. Par exemple en attribuant un nom à un certain contenu, et en utilisant ce nom pour inclure ce contenu ailleurs dans le fichier :

&🐱=nya

property_1=*🐱
property_2=*🐱

Dans ce cas le contenu que vous éditez n’est plus directement celui qui sera utilisé par le programme car le fichier va subir un traitement qui va modifier son contenu. Cela ressemble grosso-modo à une phase de compilation (ou de transpilation).

Pour certaines investigation, il peut être bien pratique d’avoir accès au contenu traité tel qu’il sera visible depuis le programme.

Dans l’exemple ci-dessus, cela signifie être en mesure d’obtenir ce contenu :

property_1=nya
property_2=nya

Si votre fichier de configuration utilise une syntaxe normalisée, les personnes éditant le fichier peuvent s’appuyer sur des outils existants pour générer eux-même le résultat.

Dans le cas contraire c’est à vous de le fournir.

La génération de fichiers

Si votre format de fichier de configuration ne permet pas ce genre de factorisation, il est possible que les personnes se retrouvent à répondre ce type de besoin en générant leurs fichiers de configuration à l’aide d’un autre outil.

Mais en fait, même si votre format de configuration fournit des fonctionnalités avancées de factorisation, il est assez probable que des personnes vont vouloir générer ces fichiers à partir d’un autre outil.

Pour un outil utilisé par des serveurs, cela peut être pour industrialiser le déploiement, mais le besoin se fera probablement sentir même pour un outil réservés aux postes de travail.

Cela signifie que plus votre format de fichier sera facile à générer, plus vous simplifierez ces cas d’utilisation.

Dans mon expérience, deux choses sont à prendre en compte pour cela :

  1. le fait d’avoir une syntaxe régulière avec un nombre d’exceptions limitées ;

  2. le fait d’utiliser des règles d’échappement standard.

La deuxième règle étant bien plus importante que la première car implémenter une gestion des échappements demande du code assez minutieux et bien testé, c’est-à-dire quelque chose qu’on préfère en général éviter quand on le peut.

La factorisation : nécessaire ou pas alors ?

Si proposer une capacité de factorisation dans une syntaxe de configuration est utile, je suis partagé sur le fait de savoir si le jeu en vaut la chandelle à cause de la complexité que cela ajoute.

Je suis probablement partial car écrire des scripts pour générer des fichiers est facile pour moi, mais d’un autre côté je soupçonne que beaucoup des personnes qui utilisent la factorisation seraient aussi en mesure de générer les fichiers.

Les comportements dynamiques

La dernière étape dans la complexité d’un fichier de configuration est d’avoir des comportements dynamiques, c’est à dire qui dépendent de l’exécution du programme.

Par exemple avoir des conditions :

if animal == cat
then nya

Par rapport à la factorisation, les comportements dynamiques ne sont pas une optimisation d’écriture des fichiers mais permettent des comportements qui sinon sont impossibles.

Si on peut continuer à voir cela comme un fichier de configuration, le comportement s’apparente véritablement à du code.

De même que la factorisation rend nécessaire d’avoir accès à une version développée du fichier, avoir un comportement dynamique rend nécessaire4 de pouvoir observer d’une manière ou d’une autre le fonctionnement du logiciel (par exemple via des logs détaillés), et même idéalement de pouvoir le débugger.

Il y a quelques années embarquer un intepréteur dans un programme exécutable n’était pas chose facile. Dans ces conditions, développer son propre format ou étendre un format de existant pour cela avait du sens.

Par exemple avec ce genre de choses :

<if>
    <equals arg1="${condition}" arg2="true"/>
    <then>
        <copy file="${some.dir}/file" todir="${another.dir}"/>
    </then>
    <elseif>
        <equals arg1="${condition}" arg2="false"/>
        <then>
            <copy file="${some.dir}/differentFile" todir="${another.dir}"/>
        </then>
    </elseif>
    <else>
        <echo message="Condition was neither true nor false"/>
    </else>
</if>

Mais de nos jours, embarquer un interpréteur comme Lua est relativement facile et assez commun.

Ce qui signifie que les personnes peuvent utiliser leurs outils de développement pour écrire leurs fichiers de configuration et peuvent même les débuger en cas de besoin.

Certes passer de fichier de configuration à des fichiers de code représente un changement qu’on n’imaginait pas forcément faire. Mais de fait quand un fichier de configuration contient du code déguisé en configuration, il est déjà un fichier de code sans oser l’assumer, et sans permettre d’utiliser les outils prévu pour ça.

Ma suggestion est donc de sauter le pas.

Si vous pensez que ce conseil est inutile car cela n’arrive plus (et notamment par ce que seules des personnes faisant du XML et du Java pourraient avoir ce genre d’idées), malheureusement c’est toujours le cas, seulement avec du YAML au lieu de XML.

tasks:
  - name: Shut down CentOS 6 and Debian 7 systems
    ansible.builtin.command: /sbin/shutdown -t now
    when: (ansible_facts['distrib'] == "CentOS" and ansible_facts['distrib_mv'] == "6") or
          (ansible_facts['distrib'] == "Debian" and ansible_facts['distrib_mv'] == "7")

En résumé

  • Dès qu’un fichier de configuration passe par une phase de transformation, il faut pouvoir observer ce qui se passe.

  • Si vous inventez votre syntaxe, pensez bien aux personnes qui voudront générer des fichiers.

  • Lorsque la configuration d’un programme nécessite des comportements dynamiques, utilisez un langage de programmation.