본문 바로가기
일반IT

Kubernetes 무중단 배포의 비밀: 안정적인 롤링 업데이트를 위한 3가지 핵심 기능

by gasbugs 2025. 7. 30.

안녕하세요! 쿠버네티스(Kubernetes) 환경에서 애플리케이션을 운영하다 보면, 새로운 버전을 배포하는 일은 피할 수 없는 숙명과도 같습니다. 이때 가장 중요한 목표는 '사용자에게 중단 없는 서비스를 제공하는 것' 이죠. 쿠버네티스의 디플로이먼트(Deployment)는 기본적으로 롤링 업데이트(Rolling Update) 전략을 통해 이 목표를 지원하지만, 단순히 업데이트 명령을 실행하는 것만으로는 완벽한 무중단 배포를 보장하기 어렵습니다.

오늘은 디플로이먼트의 롤링 업데이트를 한층 더 안정적으로 만들어, 진정한 의미의 무중단 배포를 가능하게 하는 3가지 핵심 기능, Readiness Probe, PodDisruptionBudget (PDB), 그리고 preStop Hook에 대해 아주 상세하게 알아보겠습니다.

https://kubernetes.io/ko/docs/tutorials/kubernetes-basics/update/update-intro/

 


1. Readiness Probe: "준비됐어! 이제 트래픽을 보내줘!"

롤링 업데이트는 새로운 버전의 파드(Pod)를 띄우고, 안정화되면 기존 버전의 파드를 내리는 방식으로 동작합니다. 여기서 핵심적인 질문은 "새로운 파드가 언제 안정화되었다고 판단할 것인가?" 입니다.

컨테이너가 시작(Running) 상태가 되었다고 해서, 그 안의 애플리케이션이 즉시 사용자 요청을 처리할 수 있는 상태를 의미하지는 않습니다. 애플리케이션은 필요한 설정 파일을 읽고, 데이터베이스 커넥션을 맺고, 초기 데이터를 캐시에 로딩하는 등의 준비 시간이 필요할 수 있습니다.

만약 이 준비 과정이 끝나기도 전에 서비스(Service)가 새로운 파드로 트래픽을 전달한다면 어떻게 될까요? 사용자는 500 에러 페이지를 마주하게 될 것입니다. 바로 이 문제를 해결하는 것이 Readiness Probe입니다.

🤔 Readiness Probe는 어떻게 동작하나요?

Readiness Probe는 kubelet이 컨테이너 내 애플리케이션이 트래픽을 받을 준비가 되었는지 주기적으로 확인하는 검사입니다. 이 검사가 성공해야만, 쿠버네티스 서비스의 엔드포인트(Endpoint) 목록에 해당 파드의 IP가 추가되고, 비로소 실제 서비스 트래픽이 전달되기 시작합니다.

  • 검사 시작: 파드가 시작되고 initialDelaySeconds에 설정된 시간만큼 대기한 후 첫 검사를 시작합니다.
  • 주기적 검사: 그 후 periodSeconds 간격으로 계속해서 검사를 수행합니다.
  • 성공: successThreshold 횟수만큼 연속으로 검사에 성공하면, 파드는 'Ready' 상태가 됩니다.
  • 실패: failureThreshold 횟수만큼 연속으로 검사에 실패하면, 파드는 'Not Ready' 상태가 되고 서비스 엔드포인트 목록에서 제외됩니다. (파드 자체가 재시작되지는 않습니다. 이는 Liveness Probe의 역할입니다.)

💻 YAML 예시

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app-container
        image: my-app:1.0
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /api/health/ready # 애플리케이션이 준비되었는지 확인할 수 있는 API 엔드포인트
            port: 8080
          initialDelaySeconds: 15 # 컨테이너 시작 후 15초 뒤에 첫 검사 시작
          periodSeconds: 10     # 10초마다 검사 수행
          timeoutSeconds: 5       # 검사 타임아웃 5초
          failureThreshold: 3   # 3번 연속 실패 시 Not Ready 상태로 변경

Readiness Probe는 롤링 업데이트 시 새로운 파드가 완벽하게 준비된 상태에서만 트래픽을 받도록 보장하는 첫 번째 안전장치입니다.


2. preStop Hook: "잠깐만! 나 아직 할 일이 남았어!" (우아한 종료)

새로운 파드가 성공적으로 트래픽을 받기 시작하면, 쿠버네티스는 이제 기존 버전의 파드를 종료(Terminating)해야 합니다. 이때 또 다른 문제가 발생할 수 있습니다.

파드에게 종료 신호(SIGTERM)가 전달되면, 쿠버네티스는 거의 동시에 해당 파드를 서비스 엔드포인트 목록에서 제거합니다. 하지만 네트워크 경로상의 지연이나 프록시 설정 전파 시간 등으로 인해, 엔드포인트에서 제거된 직후에도 여전히 해당 파드로 향하는 'in-flight' 요청(처리 중이던 요청)이 남아있을 수 있습니다.

만약 파드가 종료 신호를 받자마자 프로세스를 중단해 버린다면, 이 마지막 요청들은 처리되지 못하고 연결이 끊어지게 됩니다. 이것 역시 사용자 경험을 해치는 요인이죠. 이 문제를 해결하는 것이 바로 preStop Hook 입니다.

🤔 preStop Hook은 어떻게 동작하나요?

preStop Hook은 컨테이너가 종료 신호(SIGTERM)를 받은 후, 실제로 프로세스가 중단되기 직전에 실행되는 특정 명령어 또는 HTTP 요청입니다. 이를 통해 애플리케이션은 "우아한 종료(Graceful Shutdown)"를 준비할 시간을 벌 수 있습니다.

일반적으로 preStop 훅에서는 다음의 작업들을 수행합니다.

  1. 새로운 연결 거부: 더 이상 새로운 요청을 받지 않도록 설정합니다.
  2. 기존 연결 처리 완료: 현재 처리 중인 모든 요청이 완료될 때까지 기다립니다.
  3. 리소스 정리: 외부 시스템과의 연결을 해제하거나, 임시 파일을 정리하는 등의 마무리 작업을 수행합니다.

가장 간단하면서도 효과적인 방법은 sleep 명령어를 사용하는 것입니다. 파드가 종료 신호를 받으면, preStop의 sleep 시간만큼 대기합니다. 이 시간 동안 파드는 서비스 엔드포인트에서는 이미 제외되었으므로 새로운 요청은 받지 않고, 남아있던 요청들만 안전하게 처리한 후 종료될 수 있습니다.

💻 YAML 예시

apiVersion: apps/v1
kind: Deployment
# ... (metadata, spec.replicas, selector 생략) ...
  template:
    # ... (metadata 생략) ...
    spec:
      containers:
      - name: my-app-container
        image: my-app:1.0
        lifecycle:
          preStop:
            exec:
              # SIGTERM 신호를 받은 후 20초 동안 대기하여 
              # 로드밸런서에서 제외되고 기존 요청을 모두 처리할 시간을 확보합니다.
              command: ["/bin/sh", "-c", "sleep 20"]
      terminationGracePeriodSeconds: 30 # preStop Hook이 완료될 시간을 포함한 총 종료 유예 시간

preStop 훅은 terminationGracePeriodSeconds 내에 완료되어야 합니다. 만약 이 시간을 초과하면, 쿠버네티스는 강제로 컨테이너를 종료(SIGKILL)시키므로 시간을 적절히 설정하는 것이 중요합니다.

preStop 훅은 롤링 업데이트 시 기존 파드가 처리 중이던 모든 요청을 안전하게 마무리하고 퇴장하도록 보장하는 두 번째 안전장치입니다.


3. PodDisruptionBudget (PDB): "우리 팀 최소 인원은 유지해줘!"

롤링 업데이트 과정 자체는 순조롭게 진행될 수 있습니다. 하지만 만약 업데이트가 진행되는 도중에 관리자가 특정 노드를 점검하기 위해 kubectl drain 명령을 실행한다면 어떻게 될까요? 혹은 클러스터 오토스케일러(Cluster Autoscaler)가 노드를 축소하는 상황이 겹칠 수도 있습니다.

이러한 자발적인 중단(Voluntary Disruption) 으로 인해 애플리케이션의 파드가 한꺼번에 너무 많이 사라지면, 남은 파드에 과부하가 걸리거나 심한 경우 서비스 전체가 다운될 수 있습니다. PodDisruptionBudget (PDB) 는 바로 이러한 상황을 방지하기 위한 정책입니다.

🤔 PDB는 어떻게 동작하나요?

PDB는 특정 레이블을 가진 파드 그룹에 대해, 자발적인 중단 상황에서도 최소한으로 유지되어야 할 파드의 개수 또는 비율(minAvailable), 혹은 최대로 중단될 수 있는 파드의 개수 또는 비율(maxUnavailable) 을 정의합니다.

  • minAvailable: "이 애플리케이션은 어떤 상황에서도 최소 N개의 파드는 항상 사용 가능해야 해!"
  • maxUnavailable: "이 애플리케이션의 파드는 최대 N개까지만 동시에 중단될 수 있어!"

PDB가 설정된 상태에서 kubectl drain과 같은 명령이 실행되면, 쿠버네티스는 PDB 정책을 위반하지 않는 선에서만 파드를 축출(evict)합니다. 만약 파드를 축출했을 때 minAvailable 보다 파드 개수가 적어지거나 maxUnavailable을 초과하게 된다면, 해당 작업은 PDB 조건이 충족될 때까지 대기하게 됩니다.

중요: PDB는 노드 장애와 같은 비자발적인 중단(Involuntary Disruption) 으로부터는 서비스를 보호하지 않습니다. 이는 레플리카(Replica) 수를 보장하는 디플로이먼트 자체의 역할입니다.

💻 YAML 예시

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
spec:
  # 이 PDB가 적용될 파드 그룹을 선택합니다. (Deployment의 label과 일치)
  selector:
    matchLabels:
      app: my-app
  # 'my-app' 레이블을 가진 파드 중 최소 2개는 항상 사용 가능해야 함을 의미합니다.
  minAvailable: 2 
  # 또는 maxUnavailable: 1 로 설정하여 "최대 1개만 동시에 중단 가능"으로 정의할 수도 있습니다.

PDB는 롤링 업데이트와 같은 배포 과정뿐만 아니라, 클러스터의 유지보수 작업 중에도 서비스의 전반적인 가용성을 안정적으로 유지하는 강력한 세 번째 안전장치입니다.


✨ 세 기능의 환상적인 협업 시나리오

이 세 가지 기능이 롤링 업데이트 중에 어떻게 협업하는지 시나리오를 통해 정리해 보겠습니다.

  1. 배포 시작: 관리자가 my-app의 이미지를 1.0에서 2.0으로 업데이트합니다.
  2. 신규 파드(v2) 생성: 쿠버네티스는 새로운 ReplicaSet을 만들고 my-app:2.0 파드를 띄웁니다.
  3. Readiness Probe 작동: v2 파드는 아직 준비가 안 됐습니다. Readiness Probe가 계속 실패하며, 서비스는 v2 파드로 트래픽을 보내지 않습니다. 오직 기존 v1 파드들만 트래픽을 처리합니다.
  4. 신규 파드 Ready: v2 파드의 애플리케이션 초기화가 끝나고, Readiness Probe가 성공합니다. 이제 v2 파드의 IP가 서비스 엔드포인트에 추가되고, 새로운 트래픽을 받기 시작합니다.
  5. 기존 파드(v1) 종료 시작: 쿠버네티스가 이제 v1 파드 중 하나를 종료하기로 결정하고 SIGTERM 신호를 보냅니다.
  6. preStop Hook 작동: v1 파드는 preStop 훅 (sleep 20)을 실행합니다. 이 파드는 서비스 엔드포인트에서 즉시 제외되어 더 이상 새로운 트래픽을 받지 않습니다. 20초의 시간 동안, 기존에 처리하던 요청들을 모두 안전하게 마무리합니다.
  7. 기존 파드 완전 종료: preStop 훅이 끝나면 v1 파드의 프로세스가 정상적으로 종료됩니다.
  8. (안전장치) PDB의 감시: 이 모든 과정 중에 관리자가 노드 유지보수를 시도하더라도, PDB(minAvailable: 2) 때문에 전체 my-app 파드가 2개 미만으로 떨어지지 않도록 쿠버네티스가 제어합니다.
  9. 반복: 모든 v1 파드가 안정적인 v2 파드로 교체될 때까지 2~8번 과정이 반복됩니다.

결론

단순히 kubectl set image만으로도 롤링 업데이트는 가능하지만, 실제 운영 환경에서는 예기치 못한 순간에 서비스 장애가 발생할 수 있습니다.

  • Readiness Probe는 준비되지 않은 파드로 트래픽이 가는 것을 막아주고,
  • preStop Hook은 기존 파드가 마지막 요청까지 책임지고 우아하게 종료되도록 하며,
  • PodDisruptionBudget은 외부 요인으로 인한 동시 중단으로부터 서비스 전체를 보호합니다.

이 세 가지 기능을 함께 사용하여 디플로이먼트를 설정하는 것은, 쿠버네티스 위에서 견고하고 신뢰성 높은 서비스를 구축하기 위한 필수적인 습관입니다. 여러분의 서비스를 한 단계 더 높은 수준의 안정성으로 이끌어보세요!