프로젝트 서버 설정 작업을 맡게 되었습니다. 요청 사항 중에서 'n개의 서버 병렬 운용이 가능할 것' 이라는 게 있었는데 특정 상황(행사 등)에서 요청이 폭주하여 서버가 다운된 경험이 있는 듯 해 보였습니다.
서버 인스턴스를 여럿 생성하고, 각 서버의 역할을 정하여 수동으로 운영하는 경우도 생각 해 보았으나 나중에 가면 운영상 귀찮아질 것 같아(수동 배포 헬) 쿠버네티스를 사용하기로 했습니다.
애플리케이션을 병렬 운영하는 경우 당장 신경써야 할 부분이
1) 세션 클러스터링
2) 캐시 저장소
3) 업로드 파일 저장소
정도가 있습니다. 1, 2야 스프링에서 제공해 주는 세션 모듈과 캐시 모듈을 사용하면 되나, 업로드 파일 관리는 마이크로서비스로 분리해야 하나 고민했지만 서비스 성격이 모놀리틱 구조였으므로 마이크로서비스로의 확장은 고려하지 않기로 했습니다.
PersistentVolume
컨테이너를 재시작하게 되면 컨테이너 내부의 데이터는 모두 사라지기 때문에 호스트 서버 의 볼륨과 컨테이너 내부의 볼륨을 마운트하여 유지하기 위해 사용하는 것이 PersistentVolume
(이하 PV
) 입니다.
호스트 서버의 공간 일부를 PV
볼륨으로 할당하여 사용하는 것이 가장 간단하나 이 경우 하나의 노드만 PV
를 사용할 수 있기 때문에 애플리케이션의 수평 확장은 불가능합니다.
그렇다고 저장소 서비스를 사용하기에는 애매한 상황이라 NFS
(Network File System) 를 사용하여 한 번에 여러 노드의 접근을 가능하게 하는 nfs 타입의 PV
를 사용하기로 했습니다.
NFS
서버 구성우분투 환경에서 NFS
서버를 구성하기 위해서는 다음 패키지가 필요합니다.
$ sudo apt-get update
$ sudo apt-get install -y nfs-common nfs-kernel-server rpcbind portmap
NFS
서버에서 공유할 디렉터리를 생성합니다.
$ cd /mnt
$ sudo mkdir shared
$ sudo chmod 777 shared
NFS
서비스 설정파일을 수정합니다. 공유할 디렉터리 경로와 현재 사용하고 있는 서버 인스턴스의 서브넷 범위를 지정합니다.
$ sudo echo '/mnt/shared 172.26.0.0/16(rw,sync,no_subtree_check)' >> /etc/exports
설정을 적용하고 서비스를 재시작합니다.
$ sudo exportfs -a
$ sudo systemctl restart nfs-kernel-server
NFS
서버에 접근하는 노드에는 nfs-common
패키지만 설치하면 됩니다.
PersistentVolume
및 PersistentVolumeClaim
생성NFS
서버의 Private IP 를 사용하여 PV
, PVC
를 생성합니다.
# nfs-pv.yml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-storage
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes: ["ReadWriteMany"]
nfs:
server: [NFS_SERVER_IP]
path: /mnt/shared # NFS server's shared path
# nfs-pvc.yml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-storage-claim
spec:
storageClassName: "" # 빈 문자열을 줍니다(중요)
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 4Gi
selector:
matchExpressions:
- key: type
operator: In
values:
- nfs
$ kubectl apply -f nfs-pv.yml
$ kubectl apply -f nfs-pvc.yml
$ kubectl get pv,pvc
===
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/nfs-storage 10Gi RWX Retain Bound default/nfs-storage-claim 40m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/nfs-storage-claim Bound nfs-storage 10Gi RWX 40m
NFS
파일 공유가 정상적으로 처리되는지 확인하기 위해 간단한 rest application 을 작성합니다.
@Controller
class GreetingController {
private val directory: Path = Paths.get("/data")
@GetMapping
@ResponseBody
fun index(): String {
val target = this.directory.resolve("file.txt")
if (Files.exists(target).not()) {
Files.newOutputStream(target)
.use { output ->
val current = System.currentTimeMillis()
val content = "current timestamp is >> $current"
.toByteArray(StandardCharsets.UTF_8)
output.write(content, 0, content.size)
}
}
return Files.newInputStream(target)
.use { StreamUtils.copyToString(it, StandardCharsets.UTF_8) }
}
}
application.yml
파일을 생성합니다.
# application.yml
---
apiVersion: v1
kind: Service
metadata:
name: application
labels:
role: application
spec:
ports:
- protocol: TCP
port: 8080
targetPort: 8080
selector:
role: application
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: application
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
selector:
matchLabels:
role: application
template:
metadata:
labels:
role: application
spec:
imagePullSecrets:
- name: johnsuhr4542
containers:
- name: my-application
image: johnsuhr4542/my_app:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
volumeMounts:
- name: application-storage
mountPath: /data
volumes:
- name: application-storage
persistentVolumeClaim:
claimName: nfs-storage-claim
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: type
operator: In
values:
- worker
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: role
values:
- application
operator: In
topologyKey: kubernetes.io/hostname
namespaces:
- k8s-worker-1
- k8s-worker-2
$ kubectl apply -f application.yml
$ kubectl get pods -o wide
===
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
application-86b58b59f-p7hnv 1/1 Running 0 19s 10.244.2.7 k8s-worker-2 <none> <none>
application-86b58b59f-qk7xk 1/1 Running 0 19s 10.244.1.6 k8s-worker-1 <none> <none>
k8s-worker-1
에 생성된 파드 내부에서 애플리케이션 엔드포인트로 접근하면 파일이 생성됩니다.
$ kubectl exec -it pod/application-86b58b59f-qk7xk -- bash
$ apt-get update && apt-get install -y curl
$ curl http://localhost:8080 -i
===
HTTP/1.1 OK
...
current timestamp is >> 1654061688037
NFS
서버에서 공유 디렉터리에 파일이 생성되었는지 확인합니다.
ubuntu@k8s-storage:/mnt/shared$ ll
total 12
drwxrwxrwx 2 root root 4096 Jun 1 05:34 ./
drwxr-xr-x 3 root root 4096 Jun 1 04:52 ../
-rw-r--r-- 1 nobody nogroup 37 Jun 1 05:34 file.txt
k8s-worker-2
에 생성된 파드 내부에서 애플리케이션 엔드포인트로 접근하면 파일이 이미 있으니, 읽어서 내용을 반환합니다.
NFS
서버의 디렉터리에 생성된 파일의 권한과 그룹이 nobody
, nogroup
으로 지정되기 때문에 보안에 취약하다는 단점이 있으나, Public 접근이 불가능한 Private 노드이므로 괜찮지 않을까(아닌가..) 싶습니다.
위 포스트는 블로그 포스트 와 쿠버네티스 공식 문서 를 참고하여 작성되었습니다.