Recherche de site Web

Traitement des fichiers de configuration modulaires et dynamiques en shell


Apprenez à mieux gérer les modifications fréquentes dans les fichiers de configuration.

Alors que je travaillais sur une solution d'intégration continue/développement continu (CI/CD) pour un client, l'une de mes premières tâches a été d'automatiser le bootstrap d'un serveur CI/CD Jenkins dans OpenShift. Suivant les meilleures pratiques DevOps, j'ai rapidement créé un fichier de configuration qui pilotait un script pour terminer le travail. Cela s'est rapidement transformé en deux fichiers de configuration lorsque j'ai réalisé que j'avais besoin d'un serveur Jenkins distinct pour la production. Ensuite est venue la demande selon laquelle le client avait besoin de plus d'une paire de serveurs CI/CD d'ingénierie et de production pour différents groupes, et chaque serveur avait des configurations similaires mais légèrement différentes.

Lorsque les modifications inévitables devaient être apportées aux valeurs communes à deux ou plusieurs serveurs, il était très difficile et sujet aux erreurs de propager les modifications sur deux ou quatre fichiers. À mesure que des environnements CI/CD ont été ajoutés pour des tests et des déploiements plus complexes, le nombre de valeurs partagées et spécifiques pour chaque groupe et environnement a augmenté.

À mesure que les changements devenaient plus fréquents et les données plus complexes, apporter des modifications dans les fichiers de configuration devenait de plus en plus ingérable. J'avais besoin d'une meilleure solution pour résoudre ce problème séculaire et gérer les changements plus rapidement et de manière plus fiable. Plus important encore, j'avais besoin d'une solution qui permettrait à mes clients de faire de même après leur avoir confié mon travail terminé.

Définir le problème

À première vue, cela semble être un problème très simple. Étant donné le fichier my-config-file.conf (ou un *.ini ou *.properties) :

KEY_1=value-1
KEY_2=value-2

Il vous suffit d'exécuter cette ligne en haut de votre script :

#!/usr/bin/bash

set -o allexport
source my-config-file.conf
set +o allexport

Ce code réalise toutes les variables de votre fichier de configuration dans l'environnement, et set -o allexport les exporte automatiquement toutes. Le fichier d'origine, étant un fichier de propriétés clé/valeur typique, est également très standard et facile à analyser dans un autre système. Là où cela devient plus compliqué, c’est dans les scénarios suivants :

  1. Certaines valeurs sont copiées et collées d'une variable à l'autre et sont liées. En plus de violer le principe DRY (« ne vous répétez pas »), il est sujet aux erreurs, en particulier lorsque les valeurs doivent être modifiées. . Comment les valeurs du fichier peuvent-elles être réutilisées ?
  2. Des parties du fichier de configuration sont réutilisables sur plusieurs exécutions du script d'origine, et d'autres ne sont utiles que pour une exécution spécifique. Comment aller au-delà du copier-coller et modulariser les données afin que certaines parties puissent être réutilisé ailleurs ?
  3. Une fois les fichiers modularisés, comment gérer les conflits et définir les précédents ? Si une clé est définie deux fois dans le même fichier, quelle valeur prenez-vous ? Si deux fichiers de configuration définissent la même clé, lequel a la priorité ? Comment une installation spécifique peut-elle remplacer une valeur partagée ?
  4. Les fichiers de configuration sont initialement destinés à être utilisés par un script shell et sont écrits pour être traités par des scripts shell. Si les fichiers de configuration doivent être chargés ou réutilisés dans un autre environnement, existe-t-il un moyen de les rendre facilement accessibles à d'autres systèmes sans traitement supplémentaire ? Je voulais déplacer certaines paires clé/valeur dans un seul ConfigMap dans Kubernetes. Quelle est la meilleure façon de rendre les données traitées disponibles pour rendre le processus d'importation simple et direct afin que les autres systèmes n'aient pas à comprendre comment les fichiers de configuration sont structurés ?

Cet article vous présentera quelques extraits de code simples et montrera à quel point cela est facile à mettre en œuvre.

Définir le contenu du fichier de configuration

La recherche d'un fichier signifie qu'il générera des variables ainsi que d'autres instructions shell telles que des commandes. À cette fin, les fichiers de configuration ne doivent concerner que les paires clé/valeur et non la définition de fonctions ou l'exécution de code. Par conséquent, je définirai ces fichiers de la même manière que les fichiers de propriétés et .ini :

KEY_1=${KEY_2}
KEY_2=value-2
...
KEY_N=value-n

À partir de ce fichier, vous devez vous attendre au comportement suivant :

$ source my-config-file.conf
$ echo $KEY_1
value-2

J'ai volontairement rendu cela un peu contre-intuitif dans la mesure où cela fait référence à une valeur que je n'ai même pas encore définie. Plus loin dans cet article, je vais vous montrer le code permettant de gérer ce scénario.

Définir la modularisation et la priorité

Pour garder le code simple et rendre la définition des fichiers intuitive, j'ai implémenté une stratégie de priorité de gauche à droite et de haut en bas pour les fichiers et les variables, respectivement. Plus précisément, étant donné une liste de fichiers de configuration :

  1. Chaque fichier de la liste serait traité du premier au dernier (de gauche à droite)
  2. La première définition d'une clé définirait la valeur et les valeurs suivantes seraient ignorées

Il existe de nombreuses façons de procéder, mais j'ai trouvé cette stratégie simple, facile à coder et à expliquer aux autres. En d’autres termes, je ne prétends pas que c’est la meilleure décision de conception, mais cela fonctionne et simplifie le débogage.

Étant donné cette liste de deux fichiers de configuration délimités par deux points :

first.conf:second.conf

avec ce contenu :

# first.conf 
KEY_1=value-1
KEY_1=ignored-value
# first.conf 
KEY_1=ignored-value

vous vous attendriez à :

$ echo $KEY_1
value-1

La solution

Cette fonction mettra en œuvre les exigences définies :

_create_final_configuration_file() {
    # convert the list of files into an array
    local CONFIG_FILE_LIST=($(echo ${1} | tr ':' ' '))
    local WORKING_DIR=${2}

    # removes any trailing whitespace from each file, if any
    # this is absolutely required when importing into ConfigMaps
    # put quotes around values if extra spaces are necessary
    sed -i -e 's/\s*$//' -e '/^$/d' -e '/^#.*$/d' ${CONFIG_FILE_LIST[@]}

    # iterates over each file and prints (default awk behavior)
    # each unique line; only takes first value and ignores duplicates
    awk -F= '!line[$1]++' ${CONFIG_FILE_LIST[@]} > ${COMBINED_CONFIG_FILE}

    # have to export everything, and source it twice:
    # 1) first source is to realize variables
    # 2) second time is to realize references
    set -o allexport
    source ${COMBINED_CONFIG_FILE}
    source ${COMBINED_CONFIG_FILE}
    set +o allexport

    # use envsubst command to realize value references
    cat ${COMBINED_CONFIG_FILE} | envsubst > ${FINAL_CONFIG_FILE}

Il effectue les étapes suivantes :

  1. Il supprime les espaces blancs superflus de chaque ligne.
  2. Il parcourt chaque fichier et écrit chaque ligne avec une clé unique (c'est-à-dire grâce à la magie awk, il ignore les clés en double) dans un fichier de configuration intermédiaire.
  3. Il source le fichier intermédiaire deux fois pour réaliser toutes les références en mémoire.
  4. Les valeurs référencées dans le fichier intermédiaire sont réalisées à partir des valeurs actuellement en mémoire et écrites dans un fichier de configuration final, qui peut être utilisé pour un traitement ultérieur.

Comme indiqué ci-dessus, lorsque le fichier intermédiaire de configuration combinée est obtenu, cela doit être fait deux fois. Ceci afin que les valeurs référencées définies après avoir été référencées puissent être correctement réalisées en mémoire. Le envsubst remplace les valeurs des variables d'environnement et la sortie est redirigée vers le fichier de configuration final pour un éventuel post-traitement. Conformément aux exigences de l'exemple précédent, cela peut prendre la forme de la réalisation des données dans un ConfigMap :

kubectl create cm my-config-map --from-env-file=${FINAL_CONFIG_FILE} \
    -n my-namespace

Exemple de code

Vous pouvez trouver un exemple de code avec les fichiers special.conf et shared.conf démontrant comment vous pouvez combiner des fichiers représentant un fichier de configuration spécifique et un fichier de configuration général partagé dans mon référentiel GitHub. exemple de fichier de configuration modulaire. Les fichiers de configuration sont composés de :

# specific.conf
KEY_1=${KEY_2}
KEY_2='some value'
KEY_1='this value will be ignored'
# shared.conf
SHARED_KEY_1='some shared value'
SHARED_KEY_2=${SHARED_KEY_1}
SHARED_KEY_1='this value will never see the light of day'
KEY_1='this was overridden'

Notez les guillemets simples autour des valeurs. J'ai volontairement choisi des exemples de valeurs avec des espaces pour rendre les choses plus intéressantes et pour que les valeurs soient entre guillemets ; sinon, lors de la recherche des fichiers, chaque mot serait interprété comme une commande distincte. Cependant, les références aux variables n'ont pas besoin d'être entre guillemets une fois les valeurs définies.

Le référentiel contient un petit utilitaire de script shell, pconfs.sh. Voici ce qui se passe lorsque vous exécutez la commande suivante à partir du répertoire d'exemple de code :

# NOTE: see the sample code for the full set of command line options
$ ./pconfs.sh -f specific.conf:shared.conf

================== COMBINED CONFIGS BEFORE =================
KEY_1=${KEY_2}
KEY_2='some value'
SHARED_KEY_1='some shared value'
SHARED_KEY_2=${SHARED_KEY_1}
================ COMBINED CONFIGS BEFORE END ===============

================= PROOF OF SUBST IN MEMORY =================
KEY_1: some value
SHARED_KEY_2: some shared value
=============== PROOF OF SUBST IN MEMORY END ===============

================== PROOF OF SUBST IN FILE ==================
KEY_1=some value
KEY_2='some value'
SHARED_KEY_1='some shared value'
SHARED_KEY_2=some shared value
================ PROOF OF SUBST IN FILE END ================

Cela prouve que même des valeurs complexes peuvent être référencées avant et après la définition d'une valeur. Cela montre également que seule la première définition de la valeur est conservée, que ce soit dans ou entre les fichiers, et que la priorité est donnée de gauche à droite dans votre liste de fichiers. C'est pourquoi je spécifie d'abord l'analyse de Specific.conf lors de l'exécution de cette commande ; cela permet à une configuration spécifique de remplacer l'une des valeurs partagées les plus générales de l'exemple.

Vous devriez maintenant disposer d'une solution facile à implémenter pour créer et utiliser des fichiers de configuration modulaires dans le shell. De plus, les résultats du traitement des fichiers doivent être suffisamment faciles à utiliser ou à importer sans que l'autre système doive comprendre le format ou l'organisation d'origine des données.

Articles connexes: