Principes fondamentaux de la configuration applicative
Dans le développement d'applications, notamment celles qui interagissent avec des services tiers comme les bases de données (MySQL, PostgreSQL) ou les systèmes de cache (Redis), la gestion des paramètres de configuration est primordiale. L'intégration de ces paramètres directement dans le code source (c'est-à-dire le codage en dur) est une pratique à éviter. Cette approche présente plusieurs inconvénients majeurs :
- Absence d'isolation environnementale : Les applications nécessitent souvent des configurations différentes pour les environnements de développement, de test et de production. Le codage en dur rend cette adaptation fastidieuse.
- Manque de centralisation : Les informations de configuration éparpillées dans le code sont difficiles à gérer et à mettre à jour.
- Couplage étroit avec le code : Toute modification de configuration exige une recompilation et un redéploiement de l'application, ce qui réduit la flexibilité.
Pour pallier ces problèmes, l'adoption d'un module de gestion de configuration est essentielle. C'est précisément la raison pour laquelle la bibliothèque Viper est largement utilisée dans les projets Go.
Sources et hiérarchie des configurations
Les paramètres de configuration peuvent provenir de diverses sources. Une gestion efficace implique de définir une hiérarchie claire pour résoudre les conflits lorsque des paramètres identiques sont définis à plusieurs endroits. Voici les sources courantes de configuration :
- Arguments de ligne de commande : Ces paramètres sont généralement fournis au démarrage de l'appliaction, par exemple
--port=8080. - Variables d'environnement : Des variables définies au niveau du système d'exploitation ou du conteneur, telles que
DATABASE_URL. - Fichiers de configuration locaux : Des fichiers structurés (YAML, JSON, TOML, INI) stockés sur le système de fichiers, comme
config.yaml. - Centres de configuration distants : Des services dédiés (par exemple, Consul, etcd, Apache ZooKeeper, Nacos) qui fournissent des configurations de manière dynamique aux applications distribuées.
Lorsqu'un même paramètre est défini par plusieurs sources, une hiérarchie de priorité est appliquée pour déterminer la valeur finale. Une hiérarchie courante est la suivante (du plus prioritaire au moins prioritaire) : Arguments de ligne de commande > Fichiers de configuration locaux > Variables d'environnement > Centre de configuration distant. Il est important de noter que cette hiérarchie peut être ajustée selon les conventions spécifiques de chaque organisation.
Introduction à Viper
Viper est une solution de gestion de configuration complète pour les applications Go. Elle offre une flexibilité remarquable pour lire, gérer et surveiller les paramètres de configuration provenant de diverses sources. Viper permet aux développeurs de définir des configurations par défaut, de lire des fichiers de configuration, des variables d'environnement, des drapeaux de ligne de commande et même des systèmes de configuration distants, tout en gérant les priorités entre ces sources.
Prise en main rapide avec Viper
Lecture de fichiers de configuration locaux
Commençons par configurer et lire un fichier YAML local. Créez un fichier nommé app-settings.yml dans un répertoire config/ avec le contenu suivant :
app:
nom: ServiceAPI
version: 1.0.0
database:
driver: mysql
chaine_connexion: utilisateur:motdepasse@tcp(localhost:3306)/ma_base?charset=utf8mb4&parseTime=True&loc=Local
cache:
type: redis
adresse: localhost:6379
port: 6379
Voici comment charger et lire ces paramètres avec Viper :
package main
import (
"fmt"
"log"
"github.com/spf13/viper"
)
// AppSettings représente la structure de configuration de l'application
type AppSettings struct {
App Application `mapstructure:"app"`
Database DBConfig `mapstructure:"database"`
Cache CacheConfig `mapstructure:"cache"`
}
// Application représente les paramètres généraux de l'application
type Application struct {
Name string `mapstructure:"nom"`
Version string `mapstructure:"version"`
}
// DBConfig représente les paramètres de la base de données
type DBConfig struct {
Driver string `mapstructure:"driver"`
ConnectionString string `mapstructure:"chaine_connexion"`
}
// CacheConfig représente les paramètres du cache
type CacheConfig struct {
Type string `mapstructure:"type"`
Address string `mapstructure:"adresse"`
Port int `mapstructure:"port"`
}
func init() {
// 1. Définir le chemin de recherche du fichier de configuration
viper.AddConfigPath("./config") // Cherche dans le répertoire 'config'
// 2. Définir le nom du fichier de configuration (sans l'extension)
viper.SetConfigName("app-settings")
// 3. Définir le type de fichier de configuration
viper.SetConfigType("yaml")
// Optionnel : Définir des valeurs par défaut
viper.SetDefault("app.nom", "DefaultApp")
viper.SetDefault("cache.port", 6379)
// 4. Lire le fichier de configuration
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Erreur lors de la lecture du fichier de configuration : %s", err)
}
fmt.Println("Configuration locale chargée avec succès.")
}
func main() {
// Récupérer des valeurs spécifiques
appName := viper.GetString("app.nom")
dbConnStr := viper.GetString("database.chaine_connexion")
cacheAddr := viper.GetString("cache.adresse")
fmt.Printf("Nom de l'application: %s\n", appName)
fmt.Printf("Chaîne de connexion DB: %s\n", dbConnStr)
fmt.Printf("Adresse du cache: %s\n", cacheAddr)
// Désérialiser l'intégralité de la configuration dans une structure Go
var settings AppSettings
if err := viper.Unmarshal(&settings); err != nil {
log.Fatalf("Impossible de désérialiser la configuration : %s", err)
}
fmt.Printf("Configuration complète désérialisée : %+v\n", settings)
}
L'exécution de ce programme affichera les valeurs lues depuis app-settings.yml. La fonction Unmarshal est particulièrement utile pour mapper la configuration vers des structures Go typées.
Intégration des paramètres de ligne de commande
Viper s'intègre parfaitement avec les drapeaux de ligne de commande, souvent gérés par le package pflag. Cela permet de surcharger dynamiquement des paramètres de configuration. Par exemple, nous pouvons permettre à l'utilisateur de spécifier un chemin de fichier de configuration différent via un argument.
package main
import (
"fmt"
"log"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// ... (Les structures AppSettings, Application, DBConfig, CacheConfig restent les mêmes) ...
func chargerConfigLigneCommande() {
// Définir un drapeau de ligne de commande pour le chemin du fichier de configuration
configPath := pflag.String("config", "", "Chemin complet vers le fichier de configuration (ex: ./config/prod-settings.yml)")
// Analyser les drapeaux de ligne de commande
pflag.Parse()
if *configPath != "" {
// Si le drapeau --config est fourni, l'utiliser pour définir le fichier
viper.SetConfigFile(*configPath)
fmt.Printf("Utilisation du fichier de configuration spécifié : %s\n", *configPath)
} else {
// Sinon, utiliser la configuration par défaut (par exemple, locale)
viper.AddConfigPath("./config")
viper.SetConfigName("app-settings")
viper.SetConfigType("yaml")
fmt.Println("Utilisation de la configuration locale par défaut (app-settings.yml).")
}
// Lire la configuration
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Erreur lors de la lecture du fichier de configuration : %s", err)
}
fmt.Println("Configuration chargée via ligne de commande ou fichier par défaut.")
}
func main() {
chargerConfigLigneCommande()
// Accéder aux paramètres comme d'habitude
dbDriver := viper.GetString("database.driver")
cachePort := viper.GetInt("cache.port")
fmt.Printf("Pilote de la base de données : %s\n", dbDriver)
fmt.Printf("Port du cache : %d\n", cachePort)
}
Pour tester cette fonctionnalité, compilez votre application et exécutez-la avec l'argument --config :
go run main.go --config ./config/prod-settings.yml
Où prod-settings.yml pourrait être un autre fichier de configuration pour la production.
Utilisation d'un centre de configuration distant (ex: etcd)
Viper peut également récupérer des configurations depuis des centres distants. Nous utiliserons etcd comme exemple. Commencez par installer et démarrer etcd via Docker Compose :
version: "3.8"
services:
etcd-server:
image: "bitnami/etcd:latest"
restart: always
environment:
- ALLOW_NONE_AUTHENTICATION=yes # À utiliser uniquement pour les tests
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
ports:
- "12379:2379" # Mappe le port 2379 du conteneur au port 12379 de l'hôte
Lancez le conteneur : docker compose up -d
Ensuite, stockez votre configuration dans etcd. Si vous avez un fichier remote-settings.yml, vous pouvez le pousser :
# Téléchargez et installez etcdctl si ce n'est pas déjà fait :
# go install go.etcd.io/etcd/etcdctl@latest
# Créez un fichier remote-settings.yml
# echo "app: {nom: RemoteApp, version: 1.1.0}\ndatabase: {driver: pg, chaine_connexion: pg_remote_string}" > remote-settings.yml
# Poussez la configuration vers etcd
etcdctl --endpoints=127.0.0.1:12379 put /configs/my_app_go < remote-settings.yml
Maintenant, intégrez la lecture de cette configuration distante dans votre application Go :
package main
import (
"fmt"
"log"
"time"
_ "github.com/spf13/viper/remote" // Import anonyme pour enregistrer les fournisseurs distants
"github.com/spf13/viper"
)
// ... (Les structures AppSettings, Application, DBConfig, CacheConfig restent les mêmes) ...
func chargerConfigCentreDistant() {
// 1. Enregistrer le fournisseur distant (etcd3, adresse, chemin de la clé)
// L'import _ "github.com/spf13/viper/remote" est nécessaire pour que viper connaisse etcd3
err := viper.AddRemoteProvider("etcd3", "127.0.0.1:12379", "/configs/my_app_go")
if err != nil {
log.Fatalf("Impossible d'ajouter le fournisseur distant : %s", err)
}
// 2. Définir le type de configuration si elle n'est pas spécifiée dans le chemin etcd
viper.SetConfigType("yaml") // Important si le contenu est YAML
// 3. Lire la configuration depuis le centre distant
if err := viper.ReadRemoteConfig(); err != nil {
log.Fatalf("Erreur lors de la lecture de la configuration distante : %s", err)
}
fmt.Println("Configuration distante chargée avec succès.")
}
func main() {
chargerConfigCentreDistant()
// Accéder aux paramètres comme d'habitude
remoteAppName := viper.GetString("app.nom")
remoteDbConn := viper.GetString("database.chaine_connexion")
fmt.Printf("Nom de l'application (distant) : %s\n", remoteAppName)
fmt.Printf("Chaîne de connexion DB (distante) : %s\n", remoteDbConn)
// Pour garder l'application en cours d'exécution et observer d'éventuels changements (avec WatchRemoteConfig)
select {}
}
Surveillance des modifications de configuration
Viper peut également surveiller les modifications de configuration et exécuter une fonction de rappel lorsqu'un changement est détecté. Ceci est particulièrement utile pour les applications de longue durée qui doivent réagir aux mises à jour de configuration sans redémarrer.
package main
import (
"fmt"
"log"
"time"
"github.com/fsnotify/fsnotify" // Nécessaire pour WatchConfig
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
// ... (Les structures AppSettings, Application, DBConfig, CacheConfig restent les mêmes) ...
func init() {
// Définir le chemin et le nom du fichier de configuration
viper.AddConfigPath("./config")
viper.SetConfigName("app-settings")
viper.SetConfigType("yaml")
// Lire la configuration initiale
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Erreur lors de la lecture initiale du fichier de configuration : %s", err)
}
fmt.Println("Configuration initiale chargée.")
// Activer la surveillance du fichier de configuration local
viper.WatchConfig()
// Définir la fonction de rappel pour les changements de configuration
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Printf("Fichier de configuration modifié : %s\n", e.Name)
fmt.Printf("Nouvel nom de l'application : %s\n", viper.GetString("app.nom"))
fmt.Printf("Nouvelle adresse du cache : %s\n", viper.GetString("cache.adresse"))
// Ici, vous pouvez déclencher un rechargement de services ou d'autres logiques.
// Par exemple, si la chaîne de connexion DB change, vous pourriez recréer la connexion.
})
fmt.Println("Surveillance des modifications de configuration activée.")
}
func main() {
// Simuler une application web longue durée
router := gin.Default()
router.GET("/status", func(c *gin.Context) {
c.JSON(200, gin.H{
"app_name": viper.GetString("app.nom"),
"cache_addr": viper.GetString("cache.adresse"),
"message": "Statut actuel de l'application basé sur la configuration.",
})
})
fmt.Println("Démarrage du serveur web sur le port 8080. Modifiez 'config/app-settings.yml' pour voir les changements.")
// Pour que la surveillance fonctionne, l'application doit rester en vie.
// La méthode Run de Gin bloque, ce qui est parfait pour cet exemple.
if err := router.Run(":8080"); err != nil {
log.Fatalf("Impossible de démarrer le serveur Gin : %s", err)
}
// Pour WatchRemoteConfig, la logique est similaire mais utilise ReadRemoteConfig.
// viper.WatchRemoteConfig()
// viper.OnConfigChange(func(e fsnotify.Event) { ... })
}
Exécutez cette application et modifiez le fichier config/app-settings.yml. Vous verrez les messages de changement s'afficher dans la console sans redémarrer l'application. Accédez à http://localhost:8080/status dans votre navigateur pour voir la configuration actualisée.