Configurar aplicações Kubernetes com ConfigMaps e Secrets

KubernetesBeginner
Pratique Agora

Introdução

As imagens de contêiner devem ser reutilizáveis em diferentes ambientes. O Kubernetes separa as configurações não sensíveis em ConfigMaps e as credenciais em Secrets, podendo injetar ambos como variáveis de ambiente ou arquivos.

Você configurará uma aplicação usando os dois mecanismos, inspecionará o resultado com segurança, atualizará a configuração e observará por que as variáveis de ambiente e os volumes montados são atualizados de maneiras diferentes.

O objetivo não é apenas criar dois objetos de API. Você acompanhará cada valor desde o armazenamento do Kubernetes até um contêiner em execução, depois o alterará e observará como o mecanismo de entrega afeta o comportamento de atualização.

Criar a configuração da aplicação

Nesta etapa, crie um namespace e um ConfigMap a partir de valores literais. Um ConfigMap armazena chaves de texto em data; esses valores não são criptografados.

Primeiro, acesse o diretório de trabalho do laboratório. O namespace mantém juntos todos os objetos deste exemplo e evita que uma configuração com nome semelhante em outro laboratório seja selecionada acidentalmente.

cd /home/labex/project/application-config
kubectl create namespace practice-config
kubectl create configmap app-settings -n practice-config --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
kubectl get configmap app-settings -n practice-config -o yaml

Cada --from-literal=KEY=VALUE adiciona uma entrada. A saída em YAML torna visíveis as chaves armazenadas e é adequada para configurações não sensíveis.

Procure APP_MODE e LOG_LEVEL em data:. O Kubernetes armazena os valores desejados, mas nada os utiliza ainda; criar um ConfigMap, por si só, não altera um Pod.

A parte relevante da saída é propositalmente simples:

data:
  APP_MODE: production
  LOG_LEVEL: info
kind: ConfigMap

Campos como uid, resourceVersion e creationTimestamp são gerados pelo Kubernetes e serão diferentes. Nesta etapa, leia apenas data, pois esse é o conteúdo de configuração fornecido por você.

Criar um Secret com segurança

Nesta etapa, crie um Secret sem colocar seu valor em um arquivo YAML. Os dados de um Secret são codificados em base64 na API, mas não são criptografados apenas porque o tipo do objeto é Secret; o RBAC e a criptografia em repouso continuam sendo importantes.

generic indica que este é um Secret comum de chave e valor, e não um Secret especializado para TLS ou para obtenção de imagens. As aspas ao redor da senha impedem que o shell interprete a pontuação caso o valor contenha caracteres especiais.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=appuser --from-literal=password='practice-pass'
kubectl get secret app-credentials -n practice-config
kubectl get secret app-credentials -n practice-config -o jsonpath='{.data.username}' | base64 --decode; echo

O pipeline final decodifica apenas o nome de usuário para fins de demonstração. Evite exibir senhas na saída operacional de rotina.

O caractere de pipe envia o resultado codificado do JSONPath para base64 --decode. O ponto e vírgula inicia um echo final, que adiciona uma quebra de linha após o valor decodificado para manter o próximo prompt do shell legível. Ver appuser comprova como funciona a codificação e também demonstra por que o base64 nunca deve ser tratado como uma barreira de segurança.

O comando intermediário exibe metadados sem revelar nenhum dos valores:

NAME              TYPE     DATA   AGE
app-credentials   Opaque   2      ...
appuser

DATA=2 significa que o Secret contém duas chaves; não representa uma quantidade de bytes. Opaque é o tipo de API usado para um Secret comum de chave e valor.

Injetar a configuração em um Pod

Nesta etapa, use os valores do ConfigMap como variáveis de ambiente e monte o Secret como arquivos somente leitura.

Antes de criar o Pod, separe no manifesto os dois caminhos de entrega:

  • envFrom transforma cada chave do ConfigMap em uma variável de ambiente do contêiner.
  • volumes conecta o Secret ao Pod, enquanto volumeMounts define onde esse volume aparecerá dentro deste contêiner específico.

O loop do Pod não executa nenhuma tarefa da aplicação; ele apenas mantém o contêiner em execução para que você possa inspecionar os dois caminhos.

cd /home/labex/project/application-config
cat <<'EOF' > configured-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: configured-app
  namespace: practice-config
spec:
  containers:
    - name: app
      image: busybox:1.36
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "while true; do sleep 30; done"]
      envFrom:
        - configMapRef:
            name: app-settings
      volumeMounts:
        - name: credentials
          mountPath: /etc/app-credentials
          readOnly: true
  volumes:
    - name: credentials
      secret:
        secretName: app-credentials
EOF

Aplique o manifesto e aguarde o Pod ficar pronto:

kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s

Agora, inspecione cada caminho de entrega de dentro do contêiner. printenv lê o ambiente do processo, enquanto ls exibe os arquivos projetados pelo volume do Secret:

kubectl exec configured-app -n practice-config -- printenv APP_MODE LOG_LEVEL
kubectl exec configured-app -n practice-config -- ls -l /etc/app-credentials

envFrom importa todas as chaves do ConfigMap. O volume do Secret cria um arquivo por chave e é marcado como somente leitura dentro do contêiner.

A saída esperada do ambiente é production, seguida de info. O diretório deve conter arquivos chamados username e password; os nomes dos arquivos correspondem às chaves do Secret.

No contêiner real, a listagem será semelhante a esta:

production
info
total 0
lrwxrwxrwx ... password -> ..data/password
lrwxrwxrwx ... username -> ..data/username

As entradas são links simbólicos gerenciados pelo Kubernetes. Essa indireção permite que o kubelet alterne os dados montados para uma nova versão sem reescrever um arquivo enquanto a aplicação o lê. Ainda assim, sua aplicação pode abrir os caminhos conhecidos /etc/app-credentials/username e /etc/app-credentials/password.

Atualizar os dados montados do Secret

Nesta etapa, atualize o Secret e observe o arquivo montado. kubectl create ... --dry-run=client -o yaml | kubectl apply -f - gera o YAML desejado localmente e o envia diretamente para apply; - significa entrada padrão.

Esse padrão é útil para atualizar um objeto gerado de forma imperativa sem excluí-lo. O lado esquerdo imprime um manifesto completo do Secret, e o lado direito envia esse manifesto ao servidor da API.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=serviceuser --from-literal=password='rotated-pass' --dry-run=client -o yaml | kubectl apply -f -

Como o Secret foi originalmente criado com kubectl create, a primeira atualização pode incluir este aviso antes da linha de sucesso:

Warning: resource secrets/app-credentials is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
secret/app-credentials configured

O aviso explica que apply ainda não tinha um estado declarativo anterior para comparação. O Kubernetes adiciona a anotação automaticamente durante essa atualização, portanto configured confirma o sucesso; não é necessário excluir e recriar o Secret.

O kubelet atualiza de forma assíncrona o conteúdo projetado do volume do Secret. Faça consultas repetidas até que o novo nome de usuário apareça:

current_username=""
for attempt in $(seq 1 24); do
  current_username=$(kubectl exec configured-app -n practice-config -- cat /etc/app-credentials/username)
  if [ "$current_username" = "serviceuser" ]; then
    break
  fi
  sleep 5
done
echo "$current_username"
test "$current_username" = "serviceuser"

O loop tenta novamente por até dois minutos porque a projeção do volume é eventualmente consistente. No ambiente testado, o objeto da API foi alterado imediatamente, mas o arquivo montado precisou de mais de um minuto para ser atualizado.

$(...) é uma substituição de comando: o shell executa seq 1 24 e usa os números resultantes como valores do loop. A cada iteração, o conteúdo atual do arquivo é armazenado em current_username; break interrompe o loop na primeira correspondência, e sleep 5 evita consultas contínuas à API. O test final retorna uma falha se o valor nunca mudar, impedindo que o comando prossiga silenciosamente com dados desatualizados. A saída esperada é serviceuser.

Atualizar a configuração do ambiente

Nesta etapa, atualize LOG_LEVEL e compare o resultado com o Pod em execução. As variáveis de ambiente são definidas quando o contêiner é iniciado; portanto, alterar o ConfigMap não reescreve o ambiente do processo atual.

O patch de mesclagem altera apenas uma entrada em data. Ler imediatamente o processo em execução demonstra uma diferença importante em relação a um volume montado:

kubectl patch configmap app-settings -n practice-config --type=merge -p '{"data":{"LOG_LEVEL":"debug"}}'
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

O Pod ainda exibe info. Recrie-o a partir do manifesto salvo para que ele consuma o novo valor:

configmap/app-settings patched
info

Esse é o resultado esperado, não uma falha no patch: o ConfigMap contém debug, mas o processo que já estava em execução continua usando o ambiente criado na inicialização.

cd /home/labex/project/application-config
kubectl delete pod configured-app -n practice-config --wait=true
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

O Pod substituto exibe debug. Essa diferença explica por que as cargas de trabalho em produção normalmente acionam um novo rollout após alterações de configuração baseadas em variáveis de ambiente.

pod "configured-app" deleted
pod/configured-app created
pod/configured-app condition met
debug

O manifesto continua referenciando o ConfigMap pelo nome; nenhum valor foi copiado para configured-app.yaml. Recriar o Pod faz com que o kubelet resolva essa referência novamente e injete os dados mais recentes.

Resumo

Você separou as configurações da aplicação e as credenciais da imagem, consumiu dados do ConfigMap como variáveis de ambiente, montou dados do Secret como arquivos, alternou um Secret projetado e recriou um Pod para atualizar a configuração baseada no ambiente.