K8s 프로브 설계 가이드

Kubernetes에서 애플리케이션의 헬스 체크(Health Check) 를 올바르게 설계하는 것은 안정적인 서비스 운영의 핵심입니다. 특히 livenessProbe, readinessProbe, startupProbe는 각기 다른 목적을 가지며, NestJS 기반 Node.js 애플리케이션을 Kubernetes에 배포할 때 이들을 정확히 이해하고 구현해야 예상치 못한 재시작, 트래픽 누락, 롤아웃 지연 등의 문제를 예방할 수 있습니다.

처음 접할 때는 이러한 프로브 개념이 혼란스러울 수 있습니다. 예를 들어, Readiness와 Liveness의 차이를 명확히 알지 못해 둘 중 하나만 설정하거나 잘못된 기준으로 구현하기도 합니다. 또한 NestJS 애플리케이션에서 “언제 애플리케이션이 완전히 기동되어 Ready 상태로 볼 수 있는지”, 그리고 Kubernetes 롤아웃 과정에서 프로브와 설정값들이 어떤 영향을 미치는지 파악하기 어려울 수 있습니다.

이 문서에서는 이러한 혼동을 해소하기 위해 공식 문서를 통해 확인한 정확한 개념과 동작 원리, 그리고 실무적인 설정 전략을 공유합니다. 각 섹션에서는 실무에서 흔히 가지는 의문과 그에 대한 해답을 바탕으로 핵심 개념을 정리합니다. 또한 Kubernetes 공식 문서 등 신뢰할 만한 출처를 인용하여 근거를 제시하며, 잘못된 설정이 초래할 수 있는 문제점과 올바른 설정 방법을 함께 살펴보겠습니다.


1. LivenessProbe, ReadinessProbe, StartupProbe – 역할과 차이

애초에 왜 프로브가 세 가지나 필요한 걸까요? 많은 엔지니어들이 Readiness와 Liveness를 혼동하거나, 둘 중 하나만 설정하면 되지 않느냐고 질문합니다. “애플리케이션이 살아있는지(bool) 체크하면 되지, 굳이 두 가지가 필요한가?”라고 생각하기 쉽습니다. 그러나 Kubernetes의 설계를 이해하면 두 프로브는 분명한 차별화된 역할을 가집니다. 여기에 Kubernetes 1.16+에서 도입된 startupProbe까지 합쳐 세 가지 프로브가 서로 보완적 역할을 수행합니다.

  • Liveness Probe: 애플리케이션 프로세스가 살아있는지 확인합니다. kubelet은 liveness 프로브가 실패할 경우 해당 컨테이너를 재시작 합니다. 예를 들어 데드락(Deadlock) 등에 빠져서 “프로세스는 살아 있는데 작업을 처리하지 못하는” 경우를 감지해 자동으로 재기동함으로써 서비스 가용성을 높입니다. 잘 설정된 livenessProbe는 치명적 오류로 응답이 불가능한 컨테이너를 복구하지만, 반대로 잘못 설정하면 멀쩡한 애플리케이션을 과도하게 재시작해 캐스케이딩 장애 를 유발할 수 있습니다. (이 부분은 뒤에서 자세히 다룹니다.)

  • Readiness Probe: 애플리케이션이 요청을 처리할 준비가 되었는지 확인합니다. kubelet은 readiness 프로브 결과에 따라 Pod의 Ready 상태 를 결정하며, Readyfalse인 경우 해당 Pod에 서비스 트래픽을 보내지 않습니다. 예를 들어 애플리케이션 시작 시 대용량 설정이나 데이터 로드, 외부 서비스 의존성 체크 등이 끝나지 않았다면 readinessProbe를 실패시킴으로써, Kubernetes Service 로드밸런서는 준비 안 된 Pod를 트래픽 대상에서 제외하게 됩니다. 중요한 점은, readiness 프로브는 컨테이너를 죽이지 않습니다. 준비되지 않은 상태를 나타낼 뿐이고, Pod가 Ready로 돌아오면 다시 서비스에 포함됩니다. 따라서 readiness는 “지금 당장 트래픽 받아도 되나?”를 판단하는 신호이며, liveness는 “프로세스를 죽였다 살려야 하나?”를 판단하는 신호입니다.

  • Startup Probe: 애플리케이션의 초기 부팅이 완료되었는지 확인합니다. Kubernetes 1.16 이후 새로 도입된 프로브로, 초기 시작이 느린 컨테이너를 보호 하는 역할을 합니다. startupProbe를 설정하면 해당 컨테이너에 한해서 livenessProbe와 readinessProbe의 실행이 보류 됩니다. startupProbe가 성공하기 전까지 kubelet은 liveness/readiness 체크를 수행하지 않음으로써, 애플리케이션이 완전히 기동되기도 전에 다른 프로브가 실패하여 컨테이너를 재시작하거나 Readyfalse로 만드는 일을 막습니다. 한마디로 “아직 시작 중이니 건드리지 마” 신호입니다. startupProbe가 일단 한 번 성공하면 이후로는 더 이상 실행되지 않고(liveness/readiness가 그 뒤를 이어 지속 모니터링), 정해진 횟수만큼 재시도해도 성공하지 못하면 컨테이너를 죽이는 동작을 합니다. 즉, 최악의 경우 지정한 시간 내에 애플리케이션이 기동되지 않으면 애플리케이션에 문제가 있다고 보고 재시작하는 안전장치이기도 합니다.

내용을 정리하면, readinessProbe는 “서비스 트래픽을 받을 준비 여부”를, livenessProbe는 “애플리케이션 프로세스의 생존 여부”를, startupProbe는 “초기화가 완료되었는지 여부”를 판단합니다. 세 가지 모두 설정했을 경우 startupProbe → (성공 후) readiness/livenessProbe 병행 순으로 동작하게 됩니다. 특히 readiness와 liveness는 동시에 설정할 수 있으며, 각각 다른 목적으로 함께 사용하는 것이 권장됩니다. Kubernetes 공식 문서 역시 “두 프로브를 병행 사용하면 준비 안 된 컨테이너엔 트래픽이 가지 않게 하면서, 장애가 난 컨테이너는 재시작시킬 수 있어 더 안전하다” 고 강조합니다.

실무 팁: Readiness와 Liveness를 함께 사용할 때 동일한 엔드포인트 를 사용하되, Liveness의 failureThreshold를 높게 설정 하는 패턴이 자주 사용됩니다. 예를 들어 둘 다 /healthz를 조회하지만, readinessProbe는 빠르게 실패 처리(짧은 failureThreshold=1 등)하고 livenessProbe는 여러 번 연속 실패해야 비로소 재시작하도록 지연을 두는 것입니다. 이렇게 하면 애플리케이션에 문제가 생겼을 때 먼저 readinessProbe가 Pod를 서비스 대상에서 제외하여 트래픽을 중지시키고, 일정 시간 문제가 계속되면 그제서야 livenessProbe가 컨테이너를 재시작합니다. 이 접근법은 잘못된 livenessProbe로 인한 불필요한 재시작(예: 일시적인 지연으로 인한 오탐지)을 줄여줘 결과적으로 안정성을 높입니다.

마지막으로, 프로브 설정을 잘못하면 생기는 문제 도 알아둬야 합니다. 앞서 언급했듯 livenessProbe를 섣불리 설정하면 오히려 장애를 키울 수 있습니다. 공식 문서에는 “잘못 구현된 livenessProbe는 연쇄 실패로 이어질 수 있다. 높은 부하에서 컨테이너가 계속 재시작되고, 요청이 실패하며, 남은 Pod에 과부하가 걸리는 현상이 발생할 수 있다. 각 애플리케이션에 맞는 readiness와 liveness의 차이를 이해하고 적용해야 한다” 고 경고합니다. 예를 들어 응답이 일시적으로 지연된 것을 죽어버렸다고 오인해 컨테이너를 재시작하면, 오히려 시스템은 불필요한 리소스를 쓰고 현재 처리 중이던 작업도 날려버려 전체적인 안정성을 해칠 수 있습니다. 정리하면, 프로브는 신중하게 설정해야 하며, 특히 Readiness와 Liveness의 용도 차이를 정확히 알고 활용하는 것이 중요합니다.


2. NestJS에서 ‘부팅 완료’란 무엇인가?

Kubernetes 프로브 중 startupProbereadinessProbe는 애플리케이션이 완전히 기동되었는지 여부를 판단하는 데 활용됩니다. 그렇다면 NestJS 기반 Node.js 애플리케이션에서 “완전히 기동됨”의 기준은 무엇일까요? 보통 NestJS 앱의 main.ts에서 app.listen(포트)이 호출되어 서버가 시작되면 곧바로 readinessProbe를 통과시켜도 된다고 생각하기 쉽습니다. 하지만 NestJS의 라이프사이클 훅(Lifecycle Hook) 을 살펴보면 보다 정확한 기준을 알 수 있습니다.

NestJS 공식 문서에 따르면, onApplicationBootstrap() 훅은 “모든 모듈이 초기화된 후, 서버가 본격적으로 요청을 받기 직전에 호출” 됩니다. 즉, Nest 애플리케이션의 내부 모듈, 프로바이더들이 전부 생성 및 초기화되고 라우트 바인딩까지 완료된 시점에 이 훅이 실행되는 것이죠. 이때 NestJS 프레임워크는 아직 소켓을 열어 외부 연결을 받기 전이므로, onApplicationBootstrap이 끝나야 비로소 애플리케이션이 클라이언트 요청을 받을 준비가 완료 되고 서버 포트를 열게 됩니다. NestJS에서는 이 훅을 활용해 부팅 시 필요한 추가 작업(예: 캐시 프리로딩, 초기 데이터 로드 등)을 수행할 수 있으며, 훅 내에서 비동기 처리를 완료할 때까지 app.listen() 호출이 이어지지 않도록 부트스트랩 과정을 지연 시킬 수도 있습니다. (onModuleInitonApplicationBootstrap 훅은 async/Promise를 반환함으로써 애플리케이션 초기화를 지연시킬 수 있습니다.)

NestJS 애플리케이션의 “부팅 완료”는 요약하면 모든 초기화 작업이 끝나고, 애플리케이션이 수신 대기(listen) 상태로 들어갔을 때 라고 정의할 수 있습니다. 보다 현실적인 기준으로는 “필요한 비동기 초기 설정(예: DB 연결, 외부 API 준비)과 모든 라우트 바인딩이 완료” 된 순간입니다. NestJS 프로젝트를 Kubernetes에 배포할 때 readinessProbe의 기준을 이 지점에 맞추는 것이 중요합니다. NestJS 공식 리포지터리의 한 논의에서도 “Kubernetes가 컨테이너 시작 즉시 트래픽을 보내지 않도록, 애플리케이션 초기화(비동기 연결, 라우트 바인딩 등)가 모두 끝났을 때 200 OK를 반환하는 readiness 엔드포인트가 필요하다” 고 언급되어 있습니다. 반대로 말하면, 초기화 중에는 readinessProbe가 실패 상태를 내도록 하여 Kubernetes가 Pod를 서비스 엔드포인트에 등록하지 않도록 해야 합니다.

NestJS에서 부팅 완료를 감지하고 readinessProbe와 연동하는 방법으로는 여러 가지가 있습니다. 간단한 방법은 전역 변수나 서비스 상태 플래그를 두고, onApplicationBootstrap에서 이를 ready로 설정한 뒤 readiness 체크 핸들러(/readyz)에서 해당 플래그를 확인하는 것입니다. 조금 더 세련된 방식으로, NestJS의 Terminus (헬스 체크 모듈)을 사용하면 NestJS 애플리케이션 상태와 외부 종속성을 종합적으로 진단하는 헬스 체크 엔드포인트를 쉽게 만들 수 있습니다. Terminus는 내부적으로 NestJS의 애플리케이션 종료 훅(Shutdown Hooks) 도 활용하므로, 애플리케이션이 종료 신호(SIGTERM 등)를 받을 경우 자동으로 Readiness 상태를 false로 전환해주는 등의 기능도 제공합니다.

정리하면, NestJS 애플리케이션에서 readinessProbe의 성공 조건은 “NestJS 앱이 완전히 부팅되어 요청을 처리할 수 있는 상태”입니다. 이는 Nest lifecycle 상 onApplicationBootstrap 이후 시점이며, “모든 초기화가 완료되고 모든 라우트가 바인딩된 후” 라는 기준으로 삼을 수 있습니다. 이러한 상태를 판단하기 위해 애플리케이션 내부에서 플래그를 관리하거나, NestJS의 Terminus와 같은 툴을 활용하면 효율적입니다.


3. 헬스 체크 엔드포인트 설계 (/livez, /readyz, /startupz)

Kubernetes 프로브는 컨테이너 내부에서 제공하는 HTTP 엔드포인트 를 주기적으로 호출하여 동작합니다. 따라서 우리의 NestJS 애플리케이션에도 Liveness, Readiness, StartupProbe용 엔드포인트를 만들어주어야 합니다. 일반적으로 엔드포인트 경로는 팀의 규칙에 따라 정할 수 있지만, Kubernetes 자체 구성요소들이 /livez, /readyz, /healthz 등을 사용하기 때문에 이러한 이름을 따르는 경우가 많습니다. 여기서는 이해를 돕기 위해 /startupz, /livez, /readyz 세 가지 경로를 사용한다고 가정하겠습니다. 각 엔드포인트는 다음과 같은 원칙으로 설계합니다.

  • /startupz (Startup Probe용)앱 기동 완료 여부 를 반환합니다. 앱이 아직 초기화 중이라면 HTTP 500 등의 에러 상태를 응답하고, 모든 초기화가 끝났으면 HTTP 200 OK를 응답하게 합니다. 이 엔드포인트는 startupProbe 설정에서만 사용되고, 성공(200) 응답을 한 번이라도 보내면 Kubernetes는 더 이상 이 probe를 호출하지 않습니다. 따라서 /startupz는 처음 컨테이너가 뜰 때 일종의 Gate 역할을 합니다. NestJS의 경우 앞서 설명한 플래그를 활용해 onApplicationBootstrap 이전까지는 500을 리턴하다가 이후 200으로 전환하는 식으로 구현할 수 있습니다. 만약 애플리케이션이 지정된 시간 안에 /startupz에서 200을 반환하지 못하면, Kubernetes는 해당 컨테이너를 부팅 실패로 간주하여 강제 종료 및 재시작 시킬 것입니다. 이러한 동작을 통해, 일정 시간 내에 기동하지 못하는 컨테이너(예: 코드 버그로 무한 대기 상태)는 자동 복구되거나 결국 CrashLoopBackOff 상태로 표면화되어 관리자가 인지할 수 있게 됩니다.

  • /readyz (Readiness Probe용)앱이 트래픽을 처리할 준비가 되었는지 를 반환합니다. 기동 직후 초기화가 끝나지 않은 경우뿐만 아니라, 런타임 중에 일시적으로 서비스 불가한 상태 에서도 /readyz를 실패시킬 수 있습니다. 예를 들어, 애플리케이션이 의존하는 DB나 캐시 서버에 연결 불가 상태라면 현재 요청을 받아봐야 실패할 것이므로, /readyz를 500으로 응답하게 해 둔다면 Kubernetes는 Pod의 Ready 상태를 false로 바꾸고 서비스 트래픽을 일시 중단할 수 있습니다. (외부 서비스 점검 시 이러한 메커니즘을 통해 일시적으로 트래픽을 다른 Pod로 우회시키는 패턴도 있습니다.) 다만 주의점 으로, readinessProbe는 어디까지나 트래픽 분산 을 제어하기 위한 것이지, 이 실패 자체가 컨테이너 재시작을 초래하지는 않습니다. 따라서 /readyz 엔드포인트가 실패를 응답하더라도 컨테이너는 계속 살아있으며, kubelet은 주기적으로 readinessProbe를 재시도하면서 언제 다시 Ready 상태가 되는지만 관찰합니다. /readyz 엔드포인트 설계 시에는 어떤 조건에 서비스를 “비준비(Unready)” 상태로 둘지 를 잘 정의해야 합니다. 일반적으로:

    • 애플리케이션이 초기화 완료 전 이라면 (위 /startupz와 유사하지만, startupProbe 미사용 시 readinessProbe 초기 지연으로 대체 가능) Ready = false.
    • 애플리케이션 자체는 살아있지만, 핵심 의존성 장애 등으로 정상 응답을 못할 상황 이면 Ready = false. (예: DB 연결 끊김, 필수 서브시스템 오류 등)
    • 애플리케이션이 종료 시그널을 받아 종료 절차 진행 중 이라면 Ready = false. 이 경우 새로운 요청을 받지 않도록 한 후, 기존 처리 중이던 요청만 마치고 종료할 시간을 벌 수 있습니다. Kubernetes는 Pod 종료 시 SIGTERM을 보내는데, 이때 우리 애플리케이션은 Shutdown Hook에서 /readyz 상태를 false로 전환하여 서비스에서 빠지도록 하는 것이 좋습니다. (이와 반대로 /livez는 종료 중에도 true를 유지하여 정상 종료 중임을 Kubernetes에 알리는 편이 안전합니다.)

    NestJS Terminus 모듈을 사용하면 /readyz에 포함될 Health Indicator 들을 구성할 수 있습니다. 예컨대 데이터베이스 연결 상태 를 체크하는 Indicator를 포함하여, DB 다운 시 /readyz가 실패하게 만드는 식입니다. 다만, 실무에서는 readinessProbe가 너무 민감하게 동작하면 문제를 악화시킬 수 있으므로 유의해야 합니다. 예를 들어 DB 일시적 장애 시 모든 Pod가 Ready false가 되어 서비스 전체가 다운되는 상황은 피해야 합니다. 어떤 경우 readinessProbe애플리케이션 자체 준비 상태 만을 반영하고, DB 연결 등은 애플리케이션 레벨에서 재시도/오류처리로 해결하며 Readiness는 유지시키는 전략도 고려됩니다. 적절한 수준으로 /readyz의 기준을 정하는 것은 서비스 특성에 맞게 조정 해야 합니다.

  • /livez (Liveness Probe용)앱 프로세스의 생존 여부 를 반환합니다. 이상적으로, 이 엔드포인트는 애플리케이션 내부 로직을 거의 수행하지 않고도 응답 할 수 있어야 합니다. 일반적으로 /livez에서는 단순히 “OK”와 같이 즉시 반환하는 것이 좋습니다. 왜냐하면 livenessProbe의 목적은 “프로세스가 정상 동작 중인지” 를 확인하는 것이며, /livez 자체에 복잡한 체크 로직을 넣을 필요가 없기 때문입니다. 중요한 점은, Node.js에서는 메인 이벤트 루프가 살아있는지 여부 자체가 Liveness의 핵심 지표라는 것입니다. Node는 싱글 스레드 이벤트 루프로 동작하기 때문에, 이벤트 루프가 블로킹 되면 HTTP 요청에 대한 응답도 지연되거나 불가능해집니다. Kubernetes의 livenessProbe는 기본적으로 정해진 시간 내 HTTP 200 응답이 없으면 실패로 간주하므로, /livez 엔드포인트에 특별한 로직이 없어도 이벤트 루프가 정상 동작하면 200 응답을, 이벤트 루프가 막히거나 애플리케이션이 헝(Hung) 상태이면 응답 타임아웃 으로 실패를 감지할 수 있습니다. 예컨대, CPU를 많이 쓰는 동기 작업으로 이벤트 루프가 30초간 막혀있다면 그동안 /livez 요청에 응답하지 못해 kubelet의 livenessProbe 타임아웃에 걸릴 것이고, 결국 Kubernetes는 해당 컨테이너를 죽였다가 재시작 할 것입니다.

    이러한 특성 때문에 /livez가급적 빠르고 가벼워야 하며, 특별한 체크 없이도 응답 자체가 하나의 지표가 됩니다. 다만 Node.js의 가비지 컬렉션 일시 중단이나 이벤트 루프 일시 지연 등으로 간헐적 응답 지연이 발생할 수 있으므로, livenessProbe의 timeoutSecondsfailureThreshold를 너무 빡빡하게 주지 않는 것 이 안전합니다. 예를 들어 1초 응답 타임아웃에 1회 실패만으로 바로 재시작하게 하지 말고, 몇 초의 타임아웃과 여러 번 재시도 여유를 두면 일시적 스파이크로 인한 오동작을 피할 수 있습니다. 또한 애플리케이션이 SIGTERM종료 신호를 받았을 때 는, /liveztrue를 유지하도록 하여 정상 종료 절차가 진행 중임을 kubelet에 알리는 것이 좋습니다. ReadinessProbe는 이 순간 false로 전환되어 트래픽은 차단하되, LivenessProbe는 여전히 성공을 보고해야 kubelet이 terminationGracePeriod 동안 컨테이너를 살려두어 Graceful Shutdown이 가능합니다. NestJS Terminus는 enableShutdownHooks 설정 시 이러한 동작을 일부 자동화해주며, 수동으로 구현한다면 애플리케이션이 SIGTERM을 받으면 Readiness 플래그를 false, Liveness 플래그는 그대로 두고, Shutdown 완료 직전에만 Liveness를 false로 바꾸는 식으로 제어할 수 있습니다.

요약하면, Kubernetes 헬스 체크를 위한 엔드포인트는 각 Probe의 목적에 맞게 분리 해서 설계하는 것이 권장됩니다. Terminus와 같은 라이브러리를 쓰지 않고도 NestJS 애플리케이션에서 간단히 Controller를 만들어 세 경로를 만들어도 되지만, Terminus를 활용하면 DB, 메모리 등 공통 건강상태 점검을 쉽게 추가할 수 있습니다. 중요한 것은 /startupz/readyz/livez 각 엔드포인트가 언제 200을 주고 언제 500을 줘야 하는지 명확한 기준을 세우는 것 입니다. 그리고 그 기준들은 앞서 설명한 프로브의 동작과 NestJS 부팅 시점에 맞춰져야 합니다. Kubernetes 환경에서 “헬스 체크” 란 곧 특정 HTTP 엔드포인트에 대한 주기적인 GET 요청 이며, 이 요청의 HTTP Status가 곧 그 컨테이너의 상태 신호 라는 점을 기억해야 합니다. 따라서 헬스 체크 엔드포인트는 가능한 한 부작용이 없고 단순하며, 빠르게 응답하도록 만들어야 Kubernetes와 원활하게 상호작용할 수 있습니다.


4. Startup Probe의 동작 방식 – 초기화 동안 다른 프로브의 중단

이제 startupProbe에 대해 좀 더 깊이 알아보겠습니다. 앞서 간략히 언급했듯, startupProbe애플리케이션의 “초기 기동”이 완전히 끝날 때까지 livenessProbereadinessProbe일시 정지 시키는 역할을 합니다. 이 메커니즘의 의의와 활용 방법, 그리고 설정 시 주의사항을 자세히 살펴보죠.

왜 startupProbe가 도입되었을까요? 기존에는 readinessProbelivenessProbe만으로 모든 상황을 처리했습니다. 그러다 보니, 기동 시간이 긴 애플리케이션 에 대해서는 livenessProbeinitialDelaySeconds를 임의로 아주 길게 설정하는 경우가 많았습니다. 예컨대 초기 로딩에 3분 걸리는 서비스라면 initialDelaySeconds를 180으로 주는 식입니다. 하지만 이렇게 하면 단점이 있습니다. 만약 컨테이너가 진짜 데드락이나 치명적 오류로 이상 상태에 빠져도, initialDelaySeconds 동안에는 kubelet이 눈치를 채지 못해 재시작이 지연 된다는 것입니다. 반대로 initialDelaySeconds를 짧게 주면, 아직 멀쩡히 초기화 진행 중인 컨테이너를 kubelet이 오인해서 죽일 위험이 있습니다. 이 딜레마를 해소하기 위해 나온 것이 startupProbe입니다. Kubernetes 공식 문서의 표현을 빌리자면: “첫 초기화에 추가 시간이 필요한 애플리케이션들을 다룰 때, livenessProbe 파라미터를 설정하기 까다로울 수 있다… 해결책은 startupProbe를 설정 하여, failureThreshold × periodSeconds 만큼 충분히 긴 시간을 주고 애플리케이션이 최악의 경우에도 기동을 마칠 수 있게 보장하는 것이다”.

startupProbe의 동작 원리는 간단합니다. 동일한 HTTP 체크나 명령어를 사용하지만, periodSecondsfailureThreshold를 곱한 시간만큼 애플리케이션이 일어날 시간을 줍니다. 그 시간 내에 startupProbe 엔드포인트가 성공 응답을 하면 성공 으로 간주하고, 이제 이후로는 livenessProbe가 바톤을 이어받아 정상적인 헬스 체크를 수행합니다. 이때부터는 livenessProbe가 더 짧은 주기로 동작하며, 응답이 없거나 실패하면 재시작을 트리거하여 데드락 등에 빠른 대응 을 합니다. 반대로 startupProbe 시간 내내 성공을 못 하면 (예: failureThreshold=30, periodSeconds=10인 경우 300초 동안 실패), 애플리케이션이 결국 뜨지 못한 것으로 보고 kubelet이 컨테이너를 강제 종료합니다. 이때 Pod의 restartPolicy 에 따라 재시작을 시도할 것이고 (일반적으로 Always이므로 재시작), 이 패턴이 반복되면 Kubernetes는 해당 Pod를 CrashLoopBackOff 상태로 표시합니다.

중요한 점은, startupProbe가 동작하는 동안에는 livenessProbereadinessProbe카운트다운(초기 딜레이 등)이 시작되지 않는다 는 것입니다. 예를 들어 livenessProbeinitialDelaySeconds=30을 주었어도, startupProbe가 설정되면 먼저 startupProbe가 실행되고 성공한 이후에야 livenessProbe의 InitialDelay 타이머가 흘러가기 시작합니다. 즉, startupProbe로 보호받는 동안에는 다른 프로브들은 아예 신경 쓰지 않는다고 보면 됩니다. 이 격리 기간 덕분에, 우리는 startupProbe최대 예상 부팅시간 을 넉넉히 반영하더라도, 일단 부팅이 끝난 뒤엔 곧바로 Liveness/Readiness 체크를 정상화할 수 있습니다 (initialDelaySeconds를 과하게 길게 주는 것보다 유연). 그리고 나면 livenessProbe는 작은 주기로 돌면서 앱을 지속 감시하고, readinessProbe는 필요 시 Pod의 트래픽을 통제하는 역할로 돌아갑니다.

실무 적용 팁: NestJS와 같이 초기화에 시간이 좀 걸리는 앱이라면 (DB 마이그레이션, 초기 캐싱 등), startupProbe를 활용 하는 편이 좋습니다. 먼저 /startupz 엔드포인트를 만들어 startupProbe로 지정하고, 여기에 failureThreshold×periodSeconds를 여유 있게 줍니다. Kubernetes 권고는 “최악의 초기화 시간” 을 충분히 커버하라는 것인데, 예를 들어 최악의 경우 3분이면 5분으로 준다 등 보수적으로 잡습니다. 그런 다음 livenessProbe에는 initialDelaySeconds를 짧게 잡을 수 있습니다(왜냐하면 startupProbe 덕분에 부팅 중엔 호출되지 않을 테니까요). 이렇게 하면 부팅 직후 초기 대기 시간런타임 중 헬스 모니터링 주기 를 양립할 수 있습니다. 반대로 startupProbe를 사용하지 않으면, livenessProbeinitialDelaySeconds를 길게 잡을 수밖에 없고, 그동안 실제로 컨테이너가 죽어있어도 Kubernetes는 모르게 됩니다. 특히 초기화가 매우 불규칙하거나 오래 걸리는 애플리케이션에는 startupProbe 설정이 거의 필수적입니다.

마지막으로, startupProbe 자체가 실패하는 상황 도 고려해야 합니다. 만약 애플리케이션이 비정상 종료되거나 영영 응답을 못해서 startupProbe마저 실패한다면, kubelet은 컨테이너를 죽이고 재시작하게 됩니다. 이때 무한 재시작 루프 에 빠지지 않도록 주의가 필요한데, Kubernetes는 기본적으로 몇 차례 빠른 재시작 끝에 CrashLoopBackOff로 전환하면서 재시작 간격을 늘립니다. 따라서 startupProbe 실패가 반복된다면 근본적으로는 애플리케이션 코드를 점검하거나 리소스 부족 등을 해결해야겠지만, Kubernetes 레벨에서는 이러한 상태를 인지하고 롤아웃을 중단하거나 이벤트로 경고를 남기므로 운영자는 빨리 원인을 파악할 수 있습니다.


5. Kubernetes 롤아웃과 Readiness – 배포 성공 판정은 어떻게 내려지나

Kubernetes의 Deployment를 사용해 애플리케이션을 배포(롤아웃)할 때, readinessProbe는 배포 성공 여부를 판단하는 핵심 기준 이 됩니다. kubectl rollout status로 배포 상태를 지켜보다가 새로운 Pod들이 생성되긴 했는데 여전히 “x out of y new replicas have been updated…”라는 메시지만 나오고 완료가 안 되는 경험을 할 수 있습니다. 원인은 새로 띄운 Pod의 readinessProbe가 통과되지 않아 Kubernetes가 해당 Pod들을 정상 가용(Available) 상태로 간주하지 않았기 때문입니다. 이처럼 Deployment 컨트롤러는 새 ReplicaSet의 Pod들이 모두 Ready 상태가 되는 것 을 롤아웃 완료의 조건으로 삼습니다.

구체적으로, Deployment의 롤링 업데이트(Rolling Update) 는 다음과 같이 진행됩니다:

  1. 새로운 Pod 생성: 새로운 버전의 Pod를 하나 생성합니다 (또는 maxSurge 만큼 여러 개 생성). 이 새 Pod는 처음에는 Ready 상태가 아닙니다. Kubernetes는 이 Pod의 readinessProbe가 성공하기를 기다립니다. readinessProbe가 주기적으로 실행되어 200을 반환하면, kubelet은 해당 컨테이너를 Ready 상태로 표시하고 Deployment는 이를 감지합니다.

  2. 기존 Pod 축소: 새 Pod가 Ready가 되면, Deployment는 기존의 오래된 버전 Pod를 하나 종료 시킵니다. 이는 maxUnavailable 설정에 따라 한 번에 몇 개씩 종료할지 결정되지만, 기본값의 경우 보통 하나씩 줄여나갑니다. 중요한 점: 새로운 Pod가 Ready가 되기 전에는, 설정된 maxUnavailable 범위를 벗어나지 않는 한 기존 Pod를 제거하지 않으므로 서비스 가용성이 유지 됩니다. (maxUnavailable=0인 경우, 항상 기존 Pod는 새 Pod가 완전히 대체할 준비가 되기 전까지 건드리지 않음 을 의미합니다.)

  3. 새 Pod 추가 및 반복: Deployment는 원하는 레플리카 수 만큼 새 Pod가 준비되고 기존 Pod를 제거하는 과정을 반복합니다. 한 개 추가 → Ready 될 때까지 대기 → 한 개 제거 → 다음 추가… 이런 식으로 순차적으로 이루어집니다. 이 과정에서 maxSurgemaxUnavailable 제약을 지키며, Pod들의 가용 개수(Ready 상태인 Pod 수) 가 항상 replicas - maxUnavailable 이상으로 유지되도록 조정합니다. 예컨대 replicas=3, maxUnavailable=0이라면 항상 3개가 Ready 상태(새 버전+기존 버전 합쳐서)인 걸 보장하면서 업데이트가 진행됩니다.

  4. 롤아웃 완료 판정: 새로운 ReplicaSet의 Pod가 모두 Ready 상태가 되어 Desired Replicas 개수만큼 채워지고, 기존 ReplicaSet의 Pod는 모두 삭제되면 Deployment는 업데이트 완료 로 간주됩니다. kubectl rollout status는 이 시점을 “rollout complete” 이라고 보고하죠. Deployment 오브젝트의 .status.conditions에도 Progressing=True (Complete), Available=True 등의 조건이 기록되어 배포 완료를 나타냅니다. 반대로, 일정 시간 내에 새 Pod들이 Ready 상태에 도달하지 못하면 (기본 600초, progressDeadlineSeconds로 설정 가능) Deployment는 ProgressDeadlineExceeded 상태로 롤아웃 실패 를 기록합니다. 이를테면 이미지 풀 실패나 프로브 실패로 새 Pod가 계속 Ready가 안 되면, Kubernetes는 배포가 지연되고 있음을 인지하여 이벤트를 남기고 kubectl rollout status에 경고를 표시합니다.

위 설명에서 알 수 있듯, readinessProbe는 새로운 Pod가 서비스에 합류할 준비가 되었는지 를 Deployment에게 알려주는 트리거 역할입니다. readinessProbe가 성공해야만 Pod가 availableReplicas 로 집계되고, Deployment는 다음 단계 (기존 Pod 종료 등)를 진행합니다. 따라서 readinessProbe 설정이 잘못되면 롤아웃이 순조롭게 진행되지 않습니다. 예를 들어 readinessProbe 경로가 잘못되었거나 기준이 과도하게 엄격하면, 새로 띄운 Pod들이 영원히 Ready가 안 되어 Deployment 롤아웃이 영구히 Pending 상태가 되거나 결국 타임아웃에 걸릴 수 있습니다. 이 경우 Kubernetes는 배포를 중단하거나 실패 처리 하므로, 서비스 업데이트에 차질이 생기게 됩니다.

한편, Deployment의 maxUnavailablemaxSurge 설정 역시 롤아웃 중 Pod의 Ready 상태와 밀접한 관계가 있습니다. 기본값인 maxUnavailable=25%, maxSurge=25%는 작은 수의 Replicas에 대해 적절히 반올림되어 적용됩니다. 예를 들어 replicas=2인 Deployment에서 기본 설정을 그대로 쓰면:

  • 25% of 2 = 0.5이므로 maxUnavailable = 0개 (반올림 내려서)
  • 25% of 2 = 0.5이므로 maxSurge = 1개 (반올림 올려서) 가 됩니다.

결국 Pod 2개짜리 서비스는 기본 설정에서 항상 2개를 가용 한 채로 업데이트하며, 업그레이드 중 일시적으로 한 개를 추가로 띄워 최대 3개까지 운영할 수 있다는 의미입니다. Deployment는 이를 지키기 위해, 새 버전 Pod를 하나 띄워 Ready 될 때까지 기다렸다가 기존 버전 Pod 하나를 내리는 식으로 움직입니다. 만약 replicas=2에 대해 무중단(Zero-downtime) 배포 를 절대적으로 보장하려면 maxUnavailable=0으로 두면 되고, 이때 maxSurge는 1 이상이어야 합니다. 반대로 리소스가 부족해 동시에 한 개 이상 띄우기 힘들다면 maxSurge=0으로 설정하고 대신 약간의 중단을 감수할 수도 있습니다 (이때 maxUnavailable은 1 이상이어야 합니다). 다음은 replicas=2 상황에서 여러 전략 예시입니다.

  • 전형적 무중단 배포: maxUnavailable: 0, maxSurge: 1 – 새로운 Pod를 하나 추가 가동하여 (2 3개) 새 Pod가 Ready 되면, 기존 Pod 하나를 제거 (3 2개)합니다. 이 과정을 반복하여 교체를 완료하는 동안 항상 2개의 Pod가 Ready 상태를 유지하므로 서비스 중단이 발생하지 않습니다. 다만 한 시점에 최대 3개 Pod의 리소스를 소비하므로 평소보다 자원 사용률이 높아질 수 있습니다.

  • 자원 절약 배포: maxUnavailable: 1, maxSurge: 0 – 동시에 추가 Pod는 없이 기존 Pod를 내려가며 교체합니다. 먼저 기존 Pod 하나를 종료 (2 1개로 감소)한 뒤 새 버전 Pod를 띄워 (다시 2개) Ready 되기를 기다립니다. 이 동안 서비스 용량이 일시적으로 절반으로 줄어들 수 있어 일부 트래픽 손실 위험 이 있지만, 동시에 두 배 이상의 리소스를 쓰지 않고도 배포할 수 있습니다. 작은 시스템이나 개발 환경 등에서 가용성을 조금 희생하고 자원을 아껴야 할 때 선택할 수 있습니다.

  • 혼합 전략: maxUnavailable: 1, maxSurge: 1 – 새 Pod도 추가로 띄우고, 어느 정도의 비가용도 허용합니다. 이 설정에서는 컨트롤러가 상황에 따라 유연하게 동작할 수 있습니다. 기본적으로는 Surge를 활용해 새 Pod를 미리 띄우지만, 필요하면 한 개의 기존 Pod는 Ready 여부와 상관없이 내려도 허용됩니다 (항상 2개 중 1개는 유지). 이런 전략은 버전에 따라 빠르게 교체하되, 한순간 최소 1개 Pod는 서비스하도록 하려는 경우 쓰입니다. 최대 Pod 수는 3개(2+1)까지 늘 수 있고, 최소 가용 Pod 수는 1개(2-1)까지 내려갈 수 있습니다.

요약하면, Deployment 롤링 업데이트는 Pod의 Ready 상태를 기준으로 안전하게 진행 됩니다. Kubernetes는 항상 (replicas - maxUnavailable)개 이상의 Pod가 Ready 상태 로 남도록 조정하며, 새로 띄운 Pod가 Ready 되기 전에 기존 Pod를 너무 내려버리지 않습니다. 덕분에 사용자는 maxUnavailablemaxSurge만 적절히 조절하면 업데이트 중 서비스 중단 시간을 최소화 할 수 있습니다. 이때 readinessProbe가 제대로 작동하는지 가 무엇보다 중요합니다. readinessProbe는 새로운 Pod의 가용성을 Kubernetes에 알려주는 유일한 신호이므로, 서비스 코드나 설정 오류로 readinessProbe가 항상 실패한다면 Pod는 영원히 서비스에 투입되지 못하고, 롤아웃은 정지될 것입니다. 반대로 readinessProbe가 너무 이르게 성공(true) 상태를 주면 (예: 앱은 실제 준비 안 되었는데 성공 응답) Pod가 트래픽을 받아 오류를 발생시킬 수도 있습니다. 따라서 readinessProbe의 구현과 기준을 신중히 정하고, 필요하다면 Deployment의 progressDeadlineSeconds (기본 600초)도 조정하여 우리 서비스의 초기 구동 시간에 맞춰주는 것이 좋습니다.


6. 롤링 업데이트 전략: maxUnavailable과 maxSurge 완전 정복 (replica=2 예시)

앞 절에서 이미 간략히 다루었지만, maxUnavailable과 maxSurge 는 Kubernetes RollingUpdate 전략 의 핵심 매개변수입니다. 이를 정확히 이해하는 것은 헬스 체크 설정만큼이나 중요합니다. 왜냐하면 프로브 설정이 Readiness 시그널을 보내는 역할이라면, maxUnavailable/maxSurge 설정은 그 시그널을 활용하여 얼마나 공격적(혹은 보수적)으로 Pod 교체를 진행할지 를 결정하기 때문입니다.

공식 문서를 보면,

  • maxUnavailable업데이트 과정 중 동시에 몇 개의 Pod까지 서비스 불가해져도 괜찮은지 를 나타내며, 절대 숫자 또는 백분율 로 지정합니다. 백분율로 지정하면 Replicas에 대한 비율을 내림(Round Down)하여 사용하고, 만약 maxSurge를 0으로 설정한 경우 maxUnavailable는 0이 될 수 없습니다 (동시에 0개도 못 줄이면서 0개도 못 늘리면 롤링업데이트가 불가능하므로). 기본값은 25%로, 예를 들어 replicas=4라면 25% of 4 = 1 Pod (내림)으로 계산되어 한 번에 1개 Pod까지는 빠져도 된다 는 의미가 됩니다.

  • maxSurge원하는 Replicas 수를 초과하여 몇 개의 Pod까지 추가 생성할 수 있는지 를 나타내며, 역시 절대값 또는 백분율로 설정합니다. 백분율일 경우 올림(Round Up)하여 계산하며, 기본값은 25%입니다. 마찬가지로 maxUnavailable=0이면 maxSurge도 0일 수 없습니다 (둘 다 0 금지).

이 두 값의 설정에 따라 롤링 업데이트의 동작은 크게 달라집니다. 대표적인 시나리오를 replicas=2 인 Deployment로 살펴보겠습니다.

  • Scenario A: 무중단 업그레이드maxUnavailable=0, maxSurge=1인 경우입니다.

    • 의미: 업데이트 중 서비스 비가용(불완전 가동) 인 Pod는 0개 여야 하며, 대신 필요하면 원하는 Replicas(2개)보다 최대 1개 더 많이 Pod를 띄울 수 있습니다.
    • 동작: 새로운 버전 Pod를 1개 생성하여 (총 3개 가동) Ready 될 때까지 기다립니다. 이 시점에서 3개 중 2개(기존 2개)는 계속 서비스 중이므로 가용 Pod 2개 확보. 새 Pod가 Ready되면, 이제 maxUnavailable=0 규칙 하에서도 여전히 32로 줄여도 2개가 Ready (문제 없음)이므로, 기존 Pod 하나를 종료 합니다. 결과적으로 새 버전 1 + 기존 버전 1 = 2개가 남고 모두 Ready 상태입니다. 다음으로 남은 기존 Pod를 교체하기 위해 동일한 과정을 반복합니다 (새 Pod 생성 Ready 대기 기존 Pod 제거). 이 동안 항상 2개의 Pod가 Ready 상태였으므로 트래픽 중단은 발생하지 않았습니다. 대신 한때 Pod가 3개까지 늘어났었죠. 이 추가분 1개의 Pod이 바로 Surge입니다.
  • Scenario B: 자원 고정 업그레이드maxUnavailable=1, maxSurge=0인 경우입니다.

    • 의미: 동시에 최대 1개 Pod까지 비가용 될 수 있으며, 추가 Pod는 생성하지 않음 (항상 Desired 개수까지만).
    • 동작: maxSurge=0이므로 새 Pod를 만들려면 기존 Pod 하나를 제거해야 합니다. Deployment는 먼저 기존 Pod 1개를 종료 하여 (2 1개) 비워진 자리에 새 버전 Pod를 생성합니다. 이때 이미 1개 Pod는 내려갔으므로 현재 가용 Pod 수는 1개뿐입니다 (일시적인 용량 감소). 새로 띄운 Pod가 Ready 될 때까지 서비스는 1개의 Pod로 버텨야 합니다. 새 Pod가 Ready되면, 다시 두 번째 기존 Pod를 동일하게 교체합니다. 이 시나리오에서는 한동안 서비스 인스턴스가 1개로 줄어드는 상황이 발생하므로, 일부 요청이 과부하로 실패 하거나 딜레이 가 생길 위험이 있습니다. 하지만 동시에 추가 리소스를 사용하지 않으므로 노드 리소스가 빡빡한 경우 적용할 수 있는 전략입니다.
  • Scenario C: 하이브리드 업그레이드maxUnavailable=1, maxSurge=1인 경우입니다.

    • 의미: 최대 1개 Pod 비가용 허용 + 최대 1개 Pod 추가 가동 허용 입니다.
    • 동작: 컨트롤러는 새 Pod를 먼저 생성 하는 경향이 있습니다 (Surge 가능하므로). 새 Pod가 뜨면 3개가 되고, Ready 여부와 상관없이 어차피 maxUnavailable 1이 허용되므로 동시에 기존 Pod도 1개 줄일 수 있습니다. 다만 일반적으로 Kubernetes는 안전하게 진행하기 때문에, 먼저 새 Pod가 Ready되면 기존 Pod 제거하는 순서를 지킬 것입니다. 이 설정의 효과는, 동시에 업 도 하고 다운 도 어느 정도 할 수 있는 유연성 입니다. 한순간 Pod 개수는 3개까지 늘 수 있고, Ready한 Pod가 일시적으로 1개까지 줄 수도 있습니다만 (만약 새 Pod 생성 후 Ready 되기 전에 기존 Pod를 제거한다면), 최악의 상황을 1개 가용으로 버틸 수 있다고 시스템이 허용한 것입니다. 흔히 이는 트래픽에 약간 영향이 가더라도 빠르게 새 버전을 배포하고자 할 때 쓰이곤 합니다.

위 세 가지 시나리오는 replicas=2인 작은 서비스 예시지만, 원리는 Replicas가 큰 경우에도 동일합니다. maxUnavailable는 최소 가용성을 보장 하고, maxSurge는 추가 리소스를 투입해 가용성을 높이는 역할 을 합니다. 두 값을 어떻게 조합하느냐에 따라 업데이트 속도 vs. 서비스 안정성 의 트레이드오프가 결정됩니다. 안정성을 최우선으로 하면 maxUnavailable=0으로 두고, 롤아웃 시간을 다소 희생하거나 추가 리소스를 써서라도 항상 풀 가용 상태를 유지합니다. 반면 서비스 중 일시적 영향이 허용된다면 maxUnavailable를 높게 두어 동시에 여러 Pod를 교체함으로써 배포 속도를 빠르게 가져갈 수도 있습니다. Kubernetes 기본값 25%/25%는 대부분 서비스에 무난한 설정으로, 예를 들어 10개 중 3개까지 한 번에 교체(+3 Surge)하며, 최소 7개(70%)는 항상 살아있도록 하는 균형 잡힌 접근입니다.

마지막으로, maxUnavailablemaxSurge 설정은 StatefulSet 이나 DaemonSet 등의 다른 리소스에도 유사한 개념이 적용되지만(혹은 최근 지원 추가), Deployment만큼 유연하지 않을 수 있습니다. 예컨대 StatefulSet은 2023년까지는 Surge 업데이트를 지원하지 않았고 순차 업데이트만 가능했지만, 최신 버전에 MaxUnavailable 기능이 알파로 도입되기도 했습니다. 여기서는 주로 Deployment 맥락에서 이야기했지만, 애플리케이션 유형에 따라 업데이트 전략을 조정 하는 것도 고려해야 합니다. 데이터 정합성이 중요한 Stateful 서비스라면 무중단보다는 순차적 안정성 이 중요할 수 있으니 maxUnavailable=1, maxSurge=0 같은 전략이 기본일 것이고, Stateless 웹 서버라면 maxSurge를 활용한 무중단 배포가 선호될 것입니다.


7. Kubernetes Probe 설정 옵션 총정리 (httpGet, periodSeconds 등)

이제 Kubernetes 프로브(Probe)의 세부 설정 옵션들을 하나씩 짚어보겠습니다. Official Kubernetes 문서에는 프로브의 동작을 세밀하게 조정할 수 있는 여러 필드 가 정의되어 있습니다. livenessProbe, readinessProbe, startupProbe 공통으로 사용하는 필드들부터 HTTP 프로브 전용 필드 까지 차례로 살펴보겠습니다. (Exec 프로브나 TCP 소켓 프로브도 존재하지만, 여기서는 HTTP 기반으로 엔드포인트를 만들었다는 전제하에 httpGet 설정을 중심으로 설명합니다.)

  • initialDelaySeconds – 컨테이너가 시작되고 나서 프로브를 처음 수행하기 전까지 대기하는 시간(초) 입니다. 기본값은 0초이며, 최소 0초로 설정 가능합니다. 이 값은 전통적으로 “애플리케이션 기동 후 프로브 체크까지 유예시간” 으로 쓰였는데, startupProbe가 설정된 경우 에는 상황이 조금 달라집니다. Kubernetes 문서에 따르면 “startupProbe가 정의되어 있으면, 해당 컨테이너에 대해서는 startupProbe가 성공하기 전까지 liveness와 readiness 프로브의 initialDelay 카운트다운이 시작되지 않는다” 고 명시되어 있습니다. 즉, startupProbe가 없을 때만 initialDelaySeconds가 의미를 갖고, startupProbe가 있으면 먼저 startupProbe가 실행되는 동안 Liveness/Readiness는 아예 쉰다는 뜻입니다. 이 필드를 적절히 사용하여, 애플리케이션이 기동하자마자 바로 프로브를 실행해 불필요한 실패로 간주되는 일을 막을 수 있습니다. 예를 들어 startupProbe를 쓰지 않고 readinessProbe만 쓰는 경우, initialDelaySeconds를 애플리케이션 평균 부팅 시간에 맞춰 주어야 첫 번째 readinessProbe 시도가 실패하지 않을 것입니다.

  • periodSeconds프로브를 주기적으로 수행하는 간격(초) 입니다. 기본값은 10초이며, 최소 1초까지 설정 가능합니다. 이 값은 한 번 프로브를 하고 다음 프로브를 몇 초 후에 할지 를 결정합니다. 다만, 공식 문서에 흥미로운 문구가 있습니다: “컨테이너가 아직 Ready 상태가 아닐 때는, ReadinessProbe가 설정한 주기 이외의 시간에도 실행될 수 있다. 이는 Pod를 더 빨리 Ready 상태로 만들기 위함이다.”. 이 문구는 kubelet이 readinessProbe에 한해, Pod이 계속 Unready인 경우 주기적으로만 기다리지 않고 조금 더 빈번히 체크할 수도 있다는 의미로 해석됩니다 (내부 최적화인 듯합니다). 일반적으로는 periodSeconds 주기로 이해하면 되고, readinessProbe의 경우 빠른 Ready 전환을 위해 kubelet이 약간의 헤징을 할 수 있음을 알아둡니다. 한편, periodSeconds를 너무 짧게 주면 kubelet 및 애플리케이션에 프로브 트래픽 부하 가 커질 수 있으니, 평소에는 5~10초 정도가 권장됩니다.

  • timeoutSeconds – 프로브 타임아웃 시간(초) 입니다. 기본값은 1초이며 최소 1초로 설정 가능합니다. kubelet이 프로브를 실행하고 (HTTP 요청 보내거나, 명령 실행 등) 이 timeout 내에 성공/실패 여부를 판단하지 못하면 그 프로브는 실패로 간주합니다. HTTP의 경우 이 시간 내에 HTTP 응답 헤더를 받지 못하면 타임아웃 처리되고, Exec의 경우 해당 프로세스가 타임아웃 내에 종료되지 않으면 실패 처리됩니다. timeoutSeconds는 이벤트 루프나 애플리케이션 지연과 관련이 깊으므로, Node.js 앱이라면 1초는 다소 빡빡할 수 있습니다. 특히 livenessProbetimeoutSeconds너무 작게 잡으면 약간의 GC 정지나 일시적 부하에도 바로 실패 판정할 수 있으니, 실무에서는 2~5초 정도로 여유를 주는 경우가 많습니다. (성능 민감 애플리케이션이고 응답속도가 항상 빨라야 한다면 1초로 하기도 하지만, 이는 상황에 따라 다릅니다.)

  • successThreshold프로브 성공 판정에 필요한 연속 성공 횟수 입니다. 기본값은 1이며, 최소 1 이상으로 설정 가능합니다. 이 값은 주로 readinessProbe에 사용 됩니다. livenessProbestartupProbe의 경우 항상 1로 고정 되어야 하며, Kubernetes에서 이를 강제하고 있습니다. successThreshold > 1로 설정해도 Liveness/Startup에는 적용되지 않는다고 보면 됩니다. readinessProbe에서 successThreshold를 2 이상으로 주면, 여러 번 연속으로 성공해야 진짜 Ready로 인정 하게 됩니다. 예컨대 successThreshold=3, periodSeconds=5라면, 15초 동안 3번 연속 성공해야 Pod가 Ready 상태로 바뀝니다. 이를 통해 초기에는 간헐적으로 성공/실패를 오가는 애매한 상태일 때 곧바로 Ready로 오인되지 않게 할 수 있습니다. 다만, 대부분 서비스에서는 successThreshold=1로 두는 것이 일반적입니다.

  • failureThreshold프로브 실패 판정에 필요한 연속 실패 횟수 입니다. 기본값은 3이며, 최소 1 이상 설정 가능합니다. 이 값의 의미는 프로브가 연속으로 몇 번 실패해야 kubelet이 “전체적으로 이 컨테이너가 Unhealthy/Not Ready 하다” 고 간주할지를 결정합니다. 적용 맥락에 따라 동작이 약간 다른데:

    • livenessProbe / startupProbe: failureThreshold만큼 연속 실패하면 컨테이너를 Unhealthy로 보고 재시작 합니다. 예를 들어 livenessProbefailureThreshold=3, period=10초라면, 3번 연속 (30초 간) 실패해야 kubelet이 kubectl describe pod 이벤트 등에 Liveness 실패를 기록하고 컨테이너를 죽입니다. startupProbe도 마찬가지로 failureThreshold 내 연속 실패하면 (아직 한 번도 성공 못한 상태로) 컨테이너를 죽입니다. 이때 terminationGracePeriodSeconds 설정에 따른 Grace Time을 준 후 종료시킵니다.
    • readinessProbe: Readiness의 failureThresholdPod를 NotReady로 내리기까지 허용하는 ‘연속 실패 횟수’ 를 의미합니다. 공식 문서 기준으로, 프로브가 failureThreshold번 연속 실패하면 kubelet은 해당 체크가 실패했다고 판단하고 Pod의 Ready condition을 false로 설정 합니다. 이후에도 컨테이너는 계속 실행 되며, kubelet은 readiness probe를 계속 수행 하고, 성공 조건을 만족하면 다시 Ready로 전환됩니다. 따라서 failureThreshold=3이면 “실패 1번에 즉시 NotReady”가 아니라, 3번 연속 실패 시점 에 NotReady로 내려갑니다(= 일시적인 1~2회 실패를 무시하여 endpoint flapping을 줄이는 효과). 반대로 failureThreshold=1이면 한 번의 실패에도 즉시 NotReady 로 내려가므로, “진짜로 준비가 안 된 Pod를 빨리 빼야 하는” 서비스에서는 차단 속도가 빨라지는 장점이 있습니다. 정리하면, Readiness의 failureThreshold(1) Flapping 억제(완충) vs (2) 빠른 차단(민감도) 사이의 트레이드오프를 조절하는 레버입니다. 그리고 Liveness/Startup과의 차이는, failureThreshold에 도달했을 때 Liveness/Startup은 재시작을 트리거 하지만, Readiness는 재시작하지 않고 Ready=false(트래픽 제외)만 수행 한다는 점입니다.
  • terminationGracePeriodSeconds프로브 실패로 컨테이너를 종료할 때 줄 유예 시간(초) 을 별도로 지정합니다. 기본값은 Pod의 terminationGracePeriodSeconds 값을 따르며 (통상 30초), 최소 1초 이상 설정 가능합니다. Kubernetes 1.21부터 Probe 단위로 이 Termination Grace 설정을 할 수 있게 되어, livenessProbe로 죽일 때만 특별히 빨리/천천히 종료 를 유도할 수 있습니다. 예를 들어 livenessProbe 실패 시 더 빨리 재시작하고 싶다면 이 값을 5초로 낮춰서, kubelet이 컨테이너 종료 신호(SIGTERM)를 보낸 후 5초만 기다리도록 할 수 있습니다. 기본 30초를 기다리면 재시작에 최대 30초가 더 걸리므로, 애플리케이션 특성상 빨리 죽이는 게 낫다면 이 값을 쓰는 것입니다. 반대로 정리 작업이 꼭 필요해 30초도 부족하다면 늘릴 수도 있겠습니다. readinessProbe 실패에는 해당 사항이 없고, 오직 liveness/startupProbe로 컨테이너를 종료할 때만 의미가 있습니다.

위의 필드들은 프로브 행동의 주기와 조건 을 다루는 설정입니다. 정리하면: initialDelaySeconds 로 첫 체크 시점을 미루고, periodSeconds 로 체크 간격을 정하며, timeoutSeconds 로 각 체크 시한을, successThresholdfailureThreshold 로 판정에 필요한 연속 성공/실패 횟수를 정합니다. terminationGracePeriodSeconds는 Liveness용 추가 옵션이고요. 이러한 옵션을 조합하여, 애플리케이션 특성에 맞게 프로브 민감도를 조절 할 수 있습니다. 예를 들어 “초기에는 1분 여유를 주고, 이후 5초마다 체크하되, 3번 연속 실패하면 재시작” 같은 정책을 자유롭게 구성할 수 있다는 뜻입니다.

이제 HTTP Probe 전용 설정 을 보겠습니다. NestJS처럼 HTTP 서버로 Health Endpoint를 구축한 경우에는 httpGet 방식의 프로브를 쓰게 되는데, 이에 관련된 세부 필드들이 있습니다.

  • path – HTTP GET 요청을 보낼 경로 입니다. 기본값은 ”/“이며, 우리가 필요한 엔드포인트 경로(/livez 등)를 지정하면 됩니다. 다중 프로브를 쓸 때 Liveness와 Readiness 경로를 구분해야 한다면 각기 다르게 path를 설정하면 됩니다.
  • port – 체크에 사용할 포트 번호 또는 이름입니다. 통상 컨테이너가 리스닝하는 포트를 숫자로 지정하거나, Pod 명세에 포트에 이름이 있으면 그 이름으로도 지정할 수 있습니다. 예를 들어 NestJS 앱이 3000번 포트로 띄워지면 port: 3000으로 명시합니다. 참고로 만약 Pod에서 hostNetwork를 쓰거나 특수한 경우, host 필드도 설정할 수 있는데, 보통은 설정하지 않고 기본값 (Pod IP)을 사용합니다.
  • hostHTTP 요청을 보낼 호스트명 을 바꿀 때 씁니다. 기본값은 빈 값으로, kubelet이 알아서 Pod의 IP로 요청을 보냅니다. 대부분 이 필드는 필요 없는데, 예를 들어 컨테이너가 127.0.0.1에서만 리스닝하고 hostNetwork=true인 경우 host: 127.0.0.1로 설정해야 하는 등의 특수 상황에만 사용합니다. 일반적인 Kubernetes Service 구성에서는 사용하지 않습니다.
  • scheme – HTTP 또는 HTTPS 중 어떤 스킴 으로 호출할지 결정합니다. 기본값은 HTTP이며, 만약 헬스 체크 엔드포인트가 HTTPS로 보호되어 있다면 HTTPS로 설정해야 합니다. 다만 kubelet이 인증서 검증은 생략하고 요청하므로 (Self-signed도 OK), HTTPS를 써도 검증 문제는 없습니다. 대부분의 경우 Health Endpoint는 내부 호출용이므로 HTTP로 두는 편입니다.
  • httpHeaders – 요청에 커스텀 HTTP 헤더 를 넣을 수 있습니다. 거의 쓰일 일은 없지만, 간혹 어플리케이션이 특정 헤더를 요구한다면 추가 가능합니다. 여러 개 헤더도 넣을 수 있습니다.

HTTP Probe 이외에도 TCP 소켓 프로브(tcpSocket)Exec 프로브(exec), 그리고 gRPC 프로브(1.27+ 안정화)도 있습니다. TCP 프로브는 단순히 해당 포트에 TCP 연결이 되는지만 확인하고, Exec 프로브는 컨테이너 내부에서 지정된 명령을 실행하여 0 Exit Code면 성공 으로 봅니다. NestJS 앱의 경우 HTTP 서버이므로 httpGet이 제일 자연스럽지만, 만약 HTTP 서버를 띄우지 않는 Worker 형태 Node 앱 이라면 Exec 프로브로 프로세스 상태를 체크하거나, 혹은 자체 구현 없이 TCP 포트 오픈 여부 만 확인하는 것도 방법입니다.

마지막으로, 프로브 설정 시 기억해야 할 몇 가지 포인트 를 정리합니다:

  • 프로브 빈도와 타임아웃 튜닝: 프로브는 지나치게 드물면 장애 감지가 늦어지고, 지나치게 잦으면 시스템에 부하를 줍니다. 특히 timeoutSeconds가 짧으면 오탐률이 높아지고, 길면 장애 감지가 늦어집니다. 일반적으로 readinessProbe는 failureThreshold를 1로 낮춰 바로바로 감지 하고, livenessProbe는 failureThreshold를 높여 신중하게 재시작 하는 식으로 조정합니다.
  • 프로브 구현 경량화: Health 체크 엔드포인트 핸들러는 가능한 한 가벼워야 합니다. DB 쿼리처럼 시간이 오래 걸리는 작업을 livenessProbe에서 하지 말고, readinessProbe에서도 너무 복잡한 진단은 지양합니다. 응답 속도 가 생명이므로, 복잡한 내부 검증은 애플리케이션 내 별도 스레드나 캐시를 활용하고, 프로브 요청에는 즉각 결과를 줄 수 있도록 합니다.
  • 프로브와 애플리케이션 종료 시그널 연계: 앞서 언급했듯, 애플리케이션이 종료될 때 readinessProbe는 빠르게 실패 상태를 보고하고(Live Traffic을 빼기 위해), livenessProbe는 어느 정도 유지시키는 것이 좋습니다. Node.js에서는 process.on('SIGTERM') 등을 통해 이를 구현할 수 있고, NestJS의 EnableShutdownHooksTerminus가 이런 부분을 도와줍니다.
  • 헬스 체크 엔드포인트 보안: 일반적으로 Kubernetes healthz 엔드포인트는 클러스터 내부에서 kubelet만 접근하므로 공개되어도 문제없지만, 간혹 외부 노출 시 보안 이슈가 있을 수 있습니다. 필요한 경우 간단한 인증이나 IP 제한을 걸기도 합니다. 그러나 kubelet이 인증 헤더 등을 넣어주지 않으므로, healthz 엔드포인트에 인증을 요구하면 kubelet 체크에 실패 할 수 있으니 조심해야 합니다 (별도 Sidecar로 프록시 두는 방법도 있으나 일반적이지 않음).

8. Node.js 이벤트 루프와 /livez – 이벤트 루프가 막히면 죽은 것으로 간주해야 하나?

마지막으로, Node.js 애플리케이션의 이벤트 루프와 livenessProbe의 관계 를 짚어보겠습니다. 이는 고급 주제인데, 간단히 말하면 Node의 특성 때문에 livenessProbe의 성공/실패가 결정되는 주요 요인이 이벤트 루프의 응답성 이라는 점을 이해해야 한다는 것입니다. 앞서도 여러 번 언급했듯 Node.js는 단일 스레드 이벤트 루프 상에서 동작하기 때문에, 이벤트 루프가 블로킹 되면 아무리 Health 체크 엔드포인트가 있어도 응답을 돌려줄 수 없게 됩니다. Kubernetes는 정해진 Timeout 내에 응답을 못 하면 livenessProbe를 실패로 간주하므로, 결국 이벤트 루프 정지는 곧 livenessProbe 실패와 동의어가 됩니다.

현실 예시: 한 Node.js 서비스에서 대량의 데이터를 처리하던 도중 이벤트 루프가 한동안 막혀 /healthcheck 엔드포인트에 응답하지 못했고, Kubernetes는 이를 livenessProbe 실패로 보고 해당 Pod를 재시작했습니다. 이 서비스는 실제로 죽은 것이 아니라 “바쁘게 돌아가느라” 응답을 못 했던 건데, Kubernetes 입장에선 죽은 거나 마찬가지였던 것이죠. 이런 일이 발생하면 Pod는 재시작되고, 사용자는 에러를 겪을 수 있습니다. 따라서 Node.js 애플리케이션을 Kubernetes 위에 올릴 때, 이벤트 루프의 블로킹을 최소화 하는 것은 매우 중요합니다. 특히 Compute-heavy 작업은 이벤트 루프를 잠식하지 않도록 setImmediateworker_threads 같은 방법으로 분산하거나, 부하가 큰 구간은 작은 청크로 쪼개어 처리(예: 한 틱에 1000개씩 루프 실행 후 Yield)해야 합니다.

또한 livenessProbe 설정 역시 Node.js 앱의 특성을 고려해 약간의 여유 를 둘 필요가 있습니다. 예를 들어 이벤트 루프가 1~2초 막힐 수 있는 상황(큰 JSON 파싱 등)이 있다면, livenessProbetimeoutSeconds를 1초로 하면 간헐적 재시작이 일어날 수 있습니다. 이를 5초로 늘린다면 대부분의 일시적 블로킹은 커버할 수 있겠죠. failureThreshold도 마찬가지입니다. Node.js는 Stop-the-world GC 가 가끔 발생할 수 있으므로, 한 번의 연속 100ms 지연이 여러 번 겹치면 1초 이상의 스톱이 나올 수 있습니다. 그런 상황에서 3회 연속 실패 기준이라면, 3번 연속 응답 못 할 정도로 심각해야 재시작할 테니, 불필요한 재시작을 줄일 수 있습니다. 결국 노이즈와 진짜 장애를 구분 하기 위해 프로브 설정을 튜닝하는 것입니다.

만약 이벤트 루프 지연을 직접 측정해서 임계치 이상이면 /livez에서 실패를 리턴하도록 하려면, Node.js 애플리케이션 내에서 이벤트 루프 지표 를 수집해야 합니다. prom-client를 써서 nodejs_eventloop_lag_seconds와 같은 메트릭을 추적할 수도 있고, setTimeout 주기 체크나 Perf Hooks의 monitorEventLoopDelay 툴을 사용할 수도 있습니다. 그러나 이러한 세부 구현 없이도, 일반적으로 livenessProbe앱이 응답 가능하면 OK, 아니면 Not OK 라는 이분법으로 충분합니다. 왜냐하면 Node 프로세스에 심각한 문제가 없으면 대체로 /livez 요청은 빠르게 처리될 테고, 문제가 있으면 못할 테니까요.

정리하자면: Node.js 이벤트 루프는 Kubernetes livenessProbe와 떼려야 뗄 수 없는 관계입니다. 이벤트 루프가 멈추면 livenessProbe 실패 → Pod 재시작으로 이어지고, 이는 곧 장애 자동복구 로 볼 수도 있지만, 그 자체로 사용 중인 요청이 끊기는 영향을 줍니다. 따라서 최선은 이벤트 루프를 막지 않는 것 이고, 차선으로 livenessProbe 설정을 완충지대로 활용해 약간의 버퍼를 주는 것입니다. 그리고 만약 특정 패턴의 블로킹이 감지된다면 애플리케이션 레벨에서 먼저 최적화하거나, 필요 시 Kubernetes의 리소스 할당 (CPU 할당량 증가 등) 으로 여유를 주는 것도 고려해야 합니다. Node.js의 단일 스레드 모델에서는 한 Pod 내에 CPU 코어 하나만 사실상 활용되므로, 고부하 애플리케이션은 Scale-out(Pod 증설)으로 처리하고, 각 Pod는 이벤트 루프 부담을 적절히 분산하는 아키텍처가 되어야 합니다.


이상으로 NestJS + Node.js 애플리케이션을 Kubernetes 환경에 배포할 때의 프로브 설계 에 대해 심도 있게 살펴보았습니다. 요약하면,

  • LivenessProbe / ReadinessProbe / StartupProbe 는 각각 재시작 판단 / 트래픽 전달 판단 / 초기 지연 보장 용도로 구분되어 있으며, 용도에 맞게 모두 사용하는 것 이 바람직합니다 (특히 startupProbe의 도입으로 초기 지연과 런타임 헬스 체크를 깔끔하게 양립할 수 있게 되었습니다).

  • NestJS 앱의 부팅 완료 시점onApplicationBootstrap 후이며, readinessProbe는 이 시점을 기준으로 잡아야 초기 트래픽 손실이나 잘못된 신호를 막을 수 있습니다. Terminus 모듈 등을 활용하면 이러한 체크를 수월하게 구현할 수 있고, ShutdownHook과 연계해 종료 시 Graceful 하게 Readiness/Liveness를 조절하는 것도 가능합니다.

  • 헬스 체크 엔드포인트 설계 는 간명해야 하며 (/startupz, /readyz, /livez), 각각 서로 다른 역할을 수행하도록 설계해야 합니다. kubelet이 이들을 폴링하여 받은 HTTP 상태코드로 판단하므로, 엔드포인트 구현에 버그가 없도록 주의하고, 응답 지연이나 실패 시 시맨틱을 명확히 해야 합니다.

  • Deployment 롤링 업데이트 에서 readinessProbe새 버전 Pod의 투입을 결정하는 스위치 역할을 합니다. readinessProbefalse면 그 Pod는 서비스에서 빠지고, Deployment는 그것이 준비되기를 기다립니다. maxUnavailablemaxSurge 설정을 통해 얼마나 안전하게 혹은 신속하게 교체할지 결정할 수 있으며, 둘 다 0으로는 설정 불가하다는 것 등 제약을 기억해야 합니다. 작게는 2개부터 크게 수십 개 레플리카까지, 원리만 이해하면 최적의 업데이트 전략을 도출할 수 있습니다.

  • 프로브 세부 옵션 들은 기본값이 존재하지만, 애플리케이션의 특성에 맞게 조정하는 것이 건강합니다. 특히 Node.js 앱의 이벤트 루프 특성을 고려해 Timeout이나 주기를 너무 타이트하게 잡지 않는 것이 좋습니다. 공식 문서를 늘 참고하여 각 옵션의 의미를 이해하고 사용해야 합니다.

  • 이벤트 루프 블로킹 문제 는 Node.js를 Kubernetes에서 운용할 때 항상 염두에 둬야 합니다. 프로브는 이를 감지해 자동 복구하는 역할도 하지만, 근본 해결은 코드 최적화와 아키텍처 개선입니다. 헬스 체크는 결국 심플해야 유용 하다는 점을 잊지 말고, 복잡한 로직보다는 정상/비정상을 빠르게 판단 하는 용도로만 활용하는 것이 좋습니다.

마지막으로, Kubernetes의 프로브 관련 기능은 계속 발전하고 있습니다. 예를 들어 서비스 메시에 통합된 헬스 체크나, Prober가 아닌 startupGracePeriod 같은 새로운 아이디어들이 제시될 수도 있습니다. 하지만 근본 개념은 변하지 않습니다. 애플리케이션의 상태를 신호로 노출하고, 쿠버네티스가 그 신호를 보고 조치하는 구조 입니다. 이 글에서 다룬 원칙들을 응용하면, NestJS/Node.js 뿐만 아니라 다른 어떤 플랫폼의 애플리케이션이라도 Kubernetes 상에서 자체 치유(Self-healing)무정지 배포(Zero-downtime deployment) 에 한 걸음 가까워질 수 있을 것입니다.