Traitement des fichiers de configuration modulaires et dynamiques dans le shell

Tout en travaillant 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 bootstrapping d’un serveur CI / CD Jenkins dans OpenShift. En suivant les meilleures pratiques DevOps, j’ai rapidement créé un fichier de configuration qui a conduit un script pour terminer le travail. Cela est rapidement devenu deux fichiers de configuration lorsque j’ai réalisé que j’avais besoin d’un serveur Jenkins séparé pour la production. Après cela, il a été demandé au client d’avoir 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 changements inévitables devaient être apportés aux valeurs communes à deux ou plusieurs serveurs, il était très difficile et sujet aux erreurs de propager les changements sur deux ou quatre fichiers. À mesure que les 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é.
Au fur et à mesure que les modifications devenaient plus fréquentes 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é my-config-file.conf
(ou un *.ini
ou alors *.properties
) déposer:
KEY_1=value-1
KEY_2=value-2
Il vous suffit d’exécuter cette ligne en haut de votre script:
#!/usr/bin/bashset -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 tous automatiquement. 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 se complique, c’est dans les scénarios suivants:
- Certaines des 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, surtout lorsque les valeurs doivent être modifiées. Comment les valeurs du fichier peuvent-elles être réutilisées?
- Certaines 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 allez-vous au-delà du copier-coller et de la modularisation des données afin que certaines pièces puissent être réutilisées ailleurs?
- Une fois les fichiers modulaires, comment gérez-vous les conflits et définissez-vous 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é, quelle est la priorité? Comment une installation spécifique peut-elle remplacer une valeur partagée?
- 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 des 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 facile 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 vous montrera à quel point cela est facile à mettre en œuvre.
Définition du contenu du fichier de configuration
Le sourcing d’un fichier signifie qu’il va générer 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é 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 délibérément rendu cela un peu contre-intuitif en ce sens qu’il 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 pour gérer ce scénario.
Définition de la modularisation et de 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:
- Chaque fichier de la liste serait traité du premier au dernier (de gauche à droite)
- 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 le faire, mais j’ai trouvé cette stratégie simple, facile à coder et facile à 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 cela simplifie le débogage.
Compte tenu de cette liste délimitée par deux points de deux fichiers de configuration:
first.conf:second.conf
avec ces contenus:
# first.conf
KEY_1=value-1
KEY_1=ignored-value
# first.conf
KEY_1=ignored-value
vous attendez:
The solution
This function will implement the defined requirements:
_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:
- Il supprime les espaces blancs superflus de chaque ligne.
- Il parcourt chaque fichier et écrit chaque ligne avec une clé unique (c’est-à-dire grâce à
awk
magique, il saute les clés en double) vers un fichier de configuration intermédiaire. - Il source le fichier intermédiaire deux fois pour réaliser toutes les références en mémoire.
- Les valeurs référencées dans le fichier intermédiaire sont réalisées à partir des valeurs maintenant 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é est obtenu, il doit être effectué deux fois. Ceci afin que les valeurs référencées qui sont 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. Selon l’exigence 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 specific.conf
et shared.conf
fichiers montrant comment 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 délibérément choisi des valeurs d’exemple avec des espaces pour rendre les choses plus intéressantes et pour que les valeurs soient entre guillemets; sinon, lorsque les fichiers sont originaires, chaque mot serait interprété comme une commande distincte. Cependant, les références de variable 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 de code d’exemple:
# 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. Il 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 avoir une solution facile à mettre en œuvre pour créer et utiliser des fichiers de configuration modulaires dans le shell. En outre, les résultats du traitement des fichiers doivent être suffisamment faciles à utiliser ou à importer sans que l’autre système comprenne le format ou l’organisation d’origine des données.