Kubernetes Practice 05
Pod 상태 진단, 'Running'을 넘어선 세 가지 시선: exec, logs, delete
Pod가 Running이면 끝난 것처럼 보이지만, 실제 실습에서는 컨테이너 안에 들어가거나 로그를 보거나 삭제 후 상태 변화를 확인해야 하는 이유가 분명하지 않다.
근거 · Kubernetes 교안 p55-p57
1장: Luna: ‘Running’ 상태, 그 너머의 진실
1. ‘Running’ 상태, 그 너머의 진실
솔라가 놋북을 무릎에 올린 채 거실 소파에 앉아있는 루나에게 다가왔다. 평소보다 한 톤 높은, 들뜬 목소리였다.
“언니, 나 드디어 내 웹 애플리케이션 배포 성공했어! 봐봐, 터미널에 Running이라고 딱 뜨잖아!”
솔라가 가리킨 화면에는 kubectl get pods 명령어의 결과가 깔끔하게 출력되어 있었다.
NAME READY STATUS RESTARTS AGE
buggy-app 1/1 Running 0 2m
‘Running’. 그 단어는 마치 모든 것이 완벽하게 작동하고 있다는 성공의 증표처럼 보였다. 솔라의 얼굴에는 뿌듯함이 가득했다.
루나는 솔라의 화면을 잠시 들여다본 뒤, 고개를 들어 솔라를 바라보았다. “음, 축하해. Running 상태까지 잘 만들었네. 그런데, 그래서 애플리케이션은 잘 작동하는지 확인해 봤어?”
“당연하지!”
솔라는 자신만만하게 대답하며, Pod로 트래픽을 전달하기 위해 미리 설정해 둔 포트 포워딩 명령어를 다른 터미널 창에 입력했다. 이제 로컬호스트로 요청을 보내면, Pod 안에서 실행 중인 애플리케이션이 응답할 차례였다. 그녀는 curl 명령어로 간단한 HTTP 요청을 날렸다.
$ curl http://localhost:8080
그러나 화면에는 기대했던 웹페이지 내용 대신, 차가운 메시지만이 깜박였다.
curl: (7) Failed to connect to localhost port 8080 after 2 ms: Connection refused
솔라의 미간이 살짝 찌푸려졌다. “어? 왜 연결이 안 되지…?” 그녀는 다시 kubectl get pods 명령어를 실행했다. 여전히 buggy-app Pod는 씩씩하게 Running 상태를 유지하고 있었다.
“이상하다. Pod는 분명히 살아있는데… Running이잖아. 그럼 당연히 작동해야 하는 거 아니야?”
솔라의 목소리에는 당혹감이 묻어났다. 화면 속의 Running이라는 글자와 Connection refused라는 현실 사이의 불일치. 그것이 솔라를 혼란스럽게 만들고 있었다.
루나는 자신의 놋북을 옆으로 밀어두고 솔라 쪽으로 몸을 돌렸다.
“솔라, 가게 문에 ‘영업 중(Open)‘이라고 불이 켜져 있다고 생각해 봐. 그건 무슨 뜻일까?”
“음… 가게가 문을 열었고, 손님을 받을 준비가 되었다는 뜻?”
“맞아. 보통은 그렇지. 하지만 ‘영업 중’ 불은 켜져 있는데, 막상 들어가 보니 주방장이 아파서 출근을 못 했거나, 갑자기 수도가 고장 나서 요리를 전혀 할 수 없는 상황일 수도 있잖아.”
루나는 솔라의 터미널 화면을 가리켰다. “쿠버네티스가 보여주는 저 ‘Running’ 상태가 바로 그 ‘영업 중’ 불빛 같은 거야. 쿠버네티스는 우리에게 이렇게 말해주고 있는 거지. ‘네가 요청한 컨테이너를 내가 성공적으로 시작시켰어. 그리고 아직 컨테이너가 꺼지거나 비정상적으로 종료되지는 않았어.’ 딱 거기까지.”
루나의 말에 솔라는 잠시 생각에 잠겼다. ‘영업 중’ 불빛. 그 비유는 꽤 직관적으로 다가왔다. 가게의 기반 시설(전기)은 정상이지만, 실제 서비스(요리)는 불가능한 상태.
솔라는 자신의 터미널 창 두 개를 번갈아 보았다. 한쪽에는 Running이라는 희망적인 상태가, 다른 한쪽에는 Connection refused라는 절망적인 결과가 떠 있었다. 이제 그 둘은 더 이상 모순으로 보이지 않았다.
“아…” 솔라의 입에서 작은 탄식이 흘러나왔다. “그러니까 ‘Running’은 컨테이너라는 ‘가게’의 문이 열렸다는 신호일 뿐, 그 안의 ‘주방’이 제대로 돌아가고 있다는 걸 보증하는 건 아니구나.”
마치 퍼즐 조각이 맞춰지는 느낌이었다. Running이라는 상태 표시가 틀린 것이 아니었다. 그저 자신이 그 단어의 의미를 너무 넓게 해석하고 있었을 뿐이다. Pod는 실행 중이지만, 그 안의 애플리케이션이 제 역할을 하지 못하는 상황이 얼마든지 있을 수 있었다.
판단이 바뀌자, 새로운 질문이 자연스럽게 떠올랐다.
“알겠어, 언니. 그럼… ‘영업 중’ 불은 켜졌는데 음식이 안 나오는 이 상황에서, 대체 주방 안에서는 무슨 일이 벌어지고 있는지 알려면 어떻게 해야 해? 주방 문을 열고 직접 들여다봐야 하는 거 아냐?“
2장: Luna: exec, Pod 내부 관찰의 시작
2. exec, Pod 내부 관찰의 시작
솔라의 질문에 루나는 대답 대신, 솔라의 놋북을 자기 쪽으로 살짝 끌어당겼다. 그리고는 이전의 명령어들이 남아있는 터미널 창에 새로운 명령어를 타이핑하기 시작했다. 솔라는 언니의 손가락 움직임을 따라 화면에 나타나는 글자들을 조용히 지켜보았다.
kubectl로 시작하는 것은 익숙했지만, 그 뒤에 따라오는 단어는 생소했다. exec. 실행하다. 무언가를 실행한다는 뜻인가?
루나의 손가락이 멈추자, 화면에는 다음과 같은 명령어가 완성되어 있었다.
kubectl exec -it buggy-app -- bash
솔라는 고개를 갸웃했다. “exec? 이건 뭐하는 명령어이야? -it나 -- 같은 옵션도 그렇고… 꼭 예전에 쓰던 SSH 접속 명령어랑 비슷하게 생겼는데. Pod에 직접 접속하는 거야?”
루나는 엔터 키를 누르기 직전에 잠시 멈추고 말했다. “비슷하지만, 아주 중요한 차이가 있어. 지금부터 네가 보게 될 건, 마치 유령처럼 컨테이너 안으로 쑥 들어가는 것과 같을 거야.”
엔터 키가 눌리는 순간, 터미널의 모습이 극적으로 변했다. 솔라의 사용자 이름과 현재 디렉터리를 보여주던 익숙한 프롬프트가 사라지고, 새로운 줄이 나타났다.
root@buggy-app:/#
“어!” 솔라가 짧은 외마디 소리를 냈다. 분명 자신의 놋북 터미널인데, 프롬프트의 주인이 buggy-app이라는 이름으로 바뀌어 있었다. 마치 다른 세계로 통하는 문이 열린 것 같았다.
“한번 ls -l 명령어를 쳐볼래?” 루나가 제안했다.
솔라는 홀린 듯이 키보드에 ls -l을 입력하고 엔터를 쳤다. 화면에는 현재 디렉터리의 파일 목록이 주르륵 나타났다. 웹 애플리케이션을 만들 때 사용했던 소스 코드 파일들이 보였다.
“와… 내 파일들이 그대로 있네. 정말로 컨테이너 안에 들어온 거잖아?”
“정확히는, ‘들어왔다’기보다는 컨테이너 안에서 bash라는 셸 프로그램을 실행시키고, 우리 터미널을 그 셸에 연결한 거야. exec는 ‘execute’의 줄임말로, 지정한 컨테이너 안에서 명령어를 실행해 주는 도구거든. 우리는 bash 셸을 실행시켜서, 마치 그 안을 자유롭게 돌아다니는 것처럼 만든 거지.”
솔라는 이제 감을 잡았다는 듯, 다른 명령어를 시도해 보았다. 시스템에서 실행 중인 프로세스 목록을 확인하는 ps aux였다.
root@buggy-app:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 12345 6789 ? Ss 10:30 0:00 /bin/sh -c 'python app.py'
root 7 0.0 0.0 5432 1234 ? S 10:30 0:00 python app.py
root 15 0.1 0.0 9876 5432 pts/0 Ss 10:35 0:00 bash
root 22 0.0 0.0 7654 3210 pts/0 R+ 10:36 0:00 ps aux
프로세스 목록을 훑어보던 솔라의 눈이 한 곳에 멈췄다. 자신의 파이썬 애플리케이션(python app.py)이 분명히 실행 중이었다.
“어라? 이상하다. 앱이 실행 중인데? 그럼 왜 연결이 안 된 거지?”
“조금 더 자세히 들여다볼까? 웹 서버라면 특정 포트를 사용해야 할 텐데.” 루나가 힌트를 주었다.
솔라는 네트워크 연결 상태를 확인하는 netstat -tuln 명령어를 입력했다. 결과는 명확했다. 그 어떤 프로세스도 8080 포트를 사용하고 있지 않았다. 애플리케이션이 실행은 되었지만, 정작 외부 요청을 받아야 할 포트를 열지 않은 것이다. Connection refused의 원인이 드디어 밝혀지는 순간이었다.
솔라는 의자 등받이에 몸을 기댔다. “아하… ‘Running’은 그냥 컨테이너가 살아 있다는 뜻이고, exec는 그 살아있는 컨테이너의 주방 문을 열고 들어가서 ‘주방장이 지금 뭘 하고 있는지, 가스레인지 불은 켰는지’ 직접 확인하는 거구나.”
kubectl get pods가 밖에서 가게 문이 열렸는지 확인하는 것이라면, kubectl exec는 안으로 들어가 내부 상황을 정밀하게 관찰하는 현미경과도 같았다. 더 이상 Running이라는 상태에 속지 않고, 문제의 진짜 원인을 찾아낼 수 있는 첫 번째 열쇠를 손에 쥔 기분이었다.
판단이 바뀌자, 시야가 넓어졌다. 그리고 또 다른 질문이 꼬리를 물었다.
“좋아, 이제 프로세스가 포트를 열지 않았다는 건 알겠어. 그런데… 왜? 혹시 애플리케이션이 시작되다가 뭔가 오류 메시지를 출력하고 죽었거나, 아니면 지금도 계속 오류를 뱉어내고 있는 건 아닐까? exec는 지금 현재 상태를 보는 거니까, 과거에 무슨 일이 있었는지는 알기 어렵잖아. 애플리케이션이 남긴 ‘목소리’를 들을 방법은 없을까?“
3장: Luna: logs, 애플리케이션의 목소리 듣기
3. logs, 애플리케이션의 목소리 듣기
솔라가 exit를 입력하자, root@buggy-app:/# 이라며 낯선 주소를 보여주던 프롬프트가 익숙한 자신의 놋북 터미널로 돌아왔다. 컨테이너라는 다른 세계로 통하던 문이 닫힌 기분이었다. exec 덕분에 컨테이너 안을 탐색해 본 결과, 애플리케이션 프로세스는 떠 있지만 정작 8080 포트는 열려 있지 않다는 사실까지는 알아냈다.
하지만 핵심적인 질문, ‘왜?‘는 여전히 풀리지 않은 채 공중에 떠 있었다. 솔라는 그 답답함을 참지 못하고 입을 열려던 참이었다. 바로 그때, 루나가 먼저 키보드를 두드렸다. exec와는 또 다른, 새로운 단어였다.
kubectl logs buggy-app
“잠깐, 언니.” 솔라가 루나의 손을 제지했다. “logs? 로그를 보는 명령어인가 보네. 근데 왜 굳이? 아까 exec로 안에 들어갈 수 있었잖아. 들어가서 로그 파일이 어디 있는지 찾아서 열어보면 되는 거 아니야?”
솔라의 생각은 지극히 합리적이었다. 이미 컨테이너 내부를 마음대로 돌아다닐 수 있는 exec라는 만능 열쇠를 손에 넣었는데, 굳이 logs라는 별도의 도구가 필요하다는 게 이해되지 않았다. 마치 집 안에 들어가서 방마다 불을 켜볼 수 있는데, 굳이 밖에서 창문으로 안을 들여다보는 도구가 따로 있는 것 같은 느낌이었다.
루나는 솔라의 질문에 말없이 미소를 지어 보이고는, 엔터 키를 눌렀다.
화면에 몇 줄의 텍스트가 나타났다.
Starting buggy-app server...
Loading configuration from /app/config.ini
ERROR: Port number '8080' is not allowed. Application cannot start on a privileged port.
Initialization failed.
순간 정적이 흘렀다. 솔라의 눈은 화면에 나타난 ERROR라는 단어에 고정되었다. 애플리케이션이 허용되지 않은 포트 번호 때문에 시작조차 못 했다는 명백한 실패 기록이었다. 모든 조각이 맞춰지는 순간이었다.
kubectl get pods는Running을 보여줬다. (컨테이너 자체는 성공적으로 시작되었으니까)curl은Connection refused를 보냈다. (애플리케이션이 포트를 열지 않았으니까)kubectl exec로 확인한netstat결과는 8080 포트를 쓰는 프로세스가 없다고 했다. (애플리케이션이 시작에 실패했으니까)- 그리고
kubectl logs는 그 모든 일의 원인이 ‘허용되지 않은 포트 번호’ 때문이라고 말해주고 있었다.
“아…”
솔라의 입에서 낮은 탄성이 터져 나왔다. 그녀는 두 개의 명령어를 머릿속으로 나란히 놓아보았다.
| 명령어 | 확인하는 것 | 비유 |
|---|---|---|
kubectl exec | 컨테이너의 현재 상태 (실행중인 프로세스, 열린 포트, 파일 시스템) | 주방에 직접 들어가 가스레인지 불이 켜졌는지, 재료가 있는지 눈으로 확인 |
kubectl logs | 애플리케이션이 남긴 메시지 흐름 (표준 출력, 에러 메시지) | 주방 밖에서 “가스가 잠겼어요!” 또는 “재료가 다 떨어졌어요!” 라고 외치는 요리사의 목소리를 듣기 |
“이제 알겠다.” 솔라가 말했다. “두 개는 완전히 다른 걸 보고 있었구나. exec는 컨테이너라는 환경의 스냅샷을 찍는 현미경 같은 거였고, logs는 그 안에서 실행되는 애플리케이션의 목소리를 듣는 녹음기 같은 거였어.”
루나가 고개를 끄덕였다. “맞아. 요즘 컨테이너 기반 애플리케이션들은 로그를 파일에 일일이 저장하지 않는 경우가 많아. 대신 ‘표준 출력’이나 ‘표준 에러’라는 통로로 메시지를 그냥 계속 내보내지. kubectl logs는 바로 그 통로에 귀를 대고 있다가, 애플리케이션이 하는 모든 이야기를 우리에게 들려주는 거야. exec로 들어가서 아무리 파일 시스템을 뒤져도 찾을 수 없는 소리인 셈이지.”
exec로 문을 열고 들어가 내부 구조를 살피는 것과, logs로 벽에 귀를 대고 내부의 소리를 듣는 것. 두 가지 도구는 서로를 대체하는 것이 아니라, 문제를 완전히 이해하기 위해 반드시 함께 사용해야 할 한 쌍의 진단 도구였다. 더 이상 ‘Running’이라는 상태 표시나, exec로 확인한 단편적인 정보에만 의존하지 않을 수 있게 되었다.
판단이 명확해지자, 다음 행동 계획이 자연스럽게 그려졌다.
“좋아. 이제 원인도 알았으니 애플리케이션 코드에서 포트 번호를 수정하고, 다시 빌드해서 배포해야겠네. 그럼 이 문제 있는 Pod는 이제 필요 없으니까 지워야겠다. 그냥 kubectl delete pod buggy-app 이렇게 하면… 깔끔하게 사라지는 거 맞지?”
솔라는 자신 있게 말하며, 마치 문제 해결의 마지막 단계를 수행하려는 듯 키보드에 손을 올렸다. 삭제. 그것은 모든 것의 끝을 의미하는 것처럼 보였다. 정말 그럴까?
4장: Luna: delete, 수명주기 변화의 확인자
4. delete, 수명주기 변화의 확인자
문제의 원인을 파악한 솔라의 손가락이 키보드 위에서 경쾌하게 움직일 준비를 했다. buggy-app이라는 이름의 문제아를 이제 깔끔하게 정리할 시간이었다. 그녀의 손가락이 kubectl delete pod...를 향해 움직이던 바로 그 순간, 루나가 조용히 손을 들어 제지했다.
“잠깐. 그거 지우기 전에, 다른 실험을 하나 해보자.”
루나는 솔라의 놋북을 가져가는 대신, 자신의 놋북을 열어 무언가를 빠르게 입력했다. 곧이어 솔라의 터미널 창에 새로운 명령어를 복사해서 붙여넣어 주었다.
kubectl create deployment managed-app --image=nginx
kubectl get pods
솔라가 의아한 표정으로 명령어를 실행하자, 잠시 후 buggy-app과는 다른, 처음 보는 이름의 Pod가 나타났다.
NAME READY STATUS RESTARTS AGE
buggy-app 1/1 Running 0 15m
managed-app-7f5d4d5d9c-xyzab 1/1 Running 0 10s
“이건 또 뭐야? managed-app? Deployment? 나는 그냥 buggy-app을 지우고 싶은 건데…” 솔라의 목소리에는 ‘왜 일을 복잡하게 만들지?‘라는 불만이 살짝 섞여 있었다. 그녀에게 delete는 단순히 불필요한 파일을 휴지통에 버리는 것처럼, 끝을 의미하는 명확한 행위였다. 그 이상도 이하도 아니었다.
“delete는 그냥 Pod를 없애는 명령 아니야? 뭐 특별히 확인할 게 있어?”
루나는 대답 대신 화면을 가리켰다. “이제 그 새로 생긴 managed-app Pod를 한번 지워볼래? buggy-app이 아니라.”
솔라는 언니의 의도를 도무지 알 수 없었지만, 일단 시키는 대로 했다. 찝찝한 마음으로 delete 명령어를 입력했다.
kubectl delete pod managed-app-7f5d4d5d9c-xyzab
터미널은 즉시 pod "managed-app-7f5d4d5d9c-xyzab" deleted 라는 메시지를 돌려주었다. 깔끔하게 삭제되었다는 확인 메시지였다. ‘그래, 이게 끝이지. 뭐가 다르다는 거야?’ 솔라는 확인사살을 위해, 그리고 자신의 생각이 맞다는 것을 증명하기 위해 get pods 명령을 다시 실행했다. 이번에는 -w 옵션을 붙여서 실시간으로 상태 변화를 지켜보기로 했다.
kubectl get pods -w
화면이 깜박이며 변화를 보여주기 시작했다.
NAME READY STATUS RESTARTS AGE
buggy-app 1/1 Running 0 16m
managed-app-7f5d4d5d9c-xyzab 1/1 Terminating 0 80s
managed-app-7f5d4d5d9c-pqrst 0/1 Pending 0 1s
managed-app-7f5d4d5d9c-pqrst 0/1 ContainerCreating 0 1s
managed-app-7f5d4d5d9c-pqrst 1/1 Running 0 3s
“어?”
솔라의 눈이 동그래졌다. 자신이 방금 삭제한 Pod가 Terminating(종료 중) 상태로 바뀌는 것과 거의 동시에, pqrst라는 꼬리표를 단 새로운 managed-app Pod가 Pending(대기) 상태로 나타났다. 그리고는 순식간에 ContainerCreating을 거쳐 Running 상태로 바뀌었다. 마치 불사조처럼, 하나를 죽이니 다른 하나가 곧바로 살아나는 모습이었다.
“뭐야, 이거? 방금 분명히 지웠는데 왜 또 생겨? 버그인가?”
혼란에 빠진 솔라에게 루나가 조용히 설명했다. “버그가 아니야. 그게 바로 쿠버네티스의 진짜 모습이야. 솔라, 너는 Pod를 삭제하라고 ‘명령’한 게 아니야. 너는 쿠버네티스라는 거대한 시스템의 관리자에게 ‘이 Pod를 삭제해 주세요’라고 ‘요청’한 거지.”
루나는 말을 이었다. “그리고 managed-app Pod에게는 ‘Deployment’라는 직속 상사가 있어. 그 상사의 임무는 ‘어떤 상황에서든 managed-app Pod를 반드시 하나 유지할 것’이야. 네가 Pod 하나를 없애달라고 요청하자, 쿠버네티스는 그 요청을 받아들여 Pod를 없앴지. 하지만 그 직후, ‘Deployment’라는 상사는 자신의 부하가 한 명 사라진 것을 알아채고, 즉시 새로운 부하를 만들어내서 원래 약속된 상태, 즉 ‘Pod 한 개가 있는 상태’로 되돌려 놓은 거야.”
솔라는 그제야 두 가지 삭제 행위의 근본적인 차이를 깨달았다.
| 상황 | 나의 행동 (delete) | 쿠버네티스의 반응 | 비유 |
|---|---|---|---|
상사가 없는 Pod (buggy-app) | Pod 삭제 요청 | Pod를 삭제하고 끝낸다. | 혼자 일하는 프리랜서 해고하기 |
상사가 있는 Pod (managed-app) | Pod 삭제 요청 | Pod를 삭제하지만, 상사가 즉시 새 Pod를 생성한다. | 회사 직원을 해고하자, 매니저가 즉시 새 직원을 채용하기 |
delete는 단순히 자원을 지우는 파괴적인 명령이 아니었다. 그것은 시스템의 ‘원하는 상태’를 흔들어보고, 시스템이 그 상태를 유지하기 위해 어떻게 반응하는지 관찰할 수 있는 확인용 탐침이었다.
“아하! delete는 끝이 아니라, 변화의 시작을 관찰하는 도구구나. 이 명령을 통해 ‘이 Pod는 혼자인가? 아니면 누군가 관리하고 있는가?’ 그리고 ‘관리하고 있다면, 그 관리 규칙은 무엇인가?‘를 직접 확인할 수 있는 거였어.”
exec가 현미경처럼 내부를 들여다보는 도구이고, logs가 소리를 듣는 청진기였다면, delete는 시스템의 반사 신경을 테스트하는 작은 망치와 같았다. 무릎을 톡 쳤을 때 다리가 저절로 올라가는 모습을 보며 신경계의 건강 상태를 확인하는 것처럼 말이다.
판단이 바뀌자, 흩어져 있던 도구들이 하나의 목적 아래 정렬되는 느낌이었다.
“알겠어. 이제 확실히 이해했어. 그럼 이제 내 손에 세 가지 시선이 생긴 거네. 컨테이너 내부를 들여다보는 exec, 애플리케이션의 목소리를 듣는 logs, 그리고 쿠버네티스의 의지를 확인하는 delete. 그런데… 대체 이 세 가지를 어떤 순서로, 어떻게 조합해서 써야 진짜 문제를 빠르고 정확하게 진단할 수 있는 걸까?“
5장: Luna: 세 가지 시선으로 Pod 상태 진단하기
5. 세 가지 시선으로 Pod 상태 진단하기
솔라는 자신의 놋북 화면 베젤에 작은 포스트잇 세 개를 붙여 놓았다. 각각 exec: 현미경, logs: 청진기, delete: 반사망치 라고 적혀 있었다. 그녀는 손가락으로 포스트잇들을 떼었다 붙였다 하며 순서를 바꿔보았다. 현미경을 먼저 써야 하나? 아니면 청진기? 반사망치는 대체 언제 쓰는 거지? 개별 도구의 용도는 이제 선명했지만, 이들을 엮어 하나의 진단 흐름으로 만드는 것은 또 다른 문제였다. 마치 훌륭한 연장들을 잔뜩 가지고 있지만, 막상 고장 난 기계 앞에서 어떤 것부터 손에 쥐어야 할지 막막한 초보 기술자 같았다.
“헷갈리는구나.”
소파에 앉아 책을 읽던 루나가 솔라의 부산스러운 손가락 움직임을 보고는 조용히 입을 열었다.
“응. exec는 내부를 보고, logs는 소리를 듣고, delete는 반응을 본다는 건 알겠어. 그런데 실제 문제가 터졌을 때, 이 셋을 어떤 순서로 써야 가장 빨리 원인을 찾을 수 있을지 감이 안 와. 그냥 전부 다 한 번씩 써봐야 하나?” 솔라의 목소리에는 답답함이 묻어났다.
루나는 책을 덮고 솔라의 옆으로 다가왔다. “좋은 질문이야. 정해진 왕도는 없지만, 효과적인 순서는 있어. 내가 가상의 문제를 하나 내줄게. 한번 풀어볼래?”
루나는 자신의 놋북 화면에 가상의 시나리오를 띄웠다.
“자, 여기 data-processor라는 Pod가 있어. 중요한 데이터를 처리하는 임무를 맡고 있지. 네가 kubectl get pods 명령어로 상태를 확인했더니, 이런 결과가 나왔다고 상상해 봐.”
NAME READY STATUS RESTARTS AGE
data-processor-a1b2 1/1 Running 1 5m
“어?” 솔라는 즉시 이상한 점을 발견했다. “상태는 Running인데… RESTARTS가 1이네? 한 번 재시작했다는 뜻이잖아.”
“맞아. 지금은 멀쩡히 돌고 있지만, 5분 사이에 무슨 이유에선지 한 번 죽었다 살아났어. 자, 이제 넌 이 문제의 원인을 찾아야 해. 네 손에는 세 개의 도구가 있고. 첫 번째로 어떤 도구를 쓰겠어?”
솔라는 잠시 고민했다. 재시작했다는 건 과거의 사건이다. 지금 당장 exec로 들어가 봤자, 이미 새로 시작된 깨끗한 컨테이너 내부만 보게 될 가능성이 높다. 그렇다면…
“알겠다! 과거에 무슨 일이 있었는지 알아야 하니까, 애플리케이션이 남긴 마지막 말을 들어봐야 해. logs를 쓸래!”
솔라의 판단에 루나는 말없이 고개를 끄덕였다. 하지만 솔라가 kubectl logs data-processor-a1b2 라고 입력하려 하자, 루나가 손으로 막았다. “거의 맞았는데, 하나가 빠졌어. 그 명령어는 ‘지금’ 실행 중인 컨테이너의 로그를 보여줘. 우리가 궁금한 건 그게 아니지?”
“아! 죽기 직전, ‘이전’ 컨테이너가 남긴 로그!”
솔라는 즉시 명령어를 수정했다.
kubectl logs data-processor-a1b2 --previous
화면에는 이전에 실패했던 컨테이너가 남긴 유언과도 같은 메시지가 나타났다.
Processing data from /config/source.csv...
FATAL: Configuration file '/config/source.csv' not found.
Shutting down.
“찾았다!” 솔라가 외쳤다. “설정 파일을 못 찾아서 죽었던 거구나.”
“좋아. 원인은 찾았네.” 루나가 말했다. “그럼 이제 두 번째 도구를 써서, 네 추리가 맞는지 확인해 봐.”
솔라는 망설임 없이 다음 명령어를 입력했다. 이제는 exec를 쓸 차례였다. 로그가 알려준 사실을 직접 눈으로 확인하는 것이다.
kubectl exec -it data-processor-a1b2 -- ls /config
결과는 간결했다. ls: cannot access '/config': No such file or directory. 해당 디렉터리 자체가 존재하지 않았다. 원인은 명확했다. 설정 파일이 있어야 할 볼륨이 제대로 마운트되지 않은 것이다.
솔라는 이제 완전히 감을 잡았다. 그녀는 루나를 보며 자신 있게 말했다. “마지막으로 delete. 만약 이 Pod가 Deployment에 의해 관리되고 있었다면, 내가 지금 kubectl delete pod 명령어로 이걸 지워도 문제는 해결되지 않을 거야. Deployment는 똑같은 설정으로 새로운 Pod를 계속 만들어낼 거고, 그 새 Pod도 똑같은 이유로 계속 죽었다 살아나기를 반복하겠지.”
루나는 미소를 지었다. 솔라는 더 이상 개별 명령어에 매몰되지 않았다. 증상에 따라 올바른 도구를 선택하고, 각 도구로 얻은 정보를 조합하여 전체 그림을 그리고 있었다.
솔라는 자신의 놋북 앞에 앉아, 방금 겪은 진단 과정을 정리하기 시작했다. 이것은 언니가 알려준 지식이 아니라, 스스로의 힘으로 얻어낸 자신만의 문제 해결 프레임워크였다.
| 징후 (Symptom) | 첫 번째 의문 | 첫 번째 진단 도구 | 확인 가능한 것 |
|---|---|---|---|
Connection refused (연결 거부) | “앱이 포트를 열었나?” | exec (netstat) | 컨테이너 내부 네트워크 상태 (현재) |
RESTARTS > 0, CrashLoopBackOff | ”왜 죽었을까?” | logs --previous | 앱의 마지막 메시지 (과거) |
| Pod를 삭제했는데 다시 생겨남 | ”누가 이 Pod를 관리하지?” | delete (테스트) | 상위 컨트롤러의 존재와 규칙 (시스템) |
Running이지만 동작 안 함 | ”앱이 지금 뭘 하고 있지?” | logs (-f 옵션) | 앱의 실시간 출력 (현재) |
표를 완성한 솔라는 아까 흩어져 있던 세 개의 포스트잇을 다시 집었다. 그리고는 방금 만든 표 옆에, 자신만의 진단 순서도처럼 나란히 붙였다.
logs: 먼저 소리를 들어 과거와 현재의 이야기를 파악한다. (청진기)exec: 로그로 얻은 단서를 가지고 내부로 들어가 현장을 확인한다. (현미경)delete: 이 문제가 개별 Pod의 문제인지, 시스템 전체의 문제인지 반응을 떠본다. (반사망치)
“이제 알겠어.” 솔라가 중얼거렸다. “어떤 문제가 생겨도 당황하지 않고, 어떤 도구를 먼저 들어야 할지. 겉모습만 보는 게 아니라, 소리를 먼저 듣고, 현장을 확인한 다음, 전체 시스템의 반응을 살피는 거야.”
더 이상 Running이라는 단어에 속지 않고, 그 너머의 진실을 파헤칠 세 가지 단단한 시선을 손에 넣은 순간이었다. 솔라는 만족스러운 표정으로, 문제투성이였던 buggy-app Pod를 지우기 위해 터미널 창을 열었다. 이번에는 자신감이 가득했다.