Kubernetes

Kubernetes Volume/Mount/Claim

sh1220 2026. 1. 31. 00:02
이 글은 "쿠버네티스 교과서 | 엘튼 스톤맨"의 책의 내용을 참고하여 작성됨을 알립니다.

 

쿠버네티스에서 “데이터가 남는다”는 말은 결국 파드/컨테이너가 재시작/교체되더라도 데이터가 보존되는가의 문제다.

이 장은 그걸 가능하게 만드는 볼륨, 마운트, 영구볼륨(PV), 영구볼륨클레임(PVC), 스토리지클래스(StorageClass)를 다룬다.

1. 컨테이너가 재시작되면 데이터가 사라지는 이유

컨테이너의 파일시스템은 기본적으로 이미지 레이어 + writable layer로 동작한다.

이 구조를 이해하면 “왜 볼륨이 필요한지”가 바로 설명된다.

1-1) killall5로 컨테이너 종료 시키기

killall5는 일반적인 killall이 아니라 리눅스에서 시스템 종료 과정에서 거의 모든 프로세스를 죽이는 명령이다.

  • 주로 init, shutdown, reboot 같은 종료 루틴에서 사용됨
  • 컨테이너 안에서도 “거의 전체 프로세스 kill”을 수행함

컨테이너는 보통 프로세스 구조가 단순하다.

  • PID 1: 컨테이너의 메인 프로세스 (예: sleep)
  • 나머지 프로세스: kubectl exec 등으로 잠깐 들어온 프로세스

killall5는 기본적으로

  • PID 1을 제외한 모든 프로세스를 죽이거나
  • 옵션에 따라 PID 1까지 죽인다

결국 핵심 프로세스인 sleep(PID 1)이 죽으면

  • 컨테이너 종료
  • Pod 재시작
  • containerID 변경

정리하면

  • 파드 속 컨테이너 생애주기 = 해당 컨테이너의 생애주기
  • 파드의 재시작 = 그 안의 컨테이너 재시작
  • 파드 수준에 데이터 저장(볼륨)이 없으면 재시작 시 데이터 유실

2. 컨테이너 파일시스템의 Copy-on-Write(COW)

컨테이너 파일시스템의 핵심은 Copy-on-Write 구조다.

  • 레이어는 “쌓여 있다”
    • 아래: 이미지 레이어들 (read-only)
    • 위: writable layer (read-write)

파일을 수정하면

  • 아래 레이어 파일을 직접 수정하는 게 아니라
  • 위 레이어로 복사한 뒤 수정한다

이걸 Copy-on-Write(COW)라고 부른다.

이 구조 때문에

  • 컨테이너가 재시작/교체되면 writable layer는 새로 생성되고
  • writable layer에만 있던 데이터는 사라질 수 있다

그래서 파드 수준에서 볼륨을 정의하고 컨테이너 경로에 마운트해야 한다.

3. emptyDir: Pod에 종속되는 임시 볼륨

컨테이너의 파일시스템은 Copy-on-Write 구조이기 때문에,

컨테이너 writable layer에 기록한 데이터는 재시작이나 교체 상황에서 쉽게 유실될 수 있다.

 

그래서 쿠버네티스는 데이터를 컨테이너가 아니라 Pod 수준에서 관리할 수 있도록

기본 볼륨 타입을 제공하는데, 그 대표적인 예가 emptyDir이다.

3-1) emptyDir의 특징

  • Pod가 생성될 때 빈 디렉터리로 시작하고
  • Pod가 실행되는 동안만 유지되며
  • Pod가 삭제되면 완전히 사라지는 임시 볼륨이다
    => 컨테이너에 종속되는 저장소가 아니라, Pod 생애주기에 종속되는 저장소
  • life cycle
    • 컨테이너가 죽어도 Pod가 살아있으면 유지되지만
    • Pod 자체가 사라지면 emptyDir도 함께 사라진다

3-2) emptyDir이 필요한 이유

Pod 안에는 여러 컨테이너가 동시에 존재할 수 있음.

  • Ex1
    • initContainer가 파일을 생성하고
    • main container가 그 파일을 이어서 사용하는 경우
  • Ex2
    • 웹 서버가 캐시 데이터를 쓰고
    • 사이드카 컨테이너가 이를 공유하는 경우

이때 emptyDir은

  • Pod 내부 컨테이너들이 공유할 수 있는 임시 저장 공간을 제공.

3-3) emptyDir YAML 예시

apiVersion: v1
kind: Pod
metadata:
  name: cache-pod
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: cache
          mountPath: /cache

  volumes:
    - name: cache
      emptyDir: {}
  • /cache 경로는 Pod가 유지되는 동안 계속 남아 있다
  • Pod가 삭제되면 emptyDir 데이터는 완전히 제거된다

3-4) emptyDir usecases

emptyDir는 영구 데이터 저장 용도가 아니라, Pod 단위의 임시 작업 공간에 적합.

  • nginx 캐시 저장소
  • 빌드 과정에서 생기는 임시 파일 저장
  • 로그 파일을 컨테이너 간 공유
  • initContainer → main container 데이터 전달

4. hostPath 볼륨과 subPath 마운트

4-1) hostPath 볼륨의 장단점

hostPath는 노드의 특정 디렉터리를 파드에 연결하는 볼륨이다.

  • 장점
    • 쿠버로 stateful 앱을 “처음 도입”할 때 빠르게 테스트 가능
    • 노드 파일을 직접 확인/생성하는 상황에 좋음
  • 한계
    • 노드 로컬에 종속됨
    • 파드가 다른 노드로 스케줄링되면 데이터 연속성이 깨질 수 있음

또한 hostPath를 노드 전체(root /)로 마운트하면 보안적으로 위험하다.

  • 노드 전체에 대해 마운트하면 불필요한 영역까지 노출
  • 그래서 보통 하위 디렉터리만 subPath로 제한해서 마운트한다

4-2) subPath로 필요한 경로만 노출하기

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
  • volumes.hostPath.path: / 로 노드 루트를 잡되
  • 실제 컨테이너에 노출하는 건 subPath로 최소화한다

5. PV/PVC: 스토리지 계층의 추상

쿠버네티스에서 계층별 추상화는 이렇게 대응된다.

  • Pod: 컴퓨팅 계층의 추상
  • Service: 네트워크 계층의 추상
  • PV/PVC: 스토리지 계층의 추상

핵심은 파드가 스토리지를 직접 “구현”하지 않고, “요청”만 한다는 점이다.

5- 1) PersistentVolume(PV) 예시: NFS

PV는 “실제 스토리지”를 나타내는 리소스다.

예를 들어 NFS 스토리지 기반 PV는 아래처럼 정의한다.

# 예제 5-5 persistentVolume-nfs.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv01
spec:
  capacity:
    storage: 50Mi
  accessModes:
    - ReadWriteOnce

  nfs:
    server: nfs.my.network
    path: "/kubernetes-volumes"
  • PV 용량(capacity)은 “볼륨 자체 크기”
  • accessModes는 접근 방식
  • nfs.server, nfs.path로 실제 외부 스토리지를 가리킴

6. 로컬 PV(local)와 nodeAffinity가 필요한 이유

노드의 로컬 디스크를 PV로 쓰는 경우도 있다.

이때는 “특정 노드에 고정”되어야 하므로 nodeAffinity가 필요하다.

  • 로컬 볼륨은 한 노드에만 존재
  • 파드가 다른 노드로 가면 볼륨을 못 찾는다
  • 따라서 레이블로 노드를 식별하고, PV에 nodeAffinity를 걸어야 한다
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
  • local.path는 노드 로컬 경로
  • nodeAffinity로 “이 PV는 특정 노드에서만 유효”하다고 선언

6-1) hostPath vs Local PV 차이

목적 단순히 노드 경로를 Pod에 연결 노드 로컬 디스크를 “PV로 추상화”
쿠버 스토리지 모델(PV/PVC) 사용 ❌ 안 씀 ✅ PV/PVC 사용
스케줄링 제어 없음 nodeAffinity로 노드 고정
운영환경 사용 거의 안 함 (위험) 제한적으로 사용 가능
보안 루트 노출 위험 큼 PV로 관리되므로 상대적으로 안전
가장 적합한 용도 실습/디버깅 Stateful workload + 로컬 SSD

 

7. PersistentVolumeClaim(PVC): 파드가 스토리지를 요청하는 방식

PVC는 파드가 사용할 스토리지의 추상이다.

  • 애플리케이션 입장에서 “스토리지를 요청”
  • 요구조건이 일치하는 PV와 바인딩됨
  • 상세한 볼륨 구현은 PV에게 맡김
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 40Mi
  storageClassName: ""

PVC가 지정하는 것

  • 접근 유형(accessModes)
  • 요청 용량(requests.storage)
  • 스토리지 유형(storageClassName)

스토리지 유형을 지정하지 않으면

  • 존재하는 PV 중 요구사항과 일치하는 것을 찾아 바인딩한다

7-1) PV와 PVC는 1:1 관계

  • PV와 PVC는 1:1로 바인딩된다
  • 하나의 PV는 다른 PVC와 추가로 연결 불가

7-2) ReadWriteOnce(RWO)

  • 뜻: 하나의 노드에서만 Read/Write 가능
  • PVC를 여러 Pod가 “참조”할 수는 있다
  • 하지만 동시에 사용하려면 그 Pod들이 같은 Node에 있어야 한다
  • 예시
    • ReplicaSet이 같은 노드에 여러 Pod를 올리면 가능
    • 하지만 다른 노드로 분산되면 불가능

7-3) ReadOnlyMany (ROX)

  • 뜻: 여러 Pod가 동시에 Read 가능
  • PVC 하나 ← 여러 Pod (읽기 전용)
  • 예시
    • Config 공유
    • 정적 파일 제공

7-4) ReadWriteMany (RWX)

  • 여러 Pod가 동시에 Read/Write 가능
  • 예시
    • NFS
    • EFS
    • CephFS

7-5) PVC에서는 40Mi를 요청했는데, 50Mi로 나오는 이유 

PVC는 “요청 용량”을 쓰고, PV는 “실제 제공 용량(capacity)”을 가진다.

  • PVC에서 40Mi를 요청해도
  • 바인딩된 PV가 50Mi면, 화면/조회에서 50Mi가 보일 수 있다
    • 이 값은 PV의 capacity를 의미한다

8. 정적 프로비저닝 vs 동적 프로비저닝

8-1) 정적 프로비저닝(Static)

  • PV를 명시적으로 생성해야 함
  • 어떤 클러스터에서도 동작하는 가장 기본 방식

8-2) 동적 프로비저닝(Dynamic)

  • PVC를 생성하면 조건에 맞는 PV를 “자동으로 생성”
  • 클러스터마다 기본 스토리지 유형(StorageClass)이 다를 수 있음
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc-dynamic
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi

PVC만 만들었을 때 “클러스터마다” 달라질 수 있는 이유

  • 기본 StorageClass(프로비저너)가 환경마다 다름
    • Docker Desktop 기본 스토리지 유형: hostPath
    • AKS: AzureFiles
    • k3s: hostPath (파드 생성 시점에 PV 생성되는 식으로 동작)

8-3) StorageClass

StorageClass는 “이 스토리지가 어떻게 만들어지고 관리되는지”를 정의한다.

핵심 필드 3가지

  • provisioner
    • PV가 필요할 때 PV를 만드는 주체
    • 플랫폼마다 다름 (예: AKS 기본은 AzureFiles가 프로비저닝)
  • reclaimPolicy
    • PVC가 삭제되었을 때 실제 볼륨을 어떻게 처리할지
    • 삭제(Delete) 또는 유지(Retain) 등
  • volumeBindingMode
    • Immediate: PVC 생성 즉시 PV 생성/바인딩
    • WaitForFirstConsumer: 해당 PVC를 사용하는 Pod가 생성될 때 PV 생성/바인딩

8-4) 기본 StorageClass 복제해서 커스텀 StorageClass 만들기

쿠버네티스는 기본 스토리지 클래스를 label이 아니라 annotation으로 표시한다.

그래서 JSONPath로 기본 StorageClass를 찾아 복제하는 흐름이 필요하다.

#!/bin/sh

# default storageclass를 찾고, spec을 JSON으로 저장
kubectl get sc $(kubectl get storageclass \
-o jsonpath="{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class == 'true')]}{.metadata.name}") \
$(kubectl get storageclass -o jsonpath="{range.items[?(@.metadata.annotations.storageclass\.beta\.kubernetes\.io/is-default-class == 'true')]}{.metadata.name}") \
-o json > defaultStorageClass.json

# 복제 스크립트를 실행할 pod 배포
kubectl apply -f storageClass/clone-storageClass-script.yaml

# 준비될 때까지 대기
kubectl wait --for=condition=Ready pod/clone-sc

# default sc spec을 pod로 복사
kubectl cp defaultStorageClass.json clone-sc:/defaultStorageClass.json

# 복제 스크립트 실행 -> 커스텀 SC JSON 생성
kubectl exec clone-sc -- /scripts/duplicate-default-storage-class.sh > kiamolStorageClass.json

# 적용
kubectl apply -f kiamolStorageClass.json

# 복제용 pod 삭제
kubectl delete -f storageClass/clone-storageClass-script.yaml

이 과정을 거치면

  • 클러스터 기본 스토리지 유형과 동일한 동작을 하는
  • kiamol 같은 사용자 정의 StorageClass를 만들 수 있다

커스텀 StorageClass를 사용하는 PVC 예시

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc-kiamol
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: kiamol
  resources:
    requests:
      storage: 100Mi
  • DB 파드가 새 PVC를 쓰도록 업데이트하는 경우
    • 새로 생성된 PV에 연결되기 때문에 이전 데이터는 잃는다
    • 다만 기존 볼륨(PV)은 남아있을 수 있고, 다시 연결하면 데이터 복구 가능

8-5) PVC/PV가 없을 때 Pending이 되는 이유

  • PVC가 생성되었는데 바인딩될 PV가 없으면 PVC는 Pending
  • 그 PVC를 참조하는 Pod도 같이 Pending이 될 수 있다

8-6) 운영에서 “노드에 로그인 못하는 상황”과 우회 방법

실제로 노드에 직접 로그인할 권한이 없는 경우가 많다.

이럴 때 hostPath로 루트 디렉터리를 마운트한 sleep 파드를 띄워서

  • 원하는 디렉터리를 만들거나
  • 노드의 파일을 확인하는 식으로 우회할 수 있다

9. 연습문제(메모만)

9-1) nginx 캐시 디렉터리 이슈와 해결

nginx에서 캐시 경로를 지정했는데 디렉터리가 없으면 실행이 실패한다.

  • 해결: hostPath + DirectoryOrCreate 로 디렉터리를 보장

실습에서 사용한 proxy + nginx 캐시 볼륨 구성 예시

apiVersion: v1
kind: Service
metadata:
  name: todo-proxy-lab
  labels:
    app: todo-proxy-lab
spec:
  ports:
    - port: 8082
      targetPort: 80
  selector:
    app: todo-proxy-lab
  type: LoadBalancer
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: todo-proxy-lab-configmap
data:
  nginx.conf: |-
    user  nginx;
    worker_processes  1;

    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;

    events {
      worker_connections  1024;
    }

    http {
      proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;

      server {
        listen 80 default_server;
        listen [::]:80 default_server;

        location / {
            proxy_pass             http://todo-web-lab;
            proxy_set_header       Host $host;
            proxy_cache            STATIC;
            proxy_cache_valid      200  1d;
            add_header             X-Cache $upstream_cache_status;
            add_header             X-Host  $hostname;
        }
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todo-proxy-lab
  labels:
    app: todo-proxy-lab
spec:
  selector:
    matchLabels:
      app: todo-proxy-lab
  template:
    metadata:
      labels:
        app: todo-proxy-lab
    spec:
      containers:
        - image: nginx:1.17-alpine
          name: nginx
          ports:
            - containerPortriPort: 80
              name: http
          volumeMounts:
            - name: config
              mountPath: "/etc/nginx/"
              readOnly: true
            - name: cache-volume
              mountPath: /data/nginx/cache
      volumes:
        - name: config
          configMap:
            name: todo-proxy-lab-configmap
        - name: cache-volume
          hostPath:
            path: /volumes/nginx/cache
            type: DirectoryOrCreate

추가로 겪은 상황

  • 이미 8080 포트를 쓰는 컨테이너가 실행 중이었고, 종료하니 해결됨

9-2) Mac(Apple Silicon)에서 앱 이미지 아키텍처 문제

실습 중 kiamol/ch04-todo-list 이미지가 amd64용으로 빌드되어 있어

Mac M1/M2/M3(arm64)에서 SQLite 네이티브 라이브러리가 동작하지 않는 문제가 발생했다.

  • 즉, 애플리케이션 레이어에서 에러가 발생
  • 해결 방향: 책 강의에서 사용한 PostgreSQL 구성을 재사용

아래는 PostgreSQL + 웹 구성(Secret/ConfigMap/PVC 포함) 예시

# PostgreSQL PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
---
# PostgreSQL Secret
apiVersion: v1
kind: Secret
metadata:
  name: todo-db-secret
type: Opaque
stringData:
  POSTGRES_PASSWORD: kiamol-2*2*
---
# PostgreSQL Service
apiVersion: v1
kind: Service
metadata:
  name: todo-db
spec:
  ports:
    - port: 5432
      targetPort: 5432
  selector:
    app: todo-db
  type: ClusterIP
---
# PostgreSQL Deployment
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: secret
          secret:
            secretName: todo-db-secret
            defaultMode: 0400
            items:
              - key: POSTGRES_PASSWORD
                path: postgres_password
        - name: data
          persistentVolumeClaim:
            claimName: postgres-pvc
---
# Web ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: todo-web-lab-config
data:
  config.json: |-
    {
      "ConfigController": {
        "Enabled" : true
      },
      "Database" : {
        "Provider" : "Postgres"
      }
    }
---
# Web Secret
apiVersion: v1
kind: Secret
metadata:
  name: todo-web-lab-secret
type: Opaque
stringData:
  secrets.json: |-
    {
      "ConnectionStrings": {
        "ToDoDb": "Server=todo-db;Database=todo;User Id=postgres;Password=kiamol-2*2*;"
      }
    }
---
# Web Service
apiVersion: v1
kind: Service
metadata:
  name: todo-web-lab
spec:
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: todo-web-lab
  type: ClusterIP
---
# Web Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todo-web-lab
spec:
  selector:
    matchLabels:
      app: todo-web-lab
  template:
    metadata:
      labels:
        app: todo-web-lab
    spec:
      containers:
        - name: web
          image: kiamol/ch04-todo-list
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Test
          volumeMounts:
            - name: config
              mountPath: "/app/config"
              readOnly: true
            - name: secret
              mountPath: "/app/secrets"
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: todo-web-lab-config
            items:
              - key: config.json
                path: config.json
        - name: secret
          secret:
            secretName: todo-web-lab-secret
            defaultMode: 0400
            items:
              - key: secrets.json
                path: secrets.json

메모

  • PostgreSQL은 설정을 json으로 둬야 해서 ConfigMap/Secret을 따로 만들고 적용했다

결과

 

10. DB를 쿠버네티스로 옮겨야 하는가?

결론부터 말하면, 데이터 관리는 쿠버에서 쉬운 일이 아니다.

  • stateful application은 운영 난이도를 높인다
  • 백업/스냅샷/복원 같은 운영 기능이 중요해지는데, 이걸 직접 구성해야 한다

클라우드에서는 매니지드 DB가 이 부담을 상당히 줄여준다.

  • 대신 비용이 늘 수 있다

그래도 “전체 스택을 쿠버 매니페스트로 통일”하고 싶다면

  • 컨테이너 플랫폼을 위해 만들어진 현대식 DB도 선택지가 된다
    • TiDB
    • CockroachDB

'Kubernetes' 카테고리의 다른 글

Kubernetes Network  (1) 2026.01.18
Kubernetes Pod와 Deployment  (0) 2026.01.04