Featured image of post Nfs subdir external provisioner

Nfs subdir external provisioner

OCI 블록 볼륨 최소 크기 제한 해결하기

연관 포스트


문제의 시작

OCI Instance(Node)의 Boot Volume 최소값 제한

OCI Free Tier에 따르면 OKE에서 가용할 수 있는 자원은 다음과 같다.

  1. CPU : 4개
  2. 메모리 : 24GB
  3. 블록 스토리지 : 200GB

이에 맞춰 Terraform으로 OKE 클러스터와 4개의 인스턴스를 가지는 ARM Node Pool을 생성했다.

각 Node는 1개의 CPU와 6GB의 메모리를 가진다.

스토리지의 경우 추후 PVC 생성을 위해서도 쓰이므로 최대한 작게 잡아주려고 했다.

Node에는 최소한의 용량만, 나머지 대부분은 PVC로 할당


그런데 실제로 생성된 Instance의 정보를 보니 Boot Volume이 비정상적으로 크게 만들어져 있다.

k8s Node의 역할을 하는 Oracle Instance 중 하나의 Boot Volume 정보

Boot Volume은 문자 그대로 Linux 시스템이 부팅하는데 필요한 기본 디스크로, 윈도우로 치면 C드라이브 같은 존재이다.

확인 결과, 다음과 같은 이슈가 발생했다.

  • Instance의 Boot Volume의 크기는 최소 47Gi (50GB) 이상 할당해줘야 한다.

  • 당연히 이 용량은 Free Tier에서 제공되는 200GB에서 차감된다.

  • 이러면 단순히 Node 4개를 만드는 것만으로도 Free Tier의 200GB를 다 써버리게 된다.

    단 1개의 PVC도 생성할 수 없다.



Persistent Volume도 마찬가지

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
    name: my-pvc
spec:
    storageClassName: oci-bv #OCI에서 기본적으로 제공하는 Storage Class
    accessModes:
        - ReadWriteOnce
    resources:
        requests:
            storage: 100Mi 

이처럼 OKE k8s 클러스터에 pvc 하나를 만든다고 가정해보자.
oci-bv는 Oracle에서 기본적으로 제공되는 Storage class로 OCI Block Volume을 저장소로 쓴다.

약 100MB 정도의 크기를 가지는 작은 PVC이다. 그럼 실제 만들어지는 OCI Block Volume의 크기도 100MB 정도여야 하지 않을까?

어림도 없다. 여기도 크기 제약이 걸려있어서 최소 47Gi (50GB) 이상을 할당해줘야 한다.

극단적으로 Node를 1개만 쓴다고 가정해도, PVC는 3개가 한계이다. 각각 50GB씩… 오라클, 보고 있나?

PVC 자체도 50GB가 최소라 노드 1개 PVC 3개가 한계이다.



해결방안

NodePool의 스펙 및 수 조정

우선 Node에 50GB씩 기본 할당 되는 건 어쩔 도리가 없기 때문에 Node 수를 타협해야 한다.
클러스터로써 구색은 맞춰야 하므로 기존 4개에서 2개로 줄였다.
각각 CPU는 2개, 메모리는 12GB씩 할당해줬다.

이걸로 벌써 100GB가 날아갔다


시스템 구성 요소

NFS Subdir External Provisioner를 사용해서 별도의 StorageClass를 만들어줘야 한다.

NFS Storage, 즉 NFS 서버를 Source로 해서 StorageClass를 만들고, 해당 StorageClass를 통해 PVC를 생성하면 NFS 서버에 볼륨이 생성되고 데이터가 저장되는 구조이다.

주요 구성 요소를 배포 순서에 따라 나열하면 다음과 같다.

  1. OCI Block Volume

    앞서 Node 2개를 배치했으므로, 사용 가능한 용량 제한은 100GB이다.

    남은 100GB를 모두 사용하는 OCI Block Volume 1개가 필요하다.

  2. NFS Server

    1에서 생성한 OCI Block Volume을 PVC로 매핑 받아 NFS Storage를 제공하는 서비스이다.

  3. NFS Subdir External Provisioner

    이번 포스트의 핵심으로, 2에서 생성한 NFS 서버를 Source로 StorageClass를 제공한다.

  4. (선택) FileBrowser, SFTP Container

    보다 편한 관리를 위한 것으로, 선택사항이다.
    oci-bv가 제공하는 PVC는 ReadWriteOnce 모드만 제공되므로 반드시 하나의 Pod에 NFS Container와 함께 정의해줘야 한다.



사용하는 도구

  • Terraform: 인프라 자원 관리 (OCI 볼륨, 백업 정책)
  • Kubernetes Manifests: NFS 서버, File Browser, SFTP 서비스
  • Helm: NFS Subdir External Provisioner 설치

실제 구현은 CDKTF를 써서 하나의 Stack 파일로 구성했다.

GitHub 링크에서 확인 가능하다.



Terraform 인프라 구성

OCI 인프라 구성은 Terraform OCI Provider를 사용했다.

HCL 코드에 프로바이더를 연결하는 과정은 여기선 생략한다. (추후 별도 포스팅 예정)

OCI 블록 볼륨

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# main.tf
resource "oci_core_volume" "nfs_core_volume" {
  compartment_id      = var.compartment_id
  availability_domain = var.availability_domain
  size_in_gbs         = 100
  display_name        = "nfs-core-volume"

  lifecycle {
    prevent_destroy = true
  }
}

볼륨 백업 정책

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# backup_policy.tf
resource "oci_core_volume_backup_policy" "nfs_core_volume_backup_policy" {
  compartment_id = var.compartment_id
  display_name   = "nfs-volume-backup-policy"

  schedules {
    backup_type        = "INCREMENTAL"
    period             = "ONE_WEEK"
    retention_seconds  = 60 * 60 * 24 * 7 * 3  # 3주 보관
    day_of_week        = "SUNDAY"
    hour_of_day        = 2
    offset_seconds     = 0
    offset_type        = "STRUCTURED"
    time_zone          = "REGIONAL_DATA_CENTER_TIME"
  }

  schedules {
    backup_type        = "FULL"
    period             = "ONE_MONTH"
    retention_seconds  = 60 * 60 * 24 * 30 * 2  # 2개월 보관
    day_of_month       = 1
    hour_of_day        = 3
    offset_seconds     = 0
    offset_type        = "STRUCTURED"
    time_zone          = "REGIONAL_DATA_CENTER_TIME"
  }
}

백업 스케줄:

  • 증분(INCREMENTAL) 백업: 매주 일요일 2시, 3주 보관
  • 전체(FULL) 백업: 매월 1일 3시, 2개월 보관

백업 정책 할당

1
2
3
4
5
# backup_policy_assignment.tf
resource "oci_core_volume_backup_policy_assignment" "nfs_core_volume_backup_policy_assignment" {
  asset_id  = oci_core_volume.nfs_core_volume.id
  policy_id = oci_core_volume_backup_policy.nfs_core_volume_backup_policy.id
}

변수 정의

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# variables.tf
variable "compartment_id" {
  description = "OCI Compartment ID"
  type        = string
}

variable "availability_domain" {
  description = "OCI Availability Domain"
  type        = string 
}

출력값 정의

1
2
3
4
5
# outputs.tf
output "nfs_volume_id" {
  description = "NFS Core Volume OCID"
  value       = oci_core_volume.nfs_core_volume.id
}

nfs_volume_id 값은 PV를 만들 때 필요하다.


변수 파일 생성

1
2
3
4
cat > terraform.tfvars << EOF
compartment_id      = "< 배포할 OCI Compartment의 ID >"
availability_domain = "< AD 이름, 예시: ibHX:AP-CHUNCHEON-1-AD-1 >"
EOF

Terraform 배포

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Terraform 초기화
terraform init

# 인프라 계획 확인
terraform plan -var-file="terraform.tfvars"

# 인프라 배포
terraform apply -var-file="terraform.tfvars"

# 출력값 확인 (볼륨 ID 등)
terraform output



k8s Manifest

네임스페이스 생성

1
2
3
4
5
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: nfs-system

PersistentVolume 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# persistentvolume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
  annotations:
    pv.kubernetes.io/provisioned-by: blockvolume.csi.oraclecloud.com
spec:
  storageClassName: oci-bv
  persistentVolumeReclaimPolicy: Retain
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeSource:
    csi:
      driver: blockvolume.csi.oraclecloud.com
      volumeHandle: <OCI_VOLUME_OCID> # Terraform에서 Output 된 Volume ID값을 넣어준다.
                                      # ocid1.volume.oc1 어쩌구 하는 값이다. Web Console에서 복사해서 가져와도 된다.
      fsType: ext4
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: failure-domain.beta.kubernetes.io/zone
              operator: In
              values:
                - <AVAILABILITY_DOMAIN>

PersistentVolumeClaim 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
  namespace: nfs-system
spec:
  volumeName: nfs-pv # 위에서 생성한 PV의 이름이다
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
  storageClassName: oci-bv

ConfigMap (Pod의 SFTP 컨테이너용 SSH 키 관리, 선택사항)

1
2
3
4
5
6
7
8
9
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nfs-system-sftp-config
  namespace: nfs-system
data:
  ssh-public-key: |
    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... # SSH 공개키

Service 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nfs-service
  namespace: nfs-system
spec:
  selector:
    app: nfs
  ports:
    - name: nfs
      port: 2049
      targetPort: 2049
      protocol: TCP
    # -- 선택 사항 -- #
    - name: file-browser
      port: 8080
      targetPort: 8080
      protocol: TCP
    - name: sftp
      port: 22
      targetPort: 22
      protocol: TCP

Deployment 생성

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-deployment
  namespace: nfs-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs
  template:
    metadata:
      labels:
        app: nfs
    spec:
      # FileBrowser를 안 쓸 거라면 initContainers는 필요 없음   
      initContainers:
        - name: init-filebrowser-db
          image: busybox:1.35
          command:
            - /bin/sh
            - -c
            - |
              mkdir -p /exports/fb-database
              chown -R 1000:1000 /exports/fb-database
              chmod -R 755 /exports/fb-database
          volumeMounts:
            - name: nfs-storage
              mountPath: /exports

      containers:
        # NFS 서버 컨테이너 (핵심!!)
        - name: nfs-server
          image: itsthenetwork/nfs-server-alpine:latest-arm
          imagePullPolicy: Always
          ports:
            - containerPort: 2049
              protocol: TCP
          securityContext:
            capabilities:
              add:
                - SYS_ADMIN
                - SETPCAP
          command:
            - /bin/sh
            - -c
            - |
              mkdir -p /exports/services
              /usr/bin/nfsd.sh
          volumeMounts:
            - name: nfs-storage
              mountPath: /exports
          env:
            - name: SHARED_DIRECTORY
              value: /exports
            - name: SHARED_DIRECTORY_2
              value: /exports/services

        # File Browser 컨테이너(선택)
        - name: file-browser
          image: filebrowser/filebrowser
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              protocol: TCP
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
            fsGroup: 1000
          volumeMounts:
            - name: nfs-storage
              mountPath: /database
              subPath: fb-database
            - name: nfs-storage
              mountPath: /srv
              subPath: services
          env:
            - name: FB_NOAUTH
              value: 'true'
            - name: FB_DATABASE
              value: /database/database.db
            - name: FB_PORT
              value: '8080'

        # SFTP 컨테이너(선택)
        - name: sftp-server
          image: jmcombs/sftp
          imagePullPolicy: Always
          command:
            - sh
            - -c
            - |
              chmod o+w /home/sftpuser/data
              /entrypoint sftpuser::::data
          ports:
            - containerPort: 22
              protocol: TCP
          volumeMounts:
            - name: ssh-keys
              mountPath: /home/sftpuser/.ssh/keys
              readOnly: true
            - name: nfs-storage
              mountPath: /home/sftpuser/data
              subPath: services

      volumes:
        - name: nfs-storage
          persistentVolumeClaim:
            claimName: nfs-pvc
        - name: ssh-keys
          configMap:
            name: nfs-system-sftp-config

컨테이너별 역할

  1. NFS 서버: Alpine 기반 NFS 서버로 /exports 디렉토리 공유
  2. File Browser: 웹 기반 파일 관리 인터페이스 제공 (선택)
  3. SFTP 서버: SSH 키 기반 파일 전송 서비스 (선택)

FileBrowser Ingress 구성 (선택)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nfs-ingress
  namespace: nfs-system
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: HTTP
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:   
    - host: files.example.com # 소유한 도메인으로 변경 
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nfs-service
                port:
                  number: 8080

k8s 리소스 배포

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 네임스페이스 생성
kubectl apply -f namespace.yaml

# ConfigMap 생성
kubectl apply -f configmap.yaml

# PersistentVolume 생성
kubectl apply -f persistentvolume.yaml

# PersistentVolumeClaim 생성
kubectl apply -f persistentvolumeclaim.yaml

# Service 생성
kubectl apply -f service.yaml

# Deployment 생성
kubectl apply -f deployment.yaml

# Ingress 생성 (선택)
kubectl apply -f ingress.yaml



Helm

Helm 저장소 추가

1
2
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm repo update

NFS Subdir External Provisioner Values 파일

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
nfs:
  # NFS 서버 서비스 주소
  # 위 k8s manifest에서 생성한 nfs service의 k8s service dns를 사용했다.
  server: 'nfs-service.nfs-system.svc.cluster.local' 
  path: '/services' # 공유 경로

# StorageClass 설정
storageClass:
  # StorageClass 이름은 본인이 원하는대로 사용하면 된다
  storageClassName: 'nfs-client' 

  accessModes: 'ReadWriteMany'
  # PVC 할당 시 Storage에 저장 될 경로 패턴을 의미한다.
  # 예시의 경우 ./pvc/<네임스페이스 명>/<PVC 명>으로 저장된다.
  pathPattern: '.pvc/${.PVC.namespace}/${.PVC.name}' 
  • 보다 구체적인 Values 설정은 다음의 링크를 참조하길 바란다.

Helm Chart 배포

1
2
3
4
5
6
7
8
helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace nfs-system \
  --values values.yaml \
  --wait

# 설치 상태 확인
helm list -n nfs-system
helm status nfs-subdir-external-provisioner -n nfs-system



검증 및 테스트

리소스 상태 확인

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pod 상태 확인
kubectl get pods -n nfs-system

# Service 확인
kubectl get svc -n nfs-system

# PVC 상태 확인
kubectl get pvc -n nfs-system

# StorageClass 확인
kubectl get storageclass



마치며

이제 다른 네임스페이스에서 PVC를 생성해보자.

FileBrowser와 ingress까지 설정했다면 웹상으로 접근해서 확인할 수 있다.

ingress가 별도로 없다면 다음 명령어로 포트포워딩해서 localhost:8080으로 접근해보자.

1
kubectl port-forward --address localhost -n nfs-system svc/nfs-service 8080:8080

PVC가 지정된 경로 (.pvc/namespace/pvc-name)에 생성됨을 알 수 있다.



추후 계획

현재 k8s 클러스터에 Vault가 배포되어 있는데, Node의 수가 2개뿐이라 HA (High-Availability) 구성이 안 되고 있다. (최소 3개 이상 필요)

NFS Provisioner는 외부에 존재하는 NFS Storage를 사용할 수도 있으므로, NAS용 컴퓨터 한 대를 구매해서 NFS 서버를 구축한 뒤 NFS Provisioner의 Source로 활용할 예정이다.

이는 또 다른 클러스터인 On-Premise에도 적용될 예정이다. On-Premise 클러스터는 Longhorn을 설치해서 PVC를 제공하고 있는데, 이래저래 마음에 안 드는 구석이 많아 심플하게 외부 NAS로 통합하려고 한다.

Longhorn에 대해서는 조만간 포스팅할 예정이다.

Hugo로 만듦
JimmyStack 테마 사용 중