1. Introduction à Viper
Comprendre la nécessité d'une solution de configuration dans les applications Go
Pour construire un logiciel fiable et maintenable, les développeurs doivent séparer la configuration de la logique de l'application. Cela vous permet d'ajuster le comportement de l'application sans changer le code source. Une solution de configuration permet cette séparation en facilitant l'externalisation des données de configuration.
Les applications Go peuvent grandement bénéficier d'un tel système, surtout à mesure qu'elles deviennent plus complexes et font face à différents environnements de déploiement tels que le développement, la mise en scène et la production. Chacun de ces environnements peut nécessiter différents paramètres pour les connexions de base de données, les clés API, les numéros de port, et plus encore. Le codage en dur de ces valeurs peut poser problème et augmenter les risques d'exposition de données sensibles, car il entraîne la maintenance de différents chemins de code pour différentes configurations et augmente le risque d'exposition de données sensibles.
Une solution de configuration comme Viper répond à ces préoccupations en fournissant une approche unifiée qui prend en charge divers besoins et formats de configuration.
Aperçu de Viper et de son rôle dans la gestion des configurations
Viper est une bibliothèque de configuration complète pour les applications Go, visant à être la solution de facto pour tous les besoins de configuration. Elle est conforme aux pratiques stipulées dans la méthodologie des Twelve-Factor App, qui encourage le stockage de la configuration dans l'environnement pour assurer la portabilité entre les environnements d'exécution.
Viper joue un rôle essentiel dans la gestion des configurations en :
- Lisant et désérialisant des fichiers de configuration dans divers formats tels que JSON, TOML, YAML, HCL, et plus encore.
- Remplaçant les valeurs de configuration par des variables d'environnement, adhérant ainsi au principe de la configuration externe.
- Liant et lisant à partir des indicateurs de ligne de commande pour permettre le réglage dynamique des options de configuration à l'exécution.
- Permettant de définir des valeurs par défaut dans l'application pour les options de configuration non fournies de manière externe.
- Surveillant les modifications dans les fichiers de configuration et permettant le rechargement en direct, offrant ainsi une flexibilité et réduisant les temps d'arrêt des changements de configuration.
2. Installation et Configuration
Installation de Viper en utilisant les modules Go
Pour ajouter Viper à votre projet Go, assurez-vous que votre projet utilise déjà les modules Go pour la gestion des dépendances. Si vous avez déjà un projet Go, vous avez probablement un fichier go.mod
à la racine de votre projet. Sinon, vous pouvez initialiser les modules Go en exécutant la commande suivante :
go mod init <nom-du-module>
Remplacez <nom-du-module>
par le nom ou le chemin de votre projet. Une fois que vous avez initialisé les modules Go dans votre projet, vous pouvez ajouter Viper en tant que dépendance :
go get github.com/spf13/viper
Cette commande téléchargera le package Viper et enregistrera sa version dans votre fichier go.mod
.
Initialisation de Viper dans un projet Go
Pour commencer à utiliser Viper dans votre projet Go, vous devez d'abord importer le package, puis créer une nouvelle instance de Viper ou utiliser le singleton prédéfini. Voici un exemple de comment faire les deux :
package main
import (
"fmt"
"github.com/spf13/viper"
)
func main() {
// Utilisation du singleton Viper, préconfiguré et prêt à l'emploi
viper.SetDefault("nomDuService", "Mon Service Génial")
// En alternative, création d'une nouvelle instance Viper
monViper := viper.New()
monViper.SetDefault("nomDuService", "Mon Nouveau Service")
// Accéder à une valeur de configuration en utilisant le singleton
nomService := viper.GetString("nomDuService")
fmt.Println("Le nom du service est :", nomService)
// Accéder à une valeur de configuration en utilisant la nouvelle instance
nouveauNomService := monViper.GetString("nomDuService")
fmt.Println("Le nouveau nom du service est :", nouveauNomService)
}
Dans le code ci-dessus, SetDefault
est utilisé pour définir une valeur par défaut pour une clé de configuration. La méthode GetString
récupère une valeur. Lorsque vous exécutez ce code, il affiche les deux noms de service que nous avons configurés à la fois à l'aide de l'instance singleton et de la nouvelle instance.
3. Lecture et Écriture de Fichiers de Configuration
Travailler avec des fichiers de configuration est une fonctionnalité clé de Viper. Cela permet à votre application d'externaliser sa configuration afin qu'elle puisse être mise à jour sans avoir besoin de recompiler le code. Ci-dessous, nous explorerons la configuration de divers formats de configuration et montrerons comment lire et écrire dans ces fichiers.
Configuration des formats de configuration (JSON, TOML, YAML, HCL, etc.)
Viper prend en charge plusieurs formats de configuration tels que JSON, TOML, YAML, HCL, etc. Pour commencer, vous devez définir le nom et le type du fichier de configuration que Viper doit rechercher :
v := viper.New()
v.SetConfigName("app") // Nom du fichier de configuration sans l'extension
v.SetConfigType("yaml") // ou "json", "toml", "yml", "hcl", etc.
// Chemins de recherche du fichier de configuration. Ajoutez plusieurs chemins si l'emplacement de votre fichier de configuration varie.
v.AddConfigPath("$HOME/.appconfig") // Emplacement de configuration utilisateur UNIX typique
v.AddConfigPath("/etc/appconfig/") // Chemin de configuration système UNIX
v.AddConfigPath(".") // Répertoire de travail
Lecture et écriture de fichiers de configuration
Une fois que l'instance Viper sait où chercher les fichiers de configuration et quoi chercher, vous pouvez lui demander de lire la configuration :
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Fichier de configuration non trouvé ; ignorer si nécessaire ou gérer autrement
log.Printf("Aucun fichier de configuration trouvé. Utilisation des valeurs par défaut et/ou des variables d'environnement.")
} else {
// Fichier de configuration trouvé mais une autre erreur s'est produite
log.Fatalf("Erreur lors de la lecture du fichier de configuration, %s", err)
}
}
Pour écrire les modifications dans le fichier de configuration, ou en créer un nouveau, Viper propose plusieurs méthodes. Voici comment écrire la configuration actuelle dans un fichier :
err := v.WriteConfig() // Écrit la configuration actuelle dans le chemin prédéfini défini par `v.SetConfigName` et `v.AddConfigPath`
if err != nil {
log.Fatalf("Erreur lors de l'écriture du fichier de configuration, %s", err)
}
Définition des valeurs de configuration par défaut
Les valeurs par défaut servent de valeurs de repli au cas où une clé n'aurait pas été définie dans le fichier de configuration ou par des variables d'environnement :
v.SetDefault("ContentDir", "content")
v.SetDefault("LogLevel", "debug")
v.SetDefault("Database.Port", 5432)
// Une structure de données plus complexe pour la valeur par défaut
viper.SetDefault("Taxonomies", map[string]string{
"tag": "tags",
"category": "categories",
})
4. Gestion des variables d'environnement et des indicateurs
Viper ne se limite pas seulement aux fichiers de configuration, il peut également gérer les variables d'environnement et les indicateurs de ligne de commande, ce qui est particulièrement utile lors de la gestion des paramètres spécifiques à l'environnement.
Liaison des variables d'environnement et des indicateurs à Viper
Liaison des variables d'environnement :
v.AutomaticEnv() // Recherche automatiquement les clés des variables d'environnement correspondant aux clés de Viper
v.SetEnvPrefix("APP") // Préfixe pour les variables d'environnement afin de les distinguer des autres
v.BindEnv("port") // Lie la variable d'environnement PORT (par exemple, APP_PORT)
// Vous pouvez également faire correspondre des variables d'environnement avec des noms différents aux clés de votre application
v.BindEnv("database_url", "DB_URL") // Cela indique à Viper d'utiliser la valeur de la variable d'environnement DB_URL pour la clé de configuration "database_url"
Liaison des indicateurs en utilisant pflag, un package Go pour l'analyse des indicateurs :
var port int
// Définir un indicateur en utilisant pflag
pflag.IntVarP(&port, "port", "p", 808, "Port pour l'application")
// Lier l'indicateur à une clé Viper
pflag.Parse()
if err := v.BindPFlag("port", pflag.Lookup("port")); err != nil {
log.Fatalf("Erreur de liaison de l'indicateur à la clé, %s", err)
}
Gestion des configurations spécifiques à l'environnement
Une application doit souvent fonctionner différemment dans différents environnements (développement, staging, production, etc.). Viper peut récupérer la configuration à partir des variables d'environnement qui peuvent remplacer les paramètres dans le fichier de configuration, permettant ainsi des configurations spécifiques à l'environnement :
v.SetConfigName("config") // Nom de fichier de configuration par défaut
// La configuration peut être remplacée par les variables d'environnement
// avec le préfixe APP et le reste de la clé en majuscules
v.SetEnvPrefix("APP")
v.AutomaticEnv()
// Dans un environnement de production, vous pouvez utiliser la variable d'environnement APP_PORT pour remplacer le port par défaut
fmt.Println(v.GetString("port")) // La sortie sera la valeur de APP_PORT si elle est définie, sinon la valeur du fichier de configuration ou par défaut
N'oubliez pas de gérer les différences entre les environnements au sein du code de votre application, si nécessaire, en fonction des configurations chargées par Viper.
5. Prise en charge du stockage à distance des clés/valeurs
Viper offre une prise en charge robuste pour la gestion de la configuration des applications à l'aide de stockages à distance des clés/valeurs tels que etcd, Consul ou Firestore. Cela permet de centraliser les configurations et de les mettre à jour dynamiquement dans des systèmes distribués. De plus, Viper permet une manipulation sécurisée des configurations sensibles grâce au chiffrement.
Intégration de Viper avec les stockages à distance des clés/valeurs (etcd, Consul, Firestore, etc.)
Pour commencer à utiliser Viper avec des stockages à distance des clés/valeurs, vous devez effectuer un import vide du package viper/remote
dans votre application Go :
import _ "github.com/spf13/viper/remote"
Examinons un exemple d'intégration avec etcd :
import (
"log"
"github.com/spf13/viper"
_ "github.com/spf13/viper/remote"
)
func initRemoteConfig() {
viper.SetConfigType("json") // Définir le type du fichier de configuration distant
viper.AddRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json")
err := viper.ReadRemoteConfig() // Tentative de lecture de la configuration à distance
if err != nil {
log.Fatalf("Échec de la lecture de la configuration à distance : %v", err)
}
log.Println("Lecture réussie de la configuration à distance")
}
func main() {
initRemoteConfig()
// Votre logique d'application ici
}
Dans cet exemple, Viper se connecte à un serveur etcd fonctionnant sur http://127...1:4001
et lit la configuration située à /config/myapp.json
. Lorsque vous travaillez avec d'autres stores comme Consul, remplacez "etcd"
par "consul"
et ajustez les paramètres spécifiques au fournisseur en conséquence.
Gestion des configurations chiffrées
Les configurations sensibles, telles que les clés API ou les informations d'identification de la base de données, ne doivent pas être stockées en texte brut. Viper permet de stocker des configurations chiffrées dans un stockage clé/valeur et de les décrypter dans votre application.
Pour utiliser cette fonctionnalité, assurez-vous que les paramètres chiffrés sont stockés dans votre stockage clé/valeur. Ensuite, tirez parti de AddSecureRemoteProvider
de Viper. Voici un exemple d'utilisation avec etcd :
import (
"log"
"github.com/spf13/viper"
_ "github.com/spf13/viper/remote"
)
func initSecureRemoteConfig() {
const secretKeyring = "/chemin/vers/fichier/clef/secrete.gpg" // Chemin vers votre fichier d'anneau de clés
viper.SetConfigType("json")
viper.AddSecureRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json", secretKeyring)
err := viper.ReadRemoteConfig()
if err != nil {
log.Fatalf("Impossible de lire la configuration à distance : %v", err)
}
log.Println("Lecture réussie et décryptage de la configuration à distance")
}
func main() {
initSecureRemoteConfig()
// Votre logique d'application ici
}
Dans l'exemple ci-dessus, AddSecureRemoteProvider
est utilisé, en spécifiant le chemin vers un trousseau GPG contenant les clés nécessaires pour le décryptage.
6. Surveillance et gestion des changements de configuration
Une des fonctionnalités puissantes de Viper est sa capacité à surveiller et à répondre aux changements de configuration en temps réel, sans redémarrer l'application.
Surveillance des changements de configuration et rélecture des configurations
Viper utilise le package fsnotify
pour surveiller les changements apportés à votre fichier de configuration. Vous pouvez configurer un observateur pour déclencher des événements à chaque modification du fichier de configuration :
import (
"log"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
func watchConfig() {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Printf("Fichier de configuration modifié : %s", e.Name)
// Ici, vous pouvez lire la configuration mise à jour si nécessaire
// Effectuez toute action telle que la réinitialisation des services ou la mise à jour des variables
})
}
func main() {
viper.SetConfigName("monapp")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
log.Fatalf("Erreur lors de la lecture du fichier de configuration, %s", err)
}
watchConfig()
// Votre logique d'application ici
}
Déclencheurs de mise à jour des configurations dans une application en cours d'exécution
Dans une application en cours d'exécution, vous pouvez vouloir mettre à jour les configurations en réponse à divers déclencheurs, tels qu'un signal, une tâche basée sur le temps, ou une demande d'API. Vous pouvez structurer votre application pour rafraîchir son état interne en fonction de la relecture des configurations de Viper :
import (
"os"
"os/signal"
"syscall"
"time"
"log"
"github.com/spf13/viper"
)
func setupSignalHandler() {
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGHUP) // Écoute du signal SIGHUP
go func() {
for {
sig := <-signalChannel
if sig == syscall.SIGHUP {
log.Println("Signal SIGHUP reçu. Rechargement de la configuration...")
err := viper.ReadInConfig() // Relecture de la configuration
if err != nil {
log.Printf("Erreur lors de la relecture de la configuration : %s", err)
} else {
log.Println("Configuration rechargée avec succès.")
// Reconfigurez votre application en fonction de la nouvelle configuration ici
}
}
}
}()
}
func main() {
viper.SetConfigName("myapp")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
log.Fatalf("Erreur lors de la lecture du fichier de configuration, %s", err)
}
setupSignalHandler()
for {
// Logique principale de l'application
time.Sleep(10 * time.Second) // Simuler un certain travail
}
}
Dans cet exemple, nous configurons un gestionnaire pour écouter le signal SIGHUP
. Lorsqu'il est reçu, Viper recharge le fichier de configuration et l'application devrait alors mettre à jour ses configurations ou son état si nécessaire.
N'oubliez pas de toujours tester ces configurations pour vous assurer que votre application peut gérer les mises à jour dynamiques de manière élégante.