
Kubernetes 무중단 롤아웃의 핵심: Pod 종료(termination)와 Probe를 같이 설계하기
쿠버네티스에서 “무중단”을 얘기할 때 대부분은 RollingUpdate를 먼저 떠올리지만, 실제 장애는 배포 전략보다 Pod 종료(termination) 순간과 Probe(readiness/liveness/startup) 설계가 어긋날 때 더 자주 발생합니다. 이 글은 Kubernetes 공식 문서에 근거해서, 종료 신호(SIGTERM)부터 연결 드레인(drain), 그리고 readiness 전환 타이밍까지를 하나의 설계로 묶어 설명합니다.
TL;DR (현장에서 바로 쓰는 결론)
- 종료는 “즉시 죽음”이 아니라 “종료 요청 + 유예 시간”입니다. Pod가 삭제되면 kubelet은 컨테이너에 종료 신호를 보내고, 설정된 유예 시간(terminationGracePeriodSeconds) 동안 기다립니다.
- readiness는 “트래픽 받을 준비”의 스위치입니다. 종료 직전에 readiness를 먼저 내려야(=NotReady) Service 엔드포인트에서 빠지고, 새 연결 유입을 끊을 수 있습니다.
- preStop은 “종료 직전 훅”입니다. preStop은 종료 과정의 일부로 호출되며, 여기서 드레인/대기(짧게) 또는 애플리케이션에 종료 준비를 시킬 수 있습니다.
- liveness는 “죽였을 때 이득이 있을 때만” 쓰는 게 안전합니다. 무거운 liveness는 재시작 루프를 유발할 수 있습니다. 준비/기동(startupProbe)과 목적을 분리하세요.
1) Kubernetes가 Pod를 “종료”시키는 정확한 흐름
Kubernetes 공식 문서 기준으로 Pod를 삭제하면(Deployment 롤아웃/스케일 인/수동 삭제 포함) Pod는 Terminating 상태가 되며, kubelet은 컨테이너 런타임을 통해 컨테이너 종료를 트리거합니다. 이 과정에서 terminationGracePeriodSeconds로 정의된 유예 시간이 존재합니다.
핵심은 “Pod가 Terminating으로 보이는 순간”부터 “실제로 프로세스가 종료되는 순간” 사이에 트래픽 차단(readiness) + 연결 드레인 + 리소스 정리를 할 수 있는 창이 있다는 점입니다.
종료 타임라인(개념도)
이 다이어그램은 이해를 돕기 위한 개념도이며, Kubernetes 동작의 근거는 공식 문서(아래 ‘근거(원문)’ 섹션)에서 확인할 수 있습니다.
2) Probe를 “정확히” 분리해서 설계하는 법
Kubernetes는 컨테이너 상태를 판단하기 위해 livenessProbe, readinessProbe, startupProbe를 제공합니다. 이들은 목적이 다릅니다.
- readinessProbe: 트래픽을 받을 준비가 되었는지(서비스 엔드포인트 포함 여부) 판단.
- livenessProbe: 컨테이너가 “살아있지 않다”고 판단하면 재시작을 유도.
- startupProbe: 초기 기동이 느린 컨테이너에서, 기동 완료 전까지 liveness/readiness 체크를 늦추는 용도.
실무에서 흔한 실수는 liveness를 readiness처럼 쓰는 것입니다. liveness는 실패 시 컨테이너 재시작이라는 강한 조치를 유발하므로, 오탐(일시적 GC/DB 지연/스파이크)으로도 재시작 루프가 생길 수 있습니다. 반면 readiness는 “트래픽에서 제외”라는 비교적 안전한 조치입니다.
예시: HTTP 서버(예: Spring Boot/NestJS)의 Probe 설계
아래는 컨테이너가 준비되면 readiness가 true가 되고, 기동 중에는 startupProbe가 보호막 역할을 하며, liveness는 최소한의 생존 확인만 하는 형태의 예시입니다. (엔드포인트 경로는 애플리케이션에 맞게 구성하세요.)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api
image: example.com/api:1.0.0
ports:
- containerPort: 8080
lifecycle:
preStop:
httpGet:
path: /internal/drain
port: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
failureThreshold: 1
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /health/startup
port: 8080
periodSeconds: 5
failureThreshold: 24
포인트
- preStop에서 /internal/drain 같은 내부 엔드포인트를 호출해, 애플리케이션이 먼저 readiness를 내려 트래픽 유입을 끊게 만듭니다.
- terminationGracePeriodSeconds는 “드레인 + 처리 중 요청 완료 + 종료”까지 포함한 충분한 값으로 잡습니다.
- readinessProbe failureThreshold: 1처럼 빠르게 NotReady로 전환되게 하면, 롤아웃 중 유입 차단이 빨라집니다(서비스 특성에 맞춰 조정).
3) 애플리케이션 계층: SIGTERM을 제대로 처리해야 한다
Kubernetes는 종료 과정에서 컨테이너 프로세스에 종료 신호를 전달합니다. 이때 애플리케이션이 SIGTERM을 무시하면(또는 종료 훅이 너무 오래 걸리면) 유예 시간이 끝난 뒤 강제 종료될 수 있습니다.
Spring Boot: ‘graceful shutdown’ 기능 활용
Spring Boot는 웹 서버(서블릿/리액티브)에 대해 graceful shutdown 설정을 제공합니다. 설정 방식과 의미는 Spring Boot 공식 레퍼런스에서 확인할 수 있습니다.
# application.yml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
NestJS(Node.js): shutdown hook과 SIGTERM
NestJS는 애플리케이션 종료 이벤트를 처리하기 위한 shutdown hook 메커니즘을 제공합니다. Node.js는 프로세스에 전달되는 신호 이벤트(SIGTERM 등)를 다루는 API를 제공합니다. 구현에서는 (1) readiness를 내리고, (2) HTTP 서버를 새 연결을 받지 않게 닫고, (3) DB 커넥션 풀을 정리하는 흐름이 흔합니다.
// NestJS 예시(개념)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(3000);
}
4) MySQL 커넥션/트랜잭션: “종료 직전”이 제일 위험하다
애플리케이션이 MySQL을 쓰는 경우, 종료 시점에는 다음 두 가지 리스크가 큽니다.
- 새 연결 유입을 차단하지 못해 종료 직전까지 새 요청이 들어오고, 트랜잭션이 미완료로 남는 문제
- 커넥션 풀 정리 없이 프로세스가 종료되어, 응답 지연/오류가 순간적으로 튀는 문제
따라서 “Kubernetes 설정만으로 무중단”을 기대하면 부족합니다. readiness를 먼저 내려 트래픽 유입을 끊고, 애플리케이션에서 종료 훅으로 커넥션 풀 종료/타임아웃을 명시적으로 관리해야 합니다. (프레임워크별 방법은 각 공식 문서의 graceful shutdown/종료 훅 섹션을 기준으로 구현하세요.)
5) 운영 체크리스트(배포 전 10분 점검)
- Deployment RollingUpdate에서 maxUnavailable=0가 필요한 서비스인지 판단(트래픽/비용/가용성).
- readinessProbe는 “외부 의존성(예: DB)”을 어디까지 포함할지 결정(과도하면 플래핑 위험).
- livenessProbe는 최소화: 오탐으로 재시작 루프가 나지 않는지 확인.
- startupProbe를 사용해 초기 기동 중 liveness 오탐을 방지(기동이 느린 이미지/마이그레이션 포함 시 특히).
- preStop + SIGTERM 핸들링 + terminationGracePeriodSeconds의 합이 실제 드레인 시간을 충족하는지 부하테스트로 확인.
- Ingress/LoadBalancer의 idle timeout과 애플리케이션 keep-alive 설정이 종료 설계와 충돌하지 않는지 점검.
6) 장애 패턴과 해결 힌트(현장용)
| 증상 | 가능한 원인 | 우선 조치 |
|---|---|---|
| 롤아웃 때 502/504가 순간적으로 튄다 | readiness가 늦게 내려가거나, 종료 중에도 새 연결을 받음 | readiness 전환을 더 빠르게, preStop에서 드레인 엔드포인트 호출, terminationGracePeriodSeconds 상향 |
| Pod가 Terminating에서 오래 멈춘다 | 종료 훅이 과도하게 길거나 외부 의존성에서 블로킹 | 종료 단계별 타임아웃 도입, 외부 호출 최소화, 로깅으로 종료 단계 시간 측정 |
| liveness 실패로 재시작 루프 | liveness가 과하게 민감하거나, 기동 중 체크가 시작됨 | startupProbe 도입/조정, liveness는 “정말 죽었을 때만” 실패하도록 단순화 |
7) 결론: ‘Probe + 종료’는 한 세트로 설계해야 한다
무중단은 “배포 전략”이 아니라 “종료 순간의 품질”에서 갈립니다. Kubernetes가 제공하는 termination 흐름과 probe 의미를 정확히 이해하고, 애플리케이션의 SIGTERM 처리/커넥션 드레인을 함께 설계하면 롤아웃의 신뢰도가 확 올라갑니다.
근거(원문)
이 글의 동작 설명과 용어 정의는 아래 공식 문서/원문을 기준으로 작성했습니다.
- Kubernetes 문서: Pod Lifecycle / Pod termination(개념) — https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- Kubernetes 문서: Configure liveness, readiness and startup probes — https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- Kubernetes API Reference: PodSpec (terminationGracePeriodSeconds, lifecycle 등) — https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/
- Spring Boot Reference Documentation: graceful shutdown 관련 섹션 — https://docs.spring.io/spring-boot/reference/
- NestJS 공식 문서: Lifecycle events / application shutdown — https://docs.nestjs.com/fundamentals/lifecycle-events
- Node.js 공식 문서: process signal events — https://nodejs.org/api/process.html#signal-events