Execute Aplicações Stateful com Armazenamento Persistente

KubernetesBeginner
Pratique Agora

Introdução

Os Deployments tratam as réplicas como intercambiáveis. Aplicações stateful geralmente precisam de nomes estáveis para os Pods, uma ordem de inicialização previsível e armazenamento que sobreviva à substituição de Pods. Os StatefulSets combinam essas identidades com claims individuais para cada Pod.

Você examinará o armazenamento dinâmico, implantará um StatefulSet com duas réplicas, gravará dados, substituirá um Pod e comprovará que tanto a identidade quanto os dados permanecem disponíveis.

Acompanhe separadamente três identidades: o StatefulSet controla o número desejado de réplicas, cada Pod recebe um ordinal estável, como -0, e cada ordinal recebe seu próprio PersistentVolumeClaim. O Pod pode ser substituído enquanto o claim e os dados armazenados permanecem intactos.

Inspecionar o Armazenamento Dinâmico

Nesta etapa, examine a StorageClass do cluster antes de solicitar armazenamento.

Um PersistentVolumeClaim descreve o que uma aplicação precisa; uma StorageClass descreve como o cluster pode provisionar esse armazenamento. Verificar primeiro as classes disponíveis evita criar um claim que nunca consiga ser associado a um volume.

cd /home/labex/project/stateful-storage
kubectl get storageclasses
kubectl get storageclass -o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner,DEFAULT:.metadata.annotations.storageclass\.kubernetes\.io/is-default-class'

A StorageClass padrão cria dinamicamente um PersistentVolume quando um claim não especifica storageClassName. Isso evita a necessidade de configurar um hostPath específico de um nó nos manifestos do laboratório.

Procure por (default) na tabela normal ou por true na coluna personalizada DEFAULT. O nome do provisionador é um detalhe de implementação deste ambiente; o importante é que exista uma classe padrão.

Neste ambiente, o resultado é:

NAME                 PROVISIONER                ...
standard (default)   k8s.io/minikube-hostpath   ...

NAME       PROVISIONER                DEFAULT
standard   k8s.io/minikube-hostpath   true

Portanto, standard é a classe padrão. Como o StatefulSet não especificará storageClassName, o Kubernetes escolherá essa classe automaticamente.

Criar um StatefulSet e um Service Headless

Nesta etapa, crie um Service headless para fornecer DNS estável e um StatefulSet com um claim para cada réplica.

Esse manifesto apresenta vários campos relacionados entre si. Leia-os antes de aplicá-lo:

  • clusterIP: None torna o Service headless, permitindo que o DNS represente cada Pod individualmente.
  • serviceName: stateful-web associa o StatefulSet a esse Service.
  • volumeClaimTemplates cria um claim cujo nome é formado a partir do template e do ordinal do Pod.
  • volumeMounts monta o claim exclusivo de cada Pod no diretório de documentos do NGINX.

Os labels continuam realizando a conhecida seleção entre Service e Pods, apresentada no primeiro curso.

cd /home/labex/project/stateful-storage
cat <<'EOF' > stateful-web.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-stateful}
---
apiVersion: v1
kind: Service
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
  clusterIP: None
  selector: {app: stateful-web}
  ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
  serviceName: stateful-web
  replicas: 2
  selector: {matchLabels: {app: stateful-web}}
  template:
    metadata: {labels: {app: stateful-web}}
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          imagePullPolicy: IfNotPresent
          ports: [{name: http, containerPort: 80}]
          volumeMounts: [{name: web-data, mountPath: /usr/share/nginx/html}]
  volumeClaimTemplates:
    - metadata: {name: web-data}
      spec:
        accessModes: [ReadWriteOnce]
        resources: {requests: {storage: 100Mi}}
EOF

Aplique o manifesto e aguarde a implantação ordenada. Normalmente, os StatefulSets criam primeiro o ordinal 0 e depois o ordinal 1, portanto o segundo Pod pode aparecer um pouco mais tarde:

kubectl apply -f stateful-web.yaml
kubectl rollout status statefulset/stateful-web -n practice-stateful --timeout=90s
kubectl get pods,pvc -n practice-stateful

Os nomes dos Pods usam ordinais estáveis: stateful-web-0 e stateful-web-1. Cada claim inclui o nome do template, o nome do Pod e o ordinal.

Você deverá ver os claims web-data-stateful-web-0 e web-data-stateful-web-1, ambos com status Bound. Isso significa que o Kubernetes encontrou ou provisionou dinamicamente um armazenamento capaz de atender a cada solicitação.

As mensagens da implantação tornam o comportamento ordenado visível, seguidas pelos dois claims:

Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...

pod/stateful-web-0   1/1   Running   ...
pod/stateful-web-1   1/1   Running   ...

persistentvolumeclaim/web-data-stateful-web-0   Bound   ...   100Mi   RWO   standard
persistentvolumeclaim/web-data-stateful-web-1   Bound   ...   100Mi   RWO   standard

Os IDs dos volumes gerados são diferentes a cada execução. Concentre-se nos nomes estáveis dos claims, no status Bound, na capacidade solicitada e na classe de armazenamento.

Gravar Dados Persistentes Específicos de Cada Pod

Nesta etapa, grave conteúdos diferentes no claim montado de cada Pod.

Usamos kubectl exec ... -- sh -c porque o redirecionamento de saída (>) precisa ser executado dentro do contêiner. Sem sh -c, o shell local tentaria redirecionar a saída na VM, e não dentro do Pod.

cd /home/labex/project/stateful-storage
kubectl exec stateful-web-0 -n practice-stateful -- sh -c 'echo persistent-zero > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-1 -n practice-stateful -- sh -c 'echo persistent-one > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl exec stateful-web-1 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pods -n practice-stateful -o custom-columns='POD:.metadata.name,HOSTNAME:.spec.hostname,CLAIM:.spec.volumes[0].persistentVolumeClaim.claimName'

Os Pods executam a mesma imagem, mas montam claims diferentes; por isso, seus dados não são compartilhados nem intercambiáveis.

Os dois comandos cat deverão retornar strings diferentes. Em seguida, a tabela personalizada relacionará cada nome estável de Pod ao respectivo claim, tornando visível essa relação de armazenamento.

persistent-zero
persistent-one
POD              HOSTNAME         CLAIM
stateful-web-0   stateful-web-0   web-data-stateful-web-0
stateful-web-1   stateful-web-1   web-data-stateful-web-1

O ordinal aparece nas três colunas. Esse 0 ou 1 repetido representa a identidade que associa um Pod ao seu próprio armazenamento.

Substituir um Pod e Recuperar seus Dados

Nesta etapa, exclua o ordinal 0 e deixe o StatefulSet recriar a mesma identidade.

Excluir um Pod gerenciado não é o mesmo que reduzir o número desejado de réplicas. O StatefulSet ainda exige dois Pods, portanto criará um substituto chamado stateful-web-0 e remontará o claim reservado para esse ordinal.

cd /home/labex/project/stateful-storage
kubectl delete pod stateful-web-0 -n practice-stateful --wait=true
kubectl wait --for=condition=Ready pod/stateful-web-0 -n practice-stateful --timeout=90s
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pod stateful-web-0 -n practice-stateful -o jsonpath='pod={.metadata.name} claim={.spec.volumes[0].persistentVolumeClaim.claimName}{"\n"}'

O objeto Pod é novo, mas seu nome e sua associação com o claim permanecem estáveis. O arquivo recuperado comprova que os dados pertenciam ao PersistentVolume, e não ao sistema de arquivos do contêiner excluído.

pod "stateful-web-0" deleted from practice-stateful namespace
pod/stateful-web-0 condition met
persistent-zero
pod=stateful-web-0 claim=web-data-stateful-web-0

A linha mais importante é persistent-zero: ela foi gravada antes da exclusão e lida pelo Pod substituto.

Se o arquivo tivesse sido gravado fora do diretório montado, ele teria desaparecido junto com o sistema de arquivos do contêiner antigo. A persistência depende do caminho de montagem, e não apenas do nome do StatefulSet.

Resumo

Você relacionou a StorageClass padrão, o provisionamento dinâmico de PVCs, um Service headless, os ordinais estáveis de um StatefulSet e o armazenamento individual de cada Pod. Ao substituir um Pod, tanto sua identidade quanto seus dados foram preservados.