k8s 무중단 배포 완성하기 1편: PreStop Hook과 TerminationGracePeriodSeconds 실전 설정

2026. 8. 9. 22:20Kubernetes

Rolling Update 중 실행시간이 긴 API(배치 등)가 중간에 끊기는 문제를, PreStop HookTerminationGracePeriodSeconds로 해결했다. 핵심은 TerminationGracePeriodSeconds > PreStop Hook 대기시간을 반드시 지켜야 한다는 것, 그리고 그 차이만큼이 애플리케이션 자체의 graceful shutdown을 위한 시간이라는 것이다.

문제 상황

개발자로부터 이런 요청을 받았다.

"배포하는 동안 기존 Pod의 API 통신이 끊겨요. 안 끊기게 해주세요."

특히 배치처럼 실행 시간이 긴 API는 이 이슈가 치명적이었다. 원인은 단순했다.

  • Rolling Update로 기존 Pod가 종료되는데, 30초 안에 강제로 다 꺼짐
  • 반면 일부 API는 60초 이상 실행되어야 정상 처리가 끝남

즉 API가 끝나기도 전에 Pod가 죽어버리는 상황이었다.

해결 방법: PreStop Hook + TerminationGracePeriodSeconds

k8s에서는 이 문제를 두 가지 설정만으로 간편하게 해결할 수 있다.

PreStop Hook

  • Pod 종료가 시작될 때 실행되는 훅 (exec / httpGet / 1.29+부터는 sleep action 지원)
  • 애플리케이션이 SIGTERM을 받기 전에 "정리 작업"을 할 시간을 버는 용도로 주로 사용

TerminationGracePeriodSeconds

  • Pod 삭제 요청 후 SIGTERM → SIGKILL까지 kubelet이 기다려주는 최대 시간 (기본값 30초)
  • PreStop Hook 실행 시간도 이 안에 포함된다 — 별도로 추가되는 시간이 아니다

그동안 Pod가 30초쯤 지나면 종료됐던 건 사실 이상 동작이 아니라, TerminationGracePeriodSeconds 기본값 30초가 그대로 적용된 정상 동작이었다.

얼마로 설정해야 할까?

가장 오래 걸리는 API가 60초인 Pod라면, PreStop Hook과 TerminationGracePeriodSeconds를 각각 어떻게 잡아야 할까?

이론적으로도, 실험적으로도 원칙은 명확하다.

TerminationGracePeriodSeconds ≥ PreStop Hook 대기시간

이유는 단순하다. TerminationGracePeriodSeconds가 끝나는 순간, Pod가 어떤 상태에 있든 상관없이 SIGKILL(강제 종료) 이 발동하기 때문이다. PreStop이 아직 끝나지 않았어도 예외는 없다.

PreStop과 SIGTERM은 "동시"가 아니라 "순차"다

여기서 흔히 오해하는 부분이 있다. PreStop Hook과 SIGTERM이 동시에 실행된다고 생각하기 쉬운데, 실제 동작은 다음과 같은 순차 구조다.

  1. Pod가 Terminating 상태로 전환된다 (Endpoint 제거는 이 시점부터 병렬로 진행)
  2. PreStop Hook이 실행되고, 이게 끝날 때까지 kubelet은 SIGTERM을 보내지 않는다
  3. PreStop이 끝나야 (혹은 grace period 마감에 걸려 강제로 넘어가야) 비로소 kubelet이 SIGTERM을 전송한다
  4. 애플리케이션이 SIGTERM을 받고 자체 종료 처리를 시작한다 (커넥션 정리, 스레드풀 드레인 등)
  5. terminationGracePeriodSeconds가 다 차면 무조건 SIGKILL

PreStop은 SIGTERM을 "지연"시키는 역할을 할 뿐, 둘이 병렬로 도는 게 아니다. 이 순서를 정확히 이해해야 다음 설정 실수를 피할 수 있다.

만약 60초로 설정했다면 벌어졌을 일

grace period = 60초, preStop sleep = 60초로 똑같이 맞췄다면 어떻게 됐을까?

  • PreStop sleep 60이 끝나는 순간 = grace period 예산이 정확히 0이 되는 순간
  • kubelet은 sleep이 끝나자마자 SIGTERM을 보내지만, 그 순간 grace period도 이미 소진된 상태 → SIGTERM 직후 사실상 바로 SIGKILL
  • 애플리케이션 입장에서는 SIGTERM을 받고도 자체 종료 로직(Spring Boot 기준 graceful shutdown, ApplicationContext close, 커넥션 풀 정리, 로그/메트릭 flush 등)을 실행할 시간이 사실상 없다
  • 게다가 sleep 60도 fork/exec 오버헤드, 노드 부하, kubelet-런타임 간 통신 지연 등으로 실제로는 60.x초가 걸릴 수 있다. 정확히 60초짜리 API가 아주 살짝만 밀려도 grace period 만료로 강제 종료당할 위험이 있다

"딱 맞춘" 설정은 여유가 전혀 없는 설정이라는 뜻이다.

실제 설정값과 결과

yaml
terminationGracePeriodSeconds: 70
containers:
  - name: my-api
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 60"]
 
  • PreStop Hook: sh -c sleep 60
  • TerminationGracePeriodSeconds: 70초 (PreStop보다 10초 더 길게)

10초를 더 준 이유는, 60초만 두면 API 처리 시간은 확보되지만 애플리케이션 자체가 정상 종료될 시간이 확보되지 않기 때문이다. PreStop이 끝나고 SIGTERM이 전송된 뒤에도, 애플리케이션이 실제로 프로세스를 깔끔하게 내리기까지는 별도의 시간이 필요하다.

적용 결과

  1. API는 Running → Terminating 상태로 접어든 뒤 60초간 대기했고, 그 사이 호출받은 60초짜리 API가 정상 처리됐다. 이후 애플리케이션이 정상 종료되기까지 10초가 채 걸리지 않아, 총 60~70초 내에 종료되었다.
  2. Terminating 상태에서 PreStop Hook으로 sleep 중인 동안 신규 API 호출을 받지는 않는지 궁금할 수 있다. 결론은 "받지 않는다." Endpoint를 조회해보면 직관적으로 확인 가능하다. Terminating 시점에 접어들면 순간적으로 구 Pod와 신 Pod의 Endpoint가 공존하지만, 수 초 내에 구 Pod의 Endpoint가 사라지고 신 Pod의 Endpoint만 남는다. 따라서 구 Pod로는 신규 호출이 들어가지 않는다.

정리

설정 이유
PreStop Hook sleep 60 가장 오래 걸리는 API(60초) 처리 시간 확보
TerminationGracePeriodSeconds 70 PreStop 이후 애플리케이션 자체 graceful shutdown 시간(10초) 확보

핵심 원칙은 딱 하나다.

TerminationGracePeriodSeconds는 PreStop Hook보다 반드시 길어야 하고, 그 차이만큼이 애플리케이션이 진짜로 안전하게 종료될 수 있는 시간이다.

이 원칙만 지키면 Rolling Update 중에도 실행 중이던 API를 끊지 않고 무중단 배포를 완성할 수 있다.