연관 포스트
문제의 시작
OCI Instance(Node)의 Boot Volume 최소값 제한
OCI Free Tier에 따르면 OKE에서 가용할 수 있는 자원은 다음과 같다.
- CPU : 4개
- 메모리 : 24GB
- 블록 스토리지 : 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 서버에 볼륨이 생성되고 데이터가 저장되는 구조이다.
주요 구성 요소를 배포 순서에 따라 나열하면 다음과 같다.
OCI Block Volume
앞서 Node 2개를 배치했으므로, 사용 가능한 용량 제한은 100GB이다.

남은 100GB를 모두 사용하는 OCI Block Volume 1개가 필요하다.
NFS Server
1에서 생성한 OCI Block Volume을 PVC로 매핑 받아 NFS Storage를 제공하는 서비스이다.
NFS Subdir External Provisioner
이번 포스트의 핵심으로, 2에서 생성한 NFS 서버를 Source로 StorageClass를 제공한다.
(선택) 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 링크에서 확인 가능하다.
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
|
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
|
컨테이너별 역할
- NFS 서버: Alpine 기반 NFS 서버로
/exports 디렉토리 공유 - File Browser: 웹 기반 파일 관리 인터페이스 제공 (선택)
- 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에 대해서는 조만간 포스팅할 예정이다.