Kubernetes Practice 04
Pod Manifest, 단순한 '생성'을 넘어선 '실행'의 여정
Pod YAML의 apiVersion, kind, metadata, spec을 적는 것은 보이지만 이 문서가 어떻게 실제 컨테이너 실행으로 이어지는지 잘 잡히지 않는다.
근거 · Kubernetes 교안 p45-p54, yaml/po/pod.yaml
1장: YAML은 ‘주문서’, apply는 ‘접수’
솔라의 모니터에는 간결한 YAML 파일 하나가 열려 있었다. 딱 봐도 쿠버네티스 Pod를 정의하는 파일이었다. 그 옆에 열린 메모장에는 솔라가 오늘 본 강의 자료에서 복사해 둔 문장이 적혀 있었다.
YAML 파일을 작성하고 kubectl apply -f 명령으로 Pod를 생성합니다.
솔라는 턱을 괸 채 문장을 소리 내어 읽어 보았다. ”’…Pod를 생성합니다.‘라.” 문장은 명료했지만, 솔라는 어딘가 꺼림칙한 기분을 떨칠 수 없었다. 마치 마술사가 “상자 뚜껑을 닫으면 비둘기가 나옵니다”라고 말하는 것처럼, 가장 중요한 과정이 쏙 빠져있는 느낌이었다.
“언니, 이거 좀 이상하지 않아?”
마침 방으로 들어오던 루나가 솔라의 부름에 걸음을 멈췄다.
“kubectl apply를 하면 그냥 Pod가 ‘뿅’하고 생긴다는 거잖아. 무슨 마법 주문도 아니고. 내 컴퓨터에 있는 이 YAML 파일이 어떻게 저 멀리 있는 쿠버네티스 클러스터의 Pod가 되는 건데? apply라는 명령어가 대체 무슨 일을 하는 걸까?”
루나는 솔라의 화면을 잠시 들여다보더니, 빙긋 웃으며 솔라의 책상 끝에 있던 빈 노트와 펜을 집어 들었다.
“좋은 질문이야. 마법이 아니라 잘 짜인 시스템이지. 솔라, 만약 네가 아주 근사한 레스토랑에 가서 메뉴판을 보고 주문한다고 생각해 봐.”
루나는 노트에 네모난 메뉴판 모양을 쓱쓱 그렸다.
“여기 YAML 파일이 바로 그 메뉴판에서 네가 고른 요리들을 적은 ‘주문서’야.”
솔라는 루나의 말에 고개를 끄덕였다.
“내가 원하는 것들을 적어놓은 문서라는 뜻이구나. apiVersion, kind, metadata, spec… 전부 내가 클러스터에 바라는 상태를 적은 거니까. 그럼 kubectl apply는?”
“그 주문서를 주방장에게 직접 가져다주는 걸까, 아니면 카운터에 접수하는 걸까?”
“음… 카운터에 내는 거겠지? 주방은 바쁘니까.”
“바로 그거야.” 루나가 말했다. “kubectl apply 명령어는 솔라 네가 작성한 YAML이라는 주문서를 쿠버네티스 클러스터의 유일한 공식 접수 창구인 ‘API 서버’에 제출하는 행위야. Pod를 직접 ‘생성’하는 게 아니라, ‘이런 Pod를 원합니다’라는 **원하는 상태(Desired State)**를 전달하고 접수시키는 거지.”
“접수…?” 솔라의 눈이 동그래졌다. 생성과는 전혀 다른 어감이었다. “그럼 내가 명령어를 실행하는 순간은, 그냥 주문이 들어간 것뿐이라는 거야?”
“직접 확인해 볼까? 주문이 접수되면 어떤 기록이 남는지 보는 거야.”
루나는 솔라에게 터미널을 열고 간단한 NGINX Pod YAML 파일을 하나 만들라고 했다.
my-nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-nginx
spec:
containers:
- name: nginx-container
image: nginx
“좋아. 이제 이 파일을 apply 할 건데, 실행하자마자 바로 다음 명령어를 입력해서 클러스터에 어떤 이벤트가 기록되는지 확인해 볼 거야. 주문서가 접수되는 바로 그 순간을 포착하는 거지.”
솔라는 루나가 알려준 대로, 첫 번째 명령어를 실행하고 재빨리 두 번째 명령어를 터미널에 붙여 넣었다.
# 1. YAML 파일 제출 (주문 접수)
$ kubectl apply -f my-nginx-pod.yaml
pod/my-nginx created
# 2. 클러스터 이벤트 기록 확인
$ kubectl get events --field-selector type=Normal -o wide
터미널 화면에 이벤트 목록이 나타났다. 하지만 솔라가 예상했던 ‘Pod 생성 완료!’ 같은 메시지는 아니었다.
LAST SEEN TYPE REASON OBJECT SUBOBJECT SOURCE MESSAGE
...
2s Normal Scheduled pod/my-nginx default-scheduler Successfully assigned default/my-nginx to worker-node-1
1s Normal Pulling pod/my-nginx kubelet Pulling image "nginx"
0s Normal Pulled pod/my-nginx kubelet Successfully pulled image "nginx"
0s Normal Created pod/my-nginx kubelet Created container nginx-container
0s Normal Started pod/my-nginx kubelet Started container nginx-container
“어…” 솔라가 화면을 가리켰다. “Pod가 바로 만들어진 게 아니네. 스케줄러가 노드를 할당하고, kubelet이라는 애가 이미지를 받아 와서 컨테이너를 생성하고 시작시켰어. 순서가 있잖아!”
루나가 만족스러운 표정으로 고개를 끄덕였다. “이제 알겠어? kubectl apply 명령이 한 일은, 이 모든 과정의 시작점, 즉 ‘default/my-nginx 라는 Pod를 원한다’는 주문을 API 서버에 선언하고 접수시킨 것뿐이야. 그 뒤의 일들은 주문서를 받은 클러스터의 여러 구성요소들이 협력해서 처리한 결과지.”
솔라는 아하, 하는 소리를 내며 자신의 메모장을 다시 보았다. ‘Pod를 생성합니다’라는 문장이 이제 다르게 보였다. 그건 apply 명령어 하나의 동작을 설명한 게 아니라, ‘주문서 제출’부터 ‘요리 완성’까지 이어지는 전체 과정의 최종 결과를 요약한 말이었던 것이다.
“그럼 apply는 ‘실행’이 아니라 ‘접수’였구나. 내 의도를 담은 주문서를 API 서버에 제출하는 것. 그래서 ‘선언적 API’라고 하는 거구나. 나는 ‘어떻게’ 만들지는 말하지 않고 ‘무엇을’ 원하는지만 선언하면 되니까.”
솔라는 명쾌해진 기분으로 의자에 등을 기댔다. 마법 같던 과정의 첫 번째 실마리가 풀렸다. 하지만 곧 새로운 궁금증이 고개를 들었다.
“언니, 그럼 주문서가 접수되고 나서… 저 이벤트 목록에 나온 것처럼 주방 안에서는 정확히 무슨 일이 벌어지는 거야? API 서버는 접수원 겸 관리자라고 했지? 그럼 저기 보이는 kubelet이 진짜 요리사, 그러니까 컨테이너를 만드는 역할을 하는 애인가?”
주문서가 카운터를 떠나 주방으로 넘어간 순간, 그 안에서 벌어지는 분주한 움직임이 솔라의 머릿속에 그려지기 시작했다.
2장: API 서버는 ‘관리자’, Kubelet은 ‘실행자’
솔라의 질문이 채 끝나기도 전에, 루나는 아까 ‘주문서’ 비유를 그렸던 노트를 다시 집어 들었다. 그리고 솔라가 봤던 이벤트 목록 위에 노트를 펼쳤다. 노트에는 ‘주문서(YAML) -> 접수처(API 서버)‘라고 적힌 그림이 있었다. 루나는 그 그림 아래에 굵은 선을 하나 그어 공간을 둘로 나눴다.
“네 질문이 바로 이 선에 대한 거야.”
루나는 선 아래 공간에 ‘주방’이라고 적고는, 그 안에 작은 네모들을 여러 개 그렸다. “여기가 바로 실제 요리가 일어나는 주방, 즉 쿠버네티스 클러스터의 워커 노드(Worker Nodes)들이지. 그리고 각 네모 안에는 ‘쿠킹 스테이션’이 하나씩 있어.” 루나는 작은 네모 하나를 가리키며 덧붙였다. “그리고 그 스테이션의 책임 요리사가 바로 kubelet이야.”
솔라는 루나의 그림과 모니터의 이벤트 목록을 번갈아 보았다. ‘Scheduled’, ‘Pulling’, ‘Created’, ‘Started’ 같은 단어들이 눈에 들어왔다. 그 옆에는 default-scheduler와 kubelet이라는 출처가 적혀 있었다.
“언니 말대로라면, 내 주문서는 ‘접수처’인 API 서버에 갔어. 그런데 어떻게 수많은 ‘주방’의 ‘요리사’ 중 하나인 kubelet이 내 주문을 알고 요리를 시작한 거지? API 서버가 ‘worker-node-1에 있는 kubelet! 이 요리 좀 해!’ 하고 직접 외치기라도 하는 거야?”
솔라는 API 서버가 모든 것을 지시하고 통제하는 중앙 관제탑 같은 모습일 거라고 상상했다.
“그렇게 직접 소리치지 않아. 더 효율적인 시스템이지.” 루나가 말했다. “API 서버는 접수된 모든 주문을 거대한 ‘주문 게시판’에 붙여놓기만 해. 그러면 주방에 있는 모든 요리사(kubelet)들이 각자 자기 스테이션에 할당된 새 주문이 있는지 계속해서 그 게시판을 쳐다보는 거야. 명령을 기다리는 게 아니라, 스스로 할 일을 찾아가는 거지.”
이것이 바로 쿠버네티스의 핵심 원리 중 하나인 **조정 루프(Reconciliation Loop)**였다. 각 구성요소는 중앙의 ‘원하는 상태’를 계속 감시하며, 현재 상태가 그와 다를 경우 스스로 일치시키려 노력한다.
“그럼 내 주문서가 게시판에 어떻게 붙었는지, 그리고 어떤 요리사가 그걸 가져갔는지 볼 수 있어?”
“물론. 아까 본 이벤트 목록보다 더 자세한 ‘작업 일지’를 보면 돼.”
루나는 솔라에게 my-nginx 파드의 상세 정보를 조회하는 명령어를 알려주었다.
$ kubectl describe pod my-nginx
솔라가 명령어를 입력하자, 터미널 화면이 새로운 정보로 가득 찼다. 이전보다 훨씬 상세한 내용이었다.
Name: my-nginx
Namespace: default
Priority: 0
Node: worker-node-1/10.0.1.101
Start Time: ...
Labels: <none>
Annotations: <none>
Status: Running
IP: 192.168.1.5
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m default-scheduler Successfully assigned default/my-nginx to worker-node-1
Normal Pulling 90s kubelet Pulling image "nginx"
Normal Pulled 85s kubelet Successfully pulled image "nginx"
Normal Created 85s kubelet Created container nginx-container
Normal Started 84s kubelet Started container nginx-container
“와… 훨씬 자세하다.” 솔라는 출력된 내용의 Events 섹션에 다시 주목했다. “아까 봤던 이벤트들이랑 같은데, 여기엔 더 중요한 정보가 있네!”
솔라가 화면의 특정 부분을 손가락으로 가리켰다.
“여기 봐! 맨 처음 Scheduled 이벤트는 default-scheduler가 발생시켰어. 메시지를 보니까 ‘my-nginx를 worker-node-1에 성공적으로 할당했다’고 되어 있네. 이게 바로 언니가 말한 ‘주문 게시판에 어느 스테이션 담당인지 적어두는’ 과정이구나!”
루나는 말없이 고개를 끄덕였다. 솔라가 스스로 발견하길 기다려주는 듯했다.
“그리고 그 다음부터는 전부 From이 kubelet이야! worker-node-1에 있던 kubelet이 자기한테 할당된 주문을 보고, 이미지를 가져오고(Pulling), 컨테이너를 만들고(Created), 시작까지 시킨 거네(Started). API 서버는 그냥 ‘어떤 파드가 필요하다’는 선언과 ‘그건 worker-node-1에서 만들어져야 한다’는 결정만 했을 뿐, 실제 실행은 전부 kubelet이 담당한 거야.”
솔라는 이제 kubectl apply 이후의 과정을 명확하게 그릴 수 있었다. YAML 주문서는 API 서버에 접수되고, 스케줄러가 최적의 노드를 결정해 주문표에 ‘담당자’를 지정한다. 그러면 해당 노드의 kubelet이 그 주문을 발견하고, 자신의 노드 위에서 컨테이너를 만들어 실행한다. 관리자와 실행자의 역할이 완벽하게 분리되어 있었다. describe 명령어는 이 분업과정을 확인할 수 있는 훌륭한 실행자 확인 도구였다.
모든 조각이 맞춰진 기분이었다. 솔라는 만족스럽게 describe 결과의 맨 윗부분을 보았다. Status: Running.
“좋아, 이제 이 Pod는 ‘Running’ 상태니까… 완벽하게 동작하고 있다는 뜻이겠지? 주문한 음식이 완성돼서 나온 거나 마찬가지잖아.”
솔라의 목소리에는 이제 막 하나의 미스터리를 해결한 사람의 자신감이 묻어 있었다. 하지만 주방에서 요리가 완성되었다는 신호가, 손님의 식탁에 음식이 제대로 도착했음을 보장하는 것은 아니었다. ‘상태’에 대한 확인과 ‘동작’에 대한 확인 사이에는 아직 보이지 않는 간격이 남아 있었다.
3장: ‘상태’ 확인과 ‘동작’ 확인은 달라요
솔라의 시선은 kubectl describe 명령어의 출력 결과 윗부분에 고정되어 있었다. Status: Running. 모든 것이 제자리에 있는 듯한, 깔끔하고 만족스러운 단어였다. 그녀는 이제 Running이라는 상태가 kubelet이 자신의 임무, 즉 ‘컨테이너 시작시키기’를 성공적으로 마쳤다는 보고서임을 이해했다.
만족스러운 표정의 솔라를 보며 루나가 모니터를 툭툭 가리켰다. “좋아, 주방에서 요리가 완성되었다는 신호는 확인했네. 하지만 그 음식이 서빙 직원을 통해 네 테이블까지 무사히 도착했는지, 그리고 네가 맛을 봤을 때 정말 주문한 그 맛이 나는지는 또 다른 문제 아닐까?”
루나의 말에 솔라의 표정에 잠시 물음표가 떠올랐다. “‘Running’이면 다 된 거 아니었어? 컨테이너가 시작됐다는 뜻이잖아.”
“맞아. kubelet이 자신의 임무, 즉 ‘컨테이너 시작시키기’를 성공적으로 마쳤다는 보고서지. 쿠버네티스 시스템 관점에서 보면 Pod의 상태는 정상이야. 하지만 그 안에서 실행되는 애플리케이션, 즉 NGINX 웹 서버가 정말로 요청을 받아서 응답할 준비가 되었는지는 쿠버네티스가 직접 확인해주지 않아. 그건 우리가 직접 확인해야 할 몫이야.”
솔라는 ‘상태’와 ‘동작’이라는 두 단어 사이의 보이지 않는 간격을 느끼기 시작했다. 주방에서 요리가 완성되었다는 신호와, 손님의 식탁에 음식이 제대로 도착했음을 확인하는 것은 다른 차원의 일이었다.
“그럼… 손님이 음식을 직접 맛보는 것처럼, 나도 저 NGINX 서버가 진짜로 응답하는지 확인해봐야 한다는 거네? 어떻게?”
“우선, 그 Pod가 클러스터 안에서 어떤 주소를 갖고 있는지 알아내야지. 서빙할 테이블 번호를 확인하는 것처럼.”
루나는 솔라에게 get pod 명령어에 -o wide 옵션을 추가해보라고 알려주었다.
$ kubectl get pod my-nginx -o wide
솔라가 명령어를 입력하자, 이전보다 더 넓은 정보가 테이블 형식으로 나타났다.
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-nginx 1/1 Running 0 5m 192.168.1.5 worker-node-1 <none> <none>
“아! 여기 IP라는 항목이 있네. 192.168.1.5. 이게 이 Pod의 주소구나.”
“바로 그거야. 이제 그 주소로 네트워크 요청을 보내서, 정말 NGINX 서버가 응답하는지 확인해 볼 차례야. curl 명령어를 사용하면 돼.”
솔라는 설레는 마음으로 터미널에 새로운 명령어를 입력했다. Pod의 고유한 IP 주소로 직접 말을 거는 순간이었다.
$ curl http://192.168.1.5
엔터 키를 누르자마자, 익숙한 HTML 코드가 화면에 나타났다.
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
“와! 진짜로 응답이 왔어! ‘Welcome to nginx!’ 페이지야.”
솔라는 환호성을 질렀다. 이제야 모든 퍼즐 조각이 완벽하게 맞춰지는 기분이었다.
“이제 알겠다!” 솔라가 모니터 화면의 describe 결과와 curl 결과를 번갈아 가리키며 외쳤다. “이 Status: Running은 쿠버네티스 관리자가 보는 ‘컨테이너는 정상적으로 시작되었습니다’라는 상태 보고서였던 거야. 하지만 이 curl 결과야말로 사용자가 경험하는 진짜 동작 확인인 거고. 두 개는 완전히 다른 거였어!”
kubectl get이나 describe는 클러스터의 ‘원하는 상태’가 얼마나 잘 구현되었는지 확인하는 관리자의 도구였다. 반면 curl은 그 결과물인 애플리케이션이 실제 서비스를 제공하는지 검증하는 사용자의 도구였다. 솔라는 이제 이 둘을 구분할 수 있는 명확한 기준, 실제 동작 검증이라는 자신만의 도구를 얻게 되었다.
그 순간, 솔라의 눈에 맨 처음 자신을 혼란스럽게 했던 메모장의 문장이 다시 들어왔다.
YAML 파일을 작성하고 kubectl apply -f 명령으로 Pod를 생성합니다.
솔라는 펜을 집어 들고 그 문장 아래에 자신만의 주석을 달기 시작했다.
kubectl apply -f my-pod.yaml: 내 ‘원하는 상태’를 담은 주문서(YAML)를 API 서버에 **접수(선언)**한다.kubectl describe pod my-pod: 주문이 어떻게 처리되는지(스케줄링, 이미지 풀링, 컨테이너 생성) 중간 과정을 **확인(실행자 확인)**하고, 쿠버네티스 시스템이 인지하는Running상태를 확인한다.curl <Pod IP>: 그래서, 진짜로 서비스가 동작하는지 직접 확인한다!
이제 그 한 줄의 문장은 더 이상 모호한 마법 주문이 아니었다. ‘생성’이라는 단어 뒤에 숨겨진, 선언과 조정, 그리고 상태 확인과 동작 검증으로 이어지는 거대하고 정교한 시스템의 여정이 솔라의 눈앞에 선명하게 펼쳐져 있었다.