Armazenamento
Fazer dados sobreviverem a Pods projetados para desaparecer.
Armazenamento persistente
Tudo o que você aprendeu até aqui se baseia em uma hipótese conveniente: os Pods são intercambiáveis e descartáveis. Essa hipótese é correta para uma API sem estado, e desmorona assim que há dados a conservar. O sistema de arquivos de um contêiner morre com ele: destrua o Pod de um banco PostgreSQL implantado de forma ingênua, e as tabelas se perdem. Não ficam corrompidas — ficam perdidas.
Não basta “montar um volume”. A dificuldade é que, em um cluster, disco e computação são separados: um Pod pode ser reprogramado em outro nó, e um diretório local do nó de origem passa a ficar inacessível. O armazenamento precisa, portanto, ser um recurso do cluster, não do nó, e esse recurso precisa acompanhar o Pod.
O Kubernetes responde com um desacoplamento em três objetos que desconcerta à primeira vista, mas tem uma lógica clara. O PersistentVolume representa um pedaço de armazenamento real. O PersistentVolumeClaim é uma solicitação feita pela aplicação: “preciso de 10 Gio em leitura e escrita”. A StorageClass descreve como esses volumes são fabricados sob demanda. O desenvolvedor escreve apenas a solicitação; ele não sabe — e não precisa saber — se vai obter um disco EBS, um volume Ceph ou um diretório local.
Depois vem uma pergunta que não dá para contornar: mesmo com um volume que sobrevive, um Deployment continua inadequado para um banco de dados. Seus Pods são anônimos, criados em paralelo, e compartilhariam o mesmo volume. O StatefulSet existe para esse caso preciso, e esta lição explica por que ele não é simplesmente um Deployment melhorado.
Os volumes efêmeros e seu bom uso
Antes do armazenamento persistente, uma palavra sobre o emptyDir, frequentemente mal compreendido. Ele cria um diretório vazio na inicialização do Pod, compartilhado entre seus contêineres, e destruído quando o Pod desaparece.
apiVersion: v1
kind: Pod
metadata:
name: pod-avec-cache
spec:
containers:
- name: app
image: mon-api:1.0.0
volumeMounts:
- name: cache
mountPath: /var/cache/app
- name: collecteur-logs
image: busybox:1.36
command: ["sh", "-c", "tail -F /var/cache/app/app.log"]
volumeMounts:
- name: cache
mountPath: /var/cache/app
volumes:
- name: cache
emptyDir:
sizeLimit: 500Mi
Isso não é um paliativo: é a ferramenta certa para um cache, um diretório temporário ou um espaço de troca entre dois contêineres do mesmo Pod. O emptyDir só se torna um erro quando se confiam a ele dados que precisam sobreviver. Adicione medium: Memory para obter um tmpfs na RAM — rápido, mas contabilizado no limite de memória do Pod.
PersistentVolume e PersistentVolumeClaim
O volume, do lado da infraestrutura
Um PersistentVolume é um objeto do cluster, independente de qualquer Pod. Ele tem seu próprio ciclo de vida.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-donnees-01
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manuel
hostPath:
path: /mnt/donnees/pv-01 # démonstration locale uniquement
O hostPath merece um aviso franco: ele aponta para um diretório do nó, portanto o volume só pode ser usado por um Pod alocado nesse nó. Serve para um cluster local de um único nó, para entender o mecanismo, e deve ser evitado em qualquer outro lugar. Em produção, o campo de tipo é substituído por um driver CSI que conversa com o armazenamento real do provedor.
A solicitação, do lado da aplicação
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: donnees-app
namespace: atelier
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: manuel # doit correspondre à celui du PV visé
O Kubernetes procura um PersistentVolume compatível — capacidade suficiente, modo de acesso compatível, mesmo storageClassName — e faz a ligação (binding). Um PV vinculado a um PVC fica reservado exclusivamente: nenhum outro PVC o obterá.
kubectl get pv
kubectl get pvc -n atelier
# STATUS = Pending ? La cause est dans les événements.
kubectl describe pvc donnees-app -n atelier
Um PVC travado em Pending quase sempre tem uma destas três causas: nenhum PV com capacidade suficiente, um storageClassName que não corresponde a nada, ou um modo de acesso que o armazenamento não suporta. Os eventos do describe dizem isso explicitamente — é um dos raros lugares em que a mensagem de erro do Kubernetes é diretamente aproveitável.
Usar o PVC em um Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-avec-donnees
spec:
replicas: 1 # voir plus bas : ReadWriteOnce impose 1 réplique
selector:
matchLabels:
app: app-donnees
template:
metadata:
labels:
app: app-donnees
spec:
containers:
- name: app
image: mon-api:1.0.0
volumeMounts:
- name: donnees
mountPath: /var/lib/app/donnees
volumes:
- name: donnees
persistentVolumeClaim:
claimName: donnees-app
O contêiner enxerga um diretório comum. Toda a mecânica de ligação lhe é invisível, e é justamente esse o objetivo do desacoplamento.
Os modos de acesso
É o campo mais frequentemente mal compreendido, porque não depende do Kubernetes, e sim da tecnologia de armazenamento subjacente.
| Modo | Abreviação | Significado |
|---|---|---|
ReadWriteOnce | RWO | montável em leitura e escrita por um único nó |
ReadOnlyMany | ROX | montável somente leitura por vários nós |
ReadWriteMany | RWX | montável em leitura e escrita por vários nós |
ReadWriteOncePod | RWOP | um único Pod, mesmo no mesmo nó |
O que seu armazenamento realmente aceita se lê nos volumes existentes, nunca em uma documentação genérica:
kubectl get pv -o custom-columns=NOM:.metadata.name,MODES:.spec.accessModes,CLASSE:.spec.storageClassName
Dois esclarecimentos decisivos. Primeiro, ReadWriteOnce se aplica ao nó, não ao Pod: vários Pods de um mesmo nó podem compartilhar um volume RWO. É o ReadWriteOncePod que garante exclusividade ao Pod, o que um banco de dados exige.
Segundo, e é o que trava os iniciantes: a grande maioria dos armazenamentos em blocos oferece apenas RWO. Os discos EBS da AWS, os Persistent Disks do Google e os Managed Disks do Azure só se anexam a uma máquina por vez. O ReadWriteMany exige um sistema de arquivos de rede — NFS, AWS EFS, Azure Files, CephFS — frequentemente mais lento e sempre provisionado à parte.
Consequência prática imediata: um Deployment com replicas: 3 e um PVC em ReadWriteOnce não funcionará de forma confiável. Se os Pods forem parar em nós diferentes, aqueles que não estão no nó dono do volume permanecem em Pending ou em ContainerCreating. Isso não é uma esquisitice do Kubernetes, é a natureza do armazenamento em blocos.
As StorageClass e o provisionamento dinâmico
Criar os PersistentVolumes manualmente não escala. Uma StorageClass descreve um tipo de armazenamento, e um provisionador fabrica os volumes sob demanda, no momento em que um PVC chega.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rapide-ssd
provisioner: kubernetes.io/no-provisioner # à remplacer par le pilote réel
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
allowVolumeExpansion: true
Três campos a entender, em vez de simplesmente copiar:
provisionerdesigna o driver que cria o armazenamento. Ele é específico do provedor:ebs.csi.aws.comna AWS,pd.csi.storage.gke.iono Google Cloud,disk.csi.azure.comno Azure,rancher.io/local-pathcom o provisionador local do kind.volumeBindingMode: WaitForFirstConsumeradia a criação do volume até que um Pod seja agendado. Quase sempre é a configuração correta: o volume é então criado na mesma zona de disponibilidade do Pod. ComImmediate, um volume pode nascer em uma zona onde o Pod jamais poderá ser colocado, e o Pod fica travado.allowVolumeExpansion: trueautoriza o aumento posterior do PVC. Sem esse campo, o único caminho é backup, novo volume e restauração.
Com uma StorageClass, o PVC fica mais curto, e nenhum PV é escrito manualmente:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: donnees-postgres
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: rapide-ssd
# Les classes disponibles ; (default) marque celle utilisée si on omet le champ
kubectl get storageclass
# Le PV a été créé automatiquement au moment de la liaison
kubectl get pv
Deixemos claro: o que você obtém depende inteiramente do provedor. A mesma StorageClass não existe de um cluster para outro, um manifesto com storageClassName: gp3 falha em qualquer lugar que não seja a AWS, e não existe um nome universal. É o ponto em que a portabilidade do Kubernetes termina, e é melhor saber disso antes de prometer uma implantação multicloud.
Omitir storageClassName faz com que seja aplicada a classe marcada como padrão, se o cluster tiver uma. É conveniente em desenvolvimento, e frágil assim que um manifesto passa a circular entre ambientes.
A política de recuperação
Ela decide o destino do armazenamento real quando o PVC é excluído:
Delete: o volume e os dados são excluídos. É o valor padrão do provisionamento dinâmico.Retain: o volume permanece com seus dados, no statusReleased. É preciso intervir manualmente para reutilizá-lo.
Para qualquer dado que importe, use Retain. É sua única proteção contra um kubectl delete pvc infeliz — e seus backups, que continuam indispensáveis: o Kubernetes não faz backup de nada.
# Basculer un PV existant en Retain
kubectl patch pv pv-donnees-01 \
-p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
Por que um Deployment não serve para um banco de dados
Reunamos as razões, porque é a pergunta mais frequente e merece uma resposta precisa.
- Os Pods de um Deployment são anônimos. O nome deles contém um sufixo aleatório que muda a cada recriação. Uma replicação PostgreSQL precisa endereçar uma instância específica, de forma estável.
- Eles compartilham o mesmo PVC. O template de Pod é único, portanto o
claimNameé o mesmo para todos. Três réplicas escreveriam no mesmo volume, o que corrompe um banco — quando o armazenamento permite, o que o RWO geralmente não permite. - Eles são criados e destruídos em paralelo, em ordem indefinida. Um banco com um primário e secundários exige que o primário esteja pronto antes dos secundários.
- A exclusão destrói tudo. Nenhuma noção de identidade estável sobrevive.
O sintoma é imediato em um Deployment de três réplicas compartilhando um PVC em ReadWriteOnce:
kubectl get pods -l app=postgres
# postgres-6b9f7c4d8-2xk4m 1/1 Running (sur le nœud du volume)
# postgres-6b9f7c4d8-7fd2p 0/1 ContainerCreating (volume déjà attaché ailleurs)
# postgres-6b9f7c4d8-q8v1t 0/1 ContainerCreating
O StatefulSet
O StatefulSet responde a essa lista ponto por ponto.
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
spec:
clusterIP: None # Service « headless » : requis par le StatefulSet
selector:
app: postgres
ports:
- port: 5432
name: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres # doit désigner le Service headless ci-dessus
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
value: "appdb"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
# PostgreSQL exige un sous-répertoire quand le point de montage
# contient un lost+found : sans PGDATA, l'initialisation échoue.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: donnees
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "app_user", "-d", "appdb"]
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
# Un PVC distinct est créé pour CHAQUE réplique, automatiquement.
- metadata:
name: donnees
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
O que este manifesto produz, e que nenhum Deployment produz:
kubectl get pods -l app=postgres
# postgres-0, postgres-1, postgres-2 — noms stables, ordinaux, prévisibles
kubectl get pvc
# donnees-postgres-0, donnees-postgres-1, donnees-postgres-2
Quatro garantias concretas:
- Identidade estável. Um
postgres-1recriado continua se chamandopostgres-1e reencontradonnees-postgres-1, com os mesmos dados. - Volume dedicado por réplica, graças ao
volumeClaimTemplates. Chega de compartilhamento, chega de corrupção. - Ordem garantida. Criação do 0, depois do 1, depois do 2, com cada Pod aguardando o anterior ficar pronto. Exclusão na ordem inversa.
- DNS individual. O Service headless dá a cada Pod um nome alcançável:
postgres-0.postgres.atelier.svc.cluster.local. É isso que permite configurar uma replicação.
Dois pontos de atenção. Primeiro, os PVCs criados por volumeClaimTemplates não são excluídos junto com o StatefulSet: é uma proteção intencional, e isso significa que uma limpeza completa exige uma exclusão explícita.
kubectl delete statefulset postgres
kubectl get pvc # les PVC sont toujours là, données intactes
kubectl delete pvc donnees-postgres-0 # suppression explicite si voulue
Em segundo lugar, uma observação honesta sobre a operação: um StatefulSet fornece uma identidade e um volume estáveis, nada além disso. Ele não configura a replicação, não gerencia as trocas de primário, não faz backup de nada. Para um banco em produção, usa-se um operador dedicado (CloudNativePG, Zalando Postgres Operator) que traz essa lógica, ou um serviço gerenciado fora do cluster. O StatefulSet é a fundação, não a solução completa.
Aumentar um volume
Se a StorageClass tiver allowVolumeExpansion: true, basta aumentar a solicitação:
kubectl patch pvc donnees-postgres-0 \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl get pvc donnees-postgres-0
Três limites que você precisa conhecer: a redução de tamanho é impossível, alguns drivers exigem a reinicialização do Pod para que o sistema de arquivos seja expandido, e a operação não funciona de forma alguma em um PV provisionado manualmente. Portanto, dimensione com uma margem desde o início.
Pontos-chave
- O sistema de arquivos de um contêiner morre junto com ele. O
emptyDirserve para caches e para trocas entre contêineres de um Pod, nunca para dados que precisam ser mantidos. - O tripé PV / PVC / StorageClass desacopla a infraestrutura da aplicação: a pessoa desenvolvedora escreve apenas uma solicitação (claim), e o provisionador cria o volume. Um PVC em
Pendingquase sempre se explica por uma classe inexistente ou por um modo de acesso não suportado. - O
ReadWriteOncese aplica ao nó, e é o único modo oferecido pela maioria dos armazenamentos em bloco. OReadWriteManyexige um sistema de arquivos de rede, provisionado separadamente — um Deployment com várias réplicas sobre um PVC RWO não vai funcionar. - A StorageClass depende do provedor: provisionador, nomes das classes e políticas de recuperação mudam de uma nuvem para outra. Use
Retainno que é importante e faça backup: o Kubernetes não faz backup de nada. - Para um banco de dados, é preciso um StatefulSet: nomes ordinais estáveis, um PVC por réplica via
volumeClaimTemplates, inicialização ordenada e DNS individual por meio de um Service headless. Ele fornece a base, não a replicação nem os failovers — isso é papel de um operador ou de um serviço gerenciado.
Dica: pratique excluindo um Pod de um StatefulSet (
kubectl delete pod postgres-1) e verificando se ele volta com o mesmo nome e os mesmos dados. É a demonstração do que o Deployment não consegue oferecer.
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão