home..

PV 초기화하기

kubernetes msa aws eks volume storage PV PVC ansible jinja2

초기화 PVC 생성 Playbook

온프레미스에서 스토리지 이슈

온프레미스에서 쿠버네티스 클러스터를 운영하면서, volume을 위해 외부 스토리지 서비스를 이용하면서도 알 수 없는 논리적인 오류들이 발생해 volume을 정상적으로 사용할 수 없는 상황들이 존재했습니다.

첫 번째 사례로는 volumeattachment 이라는 API 오브젝트에 의해 CSI 드라이버를 통해 PV를 mount, unmount 하는 과정을 거칩니다. 클러스터 자체의 크기와 스토리지 서버에서 감당하는 크기가 크다 보니 엔터프라이즈 스토리지 서비스를 이용해도 오류가 발생하는 경우가 있었습니다. Pod가 삭제되면 Pod가 떴던 Node에서 volume attachment가 삭제되어야 하는데 이게 삭제되지 않고 남아있다가 같은 PV를 사용하는 새로운 Pod가 생성될 때 기존의 volume attachment 때문에 multi-attach error 가 발생하는 경우가 있었습니다.

  • volumeattachment 설명

    Kubernetes의 동작방식에서, 볼륨 연결 요청이 있을 때, 쿠버네티스는 VolumeAttachment 오브젝트를 생성합니다. CSI 볼륨 드라이버는 이 오브젝트를 보고 해당 노드에 볼륨을 연결합니다. 연결이 완료되면 드라이버는 VolumeAttachment 오브젝트의 status.attached 필드를 true로 설정하여 연결이 완료되었음을 알립니다. 볼륨의 연결 해제도 비슷한 방식으로 진행됩니다. 볼륨 연결 해제 요청이 있을 때, 쿠버네티스는 VolumeAttachment 오브젝트를 삭제합니다. CSI 볼륨 드라이버는 이 오브젝트의 삭제를 보고 해당 노드에서 볼륨의 연결을 해제합니다.

또 다른 이슈로는 PV자체에 문제가 발생해 파일시스템이 손상된 경우가 있습니다. 데이터 블록 또는 파일시스템에 문제가 발생해 csi driver에서 fsck를 수행하고 복구를 시도하는 과정을 거치는데 이때 i-node 정보가 올바르지 않으면 복구합니다.

하지만 이 또한 완전히 복구가 되지 않아 fsck를 수동으로 돌리는 경우도 있었으며, fsck를 수행해도 복구할 수 없는 수준으로 볼륨이 망가져 있을 땐 PV를 초기화하는 방식으로 업무 프로세스를 진행하고 있습니다.

이때 PV를 초기화 하는 프로세스로 위 다이어그램과 같은 진행 방식을 따릅니다.

Untitled

  1. PV 초기화 절차

    쿠버네티스의 PV를 초기화하려는 의도는 PV를 “깨끗한” 상태로 되돌리는 것입니다. 이를 위해서는 연결된 Persistent Volume Claim (PVC)를 먼저 삭제해야 합니다. PVC가 삭제되면, 그와 연결된 PV도 함께 삭제되고, 이에 따라 해당 PV에 할당된 스토리지 자원이 초기화됩니다.

    그 다음, 초기화된 자원에 대해 새로운 PVC를 생성합니다. 이때 생성하는 PVC는 기존 PVC와 동일한 스펙을 가집니다. 이렇게 생성한 새 PVC는 기존 Deployment에서 사용하게 됩니다.

  2. PV 초기화 절차의 자동화

    이런 PV 초기화 절차를 매번 수동으로 진행하다 보면 실수가 발생할 수 있으며, 또한 많은 시간이 소요됩니다. 따라서 이 절차를 자동화하기 위해 스크립트를 작성하는 것이 필요하다고 판단하였습니다.

    이 스크립트는 아래와 같은 작업을 자동화해야 합니다:

    • PVC의 삭제
    • 기존 PVC와 동일한 스펙으로 새 PVC의 생성
    • 새 PVC를 기존 Deployment에 바인딩

    이렇게 작성한 스크립트를 실행하면, 위의 PV 초기화 절차를 간편하게, 그리고 신속하게 수행할 수 있습니다.

스크립트는 Ansible Playbook으로 작성했으며 Playbook은 다음과 같은 구조로 이루어져있습니다.

Untitled

기본적인 동작을 하는 playbook

변수를 입력 받는 variables.yaml

그리고 pvc의 기본적인 틀을 갖고 있는 temaplate.j2 라는 jinja2 파일로 이루어져 있습니다.

우선 playbook을 실행하기에 앞서, 기본적인 파라미터로 입력 받을 파일입니다.

# variables.yml
deployment_name: mont-kim-worker-01
pvc_param: DEV

우선은 초기화할 PVC가 담겨있는 deployment의 이름을 적어줍니다.

pvc_param은 DEV / CI 두 가지 타입으로 구분이 되는데, 용도마다 StorageClass를 구분해서 관리하고 있습니다.

스토리지 현황에 따라 유동적으로 프로비저닝하는 StorageClass가 변경되고, 이를 별도의 Configmap에 보관 중이어서, 초기화하려는 시점에서 가용한 StorageClass를 조회해 유동적으로 변경할 수 있게 분기 하는 Playbook을 작성했습니다.

기본 PVC Template

기존의 PVC yaml 파일을 살펴본 결과, 꼭 남겨야 하는 파라미터들은 다음과 같았습니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
  name: 
  namespace: 
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage:  
  storageClassName:  
  volumeMode: Filesystem

따라서 실행하려는 playbook에서도 해당 틀에 맞는 데이터들을 가공할 수 있게 jinja2 형식으로 pvc 파일을 작성했습니다.

#template.j2

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ pvc.metadata.name }}
  namespace: {{ pvc.metadata.namespace }}
  labels:
    {{ pvc.metadata.labels | to_nice_yaml(indent=2) | indent(4) }}
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: {{ pvc.spec.resources.requests.storage }}
  storageClassName: {{ pvc.spec.storageClassName }}
  volumeMode: Filesystem

중괄호 안에 들어가는 값들이 실제로 동적으로 값을 읽어 수정될 데이터들입니다.

label에는 값들이 여러 개 들어가기 때문에, 빈칸의 주의해 코드를 작성했습니다.

이제 playbook에 실행될 파라미터와, PVC의 틀을 작성했으니 실제로 동작할 playbook을 살펴 볼 차례입니다.

playbook.yaml

#playbook.yaml
---
- hosts: localhost
  gather_facts: no
  vars:
    deployment_name: "{{ deployment_name }}"
    pvc_param: "{{ pvc_param }}"
  tasks:
    - name: Get Deployment
      command: kubectl -n application get deployment "{{ deployment_name }}" -o jsonpath='{.metadata.name}'
      register: deployment

    - name: Set PVC name
      set_fact:
        pvc_name: "{{ deployment.stdout }}-pvc"

    - name: Get PVC details
      command: kubectl -n application  get pvc "{{ pvc_name }}" -o json
      register: pvc_details

    - name: Set PVC details
      set_fact:
        pvc:
          metadata:
            name: "{{ pvc_name }}"
            namespace: "application"
            labels: "{{ pvc_details.stdout | from_json | json_query('metadata.labels') }}"
          spec:
            resources:
              requests:
                storage: "{{ pvc_details.stdout | from_json | json_query('spec.resources.requests.storage') }}"
            storageClassName: "{{ pvc_details.stdout | from_json | json_query('spec.storageClassName') }}"
            accessModes:
              - "ReadWriteOnce"
    - name: Ensure directory exists
      file:
        path: pvc
        state: directory

    - name: Write PVC details to file
      template:
        src: template.j2
        dest: "pvc/{{ deployment_name }}-pvc.yaml"

deployment 이름을 variables.yaml에서 입력 받은 대로 파라미터를 전달 받아 PVC파일을 생성하는 playbook입니다.

여기 에서는 storageclass를 동적으로 할당하는 코드를 따로 삽입하지 않았는데요

Set PVC Details가 실행될 때, 다음 코드를 같이 작성하면 위에 설명해 드린 대로 pvc_param에 맞게 storageclass를 동적으로 가져와 읽는 stage입니다


    - name: Get storage class based on PVC parameter
      shell: |
        if [ "{{ pvc_param }}" = "DEV" ]; then
          kubectl -n application get cm system-config -o jsonpath='{.data.DEV_STORAGE_CLASS}'
        elif [ "{{ pvc_param }}" = "CI" ]; then
          kubectl -n application get cm system-config -o jsonpath='{.data.CI_STORAGE_CLASS}'
        fi
      register: storage_class

이렇게 구성된 파일들이 있다면 이제 playbook을 실행할 차례입니다.

playbook 실행 명령어는 다음과 같습니다

ansible-playbook playbook.yaml -e @variables.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mont-kim-worker-01-pvc
  namespace: application
  labels:
    component: DEV
    imageName: application:1.0
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: worker-nonretain-economy
  volumeMode: Filesystem

이제 해당 pvc.yaml 파일로 기존 pvc를 삭제하면 자동으로 bound 되어있던 PV도 순차적으로 삭제됩니다.

다만 이때 해당 pv를 마운트 하고 있는 pod이 떠있으면 finalizer에 의해 삭제가 pending 됩니다.

kubectl delete -f mont-kim-worker-01-pvc.yaml

명령어로 기존의 volume claim을 삭제해 pv까지 자동으로 삭제합니다.

삭제가 완료되면 새로운 pvc를 생성해 volume을 초기화합니다.

kubectl apply -f mont-kim-worker-01-pvc.yaml

delete / apply(create)를 하는 것이 replace 같은 명령어로 한번에 초기화를 할 수 있는지 검토 해보았습니다.

하지만 pvc는 spec.resources.requests.storage, spec.accessModes, spec.storageClassName, spec.volumeName 등의 immutable한 schema들을 갖고있기때문에 replace를 할 수 없다고 합니다.

  1. spec.resources.requests.storage: PVC의 스토리지 용량 요청을 정의합니다. 이 값은 PVC가 생성된 후에는 변경할 수 없습니다.
  2. spec.accessModes: PVC에 연결할 수 있는 방식을 정의합니다. 이는 ‘ReadWriteOnce’, ‘ReadOnlyMany’, ‘ReadWriteMany’ 중 하나가 될 수 있습니다. 이 값도 PVC가 생성된 후에 변경할 수 없습니다.
  3. spec.storageClassName: PVC가 사용할 스토리지 클래스를 정의합니다. 이 값도 생성 이후에 변경할 수 없습니다.
  4. spec.volumeName: 이 속성을 사용하면 PVC가 특정 PersistentVolume (PV)에 바인딩되도록 지정할 수 있습니다. 이 값도 생성 이후에 변경할 수 없습니다.

Immutable Resource

Kubernetes에서 리소스의 불변성은 API 서버와 관련된 유효성 검사 및 제약 조건을 통해 구현됩니다. Kubernetes API 서버는 클라이언트의 요청을 받아들이고 검증하며, 해당 요청이 Kubernetes 클러스터의 현재 상태를 변경하도록 합니다.

각 리소스 유형은 schema를 가지며, 이 schema는 리소스의 필드와 해당 필드의 데이터 타입, 그리고 필드가 불변인지 여부를 정의합니다. 일부 필드는 생성 시에만 설정될 수 있고 이후에는 변경이 불가능합니다. 이러한 필드를 불변 필드라고 합니다.

결론

이렇게 오늘은 PV를 초기화 하는 방법을 알아보았습니다.

사실 초기화 보다는 새로 볼륨을 생성에 더 가깝지만, 그 과정을 하나씩 다루면서 공부하는 시간을 가졌네요.

PVC 파일을 가공하기 위해 Ansible Playbook을 이용하여 Script를 조금 더 유연하게 다루기도 했고, 많은 업무들을 Code로 관리하여 Human Error를 줄이는데 가까워 진 것 같습니다.

그럼 20000

© 2024 mont kim   •  Powered by Soopr   •  Theme  Moonwalk