Adicionando NFS como Default Volume Storage no Kubernetes

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!

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s