icon

Kubernetes 애플리케이션 트러블슈팅

Kubernetes 애플리케이션 트러블슈팅

개요

이 문서는 Kubernetes 클러스터에 워크로드를 배포하는 과정이나 운영 도중 발생할 수 있는 문제의 원인과 해결 방법을 안내합니다.

워크로드 유형 중 가장 많이 사용되는 웹 애플리케이션을 기준으로 설명하며, 바닐라 Kubernetes와 퍼블릭 클라우드의 관리형 Kubernetes 서비스는 물론 Samsung Cloud Platform Kubernetes Engine(이하 SKE) 환경에서도 동일하게 참고할 수 있습니다.

애플리케이션 일반 구조

Kubernetes에서 웹 애플리케이션을 외부에 노출하는 방식에는 hostNetwork, NodePort, LoadBalancer 타입 Service 등 여러 가지가 있습니다. 이 문서에서는 HTTP/HTTPS 기반 도메인 호출에 가장 적합한 Ingress 방식을 기준으로 설명합니다.

다음은 nginx 웹 애플리케이션을 Kubernetes 워크로드로 배포할 때 사용하는 리소스 조합의 예입니다.

Deployment, Service, Ingress 참조 관계도
그림. Deployment, Service, Ingress 간의 참조 관계

이 외에도 PersistentVolume이나 NetworkPolicy 등 다양한 리소스를 함께 사용할 수 있지만, 위 예시가 Ingress를 사용하는 가장 기본적인 리소스 구성입니다.

주의
위 그림은 Deployment, Service, Ingress 간의 참조 관계를 보여 주기 위한 개념도입니다. 그림의 Ingress 예시에 사용된 apiVersion: extensions/v1beta1과 kubernetes.io/ingress.class 어노테이션은 Kubernetes 1.22부터 제거된 구버전 API입니다. 실제 매니페스트를 작성할 때는 다음과 같이 networking.k8s.io/v1 API와 spec.ingressClassName 필드를 사용하세요.
배경색 변경
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /foo
        pathType: Prefix
        backend:
          service:
            name: my-service
            port:
              number: 80
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /foo
        pathType: Prefix
        backend:
          service:
            name: my-service
            port:
              number: 80
코드블록. 현재 Ingress API 기준 예시

여기서 중요한 것은 각 리소스 간의 참조 관계입니다. Deployment와 Service의 selector는 모두 Pod template의 label을 참조하고, Ingress의 backend에 지정한 서비스명은 Service 리소스의 name을 참조합니다. 이후 설명하는 대부분의 문제는 이 참조 관계가 어긋나면서 발생하므로 반드시 기억해 두세요.

이어지는 내용에서는 각 리소스가 배포되었을 때의 정상 상태 점검 방법과, 문제가 발생했을 때의 대처 방법을 살펴봅니다.

Pod 상태 점검

기본 명령어

Pod의 상태를 점검할 때 사용하는 kubectl 명령어는 다음과 같습니다.

Pod의 기본 상태를 확인하는 명령어입니다. IP와 할당된 노드 정보까지 함께 보려면 -owide 옵션을, YAML 매니페스트 형태의 원본 데이터를 보려면 -oyaml 옵션을 사용하세요.

배경색 변경
kubectl get pod
kubectl get pod -owide
kubectl get pod <pod-name> -oyaml
kubectl get pod
kubectl get pod -owide
kubectl get pod <pod-name> -oyaml
코드블록. Pod 기본 조회 명령어

문제가 발생한 Pod의 상세 정보와 Event를 확인할 때 사용하는 명령어입니다.

배경색 변경
kubectl describe po <pod-name>
kubectl describe po <pod-name>
코드블록. Pod 상세 정보 확인

실행 중인 애플리케이션에 문제가 생겼을 때 로그를 확인하는 명령어입니다. Pod 안에서 여러 Container가 구동 중이라면 -c 옵션으로 특정 Container를 지정할 수 있고, -f 옵션으로 실시간 tailing을, --previous 옵션으로 재시작 이전 Container의 로그를 확인할 수 있습니다.

배경색 변경
kubectl logs <pod-name> [-c <container-name>]
kubectl logs <pod-name> [-c <container-name>] -f
kubectl logs <pod-name> [-c <container-name>] --previous
kubectl logs <pod-name> [-c <container-name>]
kubectl logs <pod-name> [-c <container-name>] -f
kubectl logs <pod-name> [-c <container-name>] --previous
코드블록. Pod 로그 확인

네임스페이스 안에서 발생한 Event를 조회하는 명령어입니다. --sort-by 옵션으로 발생 시간순 정렬이 가능합니다.

배경색 변경
kubectl get ev --sort-by=.lastTimestamp
kubectl get ev --sort-by=.lastTimestamp
코드블록. 네임스페이스 Event 조회
참고
Event는 기본적으로 약 1시간(kube-apiserver의 --event-ttl 기본값)만 보관되므로, 오래된 장애는 Event로 확인되지 않을 수 있습니다. Kubernetes 1.26 이상에서는 kubectl events --for pod/<pod-name> 명령으로 특정 오브젝트의 Event만 시간순으로 조회할 수 있습니다.

정상적인 Pod Lifecycle

비정상 상태를 판별하려면 먼저 Pod를 배포했을 때 정상적으로 거치는 상태 변화 흐름을 알아야 합니다.

일반적인 Pod는 다음과 같은 Lifecycle을 갖습니다.

Pod Lifecycle 상태 흐름
그림. Pod Lifecycle 상태 흐름
  • Pending: Pod가 스케줄될 노드가 결정되기 전, 또는 스케줄 가능한 노드가 없는 상태입니다.
  • ContainerCreating: Pod가 특정 노드에 스케줄된 후 해당 노드의 Container Runtime이 Container를 생성하는 단계입니다. 이미지를 내려받거나 ConfigMap, PersistentVolume 등 연계 자원을 마운트하는 과정에서 표시될 수 있습니다.
  • Running: Pod 내 Container가 모두 생성되고 프로세스가 구동 중인 상태입니다.
  • Ready: Running 이후 Readiness Probe를 통과하여 Pod가 서비스할 준비가 되었음을 의미합니다.
  • Terminating: Pod가 삭제되어 해당 노드에서 정리되는 과정에서 표시됩니다.
참고

위 다섯 가지는 kubectl get pod의 STATUS/READY 컬럼에 표시되는 값을 기준으로 한 것으로, Kubernetes API가 정의한 Pod phase(status.phase)와는 다릅니다. Pod phase는 Pending, Running, Succeeded, Failed, Unknown 다섯 가지뿐이며, ContainerCreating이나 Terminating은 kubectl이 Container 상태와 삭제 시각(deletionTimestamp)을 조합해 보여 주는 값입니다.

또한 Ready는 STATUS 컬럼이 아니라 READY 컬럼(예: 1/1)으로 확인합니다. STATUS는 Running을 유지한 채 READY 값만 0/1에서 1/1로 바뀌는 것이 정상적인 흐름입니다.

kubectl get po 명령어에 -w(watch) 옵션을 사용하면 다음과 같이 Pod Lifecycle 변화를 확인할 수 있습니다. Pod의 정상 여부를 판단하는 가장 기본적인 컬럼은 READY와 STATUS입니다.

배경색 변경
kubectl get po -owide -w
NAME                      READY   STATUS              AGE   IP          NODE
nginx-5d796fc999-qkgpp    0/1     Pending             0s    <none>      <none>
nginx-5d796fc999-qkgpp    0/1     Pending             0s    <none>      node1
nginx-5d796fc999-qkgpp    0/1     ContainerCreating   1s    <none>      node1
nginx-5d796fc999-qkgpp    0/1     Running             3s    10.44.0.2   node1
nginx-5d796fc999-qkgpp    1/1     Running             9s    10.44.0.2   node1
kubectl get po -owide -w
NAME                      READY   STATUS              AGE   IP          NODE
nginx-5d796fc999-qkgpp    0/1     Pending             0s    <none>      <none>
nginx-5d796fc999-qkgpp    0/1     Pending             0s    <none>      node1
nginx-5d796fc999-qkgpp    0/1     ContainerCreating   1s    <none>      node1
nginx-5d796fc999-qkgpp    0/1     Running             3s    10.44.0.2   node1
nginx-5d796fc999-qkgpp    1/1     Running             9s    10.44.0.2   node1
코드블록. Pod Lifecycle 변화 확인

다음부터는 비정상 Pod 상태가 발생하는 사례별로 원인과 조치 방안을 알아보겠습니다.

Pending 상태가 지속되는 경우

Pod가 Pending 상태에 장시간 머무는 경우가 있습니다.

배경색 변경
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     Pending   0          10m
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     Pending   0          10m
코드블록. Pending 상태 확인

이런 경우에는 먼저 kubectl describe po 명령어로 Events 메시지를 확인하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  14s (x2 over 14s)  default-scheduler  0/3 nodes are available: 3 Insufficient cpu, 3 Insufficient memory.
kubectl describe po <pod-name>
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  14s (x2 over 14s)  default-scheduler  0/3 nodes are available: 3 Insufficient cpu, 3 Insufficient memory.
코드블록. 클러스터 자원 부족

클러스터의 할당 가능한 자원이 부족한 경우입니다. kubectl describe node 명령어로 각 노드의 Allocated resources를 확인하고, 필요하면 노드를 증설하거나 Pod의 자원 요청량을 줄이세요.

참고
스케줄러는 실제 사용량이 아니라 Pod에 선언된 resources.requests 값의 합계를 기준으로 노드를 선택합니다. 따라서 kubectl top node로 확인한 실사용률이 낮더라도 requests 합계가 노드의 Allocatable을 초과하면 스케줄되지 않습니다. kubectl describe node의 Allocated resources 항목이 requests 기준 값입니다.
배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason            Age              From               Message
  ----     ------            ----             ----               -------
  Warning  FailedScheduling  5s (x2 over 5s)  default-scheduler  0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.
kubectl describe po <pod-name>
Events:
  Type     Reason            Age              From               Message
  ----     ------            ----             ----               -------
  Warning  FailedScheduling  5s (x2 over 5s)  default-scheduler  0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.
코드블록. nodeSelector 불일치

Pod에 설정된 nodeSelector 또는 nodeAffinity와 일치하는 노드가 없는 상태입니다. Pod의 nodeSelector/nodeAffinity를 실제 노드 정보에 맞게 수정하거나, 대상 노드에 label을 추가해서 Pod가 정상적으로 스케줄되도록 조치하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason            Age              From               Message
  ----     ------            ----             ----               -------
  Warning  FailedScheduling  6s (x2 over 6s)  default-scheduler  0/3 nodes are available: 3 node(s) were unschedulable.
kubectl describe po <pod-name>
Events:
  Type     Reason            Age              From               Message
  ----     ------            ----             ----               -------
  Warning  FailedScheduling  6s (x2 over 6s)  default-scheduler  0/3 nodes are available: 3 node(s) were unschedulable.
코드블록. 노드 예약 불가

모든 노드가 Cordon(spec.unschedulable: true) 되어 스케줄 예약이 불가능한 상태입니다. kubectl get no 명령어로 STATUS가 SchedulingDisabled인 노드를 확인하고 kubectl uncordon <node-name> 명령어로 해제하세요.

참고
노드가 NotReady 상태가 되어 스케줄되지 않는 경우에는 3 node(s) had untolerated taint {node.kubernetes.io/not-ready: }처럼 taint를 언급하는 메시지가 표시됩니다. Taint 관련 메시지가 보인다면 해당 노드의 kubelet 및 CNI 상태를 먼저 점검하고, 의도적으로 지정한 taint라면 Pod에 toleration을 추가하세요.
배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  53s (x2 over 60s)  default-scheduler  error while running "VolumeBinding" filter plugin for pod "nginx-9d8d9896-k7gzj": pod has unbound immediate PersistentVolumeClaims
kubectl describe po <pod-name>
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  53s (x2 over 60s)  default-scheduler  error while running "VolumeBinding" filter plugin for pod "nginx-9d8d9896-k7gzj": pod has unbound immediate PersistentVolumeClaims
코드블록. PVC 바인딩 실패

PVC(PersistentVolumeClaim)가 Pending 상태는 아닌지 다음과 같이 확인하세요.

배경색 변경
kubectl get pvc
NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nginx-pvc   Pending                                      standard       3m
kubectl get pvc
NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nginx-pvc   Pending                                      standard       3m
코드블록. PVC 상태 확인

정적 PV(PersistentVolume)를 사용하는 경우에는 Available 상태의 PV가 먼저 생성되어 있는지, PVC의 volumeName이 해당 PV name과 일치하는지, 그리고 용량과 accessModes가 PVC 요구 사항을 충족하는지 확인하세요.

동적 PV를 사용하는 경우에는 PVC에 지정한 storageClassName의 StorageClass가 존재하는지 확인하고, 해당 Volume Provisioner(CSI Driver) Pod가 정상 동작하는지도 함께 확인하세요. PVC를 describe하면 프로비저닝 실패 사유가 Event로 기록됩니다.

참고
StorageClass의 volumeBindingMode가 WaitForFirstConsumer인 경우에는 Pod가 스케줄되기 전까지 PVC가 Pending 상태로 유지되는 것이 정상 동작입니다. 이때는 위와 같은 unbound immediate PersistentVolumeClaims 메시지가 아니라 다른 스케줄링 사유가 함께 표시되므로, PVC가 아닌 그 사유를 먼저 확인하세요.
배경색 변경
kubectl describe po <pod-name>
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  21s   default-scheduler  Successfully assigned default/nginx-5d796fc999-6ksbj to node1

kubectl get po -owide
NAME                     READY   STATUS    RESTARTS   AGE   IP       NODE
nginx-5d796fc999-6ksbj   0/1     Pending   0          37s   <none>   node1
kubectl describe po <pod-name>
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  21s   default-scheduler  Successfully assigned default/nginx-5d796fc999-6ksbj to node1

kubectl get po -owide
NAME                     READY   STATUS    RESTARTS   AGE   IP       NODE
nginx-5d796fc999-6ksbj   0/1     Pending   0          37s   <none>   node1
코드블록. kubelet 이상 여부 확인

노드에 정상적으로 할당되었다는 이벤트만 있고 이후 kubelet이 남기는 이벤트(Pulling, Created, Started 등)가 전혀 없다면, 해당 노드의 kubelet이 동작하지 않는 상태입니다. -owide 조회 결과에 표시된 노드의 kubelet 상태를 확인하고 조치하세요.

배경색 변경
kubectl describe po <pod-name>
Events: <none>

kubectl get po -n kube-system
NAME                    READY   STATUS             RESTARTS   AGE
kube-scheduler-master   0/1     CrashLoopBackOff   9          16m
kubectl describe po <pod-name>
Events: <none>

kubectl get po -n kube-system
NAME                    READY   STATUS             RESTARTS   AGE
kube-scheduler-master   0/1     CrashLoopBackOff   9          16m
코드블록. kube-scheduler 이상 여부 확인

Event가 전혀 기록되지 않고 노드 할당도 이루어지지 않았다면 kube-scheduler가 스케줄 작업을 수행하지 못하는 상태이므로, kube-scheduler의 상태와 로그를 확인하고 조치하세요.

참고
SKE를 포함한 관리형 Kubernetes 서비스에서는 kube-scheduler, kube-controller-manager 등 Control Plane 컴포넌트를 사용자가 직접 조회하거나 조치할 수 없습니다. Control Plane 이상이 의심되는 경우에는 서비스 제공자에게 문의하세요.

ContainerCreating 상태가 지속되는 경우

다음은 Pod가 ContainerCreating 상태에 머물러 있는 경우입니다.

배경색 변경
kubectl get po
NAME                     READY   STATUS              RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     ContainerCreating   0          1m
kubectl get po
NAME                     READY   STATUS              RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     ContainerCreating   0          1m
코드블록. ContainerCreating 상태 확인

이런 경우에도 먼저 kubectl describe po 명령어로 Events 메시지를 확인하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  15s   default-scheduler  Successfully assigned default/nginx-58cd5b85f7-cmb6c to node1
  Normal  Pulling    12s   kubelet            Pulling image "nginx:1.13.9"
kubectl describe po <pod-name>
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  15s   default-scheduler  Successfully assigned default/nginx-58cd5b85f7-cmb6c to node1
  Normal  Pulling    12s   kubelet            Pulling image "nginx:1.13.9"
코드블록. Image Pulling 진행 중

Events가 Pulling image 단계에서 멈춰 있다면 아직 이미지를 내려받는 중입니다. 이미지 용량이 크거나 네트워크 대역폭이 부족하면 Pulling 시간이 길어질 수 있으며, 내려받기가 끝나면 자연스럽게 해소되므로 조금 더 기다려 보세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason       Age               From      Message
  ----     ------       ----              ----      -------
  Warning  FailedMount  5s (x6 over 21s)  kubelet   MountVolume.SetUp failed for volume "config-volume" : configmap "nginx-cm" not found
kubectl describe po <pod-name>
Events:
  Type     Reason       Age               From      Message
  ----     ------       ----              ----      -------
  Warning  FailedMount  5s (x6 over 21s)  kubelet   MountVolume.SetUp failed for volume "config-volume" : configmap "nginx-cm" not found
코드블록. ConfigMap/Secret 마운트 실패

Pod가 사용하는 ConfigMap 또는 Secret이 생성되어 있지 않거나 이름을 잘못 지정한 경우입니다. Pod Specification의 volumes 필드에 지정된 리소스가 같은 네임스페이스에 실제로 존재하는지 확인하세요.

참고
ConfigMap/Secret을 볼륨이 아니라 환경변수(env.valueFrom.configMapKeyRef 등)로 참조했는데 해당 리소스나 key가 없으면, ContainerCreating이 아니라 CreateContainerConfigError 상태로 표시됩니다. 이 경우에도 확인 방법과 조치 방법은 동일합니다.
배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason       Age   From      Message
  ----     ------       ----  ----      -------
  Warning  FailedMount  18s   kubelet   MountVolume.SetUp failed for volume "pvc-d2def29a-d503-443e-9615-886713983216" : mount failed: exit status 32
kubectl describe po <pod-name>
Events:
  Type     Reason       Age   From      Message
  ----     ------       ----  ----      -------
  Warning  FailedMount  18s   kubelet   MountVolume.SetUp failed for volume "pvc-d2def29a-d503-443e-9615-886713983216" : mount failed: exit status 32
코드블록. PV 마운트 실패

Pod가 사용하는 PV가 노드에 마운트되지 않아 발생하는 문제입니다. 해당 PV를 사용하기 위한 CSI(Container Storage Interface) Driver나, NFS를 사용하는 경우 nfs-utils, rpcbind 같은 클라이언트 패키지가 노드에 설치되어 있는지 확인하세요. 추가로 노드에서 스토리지로의 접근 제어와 방화벽을 확인해서 마운트가 가능하도록 조치하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason                  Age   From      Message
  ----     ------                  ----  ----      -------
  Warning  FailedCreatePodSandBox  9s    kubelet   Failed to create pod sandbox: plugin type="calico" failed (add): failed to allocate for range 0: no IP addresses available in range set
kubectl describe po <pod-name>
Events:
  Type     Reason                  Age   From      Message
  ----     ------                  ----  ----      -------
  Warning  FailedCreatePodSandBox  9s    kubelet   Failed to create pod sandbox: plugin type="calico" failed (add): failed to allocate for range 0: no IP addresses available in range set
코드블록. Pod IP 할당 실패

노드에 할당된 Pod CIDR의 IP가 모두 소진된 경우입니다. 노드당 Pod 수(maxPods)와 Pod CIDR 크기를 확인하고, 불필요한 Pod를 정리하거나 노드를 증설하세요. CNI Plugin Pod가 비정상일 때도 FailedCreatePodSandBox가 발생할 수 있으므로 Pod 통신 실패의 CNI 점검 방법도 함께 참고하세요.

ImagePullBackOff 상태로 표시되는 경우

이번에는 ImagePullBackOff가 발생하는 상황을 알아보겠습니다.

배경색 변경
kubectl get po
NAME                     READY   STATUS             RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     ImagePullBackOff   0          6s
kubectl get po
NAME                     READY   STATUS             RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     ImagePullBackOff   0          6s
코드블록. ImagePullBackOff 상태 확인
참고
이미지 내려받기가 처음 실패하면 ErrImagePull로 표시되고, kubelet이 재시도를 backoff하기 시작하면 ImagePullBackOff로 바뀝니다. 두 상태의 원인과 조치 방법은 동일합니다.

마찬가지로 describe 명령어로 원인을 확인할 수 있습니다.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason   Age                From      Message
  ----     ------   ----               ----      -------
  Warning  Failed   23s (x2 over 44s)  kubelet   Failed to pull image "nginx:invalid-tag": rpc error: code = Unknown desc = Error response from daemon: manifest for nginx:invalid-tag not found
kubectl describe po <pod-name>
Events:
  Type     Reason   Age                From      Message
  ----     ------   ----               ----      -------
  Warning  Failed   23s (x2 over 44s)  kubelet   Failed to pull image "nginx:invalid-tag": rpc error: code = Unknown desc = Error response from daemon: manifest for nginx:invalid-tag not found
코드블록. 잘못된 Image:Tag

Image Repository에 해당 Image:Tag가 없는 경우입니다. Pod Specification의 이미지 값을 올바른 Image:Tag로 수정하거나, 해당 Image:Tag를 Image Repository에 Push하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason   Age   From      Message
  ----     ------   ----  ----      -------
  Warning  Failed   9s    kubelet   Failed to pull image "myregistry.io/nginx:1.17.8": rpc error: code = Unknown desc = Error response from daemon: Get https://myregistry.io/v2/nginx/manifests/1.17.8: no basic auth credentials
kubectl describe po <pod-name>
Events:
  Type     Reason   Age   From      Message
  ----     ------   ----  ----      -------
  Warning  Failed   9s    kubelet   Failed to pull image "myregistry.io/nginx:1.17.8": rpc error: code = Unknown desc = Error response from daemon: Get https://myregistry.io/v2/nginx/manifests/1.17.8: no basic auth credentials
코드블록. Private Registry 인증 실패

Private Registry를 사용하는 경우에는 Credential 정보가 필요합니다. 올바른 Credential로 kubernetes.io/dockerconfigjson 타입의 Secret을 만들어 Pod Specification의 imagePullSecrets에 추가하세요. 이미 imagePullSecrets을 사용 중이라면 자격 증명이 만료되지 않았는지, 그리고 Secret이 Pod와 같은 네임스페이스에 있는지 함께 확인하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason   Age   From      Message
  ----     ------   ----  ----      -------
  Warning  Failed   12s   kubelet   Failed to pull image "nginx:1.17.8": rpc error: code = Unknown desc = Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
kubectl describe po <pod-name>
Events:
  Type     Reason   Age   From      Message
  ----     ------   ----  ----      -------
  Warning  Failed   12s   kubelet   Failed to pull image "nginx:1.17.8": rpc error: code = Unknown desc = Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
코드블록. Image Repository 통신 실패

노드에서 Image Repository와의 통신이 실패한 경우입니다. 노드와 Image Repository 간 방화벽 및 프록시 설정, 그리고 Image Repository 서비스가 정상 운영 중인지 확인하세요.

참고
공용 레지스트리를 사용하는 경우에는 toomanyrequests: You have reached your pull rate limit 메시지와 함께 실패할 수 있습니다. 이는 익명 사용자에 대한 이미지 내려받기 횟수 제한 때문이므로, 인증된 계정의 imagePullSecrets을 사용하거나 Container Registry에 이미지를 미러링해서 사용하세요.

Init 컨테이너 단계에서 실패하는 경우

Pod에 초기화 컨테이너(initContainer)가 있는 경우, 애플리케이션 Container가 시작되기 전에 Init: 접두어가 붙은 상태로 멈출 수 있습니다.

배경색 변경
kubectl get po
NAME                     READY   STATUS                  RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     Init:CrashLoopBackOff   4          2m
kubectl get po
NAME                     READY   STATUS                  RESTARTS   AGE
nginx-6dcd8d4dff-njsrh   0/1     Init:CrashLoopBackOff   4          2m
코드블록. Init 컨테이너 실패 확인

Init:0/1에서 진행되지 않으면 초기화 컨테이너가 아직 실행 중인 상태이고, Init:Error 또는 Init:CrashLoopBackOff는 초기화 컨테이너가 0이 아닌 종료 코드로 끝난 상태입니다. 초기화 컨테이너의 로그를 직접 지정해서 확인하세요.

배경색 변경
kubectl logs <pod-name> -c <init-container-name>
kubectl logs <pod-name> -c <init-container-name>
코드블록. Init 컨테이너 로그 확인

초기화 컨테이너는 주로 설정 파일 생성, 스키마 마이그레이션, 선행 서비스 대기 용도로 사용되므로, 연계 서비스와의 통신 실패나 권한 부족이 원인인 경우가 많습니다. 로그에서 확인된 원인을 조치하세요.

CrashLoopBackOff와 Running이 반복되는 경우

Pod가 CrashLoopBackOff와 Running 상태를 주기적으로 반복하는 경우가 있습니다.

배경색 변경
kubectl get po -w
NAME                    READY   STATUS             RESTARTS   AGE
test-7f74c45f58-jm629   0/1     CrashLoopBackOff   8          16m
test-7f74c45f58-jm629   0/1     Running            9          16m
kubectl get po -w
NAME                    READY   STATUS             RESTARTS   AGE
test-7f74c45f58-jm629   0/1     CrashLoopBackOff   8          16m
test-7f74c45f58-jm629   0/1     Running            9          16m
코드블록. CrashLoopBackOff/Running 반복

애플리케이션 프로세스가 기동 직후 비정상 종료되어 kubelet이 재시작을 반복하는 상태입니다. 다음 명령어로 현재 또는 이전 Container의 로그를 확인하고, 로그에서 원인이 확인되면 애플리케이션 문제를 해결하세요.

배경색 변경
kubectl logs <pod-name> -f
kubectl logs <pod-name> --previous
kubectl logs <pod-name> -f
kubectl logs <pod-name> --previous
코드블록. 현재/이전 Container 로그 확인

주로 발생하는 애플리케이션 이슈로는 DB 연결 실패, 잘못된 command 사용, 애플리케이션 설정 오류 등이 있습니다.

로그를 확인할 수 없거나 로그에 아무것도 남지 않는 경우에는 describe 명령어로 Last State의 종료 사유와 Exit Code를 확인하세요.

배경색 변경
kubectl describe po <pod-name>
    Last State:     Terminated
      Reason:       ContainerCannotRun
      Message:      OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"ping\": executable file not found in $PATH": unknown
      Exit Code:    127
kubectl describe po <pod-name>
    Last State:     Terminated
      Reason:       ContainerCannotRun
      Message:      OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"ping\": executable file not found in $PATH": unknown
      Exit Code:    127
코드블록. Last State 확인
참고
Exit Code로 원인을 좁힐 수 있습니다. 127은 실행 파일을 찾을 수 없는 경우, 126은 실행 권한이 없는 경우, 137은 SIGKILL(주로 OOMKilled), 143은 SIGTERM에 의한 종료를 의미합니다.

CrashLoopBackOff와 Completed가 반복되는 경우

Pod가 CrashLoopBackOff와 Completed를 반복하는 경우도 있습니다.

배경색 변경
kubectl get po -w
NAME                    READY   STATUS             RESTARTS   AGE
test-6fd77b68b9-44hxs   0/1     CrashLoopBackOff   8          16m
test-6fd77b68b9-44hxs   0/1     Completed          9          16m
kubectl get po -w
NAME                    READY   STATUS             RESTARTS   AGE
test-6fd77b68b9-44hxs   0/1     CrashLoopBackOff   8          16m
test-6fd77b68b9-44hxs   0/1     Completed          9          16m
코드블록. CrashLoopBackOff/Completed 반복

Container의 주 프로세스가 정상 종료 코드(0)로 끝났지만, Pod의 restartPolicy가 기본값인 Always이기 때문에 kubelet이 계속 재시작하는 상태입니다. 즉, Container 안에서 계속 떠 있는 Foreground 프로세스가 없는 경우입니다.

Dockerfile의 CMD 또는 ENTRYPOINT에 Foreground로 동작하는 프로세스를 명시하거나, Deployment의 command/args를 수정하세요. 한 번 실행하고 끝나는 배치성 프로세스라면 Deployment가 아니라 Job 또는 CronJob으로 생성해야 합니다.

Ready 상태로 변경되지 않는 경우

Pod가 Running 상태이지만 READY 값이 1/1로 바뀌지 않는 경우입니다.

배경색 변경
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-659484b897-mnq4l   0/1     Running   0          2m14s
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-659484b897-mnq4l   0/1     Running   0          2m14s
코드블록. Ready 미변경 상태 확인

이 경우에도 describe 명령어로 Events를 확인하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason      Age                From      Message
  ----     ------      ----               ----      -------
  Warning  Unhealthy   5s (x7 over 65s)   kubelet   Readiness probe failed: HTTP probe failed with statuscode: 404
kubectl describe po <pod-name>
Events:
  Type     Reason      Age                From      Message
  ----     ------      ----               ----      -------
  Warning  Unhealthy   5s (x7 over 65s)   kubelet   Readiness probe failed: HTTP probe failed with statuscode: 404
코드블록. Readiness Probe 실패

Readiness Probe가 실패하고 있다면 Probe에 설정한 path, port, scheme이 애플리케이션이 실제로 응답하는 값과 일치하는지 확인하세요. 위 예시처럼 404가 반환된다면 Probe의 path가 잘못 지정된 경우입니다.

참고
애플리케이션 기동 시간이 긴 경우에는 initialDelaySeconds를 늘리는 대신 startupProbe를 사용하는 것을 권장합니다. startupProbe가 성공할 때까지 Readiness/Liveness Probe가 비활성화되므로, 기동은 충분히 기다리면서도 기동 후에는 짧은 주기로 상태를 감지할 수 있습니다.

Events에 Probe 실패 메시지가 없는데도 READY가 0/1이라면, Readiness Probe 자체가 정의되지 않았는지 또는 Pod 안의 여러 Container 중 일부만 준비된 상태(예: 1/2)는 아닌지 확인하세요.

Terminating 상태가 지속되는 경우

이번에는 Terminating 상태가 지속되는 경우를 알아보겠습니다.

배경색 변경
kubectl get po -owide
NAME                     READY   STATUS        RESTARTS   AGE   NODE
nginx-6dcd8d4dff-njsrh   0/1     Terminating   0          10m   node2
nginx-6dcd8d4dff-fvlmf   1/1     Running       0          43s   node1
kubectl get po -owide
NAME                     READY   STATUS        RESTARTS   AGE   NODE
nginx-6dcd8d4dff-njsrh   0/1     Terminating   0          10m   node2
nginx-6dcd8d4dff-fvlmf   1/1     Running       0          43s   node1
코드블록. Terminating 상태 확인

먼저 Pod가 할당된 노드가 NotReady 상태는 아닌지 확인하세요.

배경색 변경
kubectl get no
NAME     STATUS     ROLES    AGE   VERSION
master   Ready      master   39d   v1.21.2
node1    Ready      worker   39d   v1.21.2
node2    NotReady   worker   39d   v1.21.2
kubectl get no
NAME     STATUS     ROLES    AGE   VERSION
master   Ready      master   39d   v1.21.2
node1    Ready      worker   39d   v1.21.2
node2    NotReady   worker   39d   v1.21.2
코드블록. 노드 상태 확인

노드가 NotReady 상태이면 kubelet이 Pod 정리를 완료했다고 API Server에 보고할 수 없기 때문에, Pod가 Terminating 상태로 남습니다. 이때 워크로드 종류에 따라 대체 Pod의 생성 정책이 다르다는 점도 알아 두어야 합니다.

  • Deployment: 다른 노드에 대체 Pod가 생성됩니다. Failover가 이루어졌는지 확인하세요.
  • StatefulSet: 동일 identity의 Pod가 중복 실행되지 않도록 다른 노드로 이동하지 않습니다. 원본 Pod를 강제 삭제해야 이동합니다.
  • DaemonSet: 노드마다 하나씩 배치되는 특성상 다른 노드로 이동하지 않습니다.
참고
노드가 NotReady가 되면 즉시 Pod가 옮겨지는 것은 아닙니다. Kubernetes는 노드에 node.kubernetes.io/not-ready:NoExecute taint를 부여하고, Pod에 자동으로 추가되는 tolerationSeconds(기본 300초)만큼 기다린 후 Eviction을 시작합니다. 따라서 대체 Pod는 노드 이상 감지 후 약 5분 뒤에 생성됩니다.

Pod가 할당된 노드가 Ready 상태인데도 Terminating이 지속된다면 다음을 순서대로 확인하세요.

  1. Finalizer 잔존: Pod에 finalizer가 남아 있으면 삭제가 완료되지 않습니다. kubectl get po <pod-name> -oyaml로 metadata.finalizers를 확인하고, 해당 finalizer를 처리하는 컨트롤러(Operator, CSI Driver 등)가 정상 동작 중인지 점검하세요.
  2. 종료 유예 시간 및 preStop Hook: terminationGracePeriodSeconds가 크게 설정되어 있거나 preStop Hook이 끝나지 않으면 그 시간만큼 Terminating이 유지됩니다. 이는 정상 동작이므로 설정값을 먼저 확인하세요.
  3. 볼륨 정리 실패: Pod 종료 과정에서 PV의 unmount 또는 detach가 처리되지 않았을 수 있으므로 스토리지 연결 상태와 CSI Driver 로그를 확인하세요.
  4. 좀비 프로세스: 해당 노드에 좀비(defunct) 프로세스가 남아 Container가 정리되지 않는 경우가 있으므로 함께 확인하세요.
주의
kubectl delete po <pod-name> --force --grace-period=0 명령은 실제 Container가 종료되었는지 확인하지 않고 API Server의 오브젝트만 제거합니다. StatefulSet처럼 identity가 중요한 워크로드에서는 동일한 Pod가 두 노드에서 동시에 실행되어 데이터가 손상될 수 있으므로, 노드가 확실히 정지되었음을 확인한 후에만 사용하세요.

Evicted된 경우

Pod가 Evicted된 경우입니다.

배경색 변경
kubectl get po -owide
NAME                     READY   STATUS    RESTARTS   AGE   NODE
nginx-58cd5b85f7-4fz88   0/1     Evicted   0          1h    node1
nginx-58cd5b85f7-ts7ps   1/1     Running   0          12m   node2
kubectl get po -owide
NAME                     READY   STATUS    RESTARTS   AGE   NODE
nginx-58cd5b85f7-4fz88   0/1     Evicted   0          1h    node1
nginx-58cd5b85f7-ts7ps   1/1     Running   0          12m   node2
코드블록. Evicted 상태 확인

Evicted는 노드의 자원이 부족해 kubelet이 Pod를 강제로 축출한 상태입니다. 해당 노드를 describe해서 어떤 Condition이 발생했는지 확인하세요.

배경색 변경
kubectl describe no node1
Taints:  node.kubernetes.io/disk-pressure:NoSchedule
Conditions:
  Type            Status   Reason                     Message
  ----            ------   ------                     -------
  MemoryPressure  False    KubeletHasSufficientMemory kubelet has sufficient memory available
  DiskPressure    True     KubeletHasDiskPressure     kubelet has disk pressure
kubectl describe no node1
Taints:  node.kubernetes.io/disk-pressure:NoSchedule
Conditions:
  Type            Status   Reason                     Message
  ----            ------   ------                     -------
  MemoryPressure  False    KubeletHasSufficientMemory kubelet has sufficient memory available
  DiskPressure    True     KubeletHasDiskPressure     kubelet has disk pressure
코드블록. DiskPressure 확인

DiskPressure가 발생했다면 노드의 디스크 사용량을 확인하고 공간을 확보하세요.

배경색 변경
df -h /var/lib/kubelet
Filesystem                  Size  Used  Avail  Use%  Mounted on
/dev/mapper/VGROOT-LV_root   50G   46G   4.7G   91%  /
df -h /var/lib/kubelet
Filesystem                  Size  Used  Avail  Use%  Mounted on
/dev/mapper/VGROOT-LV_root   50G   46G   4.7G   91%  /
코드블록. 디스크 사용량 확인

kubelet의 기본 hard eviction 임계값은 nodefs.available<10%, imagefs.available<15%, memory.available<100Mi입니다. 따라서 /var/lib/kubelet이 속한 디스크의 사용률이 90%를 넘으면 DiskPressure가 발생하고 Eviction이 시작됩니다. 사용하지 않는 컨테이너 이미지 정리, 애플리케이션 로그 로테이션 설정, emptyDir 사용량 점검 등으로 공간을 확보하세요.

참고

MemoryPressure로 인한 Eviction도 자주 발생합니다. 이때는 resources.requests.memory를 초과해 사용하는 Pod가 우선적으로 축출되므로, requests/limits를 실제 사용량에 맞게 설정하는 것이 중요합니다.

Evicted 상태의 Pod는 자동으로 삭제되지 않고 목록에 남습니다. 원인을 확인한 뒤에는 다음 명령어로 정리하세요.

kubectl get po --field-selector=status.phase=Failed -o name | xargs -r kubectl delete

Restart가 자주 발생하는 경우

Pod가 현재는 정상 구동 중이지만 RESTARTS 값이 계속 증가하는 경우가 있습니다.

배경색 변경
kubectl get po -n calico-system
NAME                READY   STATUS    RESTARTS   AGE
calico-node-kwzvz   1/1     Running   5          5d1h
kubectl get po -n calico-system
NAME                READY   STATUS    RESTARTS   AGE
calico-node-kwzvz   1/1     Running   5          5d1h
코드블록. Restart 반복 확인

--previous 옵션으로 재시작 이전 Container의 로그를 확인해서 원인을 파악하세요.

배경색 변경
kubectl logs calico-node-kwzvz -n calico-system -c calico-node --previous
2024-01-15T04:25:03.297881Z [FATAL][1] daemon.go 216: Failed to initialize Calico datastore error=Get "https://172.24.0.1:443/api/v1/nodes": dial tcp 172.24.0.1:443: i/o timeout
kubectl logs calico-node-kwzvz -n calico-system -c calico-node --previous
2024-01-15T04:25:03.297881Z [FATAL][1] daemon.go 216: Failed to initialize Calico datastore error=Get "https://172.24.0.1:443/api/v1/nodes": dial tcp 172.24.0.1:443: i/o timeout
코드블록. 이전 Container 로그 확인

주로 애플리케이션과 연계된 외부 DB 서비스나 클러스터 내부 Pod와의 통신 실패가 원인이며, 계획된 노드 작업이나 네트워크 작업으로 인해 발생하기도 합니다. 이전 로그로 원인이 확인되지 않는다면 describe 명령어로 Last State를 확인하세요.

배경색 변경
kubectl describe po <pod-name>
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
kubectl describe po <pod-name>
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
코드블록. OOMKilled 확인

OOM(Out Of Memory)으로 종료된 경우입니다. Container의 메모리 사용량이 resources.limits.memory를 초과하면 커널의 OOM Killer가 프로세스를 종료합니다. kubectl top po 명령어로 실제 사용량을 확인한 후, 서비스 가용성을 고려하여 limits를 상향하거나 애플리케이션의 힙 크기 설정을 limits보다 작게 조정하세요.

배경색 변경
kubectl describe po <pod-name>
Events:
  Type     Reason      Age                 From      Message
  ----     ------      ----                ----      -------
  Warning  Unhealthy   48m (x6 over 49m)   kubelet   Liveness probe failed: localhost:5432 - no response
kubectl describe po <pod-name>
Events:
  Type     Reason      Age                 From      Message
  ----     ------      ----                ----      -------
  Warning  Unhealthy   48m (x6 over 49m)   kubelet   Liveness probe failed: localhost:5432 - no response
코드블록. Liveness Probe 실패

Liveness Probe가 실패하면 kubelet이 Container를 재시작합니다. 실제로는 애플리케이션이 살아 있는데 부하가 몰려 응답이 늦어져 재시작되는 경우가 많으므로, 부하 상황을 고려하여 timeoutSeconds, periodSeconds, failureThreshold 값을 조정하세요.

주의
Liveness Probe의 검사 대상으로 DB 연결 등 외부 의존성을 포함하면, 외부 서비스 장애가 애플리케이션 Pod의 전면 재시작으로 확산될 수 있습니다. Liveness Probe는 프로세스 자체의 생존 여부만 검사하고, 의존성 확인은 Readiness Probe에서 수행하도록 분리하세요.

Pod 자체가 조회되지 않는 경우

Deployment 등의 워크로드를 생성했는데도 Pod가 조회되지 않는 경우입니다. 이때는 Pod가 아니라 ReplicaSet 단계에서 생성이 거부된 것이므로, 먼저 Event를 조회하세요.

배경색 변경
kubectl get ev
LAST SEEN   TYPE      REASON         OBJECT                        MESSAGE
3m1s        Warning   FailedCreate   replicaset/nginx-847f85d779   Error creating: pods "nginx-847f85d779-j484f" is forbidden: exceeded quota: resourcequota, requested: requests.cpu=100m,requests.memory=2Gi, used: requests.cpu=4,requests.memory=128Mi, limited: requests.cpu=2,requests.memory=1Gi
kubectl get ev
LAST SEEN   TYPE      REASON         OBJECT                        MESSAGE
3m1s        Warning   FailedCreate   replicaset/nginx-847f85d779   Error creating: pods "nginx-847f85d779-j484f" is forbidden: exceeded quota: resourcequota, requested: requests.cpu=100m,requests.memory=2Gi, used: requests.cpu=4,requests.memory=128Mi, limited: requests.cpu=2,requests.memory=1Gi
코드블록. ResourceQuota 초과

네임스페이스에 설정된 ResourceQuota를 초과했거나 LimitRange 범위를 벗어난 경우입니다. ResourceQuota/LimitRange를 조정하거나 Pod의 resources 값을 조정하세요.

참고
ResourceQuota에 requests.cpu 등이 설정된 네임스페이스에서는 모든 Container가 해당 항목을 반드시 선언해야 합니다. resources를 지정하지 않은 Pod는 must specify requests.cpu 메시지와 함께 생성이 거부됩니다.
배경색 변경
kubectl get ev
LAST SEEN   TYPE      REASON         OBJECT                        MESSAGE
3s          Warning   FailedCreate   replicaset/nginx-56b5449445   Error creating: pods "nginx-56b5449445-" is forbidden: error looking up service account default/my-sa: serviceaccount "my-sa" not found
kubectl get ev
LAST SEEN   TYPE      REASON         OBJECT                        MESSAGE
3s          Warning   FailedCreate   replicaset/nginx-56b5449445   Error creating: pods "nginx-56b5449445-" is forbidden: error looking up service account default/my-sa: serviceaccount "my-sa" not found
코드블록. ServiceAccount 누락

Pod 구동에 필요한 리소스를 만들지 않은 경우입니다. Pod Specification을 조회해서 참조하는 ServiceAccount 등 선행 리소스를 같은 네임스페이스에 생성하세요.

배경색 변경
kubectl get ev
LAST SEEN   TYPE      REASON         OBJECT                        MESSAGE
5s          Warning   FailedCreate   replicaset/nginx-6b7f9c8d55   Error creating: admission webhook "validation.example.io" denied the request: privileged containers are not allowed
kubectl get ev
LAST SEEN   TYPE      REASON         OBJECT                        MESSAGE
5s          Warning   FailedCreate   replicaset/nginx-6b7f9c8d55   Error creating: admission webhook "validation.example.io" denied the request: privileged containers are not allowed
코드블록. Admission Webhook 거부

클러스터에 설치된 정책 엔진 등 Admission Webhook이 Pod 생성을 거부한 경우입니다. 메시지에 표시된 웹훅 이름으로 어떤 정책에 위배되었는지 확인하고 매니페스트를 수정하세요. Pod Security Admission이 적용된 네임스페이스에서는 violates PodSecurity "restricted:latest" 형태의 메시지가 표시됩니다.

참고
Webhook 서버 자체가 응답하지 않으면 failed calling webhook ...: connection refused 메시지가 발생하며, failurePolicy: Fail 설정에 따라 모든 Pod 생성이 차단될 수 있습니다. 이 경우 웹훅 서버 Pod의 상태를 먼저 확인하세요.

Event가 전혀 조회되지 않고 ReplicaSet조차 생성되지 않았다면, kube-controller-manager가 정상 동작하지 않는 상태이므로 해당 컴포넌트의 상태와 로그를 확인하고 조치하세요.

Service 상태 점검

Pod 구동 상태에 문제가 없다면 Service 상태를 점검합니다. 다만 그전에 Pod로의 통신 자체에 문제가 없는지부터 확인해야 원인을 좁힐 수 있습니다.

Pod 통신 확인

Kubernetes 노드에서는 Pod 네트워크에 직접 접근할 수 있으므로, <Pod IP>:<Port>로 통신 요청을 보내어 확인할 수 있습니다.

배경색 변경
kubectl get po -owide
NAME                     READY   STATUS    RESTARTS   AGE     IP
nginx-58cd5b85f7-sn5n9   1/1     Running   0          2m15s   10.36.0.1

curl 10.36.0.1:80
<title>Welcome to nginx!</title>
kubectl get po -owide
NAME                     READY   STATUS    RESTARTS   AGE     IP
nginx-58cd5b85f7-sn5n9   1/1     Running   0          2m15s   10.36.0.1

curl 10.36.0.1:80
<title>Welcome to nginx!</title>
코드블록. Pod IP 직접 통신 확인

Kubernetes 노드가 아닌 환경에서는 kubectl port-forward로 통신을 확인할 수 있습니다. port-forward는 TCP만 지원합니다.

배경색 변경
kubectl port-forward <pod-name> 8888:80
Forwarding from 127.0.0.1:8888 -> 80
Forwarding from [::1]:8888 -> 80

curl localhost:8888
<title>Welcome to nginx!</title>
kubectl port-forward <pod-name> 8888:80
Forwarding from 127.0.0.1:8888 -> 80
Forwarding from [::1]:8888 -> 80

curl localhost:8888
<title>Welcome to nginx!</title>
코드블록. port-forward를 통한 Pod 통신 확인

Pod 통신이 확인되었다면 Service 통신 점검으로 넘어가면 됩니다.

Pod 통신 실패

Pod와의 통신이 되지 않는 경우에는 먼저 Pod가 해당 Port를 Listen하고 있는지 확인하세요. Pod 안에서 netstat, ss, curl 같은 명령어를 사용할 수 있다면 내부에서 먼저 점검해 보는 것이 좋습니다.

배경색 변경
kubectl exec -it <pod-name> -- ss -lnt
State    Recv-Q   Send-Q   Local Address:Port   Peer Address:Port
LISTEN   0        4096           0.0.0.0:80           0.0.0.0:*
kubectl exec -it <pod-name> -- ss -lnt
State    Recv-Q   Send-Q   Local Address:Port   Peer Address:Port
LISTEN   0        4096           0.0.0.0:80           0.0.0.0:*
코드블록. Pod 내부에서 Listen Port 확인
참고

애플리케이션이 127.0.0.1에만 바인딩되어 있으면 Pod 내부에서는 통신되지만 외부에서는 접근할 수 없습니다. 위 예시처럼 Local Address가 0.0.0.0 또는 [::]인지 확인하세요.

디버깅 도구가 포함되지 않은 경량 이미지(distroless 등)에서는 kubectl debug -it <pod-name> --image=busybox --target=<container-name> 명령으로 임시 컨테이너를 붙여 점검할 수 있습니다.

Port Listen은 정상인데 통신이 안 된다면 NetworkPolicy에 의해 차단되었을 가능성을 확인하세요. 정책에서 허용된 다른 Pod에서는 통신이 되는지 비교해 보면 판단하기 쉽습니다.

배경색 변경
kubectl get netpol
NAME           POD-SELECTOR   AGE
default-deny   <none>         1d
kubectl get netpol
NAME           POD-SELECTOR   AGE
default-deny   <none>         1d
코드블록. NetworkPolicy 확인

또는 CNI(Container Network Interface) Plugin이 정상 동작하는지 확인하고 조치하세요.

배경색 변경
kubectl get po -n calico-system | grep calico-node
NAME                READY   STATUS             RESTARTS   AGE
calico-node-97gcr   0/1     CrashLoopBackOff   8          16m
kubectl get po -n calico-system | grep calico-node
NAME                READY   STATUS             RESTARTS   AGE
calico-node-97gcr   0/1     CrashLoopBackOff   8          16m
코드블록. CNI Plugin 상태 확인
참고
SKE는 calico-system 네임스페이스의 calico-node 데몬셋으로 CNI Plugin을 구동합니다. 특정 노드의 Pod만 통신이 되지 않는다면 해당 노드의 calico-node Pod 상태를 우선 확인하세요.

Service 통신 확인

Pod와 마찬가지로 Kubernetes 노드에서는 <Service IP>:<Port>로 직접 통신을 확인할 수 있습니다.

배경색 변경
kubectl get svc
NAME    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   10.100.215.120   <none>        80/TCP    54m

curl 10.100.215.120:80
<title>Welcome to nginx!</title>
kubectl get svc
NAME    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   10.100.215.120   <none>        80/TCP    54m

curl 10.100.215.120:80
<title>Welcome to nginx!</title>
코드블록. Service IP 직접 통신 확인
참고
ClusterIP는 kube-proxy가 규칙을 설정한 클러스터 노드 안에서만 유효한 가상 IP입니다. 클러스터 외부에서는 접근할 수 없으므로, 외부에서 확인할 때는 아래의 port-forward를 사용하세요.

Kubernetes 노드가 아닌 환경에서는 kubectl port-forward로 확인할 수 있습니다. Service를 대상으로 지정할 때의 원격 포트는 Pod의 포트가 아니라 Service의 port 값입니다.

배경색 변경
kubectl port-forward service/<service-name> 8888:80
Forwarding from 127.0.0.1:8888 -> 80
Forwarding from [::1]:8888 -> 80

curl localhost:8888
<title>Welcome to nginx!</title>
kubectl port-forward service/<service-name> 8888:80
Forwarding from 127.0.0.1:8888 -> 80
Forwarding from [::1]:8888 -> 80

curl localhost:8888
<title>Welcome to nginx!</title>
코드블록. port-forward를 통한 Service 통신 확인

클러스터 내부의 다른 Pod에서 DNS 이름으로 접근할 수 있는지도 함께 확인하면 좋습니다.

배경색 변경
kubectl exec -it <pod-name> -- nslookup <service-name>.<namespace>.svc.cluster.local
kubectl exec -it <pod-name> -- nslookup <service-name>.<namespace>.svc.cluster.local
코드블록. Service DNS 확인

DNS 조회가 실패한다면 kube-system 네임스페이스의 CoreDNS Pod 상태를 확인하세요.

여기까지 Service 통신이 확인되었다면 Ingress 통신 점검으로 넘어가면 됩니다.

Service 통신 실패

Service로의 통신이 실패한 경우에는 다음 항목을 순서대로 점검하세요.

배경색 변경
kubectl describe svc <service-name>
Selector:     name=nginx
Port:         http 80/TCP
TargetPort:   8080/TCP
Endpoints:    <none>
kubectl describe svc <service-name>
Selector:     name=nginx
Port:         http 80/TCP
TargetPort:   8080/TCP
Endpoints:    <none>
코드블록. Endpoints 없음

Service를 describe해서 Endpoints를 확인하세요. <none>이라면 트래픽을 전달할 대상 Pod가 없다는 뜻이며, 원인은 크게 두 가지입니다.

  • Selector 불일치: Service의 selector와 Pod의 label이 일치하지 않는 경우입니다. kubectl get po --show-labels 명령어로 Pod의 label을 확인하고 두 값이 일치하도록 수정하세요.
  • Ready 상태인 Pod 없음: Selector는 일치하지만 Readiness Probe를 통과한 Pod가 없는 경우입니다. Endpoints에는 Ready 상태의 Pod만 등록되므로, Ready 상태로 변경되지 않는 경우를 참고해 조치하세요.
참고
Kubernetes 1.33부터 Endpoints 리소스는 deprecated되었으며 EndpointSlice가 표준입니다. kubectl get endpointslice -l kubernetes.io/service-name=<service-name> 명령어로 조회하면 Ready 상태(conditions.ready)까지 함께 확인할 수 있습니다.

다음은 Endpoints가 있는데도 통신이 되지 않는 경우입니다.

배경색 변경
kubectl describe svc <service-name>
Selector:     name=nginx
Port:         http 80/TCP
TargetPort:   8080/TCP
Endpoints:    10.36.0.1:8080
kubectl describe svc <service-name>
Selector:     name=nginx
Port:         http 80/TCP
TargetPort:   8080/TCP
Endpoints:    10.36.0.1:8080
코드블록. targetPort 불일치

Service의 targetPort가 애플리케이션이 실제로 Listen하는 포트와 다른 경우입니다. 위 예시에서는 Endpoints가 8080으로 등록되어 있으므로, 애플리케이션이 80을 Listen하고 있다면 통신이 실패합니다. Pod 통신 확인에서 확인한 실제 Listen 포트에 맞게 targetPort를 수정하세요.

포트가 일치하는데도 통신이 되지 않는다면 NetworkPolicy에 의한 차단 여부를 확인하고, kube-proxy 데몬셋이 모든 노드에서 정상 동작하는지도 점검해서 조치하세요.

Ingress 상태 점검

Service까지 점검이 끝났다면 Ingress 통신을 확인하여 Kubernetes 워크로드 점검을 마무리할 수 있습니다.

Ingress 통신 확인

정상적인 경우라면 kubectl describe ing 결과의 Backends에 Pod IP 정보가 표시됩니다.

배경색 변경
kubectl describe ing <ingress-name>
Name:      nginx
Address:   172.28.128.11
Rules:
  Host               Path   Backends
  ----               ----   --------
  nginx.example.io
                     /      nginx:80 (10.36.0.1:80,10.44.0.2:80)
kubectl describe ing <ingress-name>
Name:      nginx
Address:   172.28.128.11
Rules:
  Host               Path   Backends
  ----               ----   --------
  nginx.example.io
                     /      nginx:80 (10.36.0.1:80,10.44.0.2:80)
코드블록. Ingress backend 확인

Ingress에 설정한 도메인으로 웹 브라우저에서 접속하거나 curl 명령어로 확인할 수 있습니다. 해당 도메인이 DNS 서버에 등록되지 않은 경우에는 다음과 같이 Host 헤더를 직접 지정해서 확인할 수 있습니다.

배경색 변경
curl -H 'Host: <host-domain>' http://<ingress-controller-external-ip>
<title>Welcome to nginx!</title>
curl -H 'Host: <host-domain>' http://<ingress-controller-external-ip>
<title>Welcome to nginx!</title>
코드블록. Host 헤더로 도메인 확인

여기까지 통신이 되었다면 Kubernetes 클러스터 내부에는 이슈가 없다고 판단할 수 있습니다.

Ingress 통신 실패

Ingress로의 통신이 실패하는 경우를 알아보겠습니다.

먼저 Ingress의 Backends에 Pod IP 정보가 없는 경우입니다.

배경색 변경
kubectl describe ing <ingress-name>
Name:      nginx
Address:   172.28.128.11
Rules:
  Host               Path   Backends
  ----               ----   --------
  nginx.example.io
                     /      nginx:80 (<none>)
kubectl describe ing <ingress-name>
Name:      nginx
Address:   172.28.128.11
Rules:
  Host               Path   Backends
  ----               ----   --------
  nginx.example.io
                     /      nginx:80 (<none>)
코드블록. Ingress backend 정보 없음

Ingress Specification의 backend(spec.rules[].http.paths[].backend.service)에 지정한 서비스명(name)과 포트(port.number)가 실제 Service 리소스와 일치하는지 확인하세요. 서비스명은 맞는데 <none>이라면 Service의 Endpoints가 비어 있는 것이므로 Service 통신 실패를 참고하세요.

참고

extensions/v1beta1 API의 backend.serviceName, backend.servicePort 필드는 현재 API(networking.k8s.io/v1)에서 backend.service.name, backend.service.port.number로 변경되었습니다.

Ingress의 Address가 비어 있다면 Ingress Controller가 해당 Ingress를 처리하지 않고 있는 상태입니다. spec.ingressClassName 값이 클러스터에 설치된 IngressClass(kubectl get ingressclass)와 일치하는지 확인하세요.

다음은 Backends 정보가 있는데도 통신이 되지 않는 경우들입니다.

배경색 변경
curl -H 'Host: nginx.example.io' http://172.28.128.11
<title>504 Gateway Time-out</title>
curl -H 'Host: nginx.example.io' http://172.28.128.11
<title>504 Gateway Time-out</title>
코드블록. 504 Gateway Time-out

Ingress Controller가 백엔드 Pod로 요청을 전달했지만 제한 시간 안에 응답을 받지 못한 경우입니다. 애플리케이션의 처리 시간이 Ingress Controller의 타임아웃(nginx의 경우 proxy-read-timeout, 기본 60초)을 초과하지 않는지 확인하고, 필요하면 애플리케이션을 개선하거나 어노테이션으로 타임아웃을 조정하세요. NetworkPolicy로 Ingress Controller에서 백엔드 Pod로의 통신이 차단된 경우에도 동일한 오류가 발생하므로 함께 확인하세요.

배경색 변경
curl -H 'Host: nginx.example.io' http://172.28.128.11
<title>503 Service Temporarily Unavailable</title>
curl -H 'Host: nginx.example.io' http://172.28.128.11
<title>503 Service Temporarily Unavailable</title>
코드블록. 503 Service Temporarily Unavailable

전달 가능한 정상 백엔드가 하나도 없는 경우입니다. Pod가 모두 비정상이거나 Ready 상태가 아니어서 Service의 Endpoints가 비어 있을 가능성이 높으므로 Pod 상태를 확인하세요.

배경색 변경
curl -H 'Host: nginx.example.io' http://172.28.128.11
<title>404 Not Found</title>
curl -H 'Host: nginx.example.io' http://172.28.128.11
<title>404 Not Found</title>
코드블록. 404 Not Found

404는 두 가지 경우에 발생합니다. 첫째, 요청한 Host나 Path와 일치하는 Ingress 규칙이 없어 Ingress Controller의 기본 백엔드가 응답한 경우입니다. Host 헤더 값과 spec.rules[].host, 그리고 path/pathType 설정이 요청과 일치하는지 확인하세요. 둘째, 규칙은 일치했지만 애플리케이션이 해당 경로를 서비스하지 않는 경우입니다. Ingress에 설정한 path가 실제 애플리케이션의 context path와 맞는지 확인하세요.

참고
어느 쪽인지는 Ingress Controller의 액세스 로그로 구분할 수 있습니다. 요청이 백엔드로 전달되었다면 로그에 upstream 주소가 기록되고, 규칙이 매칭되지 않았다면 기본 백엔드로 처리된 것으로 기록됩니다.
배경색 변경
curl -H 'Host: nginx.example.io' http://172.28.128.11
curl: (28) Failed to connect to 172.28.128.11 port 80: Connection timed out
curl -H 'Host: nginx.example.io' http://172.28.128.11
curl: (28) Failed to connect to 172.28.128.11 port 80: Connection timed out
코드블록. Connection timed out

Ingress Controller까지 요청이 도달하지 못한 경우입니다. 클라이언트와 Ingress Controller 사이의 방화벽 및 Security Group에서 80/443 포트가 열려 있는지 확인하세요.

배경색 변경
curl -H 'Host: nginx.example.io' http://172.28.128.11
curl: (7) Failed to connect to 172.28.128.11 port 80: Connection refused
curl -H 'Host: nginx.example.io' http://172.28.128.11
curl: (7) Failed to connect to 172.28.128.11 port 80: Connection refused
코드블록. Connection refused

목적지까지 패킷은 도달했지만 해당 포트를 Listen하는 프로세스가 없는 경우입니다. Ingress Controller Pod가 정상 구동 중인지, LoadBalancer Service의 포트 설정이 올바른지 확인하세요.

방화벽에 의한 차단인지 확인하려면 다음 명령어로 포트 연결 여부를 점검한 후, <ingress-controller-external-ip>의 80, 443 포트에 대한 방화벽을 여세요.

배경색 변경
nc -zv <ingress-controller-external-ip> 80

# nc 명령어를 사용할 수 없는 경우(bash 전용)
timeout 3 bash -c '</dev/tcp/<ingress-controller-external-ip>/80' && echo "open" || echo "closed"
nc -zv <ingress-controller-external-ip> 80

# nc 명령어를 사용할 수 없는 경우(bash 전용)
timeout 3 bash -c '</dev/tcp/<ingress-controller-external-ip>/80' && echo "open" || echo "closed"
코드블록. 외부 방화벽 통신 확인

도메인이 DNS에 등록되지 않은 경우에는 Could not resolve host 오류가 발생합니다. 다음 명령어로 도메인이 resolve되는지 확인한 후, DNS 서버에 도메인을 등록하거나 클라이언트의 hosts 파일에 등록해서 사용하세요.

배경색 변경
nslookup <host-domain>

# 또는
getent hosts <host-domain>
nslookup <host-domain>

# 또는
getent hosts <host-domain>
코드블록. 도메인 resolve 확인

빠른 점검 가이드

지금까지의 내용을 토대로, 문제가 발생했을 때 가장 빠르게 원인 구간을 좁히는 점검 순서는 다음과 같습니다. 앞 단계가 실패하면 뒤 단계는 반드시 실패하므로, 반드시 순서대로 확인하세요.

빠른 점검 가이드 순서도
그림. 빠른 점검 가이드 순서도
  1. Pod Running & Pod Ready 확인
배경색 변경
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-58cd5b85f7-84xlr   1/1     Running   0          31s
kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-58cd5b85f7-84xlr   1/1     Running   0          31s
코드블록. Pod Running/Ready 확인
  1. Pod 통신 확인
배경색 변경
curl <Pod IP>:<port>
curl <Pod IP>:<port>
코드블록. Pod 통신 확인
  1. Service 통신 확인
배경색 변경
curl <Service IP>:<port>
curl <Service IP>:<port>
코드블록. Service 통신 확인
  1. Ingress 통신 확인
배경색 변경
curl -H 'Host: <host-domain>' http://<ingress-controller-external-ip>
curl -H 'Host: <host-domain>' http://<ingress-controller-external-ip>
코드블록. Ingress 통신 확인