Kubernetes ConfigMaps: nuances a conhecer

Nota : este não é um artigo de guia completo, mas um lembrete / dica para quem já usa o ConfigMap no Kubernetes ou está apenas preparando seu aplicativo para trabalhar nele.



Histórico: do rsync ao ... Kubernetes


O que aconteceu antes? Na era da “administração clássica”, na versão mais simples, o arquivo de configuração foi colocado ao lado dos aplicativos (ou no repositório, se você preferir). É simples: fazemos entrega elementar (CD) para o nosso código junto com a configuração. Mesmo uma implementação condicional de rsync pode ser chamada de rudimentos de um CD.

Quando a infraestrutura cresceu, diferentes configurações foram necessárias para diferentes ambientes (dev / stage / production). O aplicativo foi treinado para entender qual configuração usar, passando-os como argumentos para iniciar ou variáveis ​​de ambiente definidas no ambiente. Um CD ainda mais complicado é o advento de um Chef / Marionete / Ansible tão útil. As funções aparecem nos servidores e os ambientes deixam de ser descritos em lugares diferentes - chegamos ao IaC (Infraestrutura como código).

O que se seguiu? Se fosse possível ver as vantagens críticas do Kubernetes e chegar a um acordo com a necessidade de modificar aplicativos para funcionarem nesse ambiente, ocorreu a migração. No caminho, esperava muitas nuances e diferenças na construção da arquitetura, mas quando consegui lidar com a parte principal, consegui o tão esperado aplicativo em execução no K8s.

Uma vez aqui, ainda podemos usar as configurações preparadas no repositório ao lado do aplicativo ou passando ENV para o contêiner. No entanto, além desses métodos, o ConfigMaps também está disponível . Essa primitiva do K8s permite que você use modelos Go em configurações, ou seja, renderize-as como páginas HTML e recarregue o aplicativo ao alterar a configuração sem reiniciar. Com o ConfigMaps, não é mais necessário manter mais de 3 configurações para diferentes ambientes e acompanhar a relevância de cada uma.

Uma introdução geral ao ConfigMaps pode ser encontrada, por exemplo, aqui . E neste artigo, focarei em alguns recursos do trabalho com eles.

ConfigMaps simples


Como eram as configurações no Kubernetes? O que eles obtiveram dos modelos em uso? Por exemplo, aqui está um ConfigMap comum para um aplicativo implantado a partir de um gráfico 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 }}
    }

Aqui, os valores substituídos .Values.welcomee .Values.nameserão retirados do arquivo values.yaml. Por que exatamente de values.yaml? Como o mecanismo do modelo Go funciona? Já falamos sobre esses detalhes em mais detalhes aqui .

A chamada pluckajuda a selecionar a linha necessária no mapa:

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

Além disso, você pode pegar linhas específicas e fragmentos inteiros da configuração.

Por exemplo, o ConfigMap pode ser assim:

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

... e em values.yaml- o seguinte conteúdo:

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

O envolvido aqui global.envé o nome do ambiente. Substituindo esse valor durante a implantação, é possível renderizar o ConfigMaps com conteúdo diferente. firstnecessário aqui porque pluckretorna uma lista, cujo primeiro elemento contém o valor desejado.

Quando há muitas configurações


Um ConfigMap pode conter vários arquivos de configuração:

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

Você pode até montar cada configuração separadamente:

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

... ou selecione todas as configurações de uma só vez com o diretório:

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

Se você alterar a descrição do recurso Deployment durante a implantação, o Kubernetes criará um novo ReplicaSet, diminuindo o antigo para 0 e aumentando o novo para o número especificado de réplicas. (Isso é verdade para a estratégia de implantação RollingUpdate.)

Essas ações levarão à recriação do pod com uma nova descrição. Por exemplo: havia uma imagem image:my-registry.example.com:v1, mas tornou-se - image:my-registry.example.com:v2. E não importa o que exatamente mudamos na descrição de nossa implantação: o principal é que isso fez com que a replicaSet (e, como resultado, o pod) fosse recriada. Nesse caso, a nova versão do arquivo de configuração na nova versão do aplicativo é montada automaticamente e não haverá problemas.

Resposta à mudança do ConfigMap


Em caso de alterações no ConfigMap, quatro cenários de eventos podem ser seguidos. Considere-os:

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

Vamos analisar com mais detalhes.

Cenário 1


Nós única corrigido configMap? O aplicativo não será reiniciado. No caso de montagem por, subPathnão haverá alterações até que o pod seja reiniciado manualmente.

Tudo é simples: o Kubernetes monta nosso ConfigMap em um pod de uma versão específica do recurso. Como é montado subPath, não há mais "influência" na configuração.

Cenário 2


Não é possível atualizar o arquivo sem recriar o pod? Ok, temos 6 réplicas em Implantação, para que possamos revezar manualmente fazendo tudo delete pod. Então, ao criar novos pods, eles "capturam" a nova versão do ConfigMap.

Cenário 3


Cansado de executar essas operações manualmente? Uma solução para esse problema é descrita em dicas e truques do Helm :

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

Portanto, o spec.templatehash da configuração da anotação renderizada é simplesmente gravado no modelo pod ( ).

As anotações são campos arbitrários de valores-chave nos quais você pode armazenar seus valores. Se você os registrar no modelo do spec.templatepod futuro, esses campos cairão no ReplicaSet e no próprio pod. O Kubernetes notará que o modelo do pod foi alterado (desde a configuração do sha256) e será iniciado RollingUpdate, no qual nada muda, exceto esta anotação.

Como resultado, salvamos a mesma versão do aplicativo e da descrição da Implantação e basicamente acionamos a recriação do pod pela máquina - semelhante à forma como faríamos manualmente kubectl delete, mas já "corretamente": automaticamente e com RollingUpdate.

Cenário 4


Talvez o aplicativo já saiba como monitorar alterações na configuração e recarregar automaticamente? Aqui está um recurso importante do ConfigMaps ...

No Kubernetes, se a configuração estiver montada subPath, ela não será atualizada até que o pod seja reiniciado (consulte os três primeiros cenários discutidos acima) . Mas se você montar o ConfigMap como um diretório, sem subPath, dentro do contêiner, haverá um diretório com uma configuração atualizada sem reiniciar o pod.

Existem outros recursos que são úteis para lembrar:

  • Um arquivo de configuração atualizado dentro do contêiner é atualizado com algum atraso. Isso ocorre porque o arquivo não está montado exatamente, mas o objeto Kubernetes.
  • O arquivo dentro é um link simbólico. Exemplo com 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

    E o que acontecerá sem subPathquando montado pelo diretório?

    $ 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

    Atualize a configuração (via deploy ou kubectl edit), aguarde 2 minutos (tempo de armazenamento em cache do apiserver) - e pronto:

    $ 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

    Observe o carimbo de data / hora alterado no diretório criado pelo Kubernetes.

Alterar rastreamento


E finalmente - um exemplo simples de como você pode monitorar as alterações na configuração.

Usaremos esse aplicativo 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)
	}
}

... adicionando-o com essa configuração:

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

Se executado, o log será:

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

E agora vamos instalar este aplicativo no Kubernetes, montando a configuração do ConfigMap no pod, em vez do arquivo da imagem. Um exemplo de gráfico Helm foi preparado no GitHub :

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

E mude apenas o ConfigMap:

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

Atualize o gráfico Helm no cluster, por exemplo, assim:

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

O que vai acontecer?

  • Os aplicativos v1 e v2 não são reiniciados porque para eles, nenhuma alteração ocorreu na implantação - eles ainda recebem Alice.
  • O aplicativo v3 foi reiniciado, releu a configuração e cumprimentou Bob.
  • O aplicativo v4 não foi reiniciado. Como o ConfigMap é montado como um diretório, foram notadas alterações na configuração e a configuração foi alterada rapidamente, sem reiniciar o pod. Sim, o aplicativo notou alterações em nosso exemplo simples - veja a mensagem do evento 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}

Você pode ver como uma situação semelhante - rastreando as alterações do ConfigMap - é resolvida em um projeto mais adulto (e simplesmente "real"), aqui .

Importante! Também é útil lembrar que tudo o que foi dito acima também é válido para o Secret'ov no Kubernetes ( kind: Secret): não é à toa que eles são tão semelhantes ao ConfigMap ...

Bônus! Soluções de Terceiros


Se você está interessado no tópico de rastreamento de alterações nas configurações, já existem utilitários prontos para isso:

  • jimmidyson / configmap-reload - Envia uma solicitação HTTP se o arquivo foi alterado. O desenvolvedor também planeja ensinar o SIGHUP a enviar, mas a falta de confirmações a partir de outubro de 2019 deixa esses planos em questão;
  • stakater / Reloader - monitora o ConfigMap / Secrets e executa a atualização sem interrupção (como o autor o chama) nos recursos associados a eles.

Será conveniente iniciar esses aplicativos com um contêiner lateral para aplicativos existentes. No entanto, se você conhece os recursos do Kubernetes / ConfigMap e configs para editar não "ao vivo" (através edit), mas apenas como parte da implantação ... os recursos desses utilitários podem parecer supérfluos, ou seja, duplicar funções básicas.

Conclusão


Com o advento do ConfigMap no Kubernetes, as configurações foram movidas para a próxima rodada de desenvolvimento: o uso de um mecanismo de modelo trouxe flexibilidade comparável à renderização de páginas HTML. Felizmente, essas complicações não substituíram as soluções existentes, mas se tornaram seu complemento . Portanto, para administradores (ou melhor, até desenvolvedores) que consideram novos recursos redundantes, bons arquivos antigos ainda estão disponíveis.

Para aqueles que já usam o ConfigMap'ami ou apenas os observam, o artigo fornece uma breve visão geral de sua essência e nuances de uso. Se você tiver suas próprias dicas e truques sobre o assunto - ficarei feliz em ver nos comentários.

PS


Leia também no nosso blog:


All Articles