Projected Volumes: Combinando Várias Secrets
Objective
Um volume secret comum monta exatamente uma Secret. Quando um container precisa de credenciais de várias Secrets (uma por tenant, uma por sistema externo), um volume projected junta todas em um único diretório. É o jeito mais limpo de dar à aplicação um lugar conhecido, como /run/secrets, de onde ler tudo. Ele também tem uma armadilha: quando duas sources têm a mesma chave, o Kubernetes não falha nem avisa, e a última source vence em silêncio. Este conceito cobre o volume projected, essa colisão, e como items e path mantêm as sources separadas.
Use Cases
- Um serviço multi-tenant que precisa das credenciais de
tenant-alphaetenant-betaao mesmo tempo, com cada Secret tendo dono e rotação independentes. - Um único ponto de montagem com credenciais, configuração e metadados do Pod juntos (sources
secret+configMap+downwardAPI). - Dar a uma aplicação uma única árvore de diretórios de onde ler todas as suas credenciais.
- Um token de ServiceAccount de curta duração e com audience definida (source
serviceAccountToken) ao lado das Secrets da própria aplicação.
Deep Dive
O ponto de partida
Duas Secrets criadas a partir de env files:
plaintextkubectl -n example create secret generic tenant-alpha \ --from-env-file=secrets/example/alpha.env kubectl -n example create secret generic tenant-beta \ --from-env-file=secrets/example/beta.env
E um volume projected que monta as duas no mesmo diretório:
plaintextvolumes: - name: secrets projected: sources: - secret: name: tenant-alpha - secret: name: tenant-beta volumeMounts: - name: secrets mountPath: /run/secrets readOnly: true
Cada chave de cada source vira um arquivo com o nome da chave: /run/secrets/<CHAVE>, conteúdo = valor. Com chaves disjuntas, é exatamente o que você quer.
A colisão silenciosa
Env files do mesmo tipo de sistema tendem a usar os mesmos nomes:
plaintext# alpha.env # beta.env DB_USER=alpha_app DB_USER=beta_app DB_PASSWORD=alpha-s3cret DB_PASSWORD=beta-s3cret ALPHA_API_KEY=ak-alpha-111 BETA_WEBHOOK_TOKEN=wt-beta-222
As duas Secrets agora têm DB_USER e DB_PASSWORD, e as duas querem escrever /run/secrets/DB_PASSWORD. O que acontece, verificado no k3s v1.36:
plaintext$ kubectl -n example exec app -- cat /run/secrets/DB_PASSWORD beta-s3cret
O API server aceita o Pod, o kubelet monta o volume, nenhum evento é emitido, e a última source da lista vence. A aplicação do tenant alpha agora autentica com a senha do tenant beta. Inverta a ordem das sources e o resultado inverte junto. Compare com o próprio kubectl create secret, que recusa chave duplicada na hora (cannot add key DB_PASSWORD, another key by that name already exists): a checagem que você tem na criação não existe na projeção.
Separando as sources com items
items mapeia uma chave para um caminho dentro do volume, e o caminho pode ter diretórios:
plaintextvolumes: - name: secrets projected: sources: - secret: name: tenant-alpha items: - key: DB_USER path: alpha/DB_USER - key: DB_PASSWORD path: alpha/DB_PASSWORD - key: ALPHA_API_KEY path: alpha/ALPHA_API_KEY - secret: name: tenant-beta items: - key: DB_USER path: beta/DB_USER - key: DB_PASSWORD path: beta/DB_PASSWORD - key: BETA_WEBHOOK_TOKEN path: beta/BETA_WEBHOOK_TOKEN
Resultado:
plaintext/run/secrets/alpha/DB_USER alpha_app /run/secrets/alpha/DB_PASSWORD alpha-s3cret /run/secrets/alpha/ALPHA_API_KEY ak-alpha-111 /run/secrets/beta/DB_USER beta_app /run/secrets/beta/DB_PASSWORD beta-s3cret /run/secrets/beta/BETA_WEBHOOK_TOKEN wt-beta-222
Duas propriedades de items para ter em mente:
- É uma allow-list. Assim que uma source tem
items, só as chaves listadas são projetadas. Uma chave esquecida (digamosALPHA_API_KEY) não é erro, ela simplesmente não está lá. - Uma chave listada que não existe é erro. O volume não monta e o Pod fica em
ContainerCreatingcom um eventoFailedMount(references non-existent secret key), a menos que a source esteja marcada comoptional: true. O mesmo vale para uma Secret inexistente.
Como a montagem fica no disco
plaintext$ ls -la /run/secrets ..2026_09_25_09_19_37.1291824148/ ..data -> ..2026_09_25_09_19_37.1291824148 alpha -> ..data/alpha beta -> ..data/beta
O kubelet escreve cada nova versão em um diretório novo com timestamp e depois troca o symlink ..data, então a atualização é atômica: quem lê nunca vê metade das credenciais antigas do alpha e metade das novas. readOnly: true vale para a montagem inteira, subdiretórios incluídos, e o volume é tmpfs, nunca escrito no disco do nó.
As permissões dos arquivos vêm de defaultMode no volume projected (padrão 0644), ou de mode por item. 0400 é um endurecimento comum quando o container roda com um usuário não-root conhecido.
Misturando tipos de source
As sources não se limitam a Secrets:
plaintextprojected: sources: - secret: name: tenant-alpha items: [{ key: DB_PASSWORD, path: alpha/DB_PASSWORD }] - configMap: name: app-settings - downwardAPI: items: - path: pod/namespace fieldRef: { fieldPath: metadata.namespace } - serviceAccountToken: audience: vault expirationSeconds: 3600 path: tokens/vault
Todas as Secrets e ConfigMaps referenciados precisam estar no namespace do Pod. A mesma regra de colisão vale entre tipos de source diferentes (uma chave DB_PASSWORD num ConfigMap listado depois sobrescreve a da Secret), então dê a cada source o seu próprio diretório.
Lendo a partir da aplicação
A maioria das aplicações só precisa de um caminho: ler /run/secrets/alpha/DB_PASSWORD ao abrir uma conexão. Muitas bibliotecas de configuração também carregam uma árvore de diretórios inteira, usando nomes de pastas e arquivos como chaves, então os diretórios por tenant criados com items viram prefixos de configuração por tenant de graça.
Trade-offs
- Um volume projected vs um volume
secretpor Secret. Volumes separados em caminhos separados tornam a colisão impossível por construção, mas cada tenant novo mexe emvolumese emvolumeMounts. Um volume projected é um único ponto de montagem, ao preço de você mesmo ter que separar as chaves.plaintext# alternativa à prova de colisão: uma montagem por Secret volumeMounts: - { name: alpha, mountPath: /run/secrets/alpha, readOnly: true } - { name: beta, mountPath: /run/secrets/beta, readOnly: true } itemsfaz o spec do Pod depender da lista de chaves. Adicionar uma chave na Secret não chega à aplicação até o spec do Pod listá-la também. É explícito e revisável, mas também é mais um lugar para esquecer.- O last-wins silencioso é um bug de runtime, não um erro de deploy. Nada no cluster o detecta. Cubra com um teste que lê os arquivos montados, como faz o lab, ou com uma policy (Kyverno, OPA Gatekeeper) que exija
itemsem sources de secret dentro de volumes projected.