Kubernetes Practice 11

emptyDir와 hostPath로 Pod 안팎의 파일 위치 구분하기

컨테이너 안에 파일을 쓰는 것과 volume을 마운트하는 것이 비슷해 보여서, emptyDir와 hostPath가 무엇을 공유하고 어디에 남기는지 헷갈린다.

근거 · Kubernetes 교안 p132-p139, yaml/volume/emptydir-pod.yaml, hostpath-pod.yaml

emptyDir와 hostPath로 Pod 안팎의 파일 위치 구분하기 대표 이미지

1장: 컨테이너 속 파일, 왜 자꾸 사라지나요?

솔라가 노트북 화면을 들여다보며 미간을 찌푸렸다. 화면에는 터미널 창 몇 개가 떠 있었고, 무언가 생각대로 되지 않는 듯했다. 옆에서 조용히 책을 읽던 루나가 솔라의 낮은 한숨 소리에 고개를 들었다.

“잘 안 돼?”

“언니, 이거 좀 이상해. 내가 테스트용 웹서버 컨테이너에 로그 파일을 분명히 남겼거든? 그런데 컨테이너를 재시작했더니 파일이 감쪽같이 사라졌어. 내가 뭘 잘못한 거지?”

솔라의 목소리에는 당혹감이 묻어났다. 파일을 생성하고 저장하는 건 가장 기본적인 작업이라고 생각했는데, 당연히 있어야 할 파일이 사라졌다는 사실을 받아들이기 어려워 보였다. 마치 마술에라도 걸린 것 같았다.

“음, 파일이 저절로 사라지지는 않았을 텐데. 혹시 컨테이너 안에 파일을 직접 만든 거야?”

“응. exec 명령어로 컨테이너에 접속해서 파일을 만들었지. 당연히 저장될 줄 알았는데… 컨테이너 안에 파일을 두는 거랑, 사람들이 말하는 ‘볼륨’을 쓰는 거랑 근본적으로 다른가?”

솔라의 질문에 루나는 잠시 생각하더니 노트북을 자신 쪽으로 살짝 돌렸다. “백 번 설명하는 것보다 한번 직접 보는 게 빠를 거야. 아주 간단한 실험을 하나 해보자. 지금 하던 복잡한 앱 말고, 깨끗한 웹서버 하나만 딱 띄워서.”

루나는 솔라가 겪은 상황을 재현해 보기로 했다. 먼저, 가장 기본적인 nginx 웹서버 Pod를 생성하기 위한 YAML 파일을 작성했다.

# pod-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - name: nginx-container
    image: nginx

“자, 이 파일을 apply해서 Pod를 만들어봐.”

솔라는 익숙하게 명령어를 입력했다.

kubectl apply -f pod-nginx.yaml

잠시 후 nginx-pod가 성공적으로 생성되어 실행 중인 것을 확인했다.

“좋아. 이제 방금 네가 했던 것처럼 exec 명령어로 저 컨테이너 안으로 들어가서, 아무 파일이나 하나 만들어봐.”

솔라는 다시 키보드를 두드렸다.

# Pod 내부의 셸에 접속
kubectl exec -it nginx-pod -- /bin/bash

# 웹서버의 기본 경로에 간단한 텍스트 파일 생성
root@nginx-pod:/# echo 'This is a temporary file.' > /usr/share/nginx/html/test-file.txt

# 파일이 잘 생성되었는지 확인
root@nginx-pod:/# cat /usr/share/nginx/html/test-file.txt
This is a temporary file.

# 셸에서 빠져나오기
root@nginx-pod:/# exit

“봐봐, 언니. 이렇게 파일이 잘 만들어졌잖아. cat으로 내용도 확인했고.” 솔라가 말했다. 그녀의 표정에는 ‘이번에는 다르겠지’ 하는 기대감이 서려 있었다.

“응, 파일은 확실히 있네.” 루나가 차분하게 대답했다. “그럼, 이제 컨테이너에 문제가 생겨서 재시작되는 상황을 흉내 내 볼까? Pod를 삭제했다가 다시 만들어보는 거야.”

솔라는 kubectl delete pod nginx-pod 명령어로 Pod를 삭제했다. 그리고 조금 전과 똑같은 kubectl apply -f pod-nginx.yaml 명령어로 새로운 nginx-pod를 생성했다. 모든 과정은 순식간에 일어났다.

“자, 이제 다시 컨테이너 안으로 들어가서 아까 만든 파일이 있는지 확인해봐.”

솔라는 다시 exec 명령어를 입력하고, 파일이 있었던 경로의 내용을 확인했다.

# 새로 생성된 Pod의 셸에 접속
kubectl exec -it nginx-pod -- /bin/bash

# 아까 파일을 만들었던 경로 확인
root@nginx-pod:/# ls /usr/share/nginx/html/
50x.html  index.html

화면에는 test-file.txt가 보이지 않았다. 오직 nginx 이미지에 원래부터 포함되어 있던 기본 파일 두 개만 덩그러니 놓여 있었다.

“어? 진짜 없네! 내가 뭘 잘못한 게 아니었구나. 그냥… 그냥 사라지는 거였어.”

솔라는 허탈한 웃음을 터뜨렸다. 방금 전까지 가졌던 의심과 혼란이 눈 녹듯 사라지고, 그 자리에 새로운 깨달음이 자리 잡았다.

“이제 알겠다. 컨테이너는 마치 일회용 종이컵 같은 거였어. 물을 담아 마실 수는 있지만, 컵을 버리면 그 안에 남은 물도 같이 버려지는 거지. 데이터를 계속 보관하려면 컵 자체가 아니라, 컵을 받쳐둘 튼튼한 ‘컵홀더’나 ‘선반’이 필요한 거였어.”

솔라의 비유에 루나가 고개를 끄덕였다.

“정확해. 컨테이너의 파일 시스템은 임시적이야. 컨테이너가 실행되는 동안에만 존재하고, 컨테이너가 사라지면 그 안의 모든 데이터도 함께 사라지지. 그래서 중요한 데이터를 보존하려면 컨테이너의 수명 주기와는 독립적인, 바로 네가 말한 그 ‘선반’이 필요해.”

드디어 문제의 핵심을 파악한 솔라의 눈이 반짝였다. 그녀는 이제 진짜 질문을 던질 준비가 되어 있었다.

“그럼 그 ‘선반’ 역할을 해주는 게 바로 ‘볼륨’이라는 거야? 컨테이너가 사라져도 데이터를 안전하게 붙잡아두는 방법 말이야. 그건 대체 어떻게 쓰는 건데?“

2장: emptyDir: Pod 안의 임시 공유 공간

솔라의 질문이 채 끝나기도 전에, 루나는 이미 새로운 YAML 파일을 화면에 띄워놓고 있었다. 이전 장에서 봤던 단순한 nginx Pod와는 사뭇 다른 구조였다. 컨테이너가 두 개였고, 파일 하단에는 volumes라는 낯선 섹션이 추가되어 있었다.

# pod-emptydir.yaml
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
spec:
  containers:
  - name: writer-container
    image: busybox
    command: ["/bin/sh", "-c", "i=0; while true; do echo \"Event $i\" >> /shared-data/events.log; i=$((i+1)); sleep 2; done"]
    volumeMounts:
    - name: shared-storage
      mountPath: /shared-data
  - name: reader-container
    image: busybox
    command: ["/bin/sh", "-c", "tail -f /shared-data/events.log"]
    volumeMounts:
    - name: shared-storage
      mountPath: /shared-data

  volumes:
  - name: shared-storage
    emptyDir: {}

솔라는 코드를 찬찬히 뜯어보았다. 컨테이너 하나는 2초마다 로그를 파일에 쓰고, 다른 하나는 그 파일을 계속 읽고 있었다. 그리고 두 컨테이너 모두 /shared-data라는 경로를 shared-storage라는 볼륨에 연결(마운트)하고 있었다. 그 볼륨의 정체는 emptyDir, 즉 ‘빈 디렉터리’였다.

“언니, 이 emptyDir라는 거, 이름 그대로 그냥 빈 디렉터리를 하나 만들어서 컨테이너에 연결해주는 것 같네. 그럼 이 디렉터리는 어디에 생기는 거야? 지난번에 컨테이너가 사라지면 데이터도 같이 사라진다고 했으니까, 이 볼륨은 컨테이너 밖 어딘가, 예를 들면 쿠버네티스 노드(워커 머신) 같은 곳에 만들어지는 거 아닐까? 그래야 Pod를 지워도 데이터가 남아있을 테니까.”

솔라는 나름의 논리를 세워 가설을 제시했다. ‘볼륨’은 데이터를 보존하기 위한 것이라는 큰 그림에 자신의 추측을 끼워 맞춘 것이다. emptyDir가 영구적인 저장소의 한 종류일 것이라는 기대가 섞인 목소리였다.

루나는 솔라의 가설에 바로 답하는 대신, 실행 버튼을 누르라는 듯 턱짓을 했다. “그럴듯한 추리네. 정말 그런지 확인해볼까? 우선 이 Pod를 실행시켜서 두 컨테이너가 정말로 데이터를 공유하는지부터 보자.”

솔라는 kubectl apply -f pod-emptydir.yaml 명령어로 Pod를 생성했다. Pod가 실행 상태가 되자, 루나가 말했다.

“좋아. writer-container가 지금 /shared-data/events.log 파일에 계속 무언가를 쓰고 있을 거야. 그럼 reader-container가 그 파일을 잘 읽고 있는지 로그를 확인해보자.”

솔라는 reader-container의 로그를 실시간으로 스트리밍하는 명령어를 입력했다.

kubectl logs -f multi-container-pod -c reader-container

터미널 화면에 2초 간격으로 새로운 줄이 나타나기 시작했다.

Event 0
Event 1
Event 2
...

“오, 신기하다! writer-container가 만든 파일을 reader-container가 보고 있어! 둘은 완전히 다른 컨테이너인데, 마치 같은 컴퓨터에 있는 파일을 공유하는 것 같네.”

솔라의 눈이 동그래졌다. emptyDir 볼륨이 두 컨테이너를 잇는 다리 역할을 하는 것을 직접 확인한 순간이었다. 컨테이너가 서로 격리되어 있다는 기본 전제를 넘어서는 첫 경험이었다.

“맞아. emptyDir 볼륨은 Pod 안에 있는 모든 컨테이너가 공유할 수 있는 공간이야. Pod가 살아있는 동안에는 컨테이너가 재시작되어도 이 공간은 그대로 유지되지.”

루나의 설명에 솔라는 고개를 끄덕였다. “그럼 이제 핵심 질문이 남았네. 이 Pod를 삭제하면 /shared-data에 저장된 events.log 파일은 어떻게 될까? 내 추측대로라면 노드 어딘가에 남아있어야 해.”

솔라는 스스로 답을 찾으려는 듯, 망설임 없이 Pod 삭제 명령어를 입력했다.

kubectl delete pod multi-container-pod

pod "multi-container-pod" deleted 메시지가 뜨자, 잠시 정적이 흘렀다. 이제 데이터가 보존되었는지 확인할 차례였다. 하지만 솔라는 이내 막막한 표정을 지었다.

“근데… 파일이 남아있다면 어디서 찾아야 하지? 노드에 접속해서 뒤져봐야 하나?”

“그럴 필요 없어, 솔라.” 루나가 차분하게 말했다. “만약 emptyDir가 네 생각처럼 노드에 영구적인 파일을 남겼다면, 쿠버네티스는 그 파일을 관리할 방법을 제공했겠지. 하지만 그런 방법은 없어. 왜냐하면…”

루나는 잠시 말을 멈추고 솔라가 스스로 결론을 내리길 기다렸다. 솔라는 방금 전의 실험 과정과 결과를 머릿속으로 되짚어보았다. 두 컨테이너가 공유하던 공간, 그리고 Pod가 사라지자 그 흔적조차 찾을 길이 없어진 상황.

“아…”

짧은 탄성과 함께 솔라의 표정이 바뀌었다.

“알겠다. emptyDir는 ‘노드’에 연결되는 게 아니라 ‘Pod’ 자체에 묶여있는 거였구나! 컨테이너가 일회용 종이컵이라면, 이 emptyDir는 여러 컵을 잠시 함께 담아두는 종이 캐리어 안의 작은 빈 공간 같은 거네. Pod라는 캐리어 자체를 버리면, 그 안의 공간도 같이 사라지는 게 당연한 거였어.”

솔라의 새로운 비유는 정확했다. emptyDir는 컨테이너의 수명 주기보다는 길지만, Pod의 수명 주기를 넘어서지는 못했다. 컨테이너가 아닌 Pod와 생사를 함께하는 임시 저장소였던 것이다.

“그럼 데이터를 Pod와 상관없이, 정말로 영구적으로 보존하려면 다른 방법이 필요하겠네. 혹시 그게 hostPath라는 볼륨이 하는 일이야? 그건 이름부터가 ‘호스트 경로’니까, 정말 노드에 파일을 남기는 거 아닐까?”

emptyDir의 한계를 명확히 이해하자, 솔라의 궁금증은 자연스럽게 다음 단계로 넘어갔다. 그녀는 이제 Pod의 수명 주기를 뛰어넘는 데이터 보존 방법을 찾을 준비가 되어 있었다.

3장: hostPath: 노드와 연결된 영속적 저장소

솔라의 시선은 emptyDir 실험이 끝난 노트북 화면에 머물러 있었다. 그녀의 머릿속은 ‘Pod의 수명 주기’와 ‘데이터 보존’이라는 두 개념 사이의 관계를 그리느라 분주했다. 컨테이너는 일회용 컵, emptyDir는 컵을 잠시 담는 종이 캐리어. 그렇다면 Pod가 사라져도 데이터를 지킬 수 있는 진정한 ‘선반’은 어떤 모습일까?

그때 루나가 키보드를 몇 번 두드리더니, 새로운 YAML 파일을 화면에 띄웠다.

# pod-hostpath.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hostpath-pod
spec:
  containers:
  - name: test-container
    image: busybox
    command: ["/bin/sh", "-c", "echo 'This file is on the NODE filesystem.' > /persistent-data/node-file.txt; sleep 3600"]
    volumeMounts:
    - name: host-storage
      mountPath: /persistent-data
  volumes:
  - name: host-storage
    hostPath:
      path: /tmp/k8s-data # 노드의 실제 파일 시스템 경로
      type: DirectoryOrCreate

솔라는 파일을 보자마자 emptyDir와의 차이점을 즉시 발견했다. volumes 섹션의 emptyDir: {} 부분이 hostPath로 바뀌어 있었고, 그 안에는 구체적인 파일 경로가 명시되어 있었다.

“이건… hostPath라고 되어 있고, path/tmp/k8s-data라는 경로가 직접 적혀 있네. emptyDir는 그냥 빈 공간을 만들어달라는 요청이었는데, 이건 마치 ‘노드의 바로 저 위치를 사용하겠다’고 콕 집어서 말하는 것 같아.”

솔라의 목소리에는 조심스러운 추측이 담겨 있었다. emptyDir가 Pod와 운명을 함께하는 임시 공간이라는 것을 깨달은 후였기에, 이름부터 ‘호스트 경로’인 이 볼륨에 대해 섣부른 기대를 하지 않으려는 듯했다.

“정확히 봤어. hostPath는 Pod가 실행되는 노드, 즉 호스트 머신의 파일 시스템에 있는 특정 경로를 컨테이너 안으로 그대로 가져와 연결하는 방식이야. emptyDir처럼 쿠버네티스가 만들어주는 가상의 공간이 아니지.”

“그럼 이 Pod를 실행하면, 저 컨테이너는 노드의 /tmp/k8s-data 디렉터리에 node-file.txt라는 파일을 직접 만들게 되는 거네?”

“직접 확인해보자.” 루나가 짧게 답했다.

솔라는 kubectl apply -f pod-hostpath.yaml 명령어로 Pod를 생성했다. Pod가 ‘Running’ 상태가 된 것을 확인한 후, 그녀는 혹시나 하는 마음에 Pod를 삭제하기 전에 먼저 파일이 정말 생성되었는지 확인해 보기로 했다.

# Pod 안의 파일 내용 확인
kubectl exec hostpath-pod -- cat /persistent-data/node-file.txt

터미널에는 This file is on the NODE filesystem. 라는 문장이 선명하게 찍혔다.

“좋아, 파일은 잘 만들어졌어. 그럼 이제… 이 Pod를 삭제해 볼게.”

솔라는 kubectl delete pod hostpath-pod 명령어를 입력했다. Pod가 사라지는 것을 확인한 그녀는 루나를 바라보았다. emptyDir 때와는 다른 질문이 그녀의 눈빛에 담겨 있었다. Pod는 사라졌지만, 파일의 ‘집’은 노드의 파일 시스템이었다.

“파일이… 남아있을까?”

“이번엔 우리가 찾아갈 곳이 명확하지.”

루나는 클러스터의 노드 중 하나에 접속할 수 있는 터미널 창을 열었다. 그리고 hostPath YAML 파일에 명시했던 바로 그 경로의 내용을 확인하는 명령어를 입력했다. (실제 환경에서는 ssh 등으로 노드에 접속해야 한다.)

# 노드(호스트 머신)의 파일 시스템을 직접 확인
ls /tmp/k8s-data/

엔터 키를 누르자, 화면에 node-file.txt 라는 파일 이름이 나타났다.

”!”

솔라는 짧은 탄성을 질렀다. Pod는 흔적도 없이 사라졌지만, 그 Pod가 남긴 데이터는 노드의 파일 시스템 위에 뚜렷하게 살아남아 있었다. emptyDir가 Pod라는 종이 캐리어와 함께 버려지는 빈 공간이었다면, hostPath는 아예 캐리어 바깥의 단단한 선반 그 자체를 이용하는 것이었다.

“이제 알겠어… 드디어 그 문장의 진짜 의미를 알 것 같아.”

솔라는 잠시 생각에 잠기더니, 처음 이 주제를 접했을 때 혼란스러웠던 문장을 입에 올렸다.

“‘Volume을 사용하면 컨테이너 수명주기와 별도로 데이터를 저장하거나 공유할 수 있습니다.’ 이 말이 emptyDir에게는 ‘컨테이너’의 수명 주기와는 별도로, ‘Pod’의 수명 주기만큼은 데이터를 지켜준다는 뜻이었고, hostPath에게는 ‘컨테이너’는 물론이고 ‘Pod’의 수명 주기와도 완전히 별개로, ‘노드’가 살아있는 한 데이터를 지켜준다는 뜻이었구나!”

마침내 조각들이 모두 제자리를 찾은 느낌이었다. 컨테이너 내부 저장, emptyDir, hostPath. 이 세 가지 방식의 데이터 수명 주기가 머릿속에 선명한 계단처럼 그려졌다.

루나는 솔라가 스스로 깨달음에 도달한 것을 확인하고는, 마지막 확인 질문을 던졌다. “그럼 한번 맞춰봐. 내가 세 가지 상황을 줄게. 각각 어떤 저장 방식을 쓰는 게 가장 적절할까?”

  1. “웹 애플리케이션이 실행되는 동안에만 필요한 임시 캐시 파일을 저장해야 해. Pod가 재시작되면 사라져도 상관없어.”
  2. “하나의 Pod 안에서, 한 컨테이너가 시스템 로그를 수집하고 다른 컨테이너가 그 로그를 분석해서 외부로 전송해야 해.”
  3. “쿠버네티스 클러스터를 모니터링하는 에이전트 Pod가 있는데, 이 Pod가 노드 자체의 /var/log 디렉터리에 쌓이는 시스템 로그를 읽어야 해.”

솔라는 망설이지 않고 대답했다.

“첫 번째는… 그냥 컨테이너 파일시스템에 두면 될 것 같아. 가장 간단하고, 어차피 Pod가 재시작되면 사라져도 되니까. 두 번째는 Pod 안에서 컨테이너끼리 파일을 공유해야 하니까 **emptyDir**가 딱이네. Pod가 살아있는 동안에는 공유 공간이 유지되어야 하니까. 마지막은… Pod가 노드의 특정 파일을 읽어야 하는 거니까, 이건 무조건 **hostPath**를 써야겠네!”

정확한 대답과 그 이유까지 덧붙이는 솔라를 보며 루나는 작게 미소 지었다. 더 이상 솔라에게 볼륨은 막연하고 혼란스러운 개념이 아니었다. 각기 다른 수명 주기와 공유 범위를 가진, 상황에 맞게 선택해서 사용하는 분명한 도구가 되어 있었다. 이제 솔라는 어떤 데이터를 어디에 두어야 할지 스스로 판단할 수 있는 자신만의 ‘선반’을 갖게 된 것이다.