0%
ConfigMaps e Secrets

Configuração

Uma imagem, vários ambientes — e o que « Secret » não quer dizer.

15-20 min

ConfigMaps e Secrets

Uma imagem de contêiner deve poder ser implantada em desenvolvimento, em homologação e em produção sem ser reconstruída. Esse é o princípio que justifica toda esta lição, e ele não é teórico: se a URL do banco de dados está gravada na imagem, o artefato que você testa não é o que você entrega, e o teste deixa de provar qualquer coisa.

A outra metade do problema é mais desagradável. Uma senha colocada em uma imagem fica lá para sempre: as camadas são imutáveis, apagá-la em uma camada posterior não a remove do histórico, e qualquer pessoa com acesso ao registry pode lê-la. Um segredo entregue dentro de uma imagem não é um segredo, é um vazamento com atraso.

O Kubernetes separa, portanto, dois objetos para dois usos: a ConfigMap guarda a configuração não sensível, e o Secret guarda os dados confidenciais. Ambos são injetados nos contêineres da mesma forma — variáveis de ambiente ou arquivos montados — mas o tratamento que o cluster dá a cada um é diferente.

Um aviso logo de início, antes de qualquer sintaxe, porque é o mal-entendido mais difundido do ecossistema: um Secret do Kubernetes não é criptografado por padrão. Ele é codificado em base64, que é um formato de transporte, não uma proteção. Veremos exatamente o que isso implica e o que é preciso acrescentar para que a palavra « secret » seja merecida.

A ConfigMap

Uma ConfigMap é um dicionário de pares chave-valor, ou um conjunto de arquivos, armazenado no cluster.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: atelier
data:
  # Valeurs simples : une clé, une chaîne
  APP_ENV: "production"
  LOG_LEVEL: "info"
  API_TIMEOUT: "30"
  FEATURE_NOUVEAU_PANIER: "true"

  # Fichier entier : la clé devient le nom du fichier si on monte en volume
  application.properties: |
    server.port=8080
    spring.jpa.show-sql=false
    logging.level.root=INFO

  nginx.conf: |
    server {
      listen 80;
      location /api/ {
        proxy_pass http://api:8080/;
      }
    }

Dois detalhes de sintaxe que custam tempo quando são ignorados. Primeiro, todos os valores de data são strings: API_TIMEOUT: 30 sem aspas é recusado pelo schema, e true sem aspas é interpretado como um booleano YAML. Adote o hábito de colocar aspas sempre. Segundo, o | do YAML preserva as quebras de linha, o que permite embutir um arquivo de configuração tal como ele é.

As variantes imperativas são práticas para exploração:

# Depuis des valeurs littérales
kubectl create configmap app-config \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=info

# Depuis un fichier : la clé est le nom du fichier
kubectl create configmap nginx-config --from-file=nginx.conf

# Depuis un répertoire entier : une clé par fichier
kubectl create configmap tous-configs --from-file=./config/

# Depuis un fichier .env : une clé par ligne
kubectl create configmap app-config --from-env-file=.env
kubectl get configmaps
kubectl describe configmap app-config     # affiche les valeurs en clair
kubectl get configmap app-config -o yaml

O Secret

Estruturalmente idêntico, com um campo a mais e uma codificação esperada.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: atelier
type: Opaque
stringData:
  # stringData : vous écrivez en clair, Kubernetes encode à l'enregistrement
  DB_USER: "app_user"
  DB_PASSWORD: "S3cr3t-a-changer"
  DATABASE_URL: "postgresql://app_user:S3cr3t-a-changer@postgres:5432/appdb"

stringData é quase sempre a boa escolha para um manifesto escrito à mão: evita que você mesmo precise codificar. O campo data, por sua vez, espera valores já em base64:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  DB_USER: YXBwX3VzZXI=              # echo -n 'app_user' | base64
  DB_PASSWORD: UzNjcjN0LWEtY2hhbmdlcg==
# Création impérative : l'encodage est fait pour vous
kubectl create secret generic db-credentials \
  --from-literal=DB_USER=app_user \
  --from-literal=DB_PASSWORD='S3cr3t-a-changer'

# Depuis un fichier (certificat, clé privée, fichier de credentials)
kubectl create secret generic tls-keys --from-file=./server.key

# Secret dédié aux registres privés, pour imagePullSecrets
kubectl create secret docker-registry registre-prive \
  --docker-server=registry.example.com \
  --docker-username=deploiement \
  --docker-password='jeton-du-registre'

Os tipos de Secret mais usados:

typeUso
Opaquedados arbitrários: o caso geral
kubernetes.io/dockerconfigjsonautenticação em um registry de imagens privado
kubernetes.io/tlscertificado e chave privada, consumidos pelos Ingress
kubernetes.io/service-account-tokentoken de uma conta de serviço

O que « Secret » realmente protege

Esta é a demonstração que vale fazer uma vez para nunca mais esquecer:

# Le contenu « protégé »
kubectl get secret db-credentials -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# S3cr3t-a-changer

Nenhuma senha, nenhuma chave, nenhuma decriptação. O base64 nunca foi um mecanismo de segurança — ele serve para transportar bytes binários em um campo de texto. O que isso implica na prática:

  • Qualquer pessoa autorizada a ler os Secrets de um namespace lê todas as senhas desse namespace em texto claro.
  • Um Secret commitado no Git é um segredo comprometido, mesmo codificado em base64.
  • Por padrão, os Secrets são armazenados em texto claro no etcd. Um backup do etcd mal protegido expõe tudo.

O que torna um Secret realmente confidencial são três medidas adicionais:

  1. O RBAC. É a única proteção que funciona por padrão: restringir quem pode ler os Secrets. Um Secret só é confidencial se os direitos de leitura também forem.
  2. A criptografia em repouso. Ela é ativada no servidor de API por meio de uma EncryptionConfiguration, com uma chave local ou um serviço de gerenciamento de chaves (KMS). Não vem ativada de fábrica em um cluster autogerenciado; as ofertas gerenciadas costumam oferecê-la, mas é preciso verificar caso a caso.
  3. Um gerenciador externo. HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, com um operador que sincroniza para o cluster. A rotação se torna possível e a auditoria passa a existir, algo que o Kubernetes sozinho não oferece.

Em comparação com uma variável de ambiente escrita em um manifesto de Deployment, o Secret ainda traz dois ganhos reais: o valor não aparece em kubectl describe deployment, e ele pode ser administrado separadamente do deployment. É um avanço, não uma garantia.

Injetar por variáveis de ambiente

O método mais direto, chave por chave:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: mon-api:1.0.0
          ports:
            - containerPort: 8080
          env:
            # Une clé de ConfigMap vers une variable, avec renommage possible
            - name: ENVIRONNEMENT
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: APP_ENV

            # Une clé de Secret vers une variable
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: DB_PASSWORD

            # Une valeur littérale, pour ce qui n'est ni partagé ni sensible
            - name: PORT
              value: "8080"

Para importar tudo de uma vez, sem enumerar:

          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: db-credentials

envFrom é conciso, mas tem uma contrapartida que é preciso avaliar: os nomes das variáveis se tornam implícitos. Ao ler o Deployment, não se sabe mais quais variáveis a aplicação recebe. Para uma aplicação cujo código você não controla, a enumeração explícita continua sendo preferível.

Por fim, adicione optional: true quando uma chave puder legitimamente estar ausente; caso contrário, o Pod fica travado em CreateContainerConfigError:

            - name: FONCTION_EXPERIMENTALE
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: FEATURE_NOUVEAU_PANIER
                  optional: true

O grande limite das variáveis de ambiente

Ele é estrutural: as variáveis de ambiente são fixadas na inicialização do processo. Modificar uma ConfigMap não altera nada nos Pods que já estão em execução e não dispara nenhum redeploy. Você acaba com um cluster em que a ConfigMap diz uma coisa e a aplicação faz outra — uma inconsistência silenciosa, muito difícil de identificar.

Há duas respostas possíveis, conforme a necessidade.

Forçar um redeploy a cada mudança, fazendo o template depender de um hash da configuração:

    metadata:
      labels:
        app: api
      annotations:
        # À recalculer à chaque livraison : toute modification du template
        # déclenche une mise à jour progressive des Pods.
        checksum/config: "9f2b7c1e4a"

Ou, de forma mais simples no dia a dia da operação:

# Redémarrage progressif du Deployment, sans changer l'image
kubectl rollout restart deployment/api

Injetar por volume

Montar uma ConfigMap como um diretório gera um arquivo por chave. É o modo obrigatório para um arquivo de configuração completo e o único que permite uma atualização sem reinicialização.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
          volumeMounts:
            # Toutes les clés de la ConfigMap deviennent des fichiers ici
            - name: config-volume
              mountPath: /etc/app/config
              readOnly: true
            # Un Secret monté : permissions restrictives, jamais dans un log
            - name: secrets-volume
              mountPath: /etc/app/secrets
              readOnly: true
      volumes:
        - name: config-volume
          configMap:
            name: app-config
            items:
              # Sans « items », toutes les clés sont montées.
              # Avec, on choisit précisément, et on renomme.
              - key: nginx.conf
                path: default.conf
        - name: secrets-volume
          secret:
            secretName: db-credentials
            defaultMode: 0400     # lecture seule pour le propriétaire

Com este manifesto, o contêiner enxerga /etc/app/config/default.conf contendo o valor da chave nginx.conf. A renomeação via items é o que permite colocar um arquivo onde a aplicação o espera, sem contorcionismos.

Para substituir um único arquivo em um diretório existente sem ocultar o restante do conteúdo, subPath é a chave:

          volumeMounts:
            - name: config-volume
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf     # remplace UN fichier, pas le répertoire

Sem subPath, montar em /etc/nginx/conf.d/ ocultaria todo o conteúdo original do diretório. É uma causa clássica de contêiner que se recusa a iniciar após a adição de uma configuração.

A atualização a quente, com suas condições

As ConfigMaps montadas em volume são atualizadas no contêiner em execução, sem reinicialização. Três ressalvas que é preciso conhecer:

  • A propagação leva cerca de um minuto, dependendo do período de sincronização do kubelet.
  • A aplicação precisa saber recarregar o arquivo. O nginx precisa de um nginx -s reload, e uma aplicação Spring Boot não relê suas propriedades espontaneamente. O arquivo muda, o comportamento não.
  • Uma montagem com subPath nunca é atualizada. É exatamente o compromisso dessa opção: precisão da montagem em troca da perda do recarregamento.
# Modifier une ConfigMap en place puis observer le fichier dans le Pod
kubectl edit configmap app-config
kubectl exec -it deploy/web -- cat /etc/app/config/default.conf

Variáveis de ambiente ou volume: qual escolher

CritérioVariáveis de ambienteVolume
Valores simples e curtosadequadoverboso
Arquivo de configuração completoinadequadoúnica opção
Atualização sem reinicializaçãoimpossívelpossível, com ressalvas
Visibilidade em /proc e nos logs de crashexpostalimitada a quem lê o arquivo
Saída de kubectl describe podnomes das variáveis visíveisapenas o caminho da montagem

Para segredos, o volume é mais seguro, por um motivo concreto que você mesmo pode verificar:

# L'environnement du processus principal est lisible dans /proc.
kubectl exec -it deploy/api -- sh -c 'tr "\0" "\n" < /proc/1/environ | grep DB_'
# DB_PASSWORD=S3cr3t-a-changer

As variáveis de ambiente também aparecem em stack traces de exceções, em relatórios de falha, e são herdadas por qualquer processo filho. Um arquivo montado no modo 0400 tem uma superfície de exposição bem mais estreita.

As ConfigMaps imutáveis

Marcar uma ConfigMap ou um Secret como imutável proíbe qualquer modificação posterior e, de quebra, reduz a carga de monitoramento sobre o servidor de API em clusters grandes.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v3
immutable: true
data:
  APP_ENV: "production"
  LOG_LEVEL: "warn"

Isso impõe uma disciplina útil: em vez de modificar app-config, você cria app-config-v4 e faz o Deployment apontar para ela. A mudança de configuração passa a ser uma modificação versionada do Deployment, portanto visível em kubectl rollout history e reversível com kubectl rollout undo. Uma ConfigMap modificada in loco, por sua vez, não deixa nenhum rastro aproveitável.

Pontos-chave

  • Uma imagem deve ser independente do seu ambiente. A ConfigMap carrega a configuração, o Secret carrega os dados sensíveis, e ambos são injetados por variáveis de ambiente ou por volume.
  • Um Secret não é criptografado: ele é codificado em base64, legível com um simples base64 -d, e armazenado em texto puro no etcd por padrão. O que realmente o protege é o RBAC, a criptografia em repouso configurada no servidor de API e um gerenciador externo.
  • As variáveis de ambiente são fixadas na inicialização: modificar uma ConfigMap não altera em nada os Pods em execução. Force a troca com uma annotation de checksum ou com um kubectl rollout restart.
  • Os volumes são propagados a quente (com até cerca de um minuto de atraso), desde que a aplicação recarregue seu arquivo — e nunca com subPath, que congela a montagem.
  • Prefira o volume para os secrets: as variáveis de ambiente acabam aparecendo em /proc, em traces de erro e em todos os processos filhos.

Dica: versione suas ConfigMaps (app-config-v3) e torne-as imutáveis. Uma mudança de configuração passa então a ser uma revisão do Deployment, portanto rastreável e reversível como qualquer outro deploy.

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!