mLog 오븐 앞에서 회중시계로 시작 준비 시간을 살피는 흰색 3D 유령 제빵사

시작 준비가 끝났는지는 오래된 경고보다 현재 상태와 시간 흐름으로 확인한다.

Jenkins Startup probe failed: 503 경고 뒤 Pod가 Running일 때

서버·인프라 Kubernetes 2026년 11월 8일

Jenkins에 Startup probe failed 경고가 남아 있어도 지금 장애가 계속된다는 뜻은 아니다. 먼저 같은 Pod UID의 현재 컨테이너 상태, 재시작 횟수의 변화, 이벤트 시각을 대조해야 한다. 반대로 Running 한 줄만으로 Jenkins가 정상 서비스를 제공한다고 결론 내릴 수도 없다.

이 글은 경고를 지우기 위한 재시작보다, 경고가 언제 어느 컨테이너에서 발생했는지 확인하는 순서를 정리한다. Portainer에서 발견했더라도 실제 판정에는 Kubernetes API에 기록된 상태를 사용한다.

검증 기준일은 2026-11-08이다. 대상은 Kubernetes/K3s에 배포된 Jenkins 컨트롤러이며, 네임스페이스·Pod·컨테이너 이름은 환경에 맞춰 바꿔야 한다. 명령에는 kubectl과 jq가 필요하다. 운영 기록과 공식 문서를 대조한 진단 글이며, probe 설정 변경으로 문제를 재현·해결한 실험 기록은 아니다.

1. 기록에서 확인한 것과 확인하지 못한 것

운영 중 남겨 둔 2026-10-28 기록에는 다음 내용이 있었다. 아래는 경고의 요약이며 Pod IP는 공개하지 않았다.

기록 시각 관측 내용
15:25:04 /login을 확인하는 startup probe에서 <Pod IP>:8080 connection refused
15:25:15 startup probe에서 HTTP 503 응답
이후 공유한 상태 출력 jenkins jenkins-0 2/2 Running 0 21m

첫 기록은 연결 거절을, 두 번째 기록은 HTTP 실패 응답을 보여준다. 서버가 준비되는 과정이었다는 해석은 가능하지만, 이 자료만으로 원인을 확정할 수는 없다. 당시 Pod UID와 전체 설정·로그를 함께 보관한 자료가 없기 때문이다.

두 경고 사이의 11초는 관측된 시각 차이다. 실제 periodSeconds 값으로 대입하면 안 된다. 상태 출력의 21m 역시 Pod의 나이이며 Jenkins가 준비되는 데 21분 걸렸다는 측정값이 아니다. 이 글에서는 특정 설정을 바꿔 문제가 해결됐다고 주장하지 않는다.

2. Running, Ready, startup 성공은 서로 다른 정보다

Pod의 Running 단계는 애플리케이션의 모든 기능이 정상이라는 판정이 아니다. READY 열과 컨테이너별 상태까지 내려가서 봐야 한다. Pod 이름이 같아도 재생성되었다면 UID가 달라진다. Kubernetes Pod 수명 주기

항목 이 글에서 확인할 의미
Pod UID 이전 경고와 지금 관측한 Pod가 같은 객체인지
ready 해당 컨테이너가 현재 준비 상태인지
started 시작 검사와 시작 훅 완료 상태. startup probe가 있는지 함께 확인
restartCount 현재 보이는 재시작 카운터와 관측 사이의 변화
state / lastState 현재 대기·실행·종료 상태와 이전 종료 이유

started가 null이면 성공 근거로 사용하지 않는다. API는 이를 false와 같이 취급한다. startup probe가 없는 컨테이너의 started: true는 해당 검사를 통과했다는 증거가 아니다. restartCount: 0도 영구적인 무재시작 이력은 아니다. 노드 재시작 등으로 상태가 유실되면 카운터가 초기화될 수 있다. ContainerStatus API

Docker의 unhealthy와 자동 재시작 관계가 헷갈린다면 Docker healthcheck와 restart 정책을 먼저 읽으면 좋다. 이 글에서는 kubelet이 수행하는 probe와 Kubernetes의 컨테이너 상태를 다룬다.

3. 첫 관측: 대상 클러스터와 컨테이너부터 확인한다

아래 예시의 네임스페이스와 Pod 이름을 실제 값으로 바꾼다. kubeconfig 전체를 출력하거나 인증 정보를 공유할 필요는 없다.

kubectl config current-context
kubectl version

ns=jenkins
pod=jenkins-0

kubectl -n "$ns" get pod "$pod" -o wide
kubectl -n "$ns" get pod "$pod" -o json | jq '{
  uid: .metadata.uid,
  created: .metadata.creationTimestamp,
  phase: .status.phase,
  conditions: [(.status.conditions // [])[] |
    select(.type == "Ready" or .type == "ContainersReady") |
    {type, status, lastTransitionTime}],
  containers: [(.status.containerStatuses // [])[] |
    {name, ready, started, restartCount, containerID, state, lastState}],
  initContainers: [(.status.initContainerStatuses // [])[] |
    {name, ready, started, restartCount, state, lastState}]
}'

2/2처럼 요약된 숫자만 보지 말고 Jenkins 컨트롤러의 이름을 찾는다. 보조 컨테이너의 재시작을 컨트롤러 재시작으로 읽으면 원인 조사 방향이 달라진다. 초기화 컨테이너 상태도 별도 목록에서 확인한다.

현재 값과 관측 시각을 기록하고, 실제 probe 주기와 시작 허용 시간을 고려해 다음 관측에서 비교한다. 모든 환경에 일률적으로 ‘몇 초만 기다리면 정상’이라는 기준을 적용하지 않는다. UID가 달라졌다면 새 Pod의 관측을 시작한 것이다.

4. 같은 UID의 이벤트를 시간순으로 확인한다

이름만으로 이벤트를 모으면 재생성 전 Pod의 기록을 잘못 읽을 수 있다. 현재 UID를 얻어 범위를 좁힌다.

pod_uid=$(kubectl -n "$ns" get pod "$pod" -o jsonpath='{.metadata.uid}')
test -n "$pod_uid" || exit 1

kubectl -n "$ns" get events \
  --field-selector "involvedObject.uid=$pod_uid" -o json |
jq '.items |
  sort_by(.series.lastObservedTime // .lastTimestamp //
          .eventTime // .metadata.creationTimestamp) |
  .[] | {
    type, reason, count, firstTimestamp, lastTimestamp, eventTime,
    series,
    container: .involvedObject.fieldPath,
    message
  }'

# 조회 도중 Pod가 교체되지 않았는지 UID를 다시 확인한다.
kubectl -n "$ns" get pod "$pod" -o jsonpath='{.metadata.uid}{"\n"}'

involvedObject.uid는 Event에서 지원하는 필드 선택자다. 필드 선택자 공식 안내

경고의 마지막 관측 시각이 현재 컨테이너 시작 시각보다 앞서는지, 대상 컨테이너가 일치하는지, 이후 비슷한 경고가 추가되는지 확인한다. 집계된 Event의 count를 startup probe의 연속 실패 횟수나 현재 재시작 횟수로 그대로 읽지 않는다.

이벤트는 보존 기간이 제한되고 모든 이력을 보장하는 장부가 아니다. 비어 있는 결과만으로 ‘경고가 한 번도 없었다’고 결론 내리지 않는다. 화면에 남아 있는 과거 경고 역시 현재 장애를 확정하지 않는다. Kubernetes Event API

5. 설치된 Pod의 검사 경로와 Jenkins 로그를 대조한다

먼저 일반 컨테이너 이름 목록을 확인하고 실제 컨트롤러 이름을 지정한다.

kubectl -n "$ns" get pod "$pod" \
  -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{end}'

# 위 출력에서 확인한 실제 컨트롤러 이름으로 변경한다.
container=jenkins

kubectl -n "$ns" get pod "$pod" -o json |
jq --arg c "$container" '
  .spec.containers[] | select(.name == $c) | {
    name, image, ports,
    startupProbe: (.startupProbe | if . == null then null else {
      initialDelaySeconds, periodSeconds, timeoutSeconds,
      failureThreshold, successThreshold,
      httpGet: (.httpGet | if . == null then null else {
        path, port, scheme, host,
        hasCustomHeaders: ((.httpHeaders // []) | length > 0)
      } end)
    } end)
  }'

kubectl -n "$ns" logs "$pod" -c "$container" \
  --timestamps --since=30m

명령에서 아무 컨테이너도 출력되지 않으면 이름부터 다시 확인한다. 30m는 로그를 가져올 예시 범위다. 문제 발생 시각에 맞춰 조정하고 시간대를 통일한다. 로그나 상태 출력을 공유할 때에는 토큰·개인 URL·민감한 경로 등을 가린다.

재시작이 확인되었고 이전 인스턴스 로그가 남아 있다면 다음 명령도 확인한다. 이는 삭제된 다른 Pod의 로그를 복구하는 명령이 아니다. kubectl logs

kubectl -n "$ns" logs "$pod" -c "$container" \
  --previous --timestamps --tail=200

검사할 항목은 다음과 같다.

  • httpGet.port가 이름이면 같은 컨테이너의 ports에서 실제 번호를 찾는다. Service 포트나 외부 HTTPS 포트와 섞지 않는다.
  • Jenkins를 경로 접두사와 함께 서비스한다면 probe 경로에도 그 조건이 반영되었는지 본다.
  • scheme, host, 사용자 지정 헤더 유무를 확인한다. HTTP probe가 아니라 exec·TCP 등의 방식이면 그 정의를 따로 확인한다.
  • 로그에서 초기화 진행, 플러그인·설정 로딩 오류, 종료 시각을 찾고 lastState의 종료 이유와 대조한다. 로그 근거 없이 플러그인 충돌이나 메모리 부족으로 단정하지 않는다.

확인일의 Jenkins 공식 Helm chart 기본 파일은 URI 접두사를 고려한 /login과 이름이 http인 포트로 검사하도록 정의하고 있다. 그러나 이 파일의 main은 변할 수 있고, 사용 중인 chart 버전·사용자 values와 다를 수 있다. 설치된 Pod에 반영된 값이 이번 진단의 출발점이다. 인터넷 예시의 시간 설정을 그대로 복사하지 않는다.

기존 OCI 환경에서 K3s와 Docker·NPM을 함께 구성한 흐름은 OCI A1 K3s 설치 기록에 정리했다. 외부 도메인이 열리는지와 Pod 내부 probe가 통과하는지는 별도로 확인한다.

6. 관측 결과에 따라 다음 행동을 나눈다

HTTP probe는 200 이상 400 미만을 성공으로 판단한다. 503은 실패 응답이지만, HTTP 상태 코드 하나가 실패 원인 전체를 설명하지는 않는다. HTTP probe 공식 예제

현재 관측 다음 행동
같은 UID에서 controller가 started·ready이고, 재시작 증가 없이 기능 확인도 통과하며 경고 시각이 과거에 머묾 일시적 시작 경고였을 가능성을 기록하고 관측을 이어간다. 경고 표시만 없애려고 설정을 바꾸지 않는다.
Running이지만 controller가 아직 ready가 아니고 시작 시간이 진행 중 로그가 실제로 진행되는지와 현재 probe 정의를 확인한다.
같은 컨테이너의 경고가 계속 추가되고 재시작도 증가 현재 장애로 조사한다. 이전 로그와 종료 이유를 확보하고 경로·포트·기동 실패를 좁힌다.
OOMKilled 등 별도 종료 이유가 있음 해당 종료 원인부터 확인한다. probe 시간만 늘리는 조치로 대체하지 않는다.
UID가 바뀌었거나 상태·로그가 부족함 전후 이력을 연결해 결론 내리지 말고 새 관측을 남긴다.

startup probe가 설정된 컨테이너에서는 시작 검사 성공 전까지 liveness·readiness 검사가 진행되지 않는다. 시작 검사가 연속 실패 한계에 도달하면 해당 컨테이너가 종료되고 재시작 정책의 적용을 받는다. 그래서 시작 허용 시간을 늘릴지는 실제 기동 진행과 실패 이유를 확인한 뒤 결정해야 한다. Kubernetes probe 동작

경로가 잘못되었거나 애플리케이션이 시작할 수 없는 상태라면 허용 시간만 늘려도 원인은 남는다. 반대로 정상적으로 준비가 진행되지만 허용 시간이 부족하다는 기록이 있다면, 설치한 chart 버전에 맞는 values에서 조정하고 전후 상태를 비교할 수 있다. 이 글에서는 운영 설정이 확인되지 않아 보편적인 숫자나 바로 실행할 변경 명령을 제시하지 않는다.

Jenkins 접속·로그인뿐 아니라 필요한 작업 동작도 기존 운영 절차에 따라 확인한다. Ready는 그 환경에 정의된 검사 범위를 만족했다는 신호이며, 모든 파이프라인의 성공을 보증하지 않는다.

7. 경고 하나만으로 하지 않을 작업

이 진단에는 Pod 삭제, 전체 재배포, PVC 삭제, probe 비활성화가 필요하지 않다. 원인을 확인하기 전에 재시작하면 직전 상태와 로그를 놓칠 수 있고, Jenkins 데이터가 든 볼륨을 삭제하면 복구 문제가 된다. 변경이 필요해진 경우에는 별도로 변경 범위와 백업·복구 방법을 준비한다.

이번 사례에서 확실한 것은 두 종류의 startup 경고와 이후 2/2 Running 출력이 관측되었다는 점이다. 그 사이의 과정을 복원하려면 UID → 컨테이너 상태 → 이벤트 시각 → 실제 probe 설정 → 로그 순서로 근거를 연결해야 한다.

태그

mLog

Spring Boot와 PostgreSQL로 서비스를 개발하고, Oracle Cloud에서 Docker로 mLog를 운영합니다. 쿼리가 느려지거나 컨테이너가 뜨지 않을 때 어떤 로그와 명령으로 원인을 찾았는지 기록합니다. 직접 확인한 결과와 공식 문서를 참고한 예제를 구분해 적습니다.