Na primeira parte do tutorial vimos o que são os PV e PVCs o relacionamento que existe entre eles e os Volumes nos Pods. Criamos um compartilhamento NFS em um servidor separado do Cluster, realizamos as configurações necessárias nos Worker Nodes e por fim criamos um PV e PVC manualmente e subimos um Pod com NGinx em que as páginas do site ficavam armazenadas neste volume. Nossa quanta coisa !
Agora vamos refinar a nossa configuração do Cluster para que ela funcione da seguinte maneira. Os usuários precisam apenas criar um PVC já com um manifesto mais simplificado e ele automaticamente irá criar um PV e realizar o Bound com o Storage. Esse modelo é o que normalmente você vai encontrar quando faz uso de um cluster em uma Cloud Pública como Azure, GCP e etc. Além de tornar bem mais simples a utilização elimina também qualquer iteração manual realizando todas as tarefas de forma automatizada.
Problema recorrente.
Para quem já deve ter tentado fazer essa configuração provavelmente já deve ter encontrado uma infinidade de tutoriais. No meu caso não foi diferente, o problema é que quando eu ia realizar as etapas de configuração sempre esbarrava em um problema na hora em que o Kubernetes tentava fazer o Bound do Volume, ficando no estado de Pending eternamente.
Quando consultava os logs havia essa mensagem:
"unexpected error getting claim reference: selfLink was empty, can't make reference"
Fiquei algum tempo tentando entender melhor o problema e, fazendo algumas pesquisas no Google, encontrei a raiz do meu problema. Havia uma issue no GitHub relativa a versão 1.20+ do Kubernetes que relatava exatamente esse sintoma. O que me deixou de certa forma um pouco menos frustrado afinal, outras pessoas já tinham passado por isso também.
A princípio para contornar o problema seria necessário realizar um Workaround. Nele a instrução basicamente era alterar uma configuração interna do Kubernetes, o que particularmente senti um friozinho na espinha. Ninguém quer ter um cluster Kubernetes com esse tipo de configuração. Quando se altera um desses parâmetros internos não garantias de que não ocorram efeitos colaterais que façam com que o cluster passe a ter um comportamento inesperado.
Porém, seguindo mais abaixo na issue havia a informação de que aquela configuração não era mais necessária, pois o problema havia sido resolvido em outra imagem do container que era utilizada. Alterei a imagem para a nova versão e, Voila! tudo funcionando como deveria.
Depois desse pequeno preâmbulo podemos seguir com o tutorial. Vamos lá?!
Melhorando nosso Modelo
Como esse post é uma sequência direta do post anterior não está a parte de configuração do servidor NFS e nem dos Worker Nodes. Clique aqui para acessá-lo.
Como eu havia dito o nosso novo modelo terá o provisionamento realizado de forma automática. Para isso iremos criar um pod que chamaremos de nfs-client-provisioner. Este pod basicamente será responsável por receber um pedido de criação de PVC vindo de um Storage Class personalizado que iremos criar. Ele então irá fazer a criação do PV de forma dinâmica e depois associá-lo ao PVC que está sendo criado.

Configuração do Serviço
Para facilitar o entendimento vamos criar todos os manifestos que vamos utilizar passando por cada um deles e parando nas partes mais importantes e no final iremos aplicá-los
Criação dos manifestos
Para deixar as coisas organizadas no nosso cluster vamos primeiramente criar um namespace para deixar dedicado para isso. Crie um arquivo chamado nfs-provisioner-namespace.yaml com o conteúdo abaixo:
# nfs-provisioner-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
kubernetes.io/metadata.name: nfs-provisioner
name: nfs-provisioner
Agora vamos começar a criar os objetos dentro deste namespace. O primeiro que iremos criar será a Service Account que será utilizada para realizar as operações. Além disso, criaremos também algumas roles que serão utilizadas por ela. Crie um arquivo chamado nfs-client-provisioner-rbac.yaml com o seguinte conteúdo:
# nfs-client-provisioner-rbac.yaml
kind: ServiceAccount
apiVersion: v1
metadata:
name: nfs-client-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: nfs-provisioner
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: nfs-provisioner
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
Agora que temos a Service Account criada podemos fazer o deploy do nosso pod que é quem efetivamente irá realizar as operações de provisionamento. Para isso crie um arquivo chamado nfs-client-provisioner-deploy.yaml com o seguinte conteúdo
# nfs-client-provisioner-deploy.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-client-provisioner
namespace: nfs-provisioner
spec:
selector:
matchLabels:
app: nfs-client-provisioner
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: gcr.io/k8s-staging-sig-storage/nfs-subdir-external-provisioner:v4.0.0
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: managed-nfs-storage
- name: NFS_SERVER
value: 192.168.122.40
- name: NFS_PATH
value: /srv/nfs/data
resources:
requests:
memory: "64Mi"
cpu: "150m"
limits:
memory: "128Mi"
cpu: "250m"
volumes:
- name: nfs-client-root
nfs:
server: 192.168.122.40
path: /srv/nfs/data
Vamos nos atentar a alguns pontos importantes nesse arquivo:
- Linha 22 : define qual imagem será utilizada para o provisionador. É aqui que eu atualizei a versão da imagem para corrigir o problema que eu relatei em Problema recorrente.
- Linhas 26 – 32: varíaveis de ambiente que são utilizadas para configurar o servidor NFS.
Por fim vamos criar um último manifesto chamado: managed-nfs-storage-class.yaml esse tem um papel muito importante pois é nele que ligamos o tipo novo de storage class com o pod que acabamos de fazer o deploy no passo anterior.
# managed-nfs-storage-class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
namespace: nfs-provisioner
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: managed-nfs-storage
parameters:
archiveOnDelete: "false"
Na linha 8 que definimos que o esse Storage Class será o default do cluster.
Aplicando os manifestos
Os passos a seguir levam em consideração os nomes que foram informados nos passos anteriores. Se você por acaso tenha utilizado outra nomenclatura, simplesmente faça os ajustes nos nomes dos arquivos, mas siga a ordem abaixo.
# Criando o Name Space nfs-provisioner
kubectl apply -f nfs-provisioner-namespace.yaml
namespace/nfs-provisioner created
# Criando a Service Account e as Roles
kubectl apply -f nfs-client-provisioner-rbac.yaml
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner configured
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
# Deploy do provisionador
kubectl apply -f nfs-client-provisioner-deploy.yaml
deployment.apps/nfs-client-provisioner created
# Criando a Storage Class personalizada
kubectl apply -f managed-nfs-storage-class.yaml
storageclass.storage.k8s.io/managed-nfs-storage created
Vamos verificar se o nosso pod subiu corretamente, para isso rode o comando abaixo:
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/nfs-client-provisioner-6b7b8c477c-v8lct 1/1 Running 0 16s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nfs-client-provisioner 1/1 1 1 16s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nfs-client-provisioner-6b7b8c477c 1 1 1 16s
Agora vamos verificar se o nosso novo storage class foi configurado para ser o default, para isso:
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
managed-nfs-storage (default) managed-nfs-storage Delete Immediate false 65m
Parece que está tudo certo agora vamos criar um PVC para verificar se ele será criado corretamente. Crie um arquivo chamado pvc-auto-nfs-test.yaml com o conteúdo:
# pvc-auto-nfs-test.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs-sc
namespace: nfs-provisioner
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
Note que não é mais necessário informar qual storageclass queremos utilizar uma vez que já definimos um default. Vamos aplicar e rodar alguns comandos para verificar se ele foi criado corretamente:
$ kubectl apply -f pvc-auto-nfs-test.yaml
persistentvolumeclaim/pvc-nfs-sc created
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs-sc Bound pvc-29d86505-8e2e-4c5d-beac-c6cd8a8ef421 1Gi RWX managed-nfs-storage 5s
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
nginx-site-pv 200Mi RWX Retain Bound site/site-nginx-pvc manual 4h32m
pvc-29d86505-8e2e-4c5d-beac-c6cd8a8ef421 1Gi RWX Delete Bound nfs-provisioner/pvc-nfs-sc managed-nfs-storage 19s
$ kubectl describe pvc pvc-nfs-sc
Name: pvc-nfs-sc
Namespace: nfs-provisioner
StorageClass: managed-nfs-storage
Status: Bound
Volume: pvc-29d86505-8e2e-4c5d-beac-c6cd8a8ef421
Labels: <none>
Annotations: pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
volume.beta.kubernetes.io/storage-provisioner: managed-nfs-storage
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 1Gi
Access Modes: RWX
VolumeMode: Filesystem
Used By: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ExternalProvisioning 38s persistentvolume-controller waiting for a volume to be created, either by external provisioner "managed-nfs-storage" or manually created by system administrator
Normal Provisioning 38s managed-nfs-storage_nfs-client-provisioner-6b7b8c477c-v8lct_103c7770-ee26-4891-b3f3-ea08dd33ad44 External provisioner is provisioning volume for claim "nfs-provisioner/pvc-nfs-sc"
Normal ProvisioningSucceeded 38s managed-nfs-storage_nfs-client-provisioner-6b7b8c477c-v8lct_103c7770-ee26-4891-b3f3-ea08dd33ad44 Successfully provisioned volume pvc-29d86505-8e2e-4c5d-beac-c6cd8a8ef421
Teste de Funcionamento
Para esse teste utilizaremos os mesmo cenário do post anterior que configuramos de forma manual o NFS como nosso Storage. Isso é proposital para que você consiga perceber as vantagens que tivemos utilizando esse configuração.
Vamos criar todos os manifestos e depois aplicamos como da ultima vez. Vamos começar criando o manifesto que cria o PVC. Crie um arquivo chamado pvc-site-nginx.yaml contendo:
# pvc-site-nginx.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: site-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Mi
Agora vamos criar o nginx-site-deployment.yaml :
# nginx-site-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: site-qa
spec:
replicas: 1
selector:
matchLabels:
app: site-nginx
template:
metadata:
labels:
app: site-nginx
env: qa
spec:
volumes:
- name: site-nginx-html
persistentVolumeClaim:
claimName: site-pvc
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
volumeMounts:
- name: site-nginx-html
mountPath: /usr/share/nginx/html
resources:
requests:
memory: "64Mi"
cpu: "150m"
limits:
memory: "128Mi"
cpu: "250m"
E por fim o do serviço nginx-service.yaml :
# nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-qa
spec:
selector:
app: site-nginx
ports:
- protocol: TCP
port: 80
Crie um página html com o nome de index.html:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>NFS VOLUME</title>
<meta name="description" content="Simple page demonstration NFS Volume">
<meta name="author" content="QuickwinsIT">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Hello NFS Volume <strong>Automatizado</strong>.</p>
</body>
</html>
Vamos aplicar os manifestos:
# Cria PVC
$ kubectl apply -f pvc-site-nginx.yaml
persistentvolumeclaim/site-pvc created
# Faz deploy da aplicação
$ kubectl apply -f nginx-site-deployment.yaml
deployment.apps/site-qa created
# Cria serviço para expor a url
$ kubectl apply -f nginx-service.yaml
service/svc-qa created
# Obtém o nome do pod para ser utilizado na cópia do index.html
$ kubectl get pods -l app=site-nginx
NAME READY STATUS RESTARTS AGE
site-qa-746949dc87-4d227 1/1 Running 0 6m37s
# Faz a cópia do arquivo para o volume
$ kubectl cp index.html site-qa-746949dc87-4d227:/usr/share/nginx/html
# Faz o port-forward para permitir o teste local
$ kubectl port-forward svc/svc-qa :80
Forwarding from 127.0.0.1:33051 -> 80
Forwarding from [::1]:33051 -> 80
Handling connection for 33051
Acesse a url a partir do seu browser apontando para http://127.0.0.1:33051

Conclusão
Chegamos ao final da configuração e com isso temos um cluster on premises com o provisionamento de Storage automatizado. Agora sabemos como funciona de baixo do capô a criação automática de PVs e para que serve o Storage Class.
Espero que essas informações possam ser uteis a todos. Até mais!