쿠버네티스 무중단 웹 서비스 배포 — Deployment, Service, 그리고 Pod 라이프사이클

로컬 개발 환경에서 도커 컨테이너를 하나 띄우는 것과, 실제 운영 환경에서 수많은 사용자 트래픽을 처리하며 웹 서비스를 무중단으로 운영하는 것은 완전히 다른 차원의 문제입니다.

새 버전의 코드를 배포하는 과정에서 수 초 동안 사용자 요청이 유실되거나, 컨테이너가 갑작스러운 오류로 재시작되는 순간 502 Bad Gateway 에러가 쏟아져 나온다면 서비스의 신뢰성은 무너집니다.

쿠버네티스는 컨테이너를 단순히 실행하는 것을 넘어, 파드(Pod)의 생명주기 관리와 선언적 롤링 배포, 그리고 지능적인 트래픽 격리 메커니즘을 통해 완벽한 무중단 서비스를 달성하도록 설계되었습니다.

1. 컨테이너를 감싸는 최소 배포 단위: 파드(Pod)의 생명주기

쿠버네티스에서 컨테이너는 결코 단독으로 실행되지 않습니다. 언제나 파드(Pod) 라는 껍데기에 감싸져 배포됩니다.

파드는 하나 이상의 컨테이너가 동일한 리눅스 네임스페이스(네트워크 IP, 포트 공간, IPC)와 스토리지 볼륨을 공유하는 프로세스 그룹입니다. 따라서 같은 파드 안에 있는 컨테이너들은 서로를 localhost로 호출하며 초고속으로 통신할 수 있습니다.

클러스터 안에서 파드는 생성부터 소멸까지 명확한 생명주기 단계(Phase)를 거칩니다:

  • Pending: 파드 생성 요청이 API 서버에 등록되었으나, 아직 스케줄러가 어느 워커 노드에 올릴지 결정하지 못했거나 컨테이너 이미지를 다운로드 중인 상태입니다.
  • Running: 노드가 확정되고, 파드 내부의 컨테이너 중 최소 하나 이상이 성공적으로 구동되어 실행 중인 정상 상태입니다.
  • Succeeded: 일회성 작업(Job 등)의 모든 컨테이너가 에러 코드 0으로 정상 종료된 상태입니다.
  • Failed: 파드 내의 컨테이너 중 하나 이상이 비정상 종료(0이 아닌 종료 코드)되었거나 시스템에 의해 강제 종료된 상태입니다.
  • Unknown: 노드와의 네트워크 단절 등으로 인해 컨트롤 플레인이 파드의 상태를 조회할 수 없는 상태입니다.

운영 환경에서 중요한 점은 “파드는 언제든 죽을 수 있는 일회성(Ephemeral) 자원” 이라는 사실입니다. 노드에 장애가 나면 파드는 복구되지 않고 버려지며, 새로운 파드가 다른 정상 노드에서 새 IP를 부여받아 다시 태어납니다.

2. 장애를 사전에 차단하는 헬스체크: 3가지 프로브(Probe)

컨테이너 프로세스가 OS 상에서 살아있다고 해서(PID가 존재한다고 해서), 그 애플리케이션이 실제 사용자 요청을 정상 처리할 수 있는 상태라는 뜻은 아닙니다. 데이터베이스 연결이 끊겼거나, 메모리 릭으로 쓰레드 풀이 고갈되어 응답이 멈춘 상태일 수도 있습니다.

쿠버네티스 노드의 kubelet은 세 가지 독립적인 프로브를 주기적으로 실행하여 애플리케이션의 진짜 상태를 검증합니다.

① startupProbe: 느린 기동을 보호하는 안전장치

자바 스프링 부트나 무거운 AI 모델처럼 부팅에 수십 초 이상 걸리는 애플리케이션에 필수적입니다. startupProbe가 성공하기 전까지는 뒤의 두 프로브가 아예 동작하지 않습니다. 이를 통해 기동 중인 파드가 응답이 없다고 판단되어 조기에 강제 재시작당하는 비극을 막아줍니다.

② livenessProbe: 교착 상태를 풀어내는 자가 치유

애플리케이션이 살아있는지 검사합니다. 데드락(Deadlock)에 빠져 복구 불가능한 상태가 되면 프로브가 실패하고, kubelet은 컨테이너를 강제로 종료한 뒤 재시작 정책(restartPolicy)에 따라 깨끗한 새 프로세스로 다시 띄웁니다.

③ readinessProbe: 트래픽 인입을 결정하는 수문장

무중단 운영에서 가장 중요한 프로브입니다. 애플리케이션이 현재 트래픽을 처리할 준비가 되었는지를 확인합니다. 만약 데이터베이스 마이그레이션 중이거나 부하로 인해 일시적으로 응답이 불가능하면, readinessProbe가 실패로 전환됩니다.

readinessProbe가 실패하면 쿠버네티스는 해당 파드를 죽이지 않고, 단지 서비스(LoadBalancer/Service)의 트래픽 라우팅 엔드포인트 목록에서만 즉시 제외 합니다. 덕분에 사용자는 에러 응답을 받지 않고, 정상 상태인 다른 파드로 요청이 안전하게 우회됩니다.

3. 배포 중 한 건의 요청도 놓치지 않는 Graceful Shutdown

새 버전 배포 시 기존 파드를 끌 때, 처리 중이던 사용자 요청이 갑자기 끊기면 안 됩니다. 파드가 종료될 때 쿠버네티스 내부에서는 두 가지 작업이 병렬로 진행됩니다:

  1. 컨트롤 플레인의 네트워크 분리: API 서버가 파드를 Terminating 상태로 바꾸고, 서비스의 엔드포인트에서 파드 IP를 제거합니다. 노드의 kube-proxy는 이 변경을 감지하고 리눅스 커널 라우팅 규칙(iptables/IPVS)에서 해당 IP를 지웁니다.
  2. 워커 노드의 프로세스 종료: kubelet이 컨테이너에 종료 시그널인 SIGTERM을 전송합니다.

여기서 현업의 치명적인 경쟁 상태(Race Condition) 가 발생합니다:

kube-proxy가 모든 노드의 라우팅 테이블에서 파드 IP를 지우는 데는 수 초의 네트워크 지연이 걸립니다. 그런데 컨테이너가 SIGTERM을 받자마자 즉시 프로세스를 닫아버리면, 미처 라우팅 테이블이 갱신되지 않아 여전히 해당 파드로 들어오던 사용자 요청들은 문이 닫힌 컨테이너에 부딪혀 Connection Refused나 502 Bad Gateway 에러를 뿜게 됩니다.

이를 해결하는 표준적인 실무 기법이 바로 preStop 라이프사이클 훅입니다:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]

preStop 훅을 설정하면, kubelet은 컨테이너에 SIGTERM을 보내기 전에 5초 동안 의도적으로 대기합니다. 이 5초 사이에 클러스터의 모든 네트워크 프록시가 파드 IP를 안전하게 지워내고 신규 요청 유입을 완전히 차단합니다. 이후 SIGTERM이 전달되면 애플리케이션은 남아있는 요청만 깔끔하게 완수하고 안전하게 퇴장(Graceful Shutdown)할 수 있습니다.

기본적으로 주어지는 대기 유예 시간은 terminationGracePeriodSeconds: 30이며, 필요에 따라 이 값을 늘려 대용량 작업 처리를 보장할 수 있습니다.

4. 선언적 무중단 롤링 배포의 엔진: 디플로이먼트(Deployment)

파드는 스스로 복제되거나 버전을 갱신하지 못합니다. 파드의 복제본 개수를 항상 일정하게 유지해 주는 관리자가 ReplicaSet이고, 이 ReplicaSet들을 교체하며 무중단 롤링 업데이트를 오케스트레이션하는 고수준 객체가 바로 디플로이먼트(Deployment) 입니다.

디플로이먼트의 롤링 업데이트 알고리즘은 두 가지 핵심 파라미터로 동작합니다:

  • maxSurge: 배포 중에 기준 레플리카(replicas) 수보다 최대로 더 생성할 수 있는 파드의 개수나 비율입니다. (기본값: 25%)
  • maxUnavailable: 배포 중에 일시적으로 사용할 수 없어도 되는 파드의 최대 개수나 비율입니다. (기본값: 25%)

예를 들어 레플리카 수가 4개이고 둘 다 25%(1개)로 설정되어 있다면:

  1. 먼저 신버전(v2) 파드 1개를 새로 띄웁니다. (전체 5개, maxSurge 만족)
  2. 신버전 파드가 readinessProbe를 통과해 트래픽을 받을 준비가 완료되면, 구버전(v1) 파드 1개를 종료합니다. (전체 4개)
  3. 이 과정을 순차적으로 반복하여 v1 파드 4개를 v2 파드 4개로 다운타임 없이 완벽하게 교체합니다.

만약 신버전 이미지에 치명적인 버그가 있어 readinessProbe를 통과하지 못하면 롤링 배포는 그 즉시 정지되고, 기존 구버전 파드들이 살아남아 서비스를 계속 지탱합니다. 관리자는 kubectl rollout undo deployment/my-app 한 줄로 이전 정상 버전으로 1초 만에 롤백할 수 있습니다.

5. 동적 파드들을 묶어주는 가상 IP: 서비스(Service)

롤링 배포가 일어나거나 노드가 교체되면 파드의 IP는 끊임없이 바뀝니다. 클라이언트는 수시로 바뀌는 파드 IP를 직접 알 수 없습니다.

서비스(Service) 는 변화무쌍한 파드 집합의 앞단에 변하지 않는 고정 가상 IP(ClusterIP) 와 내부 도메인 이름을 제공하는 로드밸런서 역할을 합니다. 서비스는 라벨 셀렉터(selector: app=web)를 보고 일치하는 파드들의 실시간 IP 목록을 Endpoints 객체로 자동 관리합니다.

서비스에는 네트워크 범위에 따라 세 가지 표준 유형이 존재합니다:

  • ClusterIP: 클러스터 내부 통신 전용 가상 IP입니다. 외부에서는 접근할 수 없으며, 백엔드 API나 내부 마이크로서비스 간 통신에 쓰입니다.
  • NodePort: 모든 워커 노드의 물리적 포트(기본 30000~32767)를 열어 외부 트래픽을 받습니다. 노드IP:NodePort로 들어온 트래픽은 서비스에 의해 내부 파드로 분산 전달됩니다.
  • LoadBalancer: AWS(NLB/ALB), GCP(Cloud Load Balancing) 등 퍼블릭 클라우드 인프라와 직접 통신하여 실제 고정 공인 IP를 가진 외부 로드밸런서를 자동으로 프로비저닝합니다.

6. 프로덕션 배포 매니페스트 표준 예제

앞서 다룬 라이프사이클, 프로브, 자원 제한, Graceful Shutdown을 모두 집약한 엔터프라이즈급 표준 웹 애플리케이션 매니페스트입니다:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: production-web
  labels:
    app: web
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0 # Zero-downtime: always maintain desired capacity
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: app
        image: nginx:1.25-alpine
        ports:
        - containerPort: 80
        # Resource isolation to prevent noisy neighbors
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
        # Wait for internal network routing propagation
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 5"]
        # Health check probes
        startupProbe:
          httpGet:
            path: /
            port: 80
          failureThreshold: 10
          periodSeconds: 3
        readinessProbe:
          httpGet:
            path: /
            port: 80
          periodSeconds: 5
          failureThreshold: 2
        livenessProbe:
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
          failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
  name: production-web-svc
spec:
  type: ClusterIP
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80

7. 정리: 무중단은 도구가 아니라 정교한 프로토콜의 합작품이다

쿠버네티스를 쓴다고 해서 저절로 무중단 서비스가 완성되지는 않습니다. 단순히 Deployment의 replicas를 3개로 늘려두는 것만으로는 배포 순간의 트래픽 유실을 막을 수 없습니다.

진정한 무중단 아키텍처는 다음 4가지가 유기적으로 맞물릴 때 비로소 달성됩니다:

  • readinessProbe 를 통한 준비되지 않은 파드의 트래픽 차단
  • preStop sleep 을 통한 커널 라우팅 테이블 갱신 시간 확보
  • 애플리케이션의 SIGTERM 처리 를 통한 남은 요청의 정상 완수
  • maxUnavailable: 0 설정을 통한 배포 중 가용 용량 100% 보장

시스템의 생명주기와 네트워크의 지연 특성을 깊이 이해하고 파드의 탄생과 죽음을 정교하게 제어할 수 있을 때, 사용자는 어떤 배포나 장애 상황에서도 단 한 번의 끊김 없이 안정적으로 동작하는 견고한 서비스를 경험하게 됩니다.