Kubernetes ConfigMaps: Nuances à connaître

Remarque : ce n'est pas un article de guide à part entière, mais plutôt un rappel / indice pour ceux qui utilisent déjà ConfigMap dans Kubernetes ou qui préparent simplement leur application pour y travailler.



Contexte: De rsync Ă  ... Kubernetes


Qu'est-il arrivé avant? À l'ère de «l'administration classique», dans la version la plus simple, le fichier de configuration était placé juste à côté des applications (ou dans le référentiel, si vous voulez). C'est simple: nous faisons la livraison élémentaire (CD) de notre code avec la config. Même une implémentation conditionnelle de rsync peut être appelée les rudiments d'un CD.

Lorsque l'infrastructure s'est développée, différentes configurations ont été nécessaires pour différents environnements (développement / stade / production). L'application a été formée pour comprendre quelle configuration utiliser, en les passant comme arguments pour démarrer ou comme variables d'environnement définies dans l'environnement. Encore plus de CD est compliqué avec l'avènement de ce Chef / Marionnette / Ansible si utile. Les rôles apparaissent sur les serveurs et les environnements ne sont plus décrits à différents endroits - nous arrivons à IaC (Infrastructure en tant que code).

Qu'est-ce qui a suivi? S'il était possible de voir par lui-même les avantages critiques de Kubernetes et même de comprendre la nécessité de modifier les applications pour qu'elles fonctionnent dans cet environnement, la migration s'est produite. Sur le chemin, je m'attendais à beaucoup de nuances et de différences dans la construction de l'architecture, mais quand j'ai réussi à faire face à la partie principale, j'ai obtenu l'application tant attendue en cours d'exécution dans K8s.

Une fois ici, nous pouvons toujours utiliser les configurations préparées dans le référentiel à côté de l'application ou passer ENV au conteneur. Cependant, en plus de ces méthodes, ConfigMaps sont également disponibles . Cette primitive K8s vous permet d'utiliser des modèles Go dans les configurations, c'est-à-dire les rendre comme des pages HTML et recharger l'application lors du changement de la configuration sans redémarrage. Avec ConfigMaps, il n'est plus nécessaire de conserver 3+ configurations pour différents environnements et de garder une trace de la pertinence de chacune.

Une introduction générale à ConfigMaps peut être trouvée, par exemple, ici . Et dans cet article, je vais me concentrer sur certaines fonctionnalités de leur travail.

ConfigMaps simples


À quoi ressemblaient les configurations dans Kubernetes? Qu'ont-ils obtenu des modèles go? Par exemple, voici un ConfigMap ordinaire pour une application déployée à partir d'un graphique Helm:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app
data:
  config.json: |
    {
      "welcome": {{ pluck .Values.global.env .Values.welcome | quote }},
      "name": {{ pluck .Values.global.env .Values.name | quote }}
    }

Ici, les valeurs substituées .Values.welcomeet .Values.nameseront extraites du fichier values.yaml. Pourquoi exactement values.yaml? Comment fonctionne le moteur de modèles Go? Nous avons déjà parlé de ces détails plus en détail ici .

L'appel pluckpermet de sélectionner la ligne nécessaire sur la carte:

$ cat .helm/values.yaml 
welcome:
  production: "Hello"
  test: "Hey"
name:
  production: "Bob"
  test: "Mike"

De plus, vous pouvez prendre à la fois des lignes spécifiques et des fragments entiers de la configuration.

Par exemple, ConfigMap pourrait ressembler Ă  ceci:

data:
  config.json: |
    {{ pluck .Values.global.env .Values.data | first | toJson | indent 4 }}

... et dans values.yaml- le contenu suivant:

data:
  production:
    welcome: "Hello"
    name: "Bob"

Celui qui global.envest impliqué ici est le nom de l'environnement. En remplaçant cette valeur pendant le déploiement, vous pouvez rendre des ConfigMaps avec un contenu différent. firstnécessaire ici parce que pluckrenvoie une liste dont le premier élément contient la valeur souhaitée.

Quand il y a beaucoup de configurations


Un ConfigMap peut contenir plusieurs fichiers de configuration:

data:
  config.json: |
    {
      "welcome": {{ pluck .Values.global.env .Values.welcome | first | quote }},
      "name": {{ pluck .Values.global.env .Values.name | first | quote }}
    }
  database.yml: |
    host: 127.0.0.1
    db: app
    user: app
    password: app

Vous pouvez même monter chaque configuration séparément:

        volumeMounts:
        - name: app-conf
          mountPath: /app/configfiles/config.json
          subPath: config.json
        - name: app-conf
          mountPath: /app/configfiles/database.yml
          subPath: database.yml

... ou récupérez toutes les configurations à la fois avec le répertoire:

        volumeMounts:
        - name: app-conf
          mountPath: /app/configfiles

Si vous modifiez la description de la ressource de déploiement pendant le déploiement, Kubernetes créera un nouveau ReplicaSet, diminuant l'ancien à 0 et augmentant le nouveau au nombre spécifié de réplicas. (Cela est vrai pour la stratégie de déploiement RollingUpdate.)

De telles actions entraîneront la recréation du pod avec une nouvelle description. Par exemple: il y avait une image image:my-registry.example.com:v1, mais est devenu - image:my-registry.example.com:v2. Et peu importe ce que nous avons changé exactement dans la description de notre déploiement: l'essentiel est que cela a entraîné la recréation du replicaSet (et, par conséquent, du pod). Dans ce cas, la nouvelle version du fichier de configuration dans la nouvelle version de l'application est automatiquement montée et il n'y aura aucun problème.

RĂ©ponse au changement de ConfigMap


En cas de changements dans ConfigMap, quatre scénarios d'événements peuvent suivre. Considérez-les:

  1. : ConfigMap, subPath.
    : .
  2. : ConfigMap, pod.
    : pod .
  3. : ConfigMap Deployment -.
    : , ConfigMap’, Deployment, pod , — .
  4. : ConfigMap, .
    : pod’ / pod’.

Nous analyserons plus en détail.

Scénario 1


Nous avons seulement corrigé ConfigMap? L'application ne redémarrera pas. Dans le cas d'un montage par, subPathil n'y aura aucun changement jusqu'à ce que le pod soit redémarré manuellement.

Tout est simple: Kubernetes monte notre ConfigMap dans un pod d'une version spécifique de la ressource. Puisqu'il est monté avec subPath, aucune "influence" supplémentaire sur la config n'est plus fournie.

Scénario 2


Vous ne pouvez pas mettre à jour le fichier sans recréer le pod? D'accord, nous avons 6 répliques dans le déploiement, nous pouvons donc nous relayer manuellement pour tout faire delete pod. Ensuite, lors de la création de nouveaux pods, ils «récupèrent» la nouvelle version de ConfigMap.

Scénario 3


Fatigué d'effectuer de telles opérations manuellement? Une solution à ce problème est décrite dans les conseils et astuces de Helm :

kind: Deployment
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
[...]

Ainsi, le spec.templatehachage de la configuration d'annotation rendue est simplement écrit dans le modèle pod ( ).

Les annotations sont des champs de valeurs-clés arbitraires dans lesquels vous pouvez stocker vos valeurs. Si vous les enregistrez dans le modèle du spec.templatefutur pod, ces champs tomberont dans le ReplicaSet et le pod lui-même. Kubernetes remarquera que le modèle de pod a changé (parce que la configuration sha256 a changé) et démarrera RollingUpdate, dans lequel rien ne change, sauf cette annotation.

En conséquence, nous enregistrons la même version de l'application et de la description du déploiement et déclenchons essentiellement la recréation du pod par la machine - de la même manière que nous le ferions manuellement kubectl delete, mais déjà «correctement»: automatiquement et avec RollingUpdate.

Scénario 4


Peut-être que l'application sait déjà comment surveiller les changements dans la configuration et recharger automatiquement? Voici une caractéristique importante de ConfigMaps ...

Dans Kubernetes, si la config est montée avec subPath, elle ne sera pas mise à jour tant que le pod n'aura pas été redémarré (voir les trois premiers scénarios discutés ci-dessus) . Mais si vous montez ConfigMap en tant que répertoire, sans subPath, à l'intérieur du conteneur, il y aura un répertoire avec une configuration mise à jour sans redémarrer le pod.

Il existe d'autres fonctionnalités utiles à retenir:

  • Un tel fichier de configuration mis Ă  jour Ă  l'intĂ©rieur du conteneur est mis Ă  jour avec un certain retard. Cela est dĂ» au fait que le fichier n'est pas montĂ© exactement, mais l'objet Kubernetes.
  • Le fichier Ă  l'intĂ©rieur est un lien symbolique. Exemple avec subPath:

    $ kubectl -n production exec go-conf-example-6b4cb86569-22vqv -- ls -lha /app/configfiles 
    total 20K    
    drwxr-xr-x    1 root     root        4.0K Mar  3 19:34 .
    drwxr-xr-x    1 app      app         4.0K Mar  3 19:34 ..
    -rw-r--r--    1 root     root          42 Mar  3 19:34 config.json
    -rw-r--r--    1 root     root          47 Mar  3 19:34 database.yml

    Et que se passera-t-il sans une subPathfois monté par le répertoire?

    $ kubectl -n production exec go-conf-example-67c768c6fc-ccpwl -- ls -lha /app/configfiles 
    total 12K    
    drwxrwxrwx    3 root     root        4.0K Mar  3 19:40 .
    drwxr-xr-x    1 app      app         4.0K Mar  3 19:34 ..
    drwxr-xr-x    2 root     root        4.0K Mar  3 19:40 ..2020_03_03_16_40_36.675612011
    lrwxrwxrwx    1 root     root          31 Mar  3 19:40 ..data -> ..2020_03_03_16_40_36.675612011
    lrwxrwxrwx    1 root     root          18 Mar  3 19:40 config.json -> ..data/config.json
    lrwxrwxrwx    1 root     root          19 Mar  3 19:40 database.yml -> ..data/database.yml

    Mettez à jour la configuration (via deploy ou kubectl edit), attendez 2 minutes (temps de mise en cache apiserver) - et le tour est joué:

    $ kubectl -n production exec go-conf-example-67c768c6fc-ccpwl -- ls -lha --color /app/configfiles 
    total 12K    
    drwxrwxrwx    3 root     root        4.0K Mar  3 19:44 .
    drwxr-xr-x    1 app      app         4.0K Mar  3 19:34 ..
    drwxr-xr-x    2 root     root        4.0K Mar  3 19:44 ..2020_03_03_16_44_38.763148336
    lrwxrwxrwx    1 root     root          31 Mar  3 19:44 ..data -> ..2020_03_03_16_44_38.763148336
    lrwxrwxrwx    1 root     root          18 Mar  3 19:40 config.json -> ..data/config.json
    lrwxrwxrwx    1 root     root          19 Mar  3 19:40 database.yml -> ..data/database.yml

    Notez l'horodatage modifié dans le répertoire créé par Kubernetes.

Suivi des modifications


Et enfin - un exemple simple de la façon dont vous pouvez surveiller les changements dans la configuration.

Nous utiliserons une telle application Go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/fsnotify/fsnotify"
)

// Config fo our application
type Config struct {
	Welcome string `json:"welcome"`
	Name    string `json:"name"`
}

var (
	globalConfig *Config
)

// LoadConfig - load our config!
func LoadConfig(path string) (*Config, error) {
	configFile, err := os.Open(path)

	if err != nil {
		return nil, fmt.Errorf("Unable to read configuration file %s", path)
	}

	config := new(Config)

	decoder := json.NewDecoder(configFile)
	err = decoder.Decode(&config)
	if err != nil {
		return nil, fmt.Errorf("Unable to parse configuration file %s", path)
	}

	return config, nil
}

// ConfigWatcher - watches config.json for changes
func ConfigWatcher() {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	done := make(chan bool)
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				log.Println("event:", event)
				if event.Op&fsnotify.Write == fsnotify.Write {
					log.Println("modified file:", event.Name)
				}
				globalConfig, _ = LoadConfig("./configfiles/config.json")
				log.Println("config:", globalConfig)
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("error:", err)
			}
		}
	}()

	err = watcher.Add("./configfiles/config.json")
	if err != nil {
		log.Fatal(err)
	}
	<-done
}

func main() {
	log.Println("Start")
	globalConfig, _ = LoadConfig("./configfiles/config.json")
	go ConfigWatcher()
	for {
		log.Println("config:", globalConfig)
		time.Sleep(30 * time.Second)
	}
}

... en l'ajoutant avec une telle configuration:

$ cat configfiles/config.json 
{
  "welcome": "Hello",
  "name": "Alice"
}

S'il est exécuté, le journal sera:

2020/03/03 22:18:22 config: &{Hello Alice}
2020/03/03 22:18:52 config: &{Hello Alice}

Et maintenant, nous allons installer cette application dans Kubernetes, après avoir monté la configuration ConfigMap dans le pod au lieu du fichier image. Un exemple de graphique Helm a été préparé sur GitHub :

helm install -n habr-configmap --namespace habr-configmap ./habr-configmap --set 'name.production=Alice' --set 'global.env=production'

Et modifiez uniquement ConfigMap:

-  production: "Alice"
+  production: "Bob"

Mettez Ă  jour le graphique Helm dans le cluster, par exemple, comme ceci:

helm upgrade habr-configmap ./habr-configmap --set 'name.production=Bob' --set 'global.env=production'

Que va-t-il se passer?

  • Les applications v1 et v2 ne redĂ©marrent pas, car pour eux, aucun changement n'est intervenu dans le dĂ©ploiement - ils accueillent toujours Alice.
  • L'application v3 a redĂ©marrĂ©, relu la configuration et saluĂ© Bob.
  • L'application v4 n'a pas redĂ©marrĂ©. Comme ConfigMap est montĂ© en tant que rĂ©pertoire, des changements dans la configuration ont Ă©tĂ© remarquĂ©s et la configuration a Ă©tĂ© modifiĂ©e Ă  la volĂ©e, sans redĂ©marrer le pod. Oui, l'application a remarquĂ© des changements dans notre exemple simple - voir le message d'Ă©vĂ©nement de fsnotify :

    2020/03/03 22:19:15 event: "configfiles/config.json": CHMOD
    2020/03/03 22:19:15 config: &{Hello Bob}
    2020/03/03 22:19:22 config: &{Hello Bob}

Vous pouvez voir comment une situation similaire - suivre les changements de ConfigMap - est résolue dans un projet plus adulte (et simplement «réel»), ici .

Important! Il est également utile de rappeler que tout ce qui précède dans l'article est également vrai pour Secret'ov dans Kubernetes ( kind: Secret): ce n'est pas pour rien qu'ils sont si similaires à ConfigMap ...

Prime! Solutions tierces


Si vous êtes intéressé par le sujet du suivi des modifications dans les configurations, il existe déjà des utilitaires prêts à l'emploi pour cela:

  • jimmidyson / configmap-reload - Envoie une requĂŞte HTTP si le fichier a changĂ©. Le dĂ©veloppeur prĂ©voit Ă©galement d'enseigner Ă  SIGHUP d'envoyer, mais le manque de validations Ă  partir d'octobre 2019 laisse ces plans en question;
  • stakater / Reloader - surveille ConfigMap / Secrets et effectue une mise Ă  niveau progressive (comme son auteur l'appelle) sur les ressources qui leur sont associĂ©es.

Il sera commode de lancer de telles applications avec un sidecar-container aux applications existantes. Cependant, si vous connaissez les fonctionnalités de Kubernetes / ConfigMap et des configurations à éditer non pas "en direct" (à travers edit), mais uniquement dans le cadre du déploiement ... alors les capacités de ces utilitaires peuvent sembler superflues, c'est-à-dire duplication des fonctions de base.

Conclusion


Avec l'avènement de ConfigMap dans Kubernetes, les configurations sont passées à la prochaine phase de développement: l'utilisation d'un moteur de modèle leur a apporté une flexibilité comparable au rendu des pages HTML. Heureusement, de telles complications n'ont pas remplacé les solutions existantes, mais sont devenues leur complément . Par conséquent, pour les administrateurs (ou plutôt, même les développeurs) qui considèrent les nouvelles fonctionnalités comme redondantes, de bons vieux fichiers sont toujours disponibles.

Pour ceux qui utilisent déjà ConfigMap'ami ou qui les regardent simplement, l'article donne un bref aperçu de leur essence et de leurs nuances d'utilisation. Si vous avez vos propres trucs et astuces sur le sujet - je serai heureux de voir dans les commentaires.

PS


Lisez aussi dans notre blog:


All Articles