관리 메뉴

근묵자흑

Kubernetes Pattern: Managed Lifecycle 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Managed Lifecycle

Luuuuu 2025. 8. 23. 13:24

클라우드 네이티브 환경에서 컨테이너의 수명주기 관리는 단순히 "시작"과 "종료"만으로는 충분하지 않습니다. 이 장에서는 Kubernetes의 Managed Lifecycle 패턴을 실제 코드와 테스트를 통해 깊이 있게 살펴보겠습니다. 

왜 Managed Lifecycle이 중요한가?

실제 운영 환경에서 마주치는 상황들을 생각해보세요:

  • Rolling Update 중 데이터 손실: 진행 중인 요청이 완료되지 않은 채 Pod가 종료
  • Cold Start 문제: Java 애플리케이션이 준비되기 전에 트래픽 수신
  • 상태 저장 실패: 갑작스러운 종료로 인한 데이터 불일치
  • 연결 누수: 외부 서비스 연결이 제대로 해제되지 않음

이러한 문제들을 해결하는 것이 바로 Managed Lifecycle 패턴입니다.

실습을 통한 Managed Lifecycle  분석 

  • 이번장의 실습 코드는 GitHub 저장소에서 확인할 수 있습니다.

 

컨테이너 수명주기 이벤트 상세 분석

1. PostStart Hook - 컨테이너 초기화의 핵심

PostStart Hook은 컨테이너가 생성된 직후 실행되는 훅입니다. 중요한 점은 ENTRYPOINT와 비동기적으로 실행된다는 것입니다.

실제 구현 예제

apiVersion: v1
kind: Pod
metadata:
  name: poststart-demo
spec:
  containers:
  - name: app
    image: busybox:1.35
    lifecycle:
      postStart:
        exec:
          command:
          - /bin/sh
          - -c
          - |
            echo "PostStart 시작: $(date)"
            # 캐시 워밍업
            for i in $(seq 1 5); do
              echo "캐시 항목 $i 로드..."
              sleep 1
            done
            # 서비스 레지스트리 등록
            echo "서비스 등록: $(hostname)"
            touch /tmp/app-initialized

테스트 결과 분석

실제 테스트를 통해 확인한 중요한 발견:

=== Main 로그 ===
메인 프로세스 시작: 2025-08-23 03:13:00
메인 프로세스 실행 중 - 1초: 2025-08-23 03:13:00

=== PostStart 로그 ===
PostStart 시작: 2025-08-23 03:13:00  # 동일 시각!
PostStart 실행 중 - 1초: 2025-08-23 03:13:00

핵심 포인트:

  • PostStart와 메인 프로세스가 정확히 같은 시각에 시작
  • 두 프로세스가 병렬로 실행됨
  • ⚠️ PostStart가 실패하면 컨테이너가 재시작됨

2. PreStop Hook - 우아한 종료의 시작

PreStop Hook은 SIGTERM 시그널 이전에 실행되는 블로킹 호출입니다.

실제 구현 예제

lifecycle:
  preStop:
    exec:
      command:
      - /bin/sh
      - -c
      - |
        echo "PreStop 시작: $(date)"

        # 1. 헬스체크 비활성화
        rm -f /tmp/ready

        # 2. 로드밸런서에서 제거
        echo "서비스 레지스트리에서 제거..."
        sleep 3

        # 3. 활성 연결 드레이닝
        CONNECTIONS=5
        while [ $CONNECTIONS -gt 0 ]; do
          echo "활성 연결: $CONNECTIONS"
          sleep 1
          CONNECTIONS=$((CONNECTIONS - 1))
        done

        echo "PreStop 완료: $(date)"

종료 시퀀스 다이어그램

sequenceDiagram
    participant K as Kubernetes
    participant P as Pod
    participant A as Application

    K->>P: Delete 명령
    P->>A: PreStop Hook 실행
    Note over A: 정리 작업 수행<br/>(동기적 실행)
    A-->>P: PreStop 완료
    P->>A: SIGTERM 전송
    Note over A: Graceful Shutdown
    A-->>P: 프로세스 종료
    Note over K,P: terminationGracePeriodSeconds 경과 시
    K->>P: SIGKILL (강제 종료)

3. SIGTERM 처리 - Go 애플리케이션 실제 구현

Production 레벨의 Go 애플리케이션 예제:

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

var (
    activeConnections int64
    mu                sync.Mutex
    serverShutdown    = false
)

func main() {
    // HTTP 서버 설정
    server := &http.Server{
        Addr:    ":8080",
        Handler: connectionCounter(mux),
    }

    // SIGTERM 시그널 처리
    sigterm := make(chan os.Signal, 1)
    signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT)

    // 서버 시작
    go func() {
        log.Println("서버 시작 중...")
        server.ListenAndServe()
    }()

    // SIGTERM 대기
    <-sigterm
    log.Println("SIGTERM 수신, Graceful Shutdown 시작...")
    serverShutdown = true

    // Graceful shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    gracefulShutdown(ctx, server)
}

func gracefulShutdown(ctx context.Context, server *http.Server) error {
    // 1. 새로운 요청 거부
    log.Println("새로운 요청 수신 중단...")

    // 2. 활성 연결 대기
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            mu.Lock()
            if activeConnections == 0 {
                mu.Unlock()
                break
            }
            log.Printf("활성 연결 %d개 대기 중...", activeConnections)
            mu.Unlock()
        }
    }

    // 3. 서버 종료
    return server.Shutdown(ctx)
}

 

실전 테스트 결과 와 결론 

테스트 환경

  • 플랫폼: Minikube on macOS
  • Kubernetes: v1.28

1. PostStart Hook 테스트 결과

시나리오: 메인 컨테이너와 PostStart의 실행 타이밍 검증

결과:

  • 동시 시작 확인 (03:13:00)
  • 비동기 병렬 실행 확인
  • PostStart 10초, 메인 프로세스 20초 독립 실행

실무 적용 팁:

# 잘못된 사용 - 메인 프로세스 의존성
postStart:
  exec:
    command: ["curl", "http://localhost:8080/init"]  # 실패할 수 있음!

# 올바른 사용 - 독립적인 초기화
postStart:
  exec:
    command: 
    - sh
    - -c
    - |
      # 외부 의존성 초기화
      curl -X POST http://service-registry/register
      # 캐시 워밍업
      /app/cache-warmer.sh

2. PreStop Hook 테스트 결과

시나리오: Pod 삭제 시 PreStop → SIGTERM 순서 검증

결과:

  • PreStop 먼저 실행 (블로킹)
  • PreStop 완료 후 SIGTERM 전송
  • 전체 종료 시간 30초 (grace period 준수)

실무 적용 팁:

spec:
  terminationGracePeriodSeconds: 60  # PreStop + SIGTERM 처리 충분한 시간
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command:
          - sh
          - -c
          - |
            # 1. 즉시 헬스체크 실패 처리
            touch /tmp/terminating

            # 2. 트래픽 유입 차단 (중요!)
            sleep 5  # 로드밸런서 업데이트 대기

            # 3. 진행 중인 요청 완료
            while [ $(netstat -an | grep ESTABLISHED | wc -l) -gt 0 ]; do
              sleep 1
            done

3. 전체 수명주기 통합 테스트

시나리오: Init Container → PostStart → Running → PreStop → SIGTERM

결과 타임라인:

시간 이벤트 상태
T+0s Init Container 시작 Pending
T+5s Init Container 완료 PodInitializing
T+6s Main Container + PostStart 시작 ContainerCreating
T+10s PostStart 완료 Running
T+15s Readiness Probe 성공 Ready
T+60s Pod 삭제 명령 Terminating
T+61s PreStop 시작 Terminating
T+70s PreStop 완료, SIGTERM 전송 Terminating
T+75s Graceful Shutdown 완료 Terminated

Production 프랙티스

1. Zero-Downtime 배포 체크리스트

apiVersion: apps/v1
kind: Deployment
metadata:
  name: production-app
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # Zero downtime!
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: app
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          successThreshold: 1
          failureThreshold: 3

        lifecycle:
          preStop:
            exec:
              command:
              - sh
              - -c
              - |
                # 트래픽 차단
                touch /tmp/terminating
                # LB 업데이트 대기
                sleep 10
                # 연결 드레이닝
                /app/drain-connections.sh

흔한 실수와 해결책

실수 1: PostStart에서 메인 프로세스 의존

# 실패할 수 있음
postStart:
  exec:
    command: ["curl", "http://localhost:8080/ready"]

해결책: PostStart는 독립적으로 실행 가능해야 함

실수 2: 불충분한 Grace Period

# PreStop이 완료되기 전에 SIGKILL
terminationGracePeriodSeconds: 10
lifecycle:
  preStop:
    exec:
      command: ["sleep", "30"]  # 실패!

해결책: PreStop + SIGTERM 처리 시간 고려

실수 3: Readiness Probe 없이 PreStop 사용

# PreStop 실행 중에도 트래픽 수신
lifecycle:
  preStop:
    exec:
      command: ["/cleanup.sh"]
# readinessProbe 없음!

해결책: PreStop에서 readiness 실패 처리

실전 시나리오별 구현 가이드

시나리오 1: 마이크로서비스 메시 환경

lifecycle:
  postStart:
    exec:
      command:
      - sh
      - -c
      - |
        # Istio sidecar 대기
        until curl -fsI http://localhost:15021/healthz/ready; do
          sleep 1
        done
        # 서비스 등록
        curl -X POST http://localhost:15000/clusters

시나리오 2: 상태 저장 애플리케이션

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: database
spec:
  podManagementPolicy: Parallel
  template:
    spec:
      terminationGracePeriodSeconds: 120
      containers:
      - name: db
        lifecycle:
          preStop:
            exec:
              command:
              - sh
              - -c
              - |
                # 복제 중단
                mysql -e "STOP SLAVE"
                # 트랜잭션 완료 대기
                mysql -e "SET GLOBAL innodb_fast_shutdown=0"
                # 체크포인트
                mysql -e "FLUSH LOGS"

시나리오 3: 배치 작업

apiVersion: batch/v1
kind: Job
metadata:
  name: data-processor
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - name: processor
        lifecycle:
          preStop:
            exec:
              command:
              - sh
              - -c
              - |
                # 체크포인트 저장
                /app/save-checkpoint.sh
                # 부분 결과 업로드
                /app/upload-partial.sh

 

결론 핵심 

  1. PostStart는 초기화, PreStop은 정리: 각각의 역할을 명확히 구분
  2. 비동기 vs 동기: PostStart는 비동기, PreStop은 동기 실행
  3. Grace Period 충분히: PreStop + SIGTERM 처리 시간 고려
  4. Health Probes 활용: 트래픽 제어의 핵심
  5. 테스트 자동화: 수명주기 시나리오를 CI/CD에 포함

참고 자료