소개
이제 매니페스트로 원하는 상태를 정의하고 Pod 와 Deployment 를 생성할 수 있습니다. 다음으로 익혀야 할 핵심 기술은 Kubernetes 가 그 원하는 상태를 구현하지 못할 때 어떻게 대응하는지 이해하는 것입니다.
이 실습에서는 작은 Deployment 두 개를 다룹니다. 하나는 정상적으로 동작하고, 다른 하나는 의도적으로 이미지 태그를 잘못 지정했습니다. 동일한 클러스터 조건에서 두 Deployment 를 비교하면서, 넓은 범위의 증상에서 구체적인 근거로 좁혀 가는 반복 가능한 절차를 따라갑니다. 또한 실행 중인 객체만 임시로 수정하는 대신 매니페스트를 복구한 다음, 로그와 컨테이너 내부 명령을 통해 복구된 애플리케이션을 점검합니다.
목표는 가능한 모든 장애를 암기하는 것이 아닙니다. 관찰하고, 범위를 좁히고, 근거를 확인하고, 원하는 상태를 복구한 뒤, 정상 복구를 검증하는 침착한 문제 해결 습관을 기르는 것입니다.
통제된 장애 만들기
환경 시작: 이 실습에서는 완전한 Kubernetes 클러스터가 시작됩니다. 컨트롤 플레인, 노드 및 네트워크 구성 요소를 설정하는 데 일반적으로 2~3분이 걸립니다. 환경 로딩이 완료될 때까지 잠시 기다린 후 작업을 시작하세요.
실제 문제 해결은 증상에서 시작합니다. 이 단계에서는 정상적으로 동작하는 워크로드와 의도적으로 문제가 발생하도록 만든 워크로드를 배포하여, 동일한 클러스터 조건에서 둘을 비교합니다.
준비된 작업 공간으로 이동한 뒤 파일을 확인합니다. cd는 현재 디렉터리를 변경하고, ls는 해당 디렉터리 안의 파일 이름을 나열합니다. 다음 명령은 별도의 줄에서 순서대로 실행됩니다.
cd /home/labex/project/debug-lab
ls
healthy-web.yaml과 broken-web.yaml이 표시되어야 합니다. 두 파일 모두 복제본 하나를 사용하는 Deployment 를 정의하지만, 하나에는 나중에 직접 진단할 미묘한 설정 오류가 들어 있습니다.
두 매니페스트를 적용합니다. kubectl apply는 원하는 상태를 API 서버에 전달하고, 각 -f 옵션은 입력 파일 하나를 지정합니다. 하나의 명령에 -f 옵션을 여러 개 사용할 수 있습니다.
kubectl apply -f healthy-web.yaml -f broken-web.yaml
먼저 정상 Deployment 가 준비될 때까지 기다립니다.
kubectl rollout status deployment/healthy-web --timeout=60s
deployment "healthy-web" successfully rolled out라는 메시지는 중요한 기준점을 제공합니다. 클러스터가 Pod 를 스케줄링할 수 있고 캐시된 NGINX 이미지를 실행할 수 있다는 뜻입니다.
이제 다른 Deployment 에도 짧은 시간 동안 롤아웃 기회를 줍니다.
kubectl rollout status deployment/broken-web --timeout=15s || true
시간 초과는 예상된 결과입니다. || true는 이 실패가 실습을 중단해야 할 이유가 아니라 실습을 위한 근거이므로 셸이 계속 진행하도록 합니다.
Deployment 요약 정보를 비교합니다.
kubectl get deployments
healthy-web은 1/1 준비 상태로 표시되고, broken-web은 0/1로 표시되어야 합니다. 이를 통해 문제가 클러스터 전체가 아니라 특정 워크로드에 국한된 것임을 확인했습니다.
리소스 요약으로 문제 범위 좁히기
이 단계에서는 세부 정보를 살펴보기 전에 넓은 범위부터 확인합니다. Kubernetes 컨트롤러는 객체들의 연결 구조를 만들기 때문에, Deployment 문제는 ReplicaSet 과 Pod 에서 먼저 드러나는 경우가 많습니다.
관련 객체 유형을 한 번에 나열합니다. 쉼표를 사용하면 하나의 kubectl get 요청으로 여러 리소스 유형을 조회할 수 있고, -o wide를 추가하면 노드와 IP 정보 같은 유용한 열이 표시됩니다.
kubectl get deployments,replicasets,pods -o wide
출력을 위에서부터 차례로 읽어 보세요.
- Deployment 는 원하는 복제본 수와 사용 가능한 복제본 수를 보고합니다.
- ReplicaSet 은 원하는 복제본 수를 Pod 에 더 가까운 수준에서 관리합니다.
- Pod 는 컨테이너의 준비 상태와 간단한 상태 이유를 보고합니다.
레이블을 사용해 문제가 있는 애플리케이션만 조회합니다.
kubectl get pods -l app=broken-web -o wide
Pod 이름에는 자동으로 생성된 접미사가 포함되므로, 이름이 바뀔 수 있는 Pod 이름을 스크립트에 직접 복사하는 것보다 레이블을 사용하는 편이 안전합니다.
현재 단계에서 필요한 필드만 요청합니다. -o custom-columns='...'는 명시적으로 지정한 객체 필드로 표를 만듭니다. 각 항목에는 NAME과 같은 열 제목이 먼저 오고, 그 뒤에 값을 제공하는 JSON 필드 경로가 옵니다. 마지막의 백슬래시는 표시된 두 셸 줄을 하나의 명령으로 이어 줍니다.
kubectl get pods -l app=broken-web \
-o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[0].ready,WAITING_REASON:.status.containerStatuses[0].state.waiting.reason,NODE:.spec.nodeName'
대기 이유는 처음에는 ErrImagePull일 수 있고, 시간이 지나면 ImagePullBackOff로 바뀔 수 있습니다. 둘 다 Kubernetes 가 이미지를 가져오지 못해 컨테이너가 시작되지 않았다는 뜻입니다. 단순히“Pod 가 중단되었다”고 말하는 것보다 훨씬 정확한 정보입니다.
describe 로 Pod 점검하기
이 단계에서는 describe를 사용해 컨테이너가 대기 중인 이유를 알아봅니다. 앞의 요약에서는 무엇이 잘못되었는지 확인했다면, 이제 그 원인을 설명해 줄 근거를 수집합니다.
생성된 Pod 이름을 셸 변수에 저장합니다. $(...)는 명령 치환 구문으로, 셸이 안쪽의 kubectl 명령을 실행한 뒤 그 출력을 BROKEN_POD에 할당합니다. JSONPath 는 일치하는 첫 번째 Pod 의 이름을 선택하고, echo는 저장된 값을 출력합니다.
BROKEN_POD=$(kubectl get pods -l app=broken-web -o jsonpath='{.items[0].metadata.name}')
echo "$BROKEN_POD"
해당 Pod 를 자세히 조회합니다.
kubectl describe pod "$BROKEN_POD"
describe는 유용한 필드와 최근 이벤트를 함께 보여 줍니다. 다음 세 영역에 집중하세요.
- Containers → Image에는 요청된 이미지가 정확히 표시됩니다.
- State → Waiting → Reason에는 현재 컨테이너 상태가 설명됩니다.
- Events에는 kubelet 의 시도와 오류 메시지가 기록됩니다.
이 시나리오의 이벤트 메시지에는 1.27-alpine-missing 태그를 찾을 수 없다고 표시됩니다. 클러스터는 매니페스트가 요청한 대로 동작하고 있으며, 문제는 원하는 상태 자체에 있습니다.
JSONPath 로 이미지를 직접 확인합니다.
kubectl get pod "$BROKEN_POD" -o jsonpath='Image: {.spec.containers[0].image}{"\n"}'
큰 YAML 출력이나 describe 결과에 필요한 정보보다 훨씬 많은 내용이 포함되어 있을 때 JSONPath 가 유용합니다. 여기서는 나중에 반드시 수정해야 할 필드만 추려서 보여 줍니다.
이벤트를 시간순으로 읽기
이 단계에서는 이벤트를 Kubernetes 활동의 시간 순서로 읽어 봅니다. 이벤트는 스케줄링, 이미지 가져오기, 컨테이너 시작과 재시작, 그 밖의 다양한 상태 변화를 설명하는 데 도움이 되는 짧은 진단 기록입니다.
네임스페이스의 최근 이벤트를 시간순으로 나열합니다. --sort-by는 지정한 메타데이터 필드를 기준으로 객체를 정렬하고, 따옴표는 JSON 형식의 필드 경로가 하나의 인자로 전달되도록 묶어 줍니다.
kubectl get events --sort-by='.metadata.creationTimestamp'
마지막 행이 일반적으로 가장 최신 이벤트입니다. OBJECT 열이 문제가 있는 Pod 를 가리키고, REASON에 Pulling, Failed, BackOff와 같은 값이 포함된 항목을 찾으세요.
생성된 Pod 이름으로 이벤트를 필터링하면 불필요한 내용을 줄일 수 있습니다. --field-selector는 레이블이 아니라 서버 측 객체 필드를 기준으로 필터링합니다. 쉼표는 두 조건이 모두 일치해야 함을 의미하며, 백슬래시는 읽기 편하도록 여러 줄에 걸쳐 하나의 명령을 작성할 수 있게 합니다.
BROKEN_POD=$(kubectl get pods -l app=broken-web -o jsonpath='{.items[0].metadata.name}')
kubectl get events \
--field-selector involvedObject.kind=Pod,involvedObject.name="$BROKEN_POD" \
--sort-by='.metadata.creationTimestamp'
get, describe, events는 서로 보완적인 관점으로 생각할 수 있습니다.
get은 문제가 있는 객체를 빠르게 찾습니다.describe는 하나의 객체에 대한 설정, 상태, 관련 이벤트를 함께 보여 줍니다.events는 반복된 시도를 드러낼 수 있는 시간순 보기를 제공합니다.
반복되는 BackOff 이벤트는 Kubernetes 가 Pod 를 포기했다는 뜻이 아닙니다. 이미지 가져오기가 실패한 뒤 재시도 간격을 늘리고 있다는 의미입니다.
원하는 상태 복구 및 정상 동작 확인
이 단계에서는 원하는 상태를 복구하고 정상 동작을 확인합니다. 이제 조치에 필요한 근거가 충분합니다. 매니페스트가 존재하지 않는 이미지 태그를 요청하고 있습니다. 먼저 저장된 매니페스트를 수정한 다음 적용하여 파일과 클러스터의 실제 상태가 일치하도록 합니다.
두 매니페스트의 이미지 줄을 비교해서 표시합니다. grep은 텍스트를 검색하고, -n은 각 일치 항목 앞에 줄 번호를 붙이며, 하나의 명령으로 두 파일을 모두 검색합니다.
grep -n 'image:' healthy-web.yaml broken-web.yaml
정상 매니페스트는 nginx:1.27-alpine을 사용하고, 문제가 있는 매니페스트에는 존재하지 않는 -missing 접미사가 추가되어 있습니다.
해당 접미사만 바꿉니다. sed는 s/old/new/ 형식으로 작성한 텍스트 치환을 수행하며, -i는 변경된 내용을 출력만 하지 않고 지정한 파일에 직접 반영합니다.
sed -i 's/nginx:1.27-alpine-missing/nginx:1.27-alpine/' broken-web.yaml
수정한 파일을 로컬에서 검증합니다.
kubectl apply --dry-run=client -f broken-web.yaml
파일과 클러스터의 실제 객체 사이의 차이를 미리 확인합니다.
kubectl diff -f broken-web.yaml || true
차이가 발견되면 kubectl diff는 종료 코드 1을 반환하므로, || true를 사용하면 학습 흐름을 계속 진행할 수 있습니다. 차이 출력에서 -로 시작하는 줄은 이전 이미지를, +로 시작하는 줄은 수정된 이미지를 나타냅니다.
수정 사항을 적용하고 복구를 기다립니다.
kubectl apply -f broken-web.yaml
kubectl rollout status deployment/broken-web --timeout=60s
두 Deployment 가 이제 정상인지 확인합니다.
kubectl get deployments
두 Deployment 모두 1/1 준비 상태로 표시되어야 합니다. Kubernetes 는 수정된 Pod 템플릿을 바탕으로 새 ReplicaSet 과 Pod 를 생성했으므로, 실패한 Pod 를 수동으로 복구할 필요가 없었습니다.
애플리케이션 로그 읽기
이 단계에서는 컨테이너가 시작되었으므로 애플리케이션 로그를 새로운 근거로 활용합니다. kubectl logs는 컨테이너의 표준 출력과 표준 오류 스트림을 가져옵니다.
broken-web이 관리하는 새 정상 Pod 를 선택합니다. 앞에서 사용한 명령 치환과 JSONPath 를 다시 활용합니다. --field-selector=status.phase=Running은 선택된 Pod 가 실행 중이어야 한다는 서버 측 조건을 추가합니다.
WEB_POD=$(kubectl get pods -l app=broken-web \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}')
echo "$WEB_POD"
아직 아무도 페이지를 요청하지 않았다면 NGINX 에 접근 로그가 없을 수 있습니다. Pod 내부에서 요청을 보내 로그를 생성합니다. kubectl exec POD -- COMMAND에서 --는 kubectl 옵션과 컨테이너 내부에서 실행할 명령을 구분합니다. wget -qO-는 조용히 페이지를 가져와 표준 출력으로 보내고, 파이프 |는 그 출력을 head로 전달하여 앞부분만 표시합니다.
kubectl exec "$WEB_POD" -- wget -qO- http://127.0.0.1 | head
HTML 이 <!DOCTYPE html>로 시작한다면 NGINX 가 포트 80 에서 로컬 요청에 응답했다는 뜻입니다.
이제 최근 로그를 읽습니다. --tail=10은 가장 최근 10 줄만 표시하므로, 시작 과정에서 발생한 많은 출력이 유용한 요청 기록을 가리지 않습니다.
kubectl logs "$WEB_POD" --tail=10
GET / HTTP/1.1을 포함한 HTTP 요청과 200 응답 코드를 찾아보세요. 로그는 컨테이너가 실행 중이지만 애플리케이션이 예상과 다르게 동작할 때 특히 유용합니다. 이미지 가져오기 실패에는 일반적으로 로그가 도움이 되지 않습니다. 컨테이너가 시작조차 하지 못했기 때문입니다.
컨테이너 내부에서 점검하기
이 단계에서는 컨테이너 내부에서 복구된 애플리케이션을 점검합니다. kubectl exec는 이미 실행 중인 컨테이너에서 명령을 실행하며, 파일 시스템, 프로세스, 환경 변수, DNS 관점 또는 로컬 네트워크 동작을 확인할 수 있습니다.
실행 중인 Pod 이름을 다시 사용합니다.
WEB_POD=$(kubectl get pods -l app=broken-web \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}')
컨테이너에 호스트 이름을 요청합니다.
kubectl exec "$WEB_POD" -- hostname
출력은 Pod 이름과 일치합니다. Kubernetes 가 기본적으로 Pod 의 호스트 이름을 Pod 이름으로 설정하기 때문입니다.
컨테이너 내부에서 NGINX 설정 문법을 확인합니다.
kubectl exec "$WEB_POD" -- nginx -t
syntax is ok와 test is successful 메시지가 표시되면 애플리케이션 설정이 내부적으로 유효하다는 뜻입니다.
마지막으로 컨테이너 내부에서 안쪽으로 확인하는 간단한 상태 점검을 수행합니다. >/dev/null은 다운로드한 HTML 을 버리고, &&는 wget이 성공한 경우에만 echo를 실행합니다. 따라서 HTTP 응답을 받은 뒤에만 성공 메시지가 표시됩니다.
kubectl exec "$WEB_POD" -- wget -qO- http://127.0.0.1 >/dev/null && echo "NGINX responded inside the Pod"
exec는 신중하게 사용해야 합니다. 실행 중인 컨테이너가 필요하므로 앞에서 발생한 이미지 가져오기 실패를 진단하는 데는 사용할 수 없었습니다. 이번 문제에서 사용한 근거 확인 순서는 다음과 같습니다.
get -> describe -> events -> repair manifest -> rollout status -> logs -> exec
장애 유형에 따라 서로 다른 단계에서 진단이 멈출 수 있지만, 비용이 적은 요약 정보에서 시작해 점점 더 깊이 점검하면 문제 해결의 초점을 유지할 수 있습니다.
요약
Kubernetes v1.35 에서 초보자에게 필요한 전체 디버깅 순환 과정을 연습했습니다. 정상 워크로드와 비정상 워크로드를 비교하고, 레이블과 간결한 필드로 문제 범위를 좁혔으며, describe와 이벤트를 사용해 잘못된 이미지 태그를 찾아냈습니다. 그런 다음 선언적 구성의 기준이 되는 매니페스트를 수정하고, 롤아웃 상태, 로그, 컨테이너 내부 명령을 통해 복구를 확인했습니다.
가장 중요한 교훈은 워크로드의 현재 생명 주기 단계에 맞는 근거를 선택하는 것입니다. 컨테이너가 아직 시작되지 않았다면 상태와 이벤트를 확인해야 합니다. 컨테이너가 실행 중이라면 로그와 exec를 통해 애플리케이션 수준의 동작을 파악할 수 있습니다. 다음 과제에서는 이 워크플로를 스스로 적용해 봅니다.


