<h2>ConfigMaps e Secrets em Kubernetes: Uma Abordagem Prática</h2>
<p>Quando desenvolvemos aplicações containerizadas, precisamos separar a configuração do código. Kubernetes oferece dois recursos nativos para isso: <strong>ConfigMaps</strong> para dados não-sensíveis e <strong>Secrets</strong> para dados sensíveis como senhas e tokens. Neste artigo, exploraremos como injetar essas configurações em seus pods através de variáveis de ambiente e volumes — duas estratégias com propósitos e comportamentos distintos que você precisa dominar para gerenciar aplicações profissionalmente.</p>
<p>A principal diferença entre essas duas abordagens reside no momento e na forma como os dados chegam ao container. Enquanto a injeção por variáveis de ambiente carrega os valores no momento do deploy, a injeção por volume cria arquivos que podem ser atualizados dinamicamente sem reiniciar o pod. Compreender quando usar cada uma é fundamental para construir infraestrutura robusta e flexível.</p>
<h2>ConfigMaps: Armazenando Configurações Não-Sensíveis</h2>
<h3>O que é um ConfigMap?</h3>
<p>Um ConfigMap é um objeto Kubernetes que armazena dados em formato chave-valor. Ele foi projetado para separar dados de configuração do código da aplicação, permitindo reutilizar a mesma imagem de container em diferentes ambientes. Os dados em ConfigMaps <strong>não são criptografados</strong> — apenas codificados em base64 — então nunca devem ser usados para informações sensíveis.</p>
<p>Você pode armazenar desde simples pares chave-valor até arquivos inteiros de configuração. Um ConfigMap típico tem limite de 1MB, o que é suficiente para a maioria dos casos reais de uso.</p>
<h3>Criando um ConfigMap</h3>
<p>Existem múltiplas formas de criar um ConfigMap. A forma mais direta é via yaml:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
database_url: "postgresql://db.example.com:5432/myapp"
log_level: "INFO"
cache_ttl: "3600"
api_timeout: "30"</code></pre>
<p>Se você tiver um arquivo de configuração específico (como <code>.env</code> ou JSON), pode carregá-lo diretamente:</p>
<pre><code class="language-bash">kubectl create configmap app-config \
--from-file=./config/application.properties \
--from-literal=environment=production</code></pre>
<p>Ou ainda, criando a partir de um diretório inteiro:</p>
<pre><code class="language-bash">kubectl create configmap config-dir --from-file=./config/</code></pre>
<h3>Injeção via Variáveis de Ambiente</h3>
<p>A forma mais comum de consumir um ConfigMap é através de variáveis de ambiente. Nesta abordagem, cada chave do ConfigMap se torna uma variável no container:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: myapp
image: myapp:1.0
env:
- name: DATABASE_URL
valueFrom:
configMapKeyRef:
name: app-config
key: database_url
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: log_level</code></pre>
<p>Neste exemplo, a variável <code>DATABASE_URL</code> dentro do container receberá o valor <code>postgresql://db.example.com:5432/myapp</code>. Este padrão é simples e direto — ideal quando você tem um pequeno número de configurações e não precisa atualizar valores em tempo real.</p>
<h3>Injeção via Volume</h3>
<p>Quando você precisa de múltiplas configurações ou quer que mudanças no ConfigMap sejam refletidas automaticamente no pod, use volumes:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: myapp
image: myapp:1.0
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
defaultMode: 0644</code></pre>
<p>Depois que o pod iniciar, todos os dados do ConfigMap aparecerão como arquivos em <code>/etc/config</code>. Se o ConfigMap for atualizado, o arquivo no volume será atualizado em até 60 segundos, sem necessidade de reiniciar o pod.</p>
<h2>Secrets: Protegendo Dados Sensíveis</h2>
<h3>O que é um Secret?</h3>
<p>Um Secret é semelhante ao ConfigMap, mas <strong>todos os seus dados são codificados em base64</strong> no etcd (o banco de dados do Kubernetes). Embora base64 não seja criptografia real, ele adiciona uma camada de proteção contra exposição acidental. Para segurança real, você deve usar encryption at rest no Kubernetes ou ferramentas como HashiCorp Vault.</p>
<p>Existem diferentes tipos de Secrets: <code>Opaque</code> (padrão, chave-valor), <code>kubernetes.io/dockercfg</code> (autenticação Docker), <code>kubernetes.io/service-account-token</code> (tokens), entre outros.</p>
<h3>Criando um Secret</h3>
<p>Para criar um Secret com dados sensíveis, use:</p>
<pre><code class="language-bash">kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=SuperSecurePass123!</code></pre>
<p>Ou via arquivo yaml:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # admin (base64)
password: U3VwZXJTZWN1cmVQYXNzMTIzIQ== # SuperSecurePass123! (base64)</code></pre>
<p>Para converter uma string para base64:</p>
<pre><code class="language-bash">echo -n "SuperSecurePass123!" | base64
Output: U3VwZXJTZWN1cmVQYXNzMTIzIQ==</code></pre>
<h3>Injeção de Secrets via Variáveis de Ambiente</h3>
<p>Similar ao ConfigMap, você pode injetar Secrets como variáveis de ambiente:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Deployment
metadata:
name: app-deployment
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:1.0
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password</code></pre>
<p>Note o uso de <code>secretKeyRef</code> em vez de <code>configMapKeyRef</code>. Uma vantagem dessa abordagem é que a variável de ambiente fica imutável durante a vida do pod — mesmo que o Secret seja atualizado no cluster, a variável mantém seu valor original até o pod ser reiniciado.</p>
<h3>Injeção de Secrets via Volume</h3>
<p>Para casos onde você precisa que o Secret seja atualizado dinamicamente, use volumes:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
name: app-pod-secret
spec:
containers:
- name: myapp
image: myapp:1.0
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: db-credentials
defaultMode: 0400 # Leitura apenas pelo owner</code></pre>
<p>Os arquivos serão montados em <code>/etc/secrets/username</code> e <code>/etc/secrets/password</code>. Note o <code>readOnly: true</code> — prática recomendada para evitar modificação acidental de segredos dentro do container.</p>
<h2>Comparação Prática: Env vs Volume</h2>
<h3>Quando Usar Variáveis de Ambiente</h3>
<p>Use injeção por variáveis de ambiente quando você tiver um número pequeno de configurações e precisar que elas sejam imutáveis durante a execução do pod. Exemplos: <code>NODE_ENV=production</code>, <code>API_KEY</code>, <code>LOG_LEVEL</code>. Essa abordagem é simples, legível e tem overhead mínimo.</p>
<pre><code class="language-yaml">env:
- name: NODE_ENV
value: "production"
- name: CACHE_TTL
valueFrom:
configMapKeyRef:
name: app-config
key: cache_ttl</code></pre>
<h3>Quando Usar Volumes</h3>
<p>Use volumes quando você precisar atualizar configurações sem reiniciar o pod, quando tiver muitas configurações (melhor usar um arquivo <code>config.json</code> do que 50 variáveis), ou quando precisar manter a estrutura de um arquivo de configuração existente.</p>
<pre><code class="language-yaml">volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config</code></pre>
<h3>Exemplo Real: Aplicação Node.js</h3>
<p>Vamos ver um cenário completo com uma aplicação Node.js que usa ambas as estratégias:</p>
<p><strong>ConfigMap com múltiplos dados:</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
name: nodejs-config
data:
database.json: |
{
"host": "postgres.default.svc.cluster.local",
"port": 5432,
"pool": {
"min": 2,
"max": 10
}
}
log_level: "debug"
api_timeout: "30000"</code></pre>
<p><strong>Secret com credenciais:</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
name: nodejs-secrets
type: Opaque
data:
db_user: dXNlcm5hbWU= # username
db_pass: cGFzc3dvcmQxMjM= # password123
jwt_secret: andGc2VjcmV0Zm9ydG9rZW4= # jwtsecretfortoken</code></pre>
<p><strong>Deployment injetando ambos:</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-app
spec:
replicas: 2
selector:
matchLabels:
app: nodejs-app
template:
metadata:
labels:
app: nodejs-app
spec:
containers:
- name: nodejs
image: nodejs-app:1.2
ports:
- containerPort: 3000
env:
Variáveis diretas
- name: NODE_ENV
value: "production"
Do ConfigMap
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: nodejs-config
key: log_level
- name: API_TIMEOUT
valueFrom:
configMapKeyRef:
name: nodejs-config
key: api_timeout
Do Secret
- name: DB_USER
valueFrom:
secretKeyRef:
name: nodejs-secrets
key: db_user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: nodejs-secrets
key: db_pass
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: nodejs-secrets
key: jwt_secret
volumeMounts:
- name: config-files
mountPath: /app/config
readOnly: true
volumes:
- name: config-files
configMap:
name: nodejs-config
items:
- key: database.json
path: database.json</code></pre>
<p>Neste exemplo, a aplicação consome variáveis de ambiente simples (<code>LOG_LEVEL</code>, <code>API_TIMEOUT</code>) e sensíveis (<code>DB_PASSWORD</code>, <code>JWT_SECRET</code>), enquanto o arquivo <code>database.json</code> é disponibilizado via volume para acesso estruturado.</p>
<p><strong>Código Node.js que consome essas configurações:</strong></p>
<pre><code class="language-javascript">const express = require('express');
const app = express();
const fs = require('fs');
// Variáveis de ambiente
const logLevel = process.env.LOG_LEVEL | | 'info'; const apiTimeout = parseInt(process.env.API_TIMEOUT) || 30000;
const dbUser = process.env.DB_USER;
const dbPassword = process.env.DB_PASSWORD;
const jwtSecret = process.env.JWT_SECRET;
// Arquivo de configuração via volume
const dbConfig = JSON.parse(
fs.readFileSync('/app/config/database.json', 'utf8')
);
console.log([${logLevel}] Starting app with API timeout: ${apiTimeout}ms);
console.log(Database host: ${dbConfig.host}:${dbConfig.port});
// Conectar ao banco
const dbConnection = {
user: dbUser,
password: dbPassword,
host: dbConfig.host,
port: dbConfig.port,
pool: dbConfig.pool
};
app.get('/health', (req, res) => {
res.json({ status: 'ok', log_level: logLevel });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});</code></pre>
<h2>Boas Práticas e Considerações de Segurança</h2>
<h3>RBAC e Acesso a Secrets</h3>
<p>Sempre configure <strong>Role-Based Access Control (RBAC)</strong> para limitar quem pode ler Secrets. Um desenvolvedor não deve conseguir fazer <code>kubectl get secret</code> em produção:</p>
<pre><code class="language-yaml">apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]</code></pre>
<h3>Encryption at Rest</h3>
<p>Configure encryption at rest no Kubernetes para proteger dados no etcd. No kubeadm:</p>
<pre><code class="language-bash"># Edite /etc/kubernetes/manifests/kube-apiserver.yaml
--encryption-provider-config=/etc/kubernetes/enc/enc.yaml</code></pre>
<h3>Nunca Commite Secrets no Git</h3>
<p>Use ferramentas como <strong>Sealed Secrets</strong> ou <strong>External Secrets Operator</strong> para gerenciar secrets versionados com segurança. Exemplo com Sealed Secrets:</p>
<pre><code class="language-bash">echo -n 'mysecretpassword' | kubectl create secret generic \ my-secret --dry-run=client --from-file=/dev/stdin -o yaml | \
kubeseal -f - > sealed-secret.yaml</code></pre>
<h3>Volume readOnly em Secrets</h3>
<p>Sempre monte volumes de Secret como <code>readOnly: true</code> para prevenir modificações acidentais dentro do container.</p>
<h3>Rotação de Secrets</h3>
<p>Implemente rotação regular de Secrets. Muitos sistemas (AWS, Azure) integram-se com External Secrets Operator para sincronizar automaticamente:</p>
<pre><code class="language-yaml">apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu que ConfigMaps e Secrets são dois lados da mesma moeda — separar configuração do código. A injeção por variáveis de ambiente é simples e ideal para valores imutáveis e pequenas quantidades de dados. A injeção por volume é poderosa para atualizar configurações dinamicamente, manter estruturas de arquivo e gerenciar múltiplas definições sem sobrecarregar a lista de variáveis de ambiente.</p>
<p>O terceiro ponto crítico é que <strong>Secrets não são verdadeiramente criptografados</strong> apenas por estarem base64-encoded — você deve implementar encryption at rest, RBAC, e ferramentas como Sealed Secrets para segurança real em produção. A escolha entre env e volume deve ser guiada pelo seu caso de uso específico: simplicidade versus flexibilidade.</p>
<h2>Referências</h2>
<ol>
<li><a href="https://kubernetes.io/docs/concepts/configuration/configmap/" target="_blank" rel="noopener noreferrer">Kubernetes Official Documentation - ConfigMaps</a></li>
<li><a href="https://kubernetes.io/docs/concepts/configuration/secret/" target="_blank" rel="noopener noreferrer">Kubernetes Official Documentation - Secrets</a></li>
<li><a href="https://github.com/bitnami-labs/sealed-secrets" target="_blank" rel="noopener noreferrer">Sealed Secrets - Bitnami</a></li>
<li><a href="https://external-secrets.io/" target="_blank" rel="noopener noreferrer">External Secrets Operator Documentation</a></li>
<li><a href="https://kubernetes.io/docs/concepts/security/secrets-good-practices/" target="_blank" rel="noopener noreferrer">Kubernetes Security Best Practices - CNCF</a></li>
</ol>
<p><!-- FIM --></p>