Configuração
Uma imagem, vários ambientes — e o que « Secret » não quer dizer.
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:
type | Uso |
|---|---|
Opaque | dados arbitrários: o caso geral |
kubernetes.io/dockerconfigjson | autenticação em um registry de imagens privado |
kubernetes.io/tls | certificado e chave privada, consumidos pelos Ingress |
kubernetes.io/service-account-token | token 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:
- 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.
- 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. - 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
subPathnunca é 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ério | Variáveis de ambiente | Volume |
|---|---|---|
| Valores simples e curtos | adequado | verboso |
| Arquivo de configuração completo | inadequado | única opção |
| Atualização sem reinicialização | impossível | possível, com ressalvas |
Visibilidade em /proc e nos logs de crash | exposta | limitada a quem lê o arquivo |
Saída de kubectl describe pod | nomes das variáveis visíveis | apenas 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