쿠버네티스에서의 컨테이너 파일 시스템 구축 과정
컨테이너에서 동작하는 애플리케이션에는 읽기 쓰기가 가능한 하나의 파일 시스템으로 보이지만, 실제로 파드 속 컨테이너의 파일 시스템은 여러 출처를 합쳐 구성된다.
컨테이너 이미지가 파일 시스템 초기 내용을 제공하고, 컨테이너가 기록 가능한 writable layer 가 얹혀진다. 컨테이너 이미지에 들어 있던 파일을 수정하는 것은 writable layer에서 원본 파일의 사본을 수정하는 것이다.
기존 컨테이너에서 동작하던 애플리케이션에서 장애가 발생해 컨테이너가 종료되면 새로운 파드가 생성되고 기존 컨테이너의 writable layer에 기록한 데이터가 유실된다.
컨피그맵과 비밀값을 통해 파드 수준에서 볼륨을 정의하고 컨테이너 파일 시스템 내부로 마운트할 수 있다. 컨피그맵과 비밀값은 읽기 전용 스토리지 단위이며, 이외에도 기록 가능한 유형의 볼륨이 여러 가지 있다.
EmptyDir
컨테이너 안에서 빈 디렉터리로 초기화되는 유형의 볼륨을 사용할 수 있다.
파드 수준의 스토리지이며, 컨테이너에 마운트되므로 디렉터리처럼 보이지만 이미지나 컨테이너 레이어에 속하지 않는다.
임시 저장 목적으로 모든 애플리케이션에서 사용할 수 있다. 상당 시간동안 유효한 API 응답이 있다면 이를 로컬 캐싱해두어 애플리케이션에 장애가 생겨 파드가 재시작되거나 애플리케이션이 느릴 때 응답을 빠르게 줄 수 있다.
EmptyDir 볼륨에 저장된 데이터는 컨테이너가 재시작되어도 유지된다. 파드가 대체될 때에는 데이터가 유실된다.
아래는 디플로이먼트에 EmptyDir 볼륨을 정의하는 예제이다.
Copy apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
아래와 같은 명령을 사용하면 EmptyDir 볼륨을 포함한 디플로이먼트를 적용한 후 emptyDir 볼륨이 마운트된 위치에 파일을 생성하고 컨테이너를 강제 종료해 파드가 재구동되어도 파일이 남아있는지 확인할 수 있다.
Copy kubectl apply -f sleep-with-emptydir.yaml
kubectl exec deploy/sleep -- ls /data
kubectl exec deploy/sleep --sh -c 'echo ch05 > /data/file.txt; ls /data'
# 컨테이너 ID 확인
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
# 컨테이느 프로세스 강제 종료
kubectl exec deploy/sleep --killall5
# 변경된 컨테이너 ID 확인
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
kubectl exec deploy/sleep -- cat /data/file.txt
볼륨과 마운트로 노드에 데이터 저장
데이터를 특정 노드에 고정시킬지 말지 결정해야 한다. 만약 특정 노드에 고정시키면 기존 파드를 대체할 새로운 파드가 동일 노드에만 배치되야 한다.
노드의 디스크를 가리키는 볼륨인 호스트 경로 볼륨은 파드에 정의되며 컨테이너 파일 시스템에 마운트되는 형태로 사용된다.
쿠버네티스가 클러스터의 모든 노드에 동일한 데이터를 복제해주지는 않는다.
파드가 항상 같은 노드에서 동작하는 한 볼륨의 생애 주기가 노드의 디스크와 같아진다.
아래는 원주율 계산 결과를 반환하는 웹 애플리케이션의 프록시 파드를 정의한 디플로이먼트로, 컨테이너에서 /data/nginx/cache
디렉토리에 데이터를 저장하면 노드의 /volumes/nginx/cache
디렉토리에 실제로 데이터가 기록되도록 정의했다.
Copy apiVersion: apps/v1
kind: Deployment
metadata:
name: pi-proxy
labels:
app: pi-proxy
spec:
selector:
matchLabels:
app: pi-proxy
template:
metadata:
labels:
app: pi-proxy
spec:
containers:
- image: nginx:1.17-alpine
name: nginx
ports:
- containerPort: 80
name: http
volumeMounts:
- name: config
mountPath: "/etc/nginx/"
readOnly: true
- name: cache-volume
mountPath: /data/nginx/cache # 프록시의 캐시 저장 경로
volumes:
- name: config
configMap:
name: pi-proxy-configmap
- name: cache-volume
hostPath:
path: /volumes/nginx/cache # 사용할 노드의 디렉토리
type: DirectoryOrCreate # 디렉터리가 없으면 생성
위 디플로이먼트를 적용하면 컨테이너가 종료될 때 대체 파드가 기존 파드와 같은 노드에 뜨기만 하면 같은 볼륨을 그대로 사용할 수 있다.
하지만 대부분의 경우 노드를 여러 개 두고 운영하고, 항상 같은 노드에서 수행하도록 강제하면 노드가 고장났을 때 애플리케이션을 실행할 수 없으므로 안정성이 떨어지게 된다.
노드 파일 시스템 전체에 접근할 수 있는 파드를 띄울 수 있다면 누구든지 호스트경로 볼륨에 사용하는 노드 상의 디렉터리에 접근할 수 있게 된다. 애플리케이션이 침투당해 공격자가 컨테이너에서 명령을 실행할 수 있다면 노드의 디스크 전체를 장악당할 수 있다.
아래는 모든 노드 파일 시스템에 접근 가능하도록 만든 디플로이먼트 예시와, 명령을 통해 내부 데이터를 조회하는 명령 예시이다.
Copy apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: node-root
mountPath: /node-root
volumes:
- name: node-root
hostPath:
path: /
type: Directory
Copy # 컨테이너 속 로그 파일 확인
kubectl exec deploy/sleep -- ls -l /var/log
# 노드 파일 시스템의 로그 파일 확인
kubectl exec deploy/sleep -- ls -l /node-root/var/log
볼륨 마운트 시 볼륨의 하위 디렉토리를 마운트해 노드의 파일 시스템을 필요 이상으로 노출하지 않아야 한다.
아래는 볼륨 마운트를 하위 디렉터리만 대상으로 하여 컨테이너에서 접근 가능한 노드의 디렉토리를 제한하는 예시이다.
Copy apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: node-root # 마운트할 볼륨 이름
mountPath: /pod-logs # 마운트 대상 컨테이너 경로
subPath: var/log/pods # 마운트 대상 볼륨 내 경로
- name: node-root
mountPath: /container-logs
subPath: var/log/containers
volumes:
- name: node-root
hostPath:
path: /
type: Directory
영구 볼륨과 클레임
분산 스토리지 시스템에 모든 노드가 접근 가능하면 파드가 어떤 노드에서 실행되더라도 볼륨에 접근할 수 있다.
파드는 컴퓨팅 계층의 추상이며, 서비스는 네트워크 계층의 추상이다. 스토리지 계층의 추상으로는 영구 볼륨(PersistentVolume, PV)과 영구 볼륨 클레임(PersistentVolumeClaim, PVC)이 있다.
영구 볼륨이란 사용 가능한 스토리지 조각을 정의한 쿠버네티스 리소스이다. 클러스터 관리자가 생성하며 각 영구 볼륨에는 이를 구현하는 스토리지 시스템에 대한 볼륨 정의가 들어 있다.
아래는 NFS 스토리지를 사용하는 영구 볼륨 정의의 예시이다.
Copy apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01 # 볼륨 이름
spec:
capacity:
storage: 50Mi # 볼륨 용량
accessModes:
- ReadWriteOnce # 파드 접근 유형
nfs:
server: nfs.my.network # NFS 서버의 도메인 이름
path: "/kubernetes-volumes" # 스토리지 경로
모든 노드에서 접근 가능한 NFS 혹은 애저 디스크를 이용하면 PV 정의에서 분산 스토리지 유형을 지정하기만 하면 되지만, 노드가 하나인 로컬에서는 로컬 볼륨을 정의하고 레이블로 볼륨 노드를 따로 식별해야 한다.
Copy apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01
spec:
capacity:
storage: 50Mi
accessModes:
- ReadWriteOnce
local:
path: /volumes/pv01
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kiamol
operator: In
values:
- ch05
Copy # 클러스터의 첫 노드에 레이블 부여
kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') kiamol=ch05
# 레이블 셀렉터로 노드 조회
kubectl get nodes -l kiamol=ch05
# 레이블이 부여된 노드의 로컬 볼륨 사용하도록 영구 볼륨 배치
kubectl apply -f persistentVolume.yaml
파드는 PV에 직접 접근하지 못하며 PVC에 볼륨 사용을 요청해야 한다.
PVC는 파드가 사용하는 스토리지 추상이며, 애플리케이션에서 사용할 스토리지를 요청한다. PVC는 요구 조건이 일치하는 영구 볼륨과 함께 쓰이며, 자세한 볼륨 정보는 PV에 위임한다.
PV와 PVC는 일대일 관계이며 하나의 PVC에 연결된 PV는 다른 PVC와 연결될 수 없다.
아래는 앞서 생성한 로컬 볼륨과 요구 조건이 일치하는 PVC 정의이다. 스토리지 유형을 지정하지 않으면 현재 존재하는 PV 중 요구사항과 일치하는 것을 자동으로 찾아준다.
Copy apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce # 접근 유형
resources:
requests:
storage: 40Mi # 스토리지 용량
storageClassName: "" # 스토리지 유형
Copy # PVC 생성
kubectl apply -f persistentVolumeClaim.yaml
# PVC 목록 확인 / STATUS가 BOUND이면 PV에 연결되어 사용중임을 의미
kubectl get pvc
# PV 목록 확인 / STATUS가 BOUND이면 PVC에 연결되어 사용중임을 의미
kubectl get pv
PVC를 볼륨으로 정의하려면 아래와 같이 디플로이먼트에 볼륨을 정의해주어야 한다.
Copy apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-db
spec:
selector:
matchLabels:
app: todo-db
template:
metadata:
labels:
app: todo-db
spec:
containers:
- name: db
image: postgres:11.6-alpine
env:
- name: POSTGRES_PASSWORD_FILE
value: /secrets/postgres_password
volumeMounts:
- name: secret
mountPath: "/secrets"
- name: data
mountPath: /var/lib/postgresql/data
volumes:
# ...
- name: data
persistentVolumeClaim: # PVC를 볼륨으로 사용한다.
claimName: postgres-pvc # PVC 이름
노드에 볼륨이 사용할 디렉토리 경로를 만들어주어야 한다. 이 때 노드에 로그인할 권한이 없으므로 hostPath를 통해 노드의 루트 디렉토리를 마운트한 파드를 이용해 디렉토리를 생성할 수 있다.
Copy apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: node-root
mountPath: /node-root
volumes:
- name: node-root
hostPath:
path: /
type: Directory
Copy kubectl apply -f sleep-with-hostpath.yaml
kubectl exec deploy/sleep -- mkdir -p /node-root/volumes/pv01
스토리지 유형과 동적 볼륨 프로비저닝
앞서 살펴본 방식은 PV를 명시적으로 생성하는 정적 프로비저닝 방식이었다. 요구 사항이 일치하는 PV가 없으면 PVC가 생성되기는 하지만 스토리지를 사용할 수 없고 대기 상태가 된다.
정적 볼륨 프로비저닝 방식은 모든 쿠버네티스 클러스터에서 사용할 수 있으며 스토리지 접근 제약이 큰 조직에서 선호하는 방식이다.
동적 볼륨 프로비저닝 방식은 PVC만 생성하면 그에 맞는 PV를 클러스터에서 동적으로 생성해준다.
스토리지 유형을 지정하거나 따로 지정하지 않고 기본 유형으로 사용할 수 있다.
PV를 따로 만들지 않아도 PVC를 배치할 수 있다.
다음은 스토리지 유형을 지정하지 않은 PVC의 정의이다. PVC를 생성하면 클러스터에 지정된 기본 스토리지 유형의 PV가 자동으로 생성될 것이다.
Copy apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc-dynamic
spec:
accessModes:
- ReadWriteOnce
# storageClassName: hostpath
resources:
requests:
storage: 100Mi
아래 명령을 통해 PVC를 생성하고 제거할 때 PV가 동적으로 생성되고 제거됨을 확인할 수 있다.
Copy # PVC 생성
kubectl apply -f persistentVolumeClaim-dynamic.yaml
# PVC, PV 목록 확인
kubectl get pvc
kubectl get pv
# PVC 제거
kubectl delete pvc persistentVolumeClaim-dynamic.yaml
# PV도 같이 제거되었는지 확인
kubectl get pv
스토리지 유형은 표준 쿠버네티스 리소스로 생성되며 다음 세 가지 필드로 스토리지의 동작 방식을 지정할 수 있다.
provisioner
영구 볼륨이 필요할 때 영구 볼륨을 만드는 주체. 플랫폼에 따라 관리 주체가 달라진다.
reclaimPolicy
연결되었던 클레임이 삭제되었을 때 남아있는 볼륨을 어떻게 처리할지 정한다.
volumeBindingMode
PVC가 생성되자마자 PV를 생성해 연결할지, PVC를 사용하는 파드가 생성될 때 PV를 생성할 지 정한다.
스토리지 선택 시 고려할 점
일반적인 경우 파드 정의에 포함된 PVC에서 필요한 스토리지 용량과 스토리지 유형을 지정하면 된다.
클라우드 환경 사용 시 스토리지 솔루션은 비용이 많이 든다. 속도가 빠른 스토리지 유형을 지정한 PVC에서 기존 볼륨을 유지하도록 설정하면, PVC를 사용하는 파드가 삭제되더라도 볼륨이 유지되므로 비용이 계속 청구될 것이다.
데이터베이스 같은 유상태 애플리케이션의 경우 쿠버네티스에서 데이터 복제 기능과 함께 고가용성을 확보할 수 있지만, 데이터의 백업과 스냅샷 남기기와 복원 등 직접 고려해야할 점이 많다.
Last updated 4 months ago