13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] Kubernetes

  • [k8s] Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    [k8s] Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    Kubernetes 프로브 트러블슈팅이 중요한 이유

    Kubernetes 프로브 트러블슈팅은 단순히 /health 경로가 200을 반환하는지 확인하는 일이 아닙니다. 운영에서는 이 설정 하나가 재시작 루프, 트래픽 블랙홀, 배포 지연, 의존성 장애 전파를 만들기도 하고 막기도 하거든요. 13년 넘게 서비스를 운영하면서 느낀 건, 프로브는 애플리케이션의 상태를 보는 기능이라기보다 쿠버네티스가 장애에 어떻게 반응할지 정하는 정책에 가깝다는 점입니다.

    대표적인 장면은 비슷합니다. 앱 로그만 보면 큰 문제가 없는데 kubectl get pods의 RESTARTS가 계속 올라갑니다. 또는 배포는 끝났는데 Service 뒤에서 파드가 빠져 502, 503이 발생합니다. 이때 무작정 앱을 재배포하거나 노드를 의심하면 시간이 정말 빨리 녹습니다. 먼저 봐야 할 것은 kubelet 이벤트입니다. Liveness probe failed 뒤에 Killing container가 붙는지, Readiness probe failed만 반복되는지에 따라 원인이 완전히 갈립니다.

    위 흐름에서 핵심은 하나입니다. Liveness 실패는 재시작을 부르고, Readiness 실패는 트래픽 제외를 부릅니다. 둘 다 헬스 체크처럼 보이지만 운영 결과는 다릅니다. 그래서 프로브를 설계할 때는 “정상인가?”보다 “실패했을 때 쿠버네티스가 무엇을 해야 하는가?”를 먼저 정해야 합니다.

    Liveness Probe 실패와 Readiness Probe 오류의 차이

    Liveness Probe는 컨테이너가 복구 불가능한 상태에 빠졌는지 판단합니다. 실패가 임계치에 도달하면 kubelet이 해당 컨테이너를 재시작합니다. Readiness Probe는 지금 요청을 받아도 되는지 판단합니다. 실패해도 컨테이너는 계속 실행되지만, 해당 파드는 Service 엔드포인트에서 제외됩니다. Startup Probe는 초기 기동 중인 앱을 보호합니다. Startup Probe가 설정되면 성공하기 전까지 Liveness와 Readiness가 실행되지 않으므로, 기동이 느린 앱에서 조기 재시작을 막는 데 유용합니다.

    운영에서 많이 터지는 실수는 외부 의존성까지 Liveness에 넣는 겁니다. DB가 잠깐 느려졌을 뿐인데 모든 파드가 동시에 재시작되면, 커넥션 재생성·캐시 워밍업·JIT/클래스 로딩 같은 비용이 한꺼번에 몰립니다. 장애를 복구하는 게 아니라 장애에 연료를 붓는 모양이 되더라고요. DB, Redis, 메시지 브로커, 외부 API는 대부분 Readiness에서 다루는 편이 안전합니다.

    구분 Liveness Probe Readiness Probe Startup Probe 운영 판단 기준
    목적 컨테이너를 재시작해야 하는지 판단 트래픽을 받을 준비가 됐는지 판단 초기 기동이 끝났는지 판단 실패 시 원하는 조치가 재시작인지, 트래픽 차단인지부터 정합니다.
    실패 시 동작 컨테이너 재시작 Service 엔드포인트에서 제외 성공 전까지 다른 프로브 보류, 실패 시 컨테이너 재시작 RESTARTS, Events, EndpointSlice를 같이 봐야 오판이 줄어듭니다.
    적합한 검사 이벤트 루프 멈춤, 데드락, 기본 핸들러 무응답 DB 연결, 캐시 준비, 필수 설정 로딩, 마이그레이션 대기 JVM/Node.js/.NET 앱의 긴 부팅, 모델 로딩, 초기 캐시 구성 외부 의존성 실패가 곧 프로세스 사망을 의미하지는 않습니다.
    과도하게 엄격할 때 재시작 루프, 콜드 스타트 비용 증가 정상 파드가 트래픽을 못 받아 가용 용량 감소 기동 실패 감지가 늦어짐 엄격함은 안정성이 아니라 운영 정책입니다. 비용을 함께 계산해야 합니다.
    주로 확인할 증거 Killing container, BackOff, --previous 로그 Ready 0/1, Endpoint/EndpointSlice 누락 기동 로그, 초기화 완료 시점, Startup 실패 이벤트 로그보다 먼저 Events를 보면 방향을 빨리 잡습니다.

    쿠버네티스 헬스 체크 기본 설정 예시

    아래 예시는 세 프로브를 분리한 Deployment입니다. 실제 애플리케이션 이미지가 /livez, /readyz, /startupz 엔드포인트를 제공한다는 전제로 봐주세요. /livez는 프로세스와 HTTP 서버가 응답 가능한지만 확인하고, /readyz는 실제 요청 처리 준비 상태를 확인합니다. /startupz는 초기화가 끝나기 전 Liveness가 끼어들지 않도록 막습니다. 운영 매니페스트에서는 이 정도 분리가 나중에 장애 분석 시간을 꽤 줄여줍니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: probe-demo
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: probe-demo
      template:
        metadata:
          labels:
            app: probe-demo
        spec:
          terminationGracePeriodSeconds: 30
          containers:
            - name: app
              image: your-registry.example.com/probe-demo:1.0.0
              ports:
                - name: http
                  containerPort: 8080
              startupProbe:
                httpGet:
                  path: /startupz
                  port: http
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 30
              livenessProbe:
                httpGet:
                  path: /livez
                  port: http
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3
              readinessProbe:
                httpGet:
                  path: /readyz
                  port: http
                periodSeconds: 5
                timeoutSeconds: 2
                successThreshold: 1
                failureThreshold: 3

    값을 고를 때는 감이 아니라 앱의 실제 시간을 기준으로 잡습니다. 기동 로그에서 “서버 포트 바인딩 완료”, “마이그레이션 완료”, “캐시 로딩 완료”, “요청 처리 가능” 시점을 분리해 보세요. initialDelaySeconds를 길게 늘리는 방식은 단순하지만, 앱이 빨리 떠도 무조건 기다립니다. 반면 startupProbe는 성공하는 순간 다음 단계로 넘어가므로, 느린 기동과 빠른 회복을 같이 다루기 좋습니다.

    프로브 파라미터는 서로 곱해져 운영 결과를 만듭니다. 예를 들어 periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 2라면 실패를 확정하기까지 여러 번의 검사 주기가 필요합니다. 대략 30초 안팎으로 생각할 수 있지만, 정확한 감지 시간은 스케줄링과 응답 지연에 영향을 받습니다. 고정 수치처럼 외우기보다 “얼마나 빨리 빼고 얼마나 늦게 죽일 것인가”라는 정책으로 보는 편이 낫습니다.

    매니페스트에서 확인할 필드는 httpGet.path, port, periodSeconds, timeoutSeconds, failureThreshold, successThreshold입니다. 참고로 successThreshold는 Liveness와 Startup에서는 1이어야 하고, Readiness에서만 1보다 크게 둘 수 있습니다. Readiness 복귀를 일부러 보수적으로 만들고 싶을 때만 조정하세요.

    실전 디버깅: Kubernetes 프로브 트러블슈팅 명령어

    장애가 났을 때 저는 애플리케이션 로그보다 먼저 쿠버네티스 이벤트를 봅니다. 이벤트는 kubelet이 왜 컨테이너를 죽였는지, 왜 Ready에서 제외했는지 비교적 솔직하게 남깁니다. 아래 순서대로 보면 “앱 문제인지, 프로브 설정 문제인지, Service 라우팅 문제인지”가 빠르게 갈립니다.

    1. 파드의 Ready 상태와 재시작 횟수를 확인합니다.
    2. Events에서 Liveness probe failed, Readiness probe failed, Startup probe failed를 찾습니다.
    3. 현재 로그와 이전 컨테이너 로그를 비교합니다.
    4. Service, Endpoint, EndpointSlice에 파드 IP가 들어갔는지 확인합니다.
    5. 클러스터 내부에서 DNS, 포트, HTTP 경로를 직접 호출합니다.
    NS=default
    APP=probe-demo
    POD=$(kubectl get pod -n "$NS" -l app="$APP" -o jsonpath='{.items[0].metadata.name}')
    
    kubectl get pods -n "$NS" -l app="$APP" -o wide
    kubectl describe pod "$POD" -n "$NS"
    kubectl logs "$POD" -n "$NS" --all-containers=true --tail=200
    kubectl logs "$POD" -n "$NS" --previous --tail=200
    kubectl get events -n "$NS" --sort-by=.lastTimestamp
    kubectl get svc "$APP" -n "$NS"
    kubectl get endpoints "$APP" -n "$NS"
    kubectl get endpointslice -n "$NS" -l kubernetes.io/service-name="$APP"

    해석은 이렇게 합니다. Liveness probe failed 다음에 Killing container가 이어지면 재시작 원인은 Liveness입니다. Readiness probe failed만 반복되고 RESTARTS가 늘지 않으면 파드는 살아 있지만 Service 트래픽에서 제외된 겁니다. connection refused는 포트 미오픈, 잘못된 포트명, 앱 기동 전 검사 시작, 또는 앱이 127.0.0.1에만 바인딩된 경우를 의심합니다. HTTP probe failed with statuscode: 404는 대개 경로 오타입니다. context deadline exceeded나 timeout 계열 메시지는 앱 내부 지연, CPU throttling, I/O 대기, GC 지연까지 같이 봐야 합니다.

    kubectl run curl-debug \
      -n default \
      --image=curlimages/curl:8.5.0 \
      --restart=Never \
      --rm -it \
      -- sh
    
    # 디버그 파드 안에서 실행
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/readyz
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/livez
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local:80/readyz

    여기서 중요한 건 “Service DNS로 되는가”와 “Pod IP로 직접 되는가”를 나눠 보는 겁니다. Service DNS는 실패하는데 Pod IP 직접 호출은 성공하면 Service selector나 EndpointSlice 쪽을 봅니다. Pod IP 직접 호출도 실패하면 컨테이너 포트, 앱 바인딩 주소, 경로, 응답 시간을 봅니다. 프로브는 kubelet이 Pod 네트워크의 Pod IP로 직접 보내는 검사라서, Ingress에서 보이는 증상과 다를 수 있습니다.

    재현 가능한 시나리오: Readiness Probe 오류 만들고 확인하기

    테스트 클러스터에서 일부러 잘못된 Readiness 경로를 넣어보면 동작이 선명해집니다. 아래 Pod는 nginx를 띄우지만 존재하지 않는 /not-ready를 Readiness로 검사합니다. 컨테이너는 실행되지만 Ready가 되지 않고, Service 엔드포인트에도 들어가지 않는 상태를 만들 수 있습니다. 이거 한 번 직접 보면 실장애 때 감이 훨씬 빨리 옵니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: bad-readiness
      labels:
        app: bad-readiness
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /not-ready
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: bad-readiness
    spec:
      selector:
        app: bad-readiness
      ports:
        - name: http
          port: 80
          targetPort: http
    kubectl apply -f bad-readiness.yaml
    kubectl get pod bad-readiness -w
    kubectl describe pod bad-readiness
    kubectl get endpoints bad-readiness
    kubectl get endpointslice -l kubernetes.io/service-name=bad-readiness
    kubectl delete -f bad-readiness.yaml

    정상적인 관찰 결과는 이렇습니다. kubectl get pod에서 READY가 0/1로 보이고, describe Events에는 Readiness 실패가 찍힙니다. 하지만 RESTARTS는 증가하지 않습니다. Service의 Endpoint 또는 EndpointSlice도 비어 있거나 해당 파드 IP가 빠져 있어야 합니다. 이 작은 실험을 해두면 “왜 컨테이너는 Running인데 트래픽이 안 가지?”라는 질문에 훨씬 빨리 답할 수 있습니다.

    반대로 Liveness 경로를 잘못 지정하면 결과가 달라집니다. READY 이전에 컨테이너가 재시작될 수 있고, Events에는 Liveness 실패와 컨테이너 종료 메시지가 같이 나타납니다. 같은 404라도 Readiness에서는 트래픽 제외, Liveness에서는 재시작입니다. 이 차이를 머리에 넣어두면 디버깅 방향이 흔들리지 않습니다.

    제가 겪은 Liveness Probe 실패 패턴

    가장 위험했던 패턴은 종합 건강검진식 Liveness였습니다. /health 요청 하나에 DB 쿼리, Redis ping, 외부 API 호출, 디스크 체크를 전부 넣는 방식입니다. 겉보기에는 꼼꼼합니다. 그런데 DB가 순간적으로 느려지면 모든 파드가 Liveness 실패로 재시작됩니다. 재시작된 파드는 다시 DB 커넥션을 만들고, 캐시를 채우고, 준비되지 않은 상태에서 또 검사를 받습니다. 문제의 뿌리는 DB 지연인데 증상은 애플리케이션 재시작 폭풍으로 바뀝니다.

    제가 쓰는 기준은 꽤 엄격합니다. Liveness는 “이 프로세스를 죽이면 나아지는가?”에 예라고 답할 수 있을 때만 실패시킵니다. 외부 의존성이 실패했지만 프로세스가 요청 큐를 비우고 회복할 수 있다면 Liveness가 아니라 Readiness입니다. 이 기준이 약간 보수적으로 보여도, 운영에서는 불필요한 재시작을 줄이는 쪽이 대체로 더 싸고 안전했습니다.

    • Liveness: 기본 HTTP 핸들러, 이벤트 루프, 내부 상태 플래그처럼 가볍고 로컬인 검사만 둡니다.
    • Readiness: DB 연결, 브로커 구독, 필수 설정 로딩, 캐시 워밍업처럼 트래픽 처리 조건을 둡니다.
    • Startup: 앱 부팅이 느리거나 초기화 단계가 여러 개인 서비스에서 Liveness 개입을 늦춥니다.
    • 외부 API: 핵심 경로가 아니라면 Readiness에서도 과하게 엄격하게 묶지 않습니다. 서킷 브레이커와 graceful degradation이 더 나은 경우가 많습니다.
    startupProbe:
      httpGet:
        path: /startupz
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 30
    livenessProbe:
      httpGet:
        path: /livez
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /readyz
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3

    이 구성을 그대로 복사하기보다, 먼저 앱의 실제 기동 경로를 관찰하세요. 포트가 열리는 시점과 요청을 안전하게 처리할 수 있는 시점은 다를 수 있습니다. 특히 Java, .NET, 대형 Node.js 앱처럼 초기 로딩이 긴 서비스는 포트가 먼저 열리고 내부 초기화가 나중에 끝나는 경우가 있습니다. 이때 Liveness를 빨리 켜면 멀쩡히 뜨는 중인 앱을 kubelet이 반복해서 죽입니다.

    검증/결과: 무엇을 보고 정상이라고 판단할까

    프로브 설정을 바꾼 뒤 “배포 성공”만 보고 끝내면 위험합니다. 제가 확인하는 기준은 네 가지입니다. 파드가 Ready인지, 재시작이 멈췄는지, Service 엔드포인트에 포함됐는지, 그리고 실패 이벤트가 더 이상 증가하지 않는지입니다. 이 확인 루틴은 짧지만, 운영에서는 진짜 편하더라고요.

    NS=default
    APP=probe-demo
    
    kubectl rollout status deployment/$APP -n $NS
    kubectl get pods -n $NS -l app=$APP -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,PHASE:.status.phase,NODE:.spec.nodeName'
    kubectl get endpoints $APP -n $NS -o wide
    kubectl get endpointslice -n $NS -l kubernetes.io/service-name=$APP -o wide
    kubectl describe deployment $APP -n $NS
    kubectl get events -n $NS --field-selector involvedObject.kind=Pod --sort-by=.lastTimestamp

    판단은 이렇게 가져갑니다. READY가 모든 컨테이너 수와 일치하면 요청을 받을 준비가 된 상태입니다. RESTARTS가 계속 늘면 Liveness 실패 또는 애플리케이션 크래시를 봅니다. EndpointSlice에 파드 IP가 없으면 Readiness 실패, Service selector 불일치, 포트명 불일치, 또는 라벨 문제를 봅니다. connection refused는 포트와 바인딩 주소, timeout은 앱 지연과 리소스 압박, 404는 경로 불일치부터 확인하세요.

    Kubernetes 프로브 트러블슈팅을 위한 kubectl 진단 결과 화면

    운영 대시보드가 있다면 프로브 실패율만 따로 보는 것도 좋습니다. 다만 프로브 자체가 너무 자주 실행되면 노드와 애플리케이션에 부하가 됩니다. 특히 exec 프로브는 컨테이너 안에서 프로세스를 실행하므로, 파드 수가 많고 주기가 짧으면 비용이 눈에 띄게 늘 수 있습니다. HTTP나 TCP로 충분한 경우라면 굳이 셸 명령을 매번 실행하지 않는 편이 낫습니다.

    Kubernetes 프로브 모범 사례와 선택 기준

    제가 팀에 리뷰할 때 가장 많이 보는 항목은 세 가지입니다. 첫째, Liveness가 외부 의존성을 검사하지 않는가. 둘째, Readiness가 실제 트래픽 처리 가능성을 반영하는가. 셋째, 느린 기동을 initialDelaySeconds 땜질이 아니라 Startup Probe로 풀었는가. 이 세 가지만 잡아도 Kubernetes 프로브 트러블슈팅의 절반은 줄어듭니다.

    상황별 추천

    • 애플리케이션 시작이 느리다면: Startup Probe를 추가하고 Liveness의 initialDelaySeconds를 과하게 늘리지 않습니다.
    • DB가 잠깐 끊길 수 있다면: DB 검사는 Readiness Probe에 둡니다. Liveness에서 DB를 때리지 않습니다.
    • 프로세스가 가끔 멈춘다면: 로컬 핸들러 기반의 가벼운 Liveness Probe로 재시작하게 합니다.
    • 경로가 자주 바뀐다면: 비즈니스 API와 헬스 체크 엔드포인트를 분리하고, 라우팅 리팩터링에 영향받지 않는 고정 경로를 씁니다.
    • 서비스 메시나 Ingress를 쓴다면: kubelet의 Pod 프로브, Service 라우팅, 외부 로드밸런서 헬스 체크를 서로 다른 계층으로 보고 분리해서 검증합니다.
    • 파드 수가 많다면: 짧은 periodSeconds와 무거운 exec 프로브 조합을 피합니다. 헬스 체크도 트래픽입니다.

    자주 묻는 질문

    Q. Liveness와 Readiness를 같은 경로로 써도 되나요?
    가능은 하지만 운영 기본값으로 추천하지 않습니다. 같은 경로가 DB나 외부 API를 검사하면 Readiness 실패로 끝날 문제가 Liveness 재시작으로 커질 수 있습니다. 정말 같은 경로를 써야 한다면 그 경로는 로컬 상태만 검사해야 합니다.

    Q. TCP Probe와 HTTP Probe 중 뭘 써야 하나요?
    HTTP 서비스라면 HTTP Probe가 보통 더 낫습니다. 상태 코드와 경로로 의도를 표현할 수 있기 때문입니다. TCP Probe는 포트가 열렸는지만 확인하므로, 앱이 내부 초기화 중이어도 성공할 수 있습니다.

    Q. Exec Probe는 언제 쓰나요?
    HTTP 엔드포인트를 만들기 어려운 배치성 프로세스나 레거시 앱에서 컨테이너 내부 상태 파일·명령 결과를 확인할 때 씁니다. 다만 명령 실행 비용, 이미지 안의 바이너리 존재 여부, 셸 의존성을 반드시 확인해야 합니다.

    Q. gRPC 서비스는 어떻게 하나요?
    gRPC Health Checking Protocol을 구현했다면 Kubernetes v1.27부터 안정화된 gRPC probe를 검토할 수 있습니다. gRPC probe는 HTTP/TCP probe와 달리 named port를 지원하지 않으므로 숫자 포트를 명시해야 합니다. 서비스 이름과 포트 설정을 명확히 하고, HTTP 게이트웨이와 혼동하지 않도록 매니페스트 리뷰를 거치는 편이 좋습니다.

    Liveness Probe Readiness Probe Startup Probe 선택 기준 요약

    공식 동작 기준은 Kubernetes 문서의 Configure Liveness, Readiness and Startup Probes와 Liveness, Readiness, and Startup Probes를 기준으로 확인하되, 실제 값은 서비스의 기동 로그와 장애 패턴에 맞춰 조정하세요. 문서의 예시는 출발점이지 운영 정책 그 자체는 아닙니다. 관련 배포 전략이나 장애 대응 문서가 있다면 내부 링크로 함께 묶어두면 독자가 다음 글로 자연스럽게 이동합니다.

    마무리: 운영에서 덜 아프게 쓰는 결론

    프로브 설계의 기준은 명확합니다. 재시작하면 좋아지는 문제만 Liveness에 넣고, 트래픽만 빼면 되는 문제는 Readiness에 넣고, 느린 기동은 Startup Probe로 보호하세요. 이 원칙을 어기면 프로브는 장애 감지 장치가 아니라 장애 증폭 장치가 됩니다.

    일반 웹 서비스라면 저는 이렇게 시작합니다. Liveness는 /livez에서 프로세스와 기본 핸들러만 확인합니다. Readiness는 /readyz에서 DB 연결, 필수 설정, 내부 큐 상태처럼 요청 처리 조건을 확인합니다. 기동이 느린 서비스는 /startupz를 두고, Startup Probe가 성공하기 전까지 Liveness가 개입하지 않게 합니다. 배포 후에는 kubectl describe pod, kubectl logs --previous, kubectl get endpoints, kubectl get endpointslice를 함께 확인합니다. 이 네 가지를 습관처럼 보면 Kubernetes 프로브 트러블슈팅에서 가장 흔한 삽질을 꽤 많이 줄일 수 있습니다.

  • [k8s] Cluster Autoscaler에서 Karpenter로 마이그레이션 결정 기준 및 전환 전략

    [k8s] Cluster Autoscaler에서 Karpenter로 마이그레이션 결정 기준 및 전환 전략

    [Kubernetes] Cluster Autoscaler에서 Karpenter로 마이그레이션: 언제, 어떻게 전환할까요?

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 Kubernetes(쿠버네티스) 환경에서 노드 오토스케일링(Auto Scaling)을 고민하는 분들을 위한 이야기를 해볼까 합니다. 특히 EKS (Amazon Elastic Kubernetes Service)를 운영하면서 Cluster Autoscaler (CA)의 한계를 느끼고 Karpenter로의 마이그레이션을 고민하는 분들이라면, 제가 직접 겪었던 경험과 삽질 끝에 얻은 노하우가 도움이 될 거예요.

    클라우드 환경에서 Kubernetes 클러스터를 운영하다 보면, 워크로드(Workload)의 변화에 따라 노드를 유연하게 늘리고 줄이는 것이 정말 중요합니다. 비용 최적화는 물론이고, 서비스 안정성에도 직결되거든요. 처음에는 Cluster Autoscaler(클러스터 오토스케일러)가 만능인 줄 알았는데, 막상 써보니 아쉬운 점들이 꽤 있더라고요. 특히 급작스러운 트래픽 증가나 스팟 인스턴스(Spot Instance) 활용 시 Karpenter가 보여주는 퍼포먼스는 저를 깜짝 놀라게 했습니다.

    오늘은 Cluster Autoscaler에서 Karpenter로의 전환을 언제 고려해야 하는지, 그리고 실제 전환은 어떻게 진행해야 하는지에 대한 저만의 결정 기준과 전략을 솔직하게 공유해 드릴게요. 삽질 과정도 가감 없이 보여드릴 테니, 함께 살펴보시죠!

    Karpenter가 도입된 Kubernetes 클러스터의 노드 오토스케일링 아키텍처 개요도

    Cluster Autoscaler와 Karpenter: 핵심 개념 차이

    본격적인 마이그레이션 이야기를 하기 전에, 두 오토스케일링 도구가 어떻게 다른지 간단히 짚고 넘어갈게요. 쉽게 말해 접근 방식 자체가 다릅니다.

    Cluster Autoscaler (CA): 그룹 기반 오토스케일링

    • 작동 방식: Cluster Autoscaler는 AWS Auto Scaling Group (ASG)을 기반으로 작동합니다. 즉, 미리 정의된 ASG의 최소/최대 인스턴스 개수 범위 내에서 노드를 추가하거나 줄이죠.
    • 장점: 비교적 설정이 간단하고, 오랫동안 사용되어 안정성이 검증되었습니다. 기존 EC2 인스턴스 관리 방식에 익숙하다면 접근하기 쉽습니다.
    • 단점:
      • 느린 스케일 아웃(Scale Out) 속도: ASG가 새 인스턴스를 프로비저닝(Provisioning)하는 데 시간이 걸립니다. 워크로드 요청이 급증할 때 노드 부족 현상이 생길 수 있어요.
      • 비효율적인 리소스 활용: 특정 파드(Pod)에 필요한 리소스 타입을 정확히 맞추기 어렵습니다. ASG는 특정 인스턴스 타입이나 몇 가지 인스턴스 타입 묶음으로 구성되니까요. 이 때문에 kube-scheduler (쿠베 스케줄러)가 파드를 노드에 배치하지 못하는 ‘스케줄링 불가(unschedulable)’ 상태가 발생할 수 있습니다.
      • 스팟 인스턴스 활용 제약: 스팟 인스턴스를 활용하더라도, ASG의 인스턴스 풀(Pool)이 제한적이라 유연성이 떨어집니다.

    Karpenter: 파드 기반 오토스케일링

    • 작동 방식: Karpenter는 스케줄링되지 않은 파드를 직접 감지하고, 해당 파드에 가장 적합한 EC2 인스턴스를 직접 프로비저닝합니다. ASG를 거치지 않고 EC2 RunInstances API를 직접 호출해서 노드를 띄우는 거죠.
    • 장점:
      • 압도적인 스케일 아웃 속도: 필요한 노드를 거의 실시간으로 생성하기 때문에 워크로드 변화에 매우 빠르게 대응합니다. 제가 직접 써보니 CA보다 훨씬 빨랐습니다.
      • 최적의 리소스 활용: 파드가 요구하는 CPU, 메모리, GPU 등 리소스에 맞춰 가장 효율적인 EC2 인스턴스 타입을 선택합니다. 스팟 인스턴스 활용도 극대화해서 비용 절감 효과가 커요.
      • 단순한 관리: ASG를 직접 관리할 필요 없이, Karpenter의 Provisioner (프로비저너) 설정 하나로 노드 프로비저닝 정책을 관리할 수 있습니다.
    • 단점:
      • 초기 학습 곡선: 새로운 개념과 설정이 필요해서 처음에는 좀 낯설 수 있습니다.
      • 클라우드 종속성: 현재는 AWS EKS에 특화되어 있습니다 (다른 클라우드 지원도 개발 중이지만, 현재로선 AWS가 주력입니다).

    Karpenter로의 마이그레이션 결정 기준: 언제 전환해야 할까요?

    제가 13년차 인프라 엔지니어로서 여러 환경을 경험해보니, 마이그레이션은 항상 신중해야 하더라고요. 무조건 좋다고 따라가는 것보다, 우리 서비스에 어떤 이점이 있을지 명확히 파악하는 게 중요합니다. 다음 질문들에 해당한다면 Karpenter로의 전환을 적극적으로 고려해볼 때입니다.

    • 잦은 스케일링 이벤트와 느린 노드 확장 속도에 불만이 있다:

      특히 이벤트 기반(Event-driven) 서비스나 예측 불가능한 트래픽 패턴을 가진 서비스라면 CA의 스케일 아웃 속도가 병목이 될 수 있습니다. "파드가 Pending(대기) 상태인데 노드가 왜 이렇게 늦게 뜨지?" 라는 질문을 자주 하셨다면 Karpenter가 답이 될 수 있습니다.

    • 클라우드 비용 최적화가 시급하다:

      CA는 ASG가 정해준 인스턴스 타입 내에서만 노드를 띄우다 보니, 파드의 리소스 요구사항과 정확히 일치하는 인스턴스를 찾기 어렵습니다. Karpenter는 파드에 딱 맞는 최적의 인스턴스를 찾아 스팟 인스턴스까지 적극적으로 활용하기 때문에, 불필요한 리소스 낭비를 줄여줍니다. 저희 홈랩에서도 스팟 인스턴스 활용률이 훨씬 높아지면서 비용이 꽤 절감됐습니다.

    • 노드 타입 관리가 복잡하다고 느낀다:

      CA를 사용하면 다양한 워크로드를 위해 여러 ASG를 만들고 관리해야 할 때가 많습니다. Karpenter는 Provisioner (프로비저너) 하나로 다양한 인스턴스 타입을 유연하게 관리할 수 있어 운영 부담이 줄어듭니다.

    • 특정 리소스(GPU, 고성능 CPU 등) 요구사항이 있는 파드가 많다:

      머신러닝(Machine Learning) 워크로드처럼 특정 GPU나 고성능 CPU를 요구하는 파드들이 있다면, Karpenter가 해당 파드에 맞는 노드를 즉시 프로비저닝해서 스케줄링 효율을 극대화할 수 있습니다.

    Karpenter로의 전환 전략: 실전 구현

    이제 Karpenter로 마이그레이션하는 실제 과정을 단계별로 살펴보겠습니다. 저는 EKS 환경을 기준으로 설명드릴게요.

    단계 1: Karpenter 설치 및 IAM 권한 설정

    Karpenter는 AWS API를 직접 호출해서 EC2 인스턴스를 생성하고 관리해야 하므로, 적절한 IAM(Identity and Access Management) 권한이 필수입니다. 공식 문서에 따라 IAM Role과 Service Account를 생성하고, Karpenter 컨트롤러를 설치합니다. 이 부분은 공식 문서가 워낙 잘 되어 있어서 그대로 따라가시면 됩니다. 핵심은 Karpenter가 EC2, IAM, SSM 등의 리소스에 접근할 수 있는 권한을 부여하는 것입니다.

    설치 후에는 Karpenter Controller Pod가 잘 뜨는지 확인해야 합니다.

    kubectl get pods -n karpenter
    # 예시 출력:
    # NAME                         READY   STATUS    RESTARTS   AGE
    # karpenter-controller-xxxxx   1/1     Running   0          5m
    

    단계 2: Provisioner 설정

    Karpenter의 핵심은 바로 Provisioner 리소스입니다. 어떤 종류의 노드를, 어떤 조건으로 생성할지 여기에 정의합니다. 처음에는 너무 복잡하게 생각하지 마시고, 가장 기본적인 설정으로 시작하는 것을 추천합니다.

    Karpenter Provisioner YAML 설정 예시와 주요 파라미터 설명

    Karpenter Provisioner YAML 설정 예시와 주요 파라미터 설명

    apiVersion: karpenter.sh/v1beta1
    kind: Provisioner
    metadata:
      name: default
    spec:
      # Karpenter가 사용할 AMI Family를 정의합니다. EKS 최신 Optimized AMI를 사용하세요.
      # (예: AL2, AL2023, Bottlerocket, Ubuntu)
      # Karpenter는 여기에 지정된 AMI Family의 최신 EKS Optimized AMI를 자동으로 선택합니다.
      # official EKS AMIs: https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html
      amiFamily: AL2 # Amazon Linux 2 (EKS Optimized AMI) 또는 AL2023
    
      # 인스턴스 프로비저닝에 대한 요구사항을 정의합니다.
      # 여기서는 amd64 아키텍처의 온디맨드 또는 스팟 인스턴스를 사용하도록 설정했습니다.
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand", "spot"] # 스팟 인스턴스 활용을 적극 권장합니다!
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"] # 컴퓨팅, 메모리, 범용 인스턴스 카테고리
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["c5", "c6i", "m5", "m6i", "r5", "r6i"] # 자주 사용하는 인스턴스 패밀리
        - key: karpenter.k8s.aws/instance-size
          operator: NotIn
          values: ["nano", "micro"] # 너무 작은 인스턴스는 제외
    
      # 노드가 어떤 서브넷(Subnet)에 생성될지 태그(Tag)로 정의합니다.
      # EKS 클러스터가 사용하는 서브넷 태그와 일치해야 합니다.
      subnetSelector:
        karpenter.sh/discovery: "your-eks-cluster-name" # EKS 클러스터 이름으로 교체
    
      # 노드가 어떤 보안 그룹(Security Group)을 사용할지 태그로 정의합니다.
      securityGroupSelector:
        karpenter.sh/discovery: "your-eks-cluster-name" # EKS 클러스터 이름으로 교체
    
      # 노드에 적용될 테인츠(Taints)를 정의합니다.
      # 여기에 테인트가 있으면, 해당 테인트를 허용하는 파드만 스케줄링됩니다.
      # - key: my-app
      #   effect: NoSchedule
    
      # 노드 템플릿입니다. SSH 키페어, IAM 인스턴스 프로파일 등을 지정할 수 있습니다.
      providerRef:
        name: default
    
      # 노드 삭제 정책 (consolidation).
      # 기본적으로 파드가 없거나, 더 효율적인 노드로 대체될 수 있으면 노드를 삭제합니다.
      consolidation:
        enabled: true
        # consolidateAfter: 5m # 노드 생성 후 최소 5분은 유지 (선택 사항)
    
      # 노드 TTL(Time To Live) 설정. 노드가 일정 시간 동안 유휴 상태이거나
      # 최대 수명을 넘으면 삭제됩니다.
      ttlSecondsAfterEmpty: 300 # 노드에 파드가 없으면 300초(5분) 후 삭제
      ttlSecondsUntilExpired: 604800 # 노드는 최대 7일(604800초)까지만 유지 (수명 관리)
    
    ---
    apiVersion: karpenter.k8s.aws/v1beta1
    kind: AWSNodeTemplate
    metadata:
      name: default
    spec:
      # Karpenter가 사용할 IAM 인스턴스 프로파일을 지정합니다.
      # EKS 노드에 필요한 권한이 포함된 프로파일이어야 합니다.
      # 보통 `eksctl create iamserviceaccount` 등으로 생성됩니다.
      instanceProfile: "KarpenterNodeInstanceProfile-your-cluster-name" # 실제 프로파일 이름으로 교체
    
      # SSH 키페어를 지정하여 노드에 SSH 접속이 가능하도록 합니다. (선택 사항)
      # sshName: your-ssh-key-name
    
      # 노드에 추가적인 태그를 붙일 수 있습니다.
      tags:
        environment: production
        managed-by: karpenter
        karpenter.sh/cluster-name: "your-eks-cluster-name" # 클러스터 이름으로 교체
    

    위 YAML을 적용하면 Karpenter가 이제 파드를 감지하고 노드를 프로비저닝할 준비가 됩니다. 여기서 중요한 포인트는 requirements 섹션입니다. 어떤 인스턴스 타입을 사용할지, 스팟(Spot)을 쓸지 온디맨드(On-Demand)를 쓸지 등 Karpenter의 동작 방식을 결정하는 핵심 설정입니다. 저는 스팟 인스턴스를 적극적으로 활용해서 비용을 절감하는 방향으로 설정했습니다.

    단계 3: Cluster Autoscaler (CA) 노드 그룹 점진적 비활성화

    Karpenter가 잘 작동하는지 확인하면서, 기존 CA가 관리하던 노드 그룹(ASG)을 점진적으로 스케일 다운(Scale Down)해야 합니다. 한 번에 모든 노드 그룹을 비활성화하면 서비스 중단 위험이 있으니, 워밍업(Warm-up) 기간을 두는 것이 좋습니다.

    1. 새로운 파드를 배포하거나 기존 파드를 재시작하여 Karpenter가 새 노드를 잘 띄우는지 관찰합니다.
    2. Karpenter가 프로비저닝한 노드가 충분히 확보되었다고 판단되면, CA가 관리하는 ASG의 desired capacity를 0으로 설정합니다.
    3. ASG의 최소/최대 인스턴스 수도 0으로 설정하여 CA가 더 이상 노드를 관리하지 않도록 합니다.
    # EKS 노드 그룹 목록 확인 (eksctl 사용 예시)
    eksctl get nodegroup --cluster your-eks-cluster-name
    
    # 특정 노드 그룹의 desired capacity를 0으로 설정
    # (eksctl 명령은 ASG도 함께 조정합니다)
    eksctl scale nodegroup --cluster your-eks-cluster-name --name your-nodegroup-name --nodes 0 --nodes-min 0 --nodes-max 0
    
    # 또는 AWS CLI로 ASG 직접 수정 (ASG 이름 확인 필요)
    # aws autoscaling update-auto-scaling-group --auto-scaling-group-name your-asg-name --desired-capacity 0 --min-size 0 --max-size 0
    

    이렇게 하면 CA가 관리하던 노드들은 서서히 드레인(Drain)되고 종료되며, Karpenter가 그 역할을 완전히 넘겨받게 됩니다. 이 과정에서 파드들이 새로운 Karpenter 노드로 잘 재스케줄링되는지 꼼꼼히 확인해야 합니다.

    주의사항 및 트러블슈팅: 삽질 경험

    제가 Karpenter를 도입하면서 겪었던 몇 가지 삽질 경험과 주의사항을 공유합니다. 이걸 미리 아셨다면 여러분은 저보다 훨씬 적게 고생하실 거예요. 😅

    • ⚠️ IAM 권한은 정말 중요합니다!

      Karpenter가 EC2 인스턴스를 직접 다루기 때문에 IAM 권한이 조금만 잘못되어도 노드가 뜨지 않습니다. 특히 ec2:RunInstances, ec2:TerminateInstances, iam:PassRole 등의 권한이 제대로 부여되었는지 반드시 확인하세요. 저는 iam:PassRole을 빼먹어서 한참 헤맸습니다. Karpenter Controller 로그를 확인하면 어떤 권한이 부족한지 명확하게 나옵니다.

      # Karpenter Controller 로그 확인
      kubectl logs -f -n karpenter $(kubectl get pods -n karpenter -l app.kubernetes.io/name=karpenter -o name)
      
    • Pod Disruption Budget (PDB) 고려:

      Karpenter는 노드 통합(Consolidation) 기능을 통해 불필요한 노드를 효율적으로 제거합니다. 이때 PDB (Pod Disruption Budget)가 설정된 파드들은 Karpenter의 노드 종료 작업을 방해할 수 있습니다. PDB가 너무 엄격하면 Karpenter가 노드를 줄이지 못하고 비용이 낭비될 수 있으니, 워크로드의 특성에 맞춰 PDB를 적절히 설정해야 합니다.

    • Initial Scale-up Delay (초기 스케일업 지연) 착시:

      Karpenter는 ASG를 사용하지 않고 바로 EC2 인스턴스를 띄우기 때문에, 처음 노드가 올라올 때까지의 시간이 CA보다 오히려 길게 느껴질 수 있습니다. 하지만 이는 인스턴스 부팅 및 EKS 클러스터 조인(Join) 과정이 포함되기 때문이고, 실제 “스케줄링되지 않은 파드”를 “노드에 할당”하는 과정은 훨씬 빠릅니다. 이 차이를 이해하고 인내심을 가져야 합니다. 🙂

    • Spot Instance Interruptions (스팟 인스턴스 중단) 대비:

      Karpenter는 스팟 인스턴스를 적극 활용하여 비용을 절감하지만, 스팟 인스턴스는 언제든지 AWS에 의해 중단될 수 있습니다. 중요한 스테이트풀(Stateful) 워크로드에는 스팟 인스턴스를 사용하지 않거나, 중단에 강한 아키텍처(예: 분산 시스템, 재시작 가능한 작업)를 구성하는 것이 중요합니다.

    검증 및 결과 확인

    Karpenter 마이그레이션이 성공적으로 이루어졌는지 확인하는 방법은 다음과 같습니다.

    1. 노드 상태 확인: kubectl get nodes 명령으로 노드가 잘 뜨고 준비(Ready) 상태인지 확인합니다. Karpenter가 띄운 노드에는 특정 레이블(Label)이나 태그가 붙어있으니 쉽게 구분할 수 있습니다.
      kubectl get nodes -l karpenter.sh/provisioner-name=default
      # 예시 출력:
      # NAME                                           STATUS   ROLES    AGE   VERSION
      # ip-10-0-x-x.ap-northeast-2.compute.internal    Ready    <none>   5m    v1.28.x
      
    2. 파드 스케줄링 확인: 새로운 파드를 배포하거나, 기존 파드의 리소스 요청을 늘려서 Karpenter가 노드를 빠르게 프로비저닝하고 파드를 스케줄링하는지 확인합니다.
    3. Karpenter Provisioner 상태 확인:
      kubectl describe provisioner default
      # 이 명령을 통해 Karpenter가 어떤 노드를 프로비저닝하고 있는지,
      # 그리고 어떤 이벤트가 발생했는지 상세히 볼 수 있습니다.
      
    4. AWS EC2 콘솔 확인: EC2 콘솔에서 인스턴스가 Karpenter에 의해 생성되고 종료되는 것을 직접 관찰합니다. 태그를 통해 Karpenter가 관리하는 인스턴스인지 쉽게 알 수 있습니다.
    5. 비용 모니터링: AWS Cost Explorer를 통해 마이그레이션 전후의 비용 변화를 모니터링합니다. 특히 스팟 인스턴스 활용률이 높아지면서 비용이 얼마나 절감되었는지 확인하는 것이 중요합니다.
    Karpenter 도입 후 EKS 클러스터의 노드 및 파드 스케일링 성능 대시보드

    Karpenter 도입 후 EKS 클러스터의 노드 및 파드 스케일링 성능 대시보드

    Cluster Autoscaler vs. Karpenter: 비교 및 선택 기준

    제가 경험한 바를 바탕으로 두 오토스케일러의 핵심 차이점을 표로 정리해봤습니다. 어떤 상황에서 어떤 도구가 더 유리한지 판단하는 데 도움이 될 거예요.

    구분 Cluster Autoscaler (CA) Karpenter
    작동 방식 Auto Scaling Group (ASG) 기반 스케줄링되지 않은 파드 직접 감지, EC2 API 호출
    노드 프로비저닝 속도 상대적으로 느림 (ASG의 인스턴스 생성 시간) 매우 빠름 (파드 요구사항에 맞춰 즉시 생성)
    리소스 효율성 ASG 인스턴스 타입 내에서 선택, 비효율 가능성 파드 요구사항에 가장 적합한 인스턴스 선택, 높은 효율성
    비용 최적화 ASG 설정에 의존, 스팟 활용 제한적 스팟 인스턴스 적극 활용, 비용 절감 효과 큼
    관리 복잡성 여러 ASG 관리 필요 Provisioner 리소스 하나로 관리, 단순함
    주요 사용 사례 워크로드 변동이 적고 안정적인 환경, 온디맨드 위주 잦은 스케일링, 비용 최적화, 스팟 인스턴스 적극 활용 환경
    트러블슈팅 난이도 ASG 문제, CA 로그 Karpenter Controller 로그, IAM 권한, Provisioner 설정
    Kubernetes Cluster Autoscaler와 Karpenter의 주요 특징 및 마이그레이션 결정 기준 비교

    Kubernetes Cluster Autoscaler와 Karpenter의 주요 특징 및 마이그레이션 결정 기준 비교

    마무리하며: 더 나은 클러스터 운영을 위한 선택

    Cluster Autoscaler에서 Karpenter로의 마이그레이션은 단순히 도구를 바꾸는 것을 넘어, Kubernetes 클러스터의 리소스 관리 효율성과 비용 최적화를 한 단계 끌어올리는 중요한 결정입니다. 제가 직접 경험해보니, 특히 워크로드 변동성이 크고 스팟 인스턴스 활용을 통해 비용을 절감하고자 하는 환경이라면 Karpenter가 단연코 훌륭한 선택지였습니다.

    물론 새로운 도구를 도입하는 데는 초기 설정과 학습 비용이 따릅니다. 하지만 Karpenter가 제공하는 유연성과 효율성을 고려하면 충분히 투자할 가치가 있다고 생각합니다. 이 글이 여러분의 Karpenter 마이그레이션 여정에 작은 등불이 되었기를 바랍니다. 혹시 더 궁금한 점이나 제가 겪지 못했던 다른 삽질 경험이 있다면 댓글로 공유해주세요. 함께 배우고 성장하는 것이 인프라 엔지니어의 숙명이 아니겠습니까! 다음번에는 Karpenter의 고급 기능이나 다른 클라우드 환경에서의 활용 방안에 대해서도 이야기해볼 수 있으면 좋겠네요. 🎉

  • [k8s] Kubernetes 리소스 최적화 체크리스트: Karpenter로 클라우드 비용 절감하기

    [k8s] Kubernetes 리소스 최적화 체크리스트: Karpenter로 클라우드 비용 절감하기

    Kubernetes 리소스 최적화 체크리스트: Karpenter로 클라우드 비용 절감하기

    안녕하세요, 13년차 서버실 지킴이, 인프라 엔지니어입니다.
    오늘은 Kubernetes(쿠버네티스) 환경에서 Karpenter(카펜터)를 쓰시는 분들이라면 꼭 한번쯤 겪어보셨을 ‘리소스 최적화’에 대한 이야기를 해볼게요. 특히 Karpenter처럼 비용 효율을 극대화할 수 있는 오토스케일링 도구를 사용 중이라면, Pod(파드)의 리소스 요청(requests)과 제한(limits) 설정에 따라 비용이 천차만별로 달라지거든요. 저도 홈랩에서 Karpenter를 굴리면서 삽질을 좀 했었죠. 처음엔 그냥 대충 설정했다가 불필요한 노드들이 마구 스케일업 되는 걸 보고 깜짝 놀랐습니다. “아, 이거 뭔가 잘못됐구나!” 싶었어요. 그래서 오늘은 Karpenter의 효율을 최대한 끌어올리기 위한 Kubernetes 리소스 최적화 체크리스트를 공유해보겠습니다.

    쿠버네티스 파드 리소스 요청에 따른 카펜터 오토스케일링 개념도

    파드 리소스 설정에 따라 Karpenter가 노드를 어떻게 스케일링하는지 보여주는 개념도입니다.

    개념 설명: Requests와 Limits, 그리고 Kubernetes 리소스 최적화

    Kubernetes에서 Pod의 리소스 설정은 크게 두 가지로 나뉩니다.

    • Requests (요청): Pod가 스케줄링될 때 필요한 최소한의 리소스 양이에요. 스케줄러는 이 requests 값을 기준으로 Pod를 어느 노드에 배치할지 결정합니다. 즉, Pod가 안정적으로 시작하고 실행될 수 있는 ‘보장된’ 리소스죠. 만약 노드에 이만큼의 리소스 여유가 없으면 Pod는 스케줄링이 안 되고 Pending 상태로 머물게 됩니다.
    • Limits (제한): Pod가 사용할 수 있는 최대 리소스 양을 의미해요. limits를 설정하면 Pod가 이 값을 초과하여 리소스를 사용하는 것을 막을 수 있습니다. 예를 들어, CPU limits를 설정하면 Pod가 해당 CPU 코어 이상을 사용하지 못하게 OS 레벨에서 제한하고, 메모리 limits를 초과하면 OOMKilled(Out Of Memory Killed)되어 Pod가 재시작될 수 있으니까요.

    Karpenter는 바로 이 requests 값을 기반으로 노드를 프로비저닝(provisioning)합니다. 만약 Pod들이 요청하는 리소스가 너무 적거나, 아예 requests를 설정하지 않으면 Karpenter는 최적의 노드 타입을 찾기 어려워하거나, 너무 작은 노드를 프로비저닝해서 Pod들이 제대로 실행되지 못하게 될 수 있어요. 반대로 requests가 너무 높으면 불필요하게 큰 노드들이 프로비저닝되어 비용이 낭비될 수밖에 없죠.

    저 홈랩에서 겪었던 대표적인 삽질이 바로 이 requests 값 설정 미스였어요. Pod들이 필요한 리소스는 꽤 되는데 requests를 너무 낮게 잡았더니, Karpenter가 작은 노드를 띄우고 Pod들이 노드에서 CPU Throttling(쓰로틀링, CPU 사용량 제한)에 걸리거나 OOMKilled 되는 일이 빈번했습니다. 결국 노드가 계속 스케일업 되면서 비용만 더 나가는 상황이 발생하더라고요.

    Karpenter 효율 극대화를 위한 리소스 최적화 체크리스트

    자, 그럼 본론으로 들어가서 Karpenter와 Kubernetes 리소스를 효율적으로 관리하기 위한 체크리스트를 하나씩 짚어볼게요.

    1. 모든 컨테이너에 requests 설정하기 (필수!) 💡

    가장 기본적인 원칙이자 제가 가장 강조하는 부분입니다. 모든 컨테이너에는 requests를 명시적으로 설정해야 해요. limits는 선택 사항일 수 있지만, requests는 반드시 있어야 Karpenter가 노드 용량을 정확하게 예측하고 효율적인 노드를 스케일링할 수 있습니다. requests가 없으면 Kubernetes는 기본적으로 Pod가 노드의 모든 리소스를 요청한다고 간주하거나, 아예 스케줄링 판단을 내리기 어려워집니다.

    # Pod 리소스 요청(requests)과 제한(limits) 예시
    apiVersion: v1
    kind: Pod
    metadata:
      name: my-app-pod
    spec:
      containers:
      - name: my-app-container
        image: my-repo/my-app:latest
        resources:
          requests:
            cpu: "200m"  # 0.2 vCPU 요청
            memory: "256Mi" # 256 MiB 메모리 요청
          limits:
            cpu: "500m"  # 0.5 vCPU 제한 (선택 사항)
            memory: "512Mi" # 512 MiB 메모리 제한 (선택 사항)
    

    Kubernetes Pod 정의에서 resources.requests를 명시하는 YAML 예시입니다.

    쿠버네티스 파드 리소스 요청(requests) 및 제한(limits) 적용 개념도

    Kubernetes 파드 리소스 요청(requests) 및 제한(limits) 적용 개념도입니다.

    여기서 200m는 0.2 CPU 코어를 의미하고, 256Mi는 256 메비바이트(MiB)를 의미해요. ‘m’은 밀리코어(millicore), ‘Mi’는 메비바이트입니다. 이 단위를 정확히 아는 게 중요하더라고요. 저도 처음엔 MB랑 MiB가 같은 건 줄 알았다가 삽질했었죠.

    2. requests는 실제 평균 사용량에 가깝게 설정하기 ✅

    requests는 Pod가 안정적으로 동작하는 데 필요한 최소한의 리소스여야 합니다. 너무 낮게 설정하면 Pod가 스케줄링되더라도 리소스 부족으로 성능 저하를 겪거나 OOMKilled될 수 있어요. 반대로 너무 높게 설정하면 Karpenter가 필요 이상으로 큰 노드를 프로비저닝해서 비용 낭비로 이어지니까요.

    팁: Pod의 실제 리소스 사용량을 모니터링하세요. Prometheus(프로메테우스) + Grafana(그라파나) 조합으로 Pod의 CPU 및 메모리 사용량 추이를 확인하고, 평균 사용량에 약간의 버퍼를 더한 값으로 requests를 설정하는 게 좋습니다.

    # 특정 Pod의 리소스 사용량 확인 (metrics-server 설치 필수)
    # 현재 CPU, Memory 사용량은 확인할 수 있지만, 과거 트렌드는 모니터링 툴로 봐야 합니다.
    kubectl top pod <pod-name> -n <namespace> --containers
    
    # 예시:
    # kubectl top pod my-app-pod-abcde -n default --containers
    # CONTAINER      CPU(cores)   MEMORY(bytes)
    # my-app-container   120m         180Mi
    

    kubectl top pod 명령어로 특정 파드의 현재 리소스 사용량을 확인하는 예시입니다.

    3. CPU Limits는 신중하게 설정하기 ⚠️

    CPU Limits는 조금 복잡한 이야기네요. CPU는 ‘압축 가능한(compressible)’ 리소스라서, limits를 넘어도 Pod가 바로 죽는 대신 CPU 스케줄러에 의해 사용량이 제한(throttling)됩니다. 문제는 이 CPU 스로틀링이 애플리케이션의 응답 시간을 지연시키거나 성능 문제를 일으킬 수 있다는 점이에요.

    • CPU Limits를 requests와 동일하게 설정: 가장 안전한 방법입니다. Pod가 요청한 만큼만 CPU를 사용하도록 제한하여 다른 Pod에 영향을 주지 않죠. 하지만 버스트(burst) 성능이 필요한 경우 불리할 수 있습니다.
    • CPU Limits를 requests보다 높게 설정: Pod가 requests 이상으로 CPU를 사용할 수 있도록 여유를 줍니다. 버스트 성능이 중요하거나, 평소에는 CPU 사용량이 낮지만 가끔 순간적으로 높아지는 애플리케이션에 적합해요. 이 경우, 노드의 전체 CPU 사용량이 높아지면 특정 Pod들이 CPU 스로틀링을 겪을 가능성이 있습니다.
    • CPU Limits를 아예 설정하지 않음: Pod가 노드의 남은 CPU 리소스를 최대한 활용할 수 있으니까요. 이는 CPU 사용량이 예측 불가능하거나 매우 동적인 워크로드에 유리할 수 있지만, 특정 Pod가 다른 Pod의 성능에 영향을 줄 수 있는 위험이 있습니다.

    제 경험상, 대부분의 웹 서비스나 API 서버는 CPU Limits를 requests보다 조금 높게 설정하거나 아예 설정하지 않는 게 더 나았어요. 하지만 배치 작업이나 CPU 집약적인 워크로드는 다른 Pod에 영향을 주지 않도록 requests와 limits를 동일하게 설정하는 게 좋더라고요. 결국 워크로드의 특성을 이해하고 트레이드오프(trade-off)를 고려해야 합니다.

    설정 방식 장점 단점 권장 워크로드
    requests == limits 리소스 사용 예측 가능
    CPU 스로틀링 예측 가능
    버스트 성능 제한 CPU 집약적 배치, 예측 가능한 워크로드
    requests < limits 버스트 성능 허용
    평균 사용량 최적화
    노드 과부하 시 스로틀링 가능성
    리소스 사용량 예측 어려움
    웹 서비스, API 서버 (간헐적 트래픽 증가)
    limits 미설정 최대 성능 활용
    유연한 리소스 사용
    특정 Pod의 노드 CPU 독점 가능성
    스케줄링 불확실성 증가
    탐색적 분석, 개발 환경, CPU 사용량 예측 불가능한 워크로드

    CPU 요청(requests)과 제한(limits) 설정 방식에 따른 장단점 및 권장 워크로드를 비교한 표입니다.

    4. Memory Limits는 항상 설정하기 (매우 중요!) ⚠️

    메모리는 ‘압축 불가능한(non-compressible)’ 리소스에요. Memory Limits를 초과하면 Pod는 즉시 OOMKilled되어 재시작됩니다. 이는 서비스 가용성에 직접적인 영향을 미치죠. 따라서 Memory Limits는 항상 설정하는 걸 권장합니다.

    Memory Limits는 Pod가 최대로 사용할 수 있는 메모리 양에 약간의 여유를 더한 값으로 설정하는 게 좋아요. 만약 requests와 limits를 동일하게 설정하면 Pod가 예상치 못한 메모리 스파이크(spike)에 취약해질 수 있거든요. 저도 처음엔 requests와 limits를 동일하게 설정했다가, 순간적인 메모리 사용량 증가로 Pod가 계속 죽는 경험을 했었어요. 로그를 보니 OOMKilled가 찍혀있더군요. 결국 limits를 requests보다 넉넉하게 설정해서 해결했습니다.

    5. Namespace(네임스페이스)별 LimitRange 활용 고려

    만약 클러스터에 배포되는 모든 Pod에 일일이 requests와 limits를 설정하기 어렵다면, LimitRange 리소스를 활용하는 것도 좋은 방법입니다. LimitRange는 특정 네임스페이스 내에서 Pod의 기본 리소스 requests/limits를 설정하거나, 최소/최대 값을 강제할 수 있으니까요.

    # LimitRange 예시
    apiVersion: v1
    kind: LimitRange
    metadata:
      name: pod-resource-limits
    spec:
      limits:
      - default:
          cpu: 500m
          memory: 512Mi
        defaultRequest:
          cpu: 100m
          memory: 128Mi
        type: Container
    

    LimitRange를 사용하여 네임스페이스 내 컨테이너의 기본 리소스 요청/제한을 설정하는 YAML 예시입니다.

    이렇게 설정하면 해당 네임스페이스에 배포되는 Pod들은 별도의 resources 설정을 하지 않아도 defaultRequest와 default 값이 자동으로 적용됩니다. 물론, Pod 자체에서 resources를 명시하면 그 값이 우선합니다.

    6. HPA(Horizontal Pod Autoscaler)와 함께 사용 시 유의점

    HPA는 Pod의 CPU/메모리 사용량이나 커스텀 지표를 기반으로 Pod의 개수를 자동으로 조절해요. 여기서 CPU 사용량 기반 HPA는 Pod의 requests.cpu 값을 기준으로 동작합니다. 예를 들어, HPA 설정에서 targetCPUUtilizationPercentage: 80으로 지정했다면, Pod의 실제 CPU 사용량이 requests.cpu의 80%를 초과할 때 스케일 아웃(scale-out)이 발생하죠.

    만약 requests.cpu를 너무 낮게 설정하면, Pod의 실제 CPU 사용량이 낮더라도 requests 대비 높은 비율로 계산되어 불필요하게 HPA가 스케일 아웃을 유발할 수 있어요. 반대로 너무 높게 설정하면, 실제 Pod가 힘들게 일하고 있어도 requests 대비 낮은 비율로 계산되어 스케일 아웃이 늦어질 수 있습니다.

    이 때문에 requests.cpu는 Pod의 실제 부하 패턴을 잘 반영하는 값으로 설정하는 게 중요합니다.

    검증 및 결과 확인

    자, 이렇게 Kubernetes 리소스 설정을 최적화했다면, 실제로 Karpenter가 효율적으로 노드를 스케일링하고 있는지, Pod들이 안정적으로 동작하는지 확인해야겠죠?

    1. Karpenter 노드 프로비저닝 확인: kubectl get nodes 명령어로 노드들이 예상했던 인스턴스 타입으로, 필요한 만큼만 프로비저닝되었는지 확인합니다.
    2. Pod 상태 확인: kubectl get pods로 모든 Pod가 Running 상태인지, OOMKilled나 CrashLoopBackOff 같은 문제가 없는지 확인해요.
    3. 리소스 사용량 모니터링: Prometheus, Grafana 등을 통해 Pod들의 CPU/메모리 사용량, 스로틀링 발생 여부 등을 지속적으로 모니터링합니다. 특히 노드의 Allocatable(할당 가능) 리소스와 Pod들의 requests 합계를 비교하여 노드 활용률을 확인하는 게 중요합니다.
    4. 비용 절감 효과 측정: 클라우드 비용 청구서를 통해 이전과 비교하여 실제 비용이 얼마나 절감되었는지 확인하는 게 가장 확실한 지표입니다.
    Grafana 대시보드에서 파드 및 노드 리소스 사용량과 Karpenter 스케일링 결과 모니터링 화면

    최적화 후 Grafana 대시보드에서 파드 및 노드의 리소스 사용량과 Karpenter 스케일링 결과를 확인하는 모습입니다.

    마무리

    오늘은 Karpenter의 효율을 극대화하기 위한 Kubernetes 리소스 요청/제한 최적화 체크리스트를 살펴봤습니다. 13년차 인프라 엔지니어로 제가 직접 겪었던 삽질과 해결 경험을 바탕으로 이야기해드렸는데, 도움이 되셨으면 좋겠네요.

    결론적으로, Karpenter와 같은 오토스케일링 도구의 진정한 가치를 끌어내려면 Pod의 requests와 limits 설정을 제대로 이해하고, 워크로드의 특성에 맞춰 세밀하게 튜닝하는 과정이 필수적입니다.

    • requests는 모든 컨테이너에 반드시 설정하고, 실제 평균 사용량에 가깝게 맞춰야 해요.
    • Memory Limits는 항상 설정하여 OOMKilled를 방지하고, requests보다 넉넉하게 주는 게 좋습니다.
    • CPU Limits는 워크로드 특성을 고려하여 신중하게 설정해야 합니다. 버스트 성능이 중요하다면 requests보다 높게, 안정성이 우선이라면 requests와 동일하게 설정하는 것을 고려해보세요.

    이러한 최적화 작업을 통해 불필요한 노드 스케일업을 줄이고, 클라우드 비용을 절감하면서도 안정적인 서비스를 운영할 수 있을 거예요. 당장 눈에 띄는 효과는 없을지라도, 꾸준히 모니터링하고 튜닝하는 노력이 결국 큰 비용 절감으로 이어질 거라고 확신합니다. 여러분의 Kubernetes 클러스터 운영에 이 글이 작은 도움이 되었으면 좋겠습니다! 다음에는 Karpenter NodePool(노드풀) 전략에 대해 좀 더 깊이 있게 다뤄보겠습니다.

    Kubernetes 리소스 최적화 및 Karpenter 효율성 향상 핵심 체크리스트 인포그래픽

    Kubernetes 리소스 최적화 및 Karpenter 효율성 향상을 위한 핵심 체크리스트 요약 인포그래픽입니다.

  • [k8s] Kubernetes Liveness Probe: Readiness·Startup 차이와 설정 가이드

    [k8s] Kubernetes Liveness Probe: Readiness·Startup 차이와 설정 가이드

    Kubernetes Liveness Probe: Readiness, Startup 차이와 운영 가이드

    Kubernetes Liveness Probe를 처음 만졌을 때 가장 크게 데인 지점은, 프로브를 “상태 확인”이 아니라 “불안하면 일단 재시작” 버튼처럼 썼다는 점이었습니다. 앱이 조금 늦게 뜨기만 해도 죽은 것으로 판정했고, 결과는 뻔했죠. Pod(파드, 쿠버네티스의 최소 배포 단위)는 부팅 도중 반복 재시작했고, 실제 버그보다 운영 설정이 더 큰 장애를 만들더라고요.

    운영에서 프로브는 단순 헬스 체크가 아닙니다. 저는 이걸 장애 감지 장치보다 복구 정책 스위치에 가깝게 봅니다. Kubernetes Liveness Probe를 실패시키면 kubelet이 해당 컨테이너를 다시 시작하고, Readiness Probe가 실패하면 Service 트래픽 대상에서 빠지며, Startup Probe는 느린 기동 구간을 보호합니다. 같은 probe라도 실패 뒤에 이어지는 동작이 완전히 다르기 때문에, 이 차이를 설계하지 않고 문법만 맞추면 장애가 길어집니다.

    이번 글에서는 홈랩과 실무에서 반복해서 검증했던 기준으로, 언제 Liveness를 써야 하는지, 언제 오히려 빼는 게 나은지, Readiness를 어디까지 엄격하게 볼지, Startup Probe를 어떤 식으로 계산해 붙일지를 실행 가능한 예시와 함께 정리해보겠습니다. 배포 전략이나 Service 동작 원리가 헷갈린다면 블로그의 관련 글도 함께 읽어보시면 흐름이 더 잘 잡힙니다.

    Kubernetes Liveness Probe를 포함한 프로브 전체 흐름 아키텍처 이미지

    liveness, readiness, startup probe가 각각 어떤 순간에 동작하고 서비스 트래픽에 어떤 영향을 주는지 보여주는 개요 다이어그램입니다.

    Kubernetes Liveness Probe가 왜 중요한가

    Liveness Probe(라이브니스 프로브, 생존 확인)는 “이 프로세스를 계속 살려둘 가치가 있는가”를 묻습니다. 핵심은 회복 불가능성입니다. 요청이 일시적으로 느린 상태, DB가 잠깐 흔들린 상태, 외부 API가 timeout 나는 상태는 대개 liveness의 대상이 아니거든요. 반대로 이벤트 루프가 멈췄다거나, 스레드 데드락으로 더 이상 요청을 처리할 수 없고 자체 회복도 기대하기 어렵다면 liveness를 실패시켜 재기동시키는 편이 낫습니다.

    Readiness Probe(레디니스 프로브, 요청 처리 준비 상태 확인)는 질문이 다릅니다. 지금 이 Pod로 트래픽을 보내도 되나?를 판단하죠. 살아는 있지만 아직 준비가 안 됐거나, 의존 서비스가 불안정해 새 요청을 받으면 실패율만 올릴 상황이라면 readiness를 false로 두고 엔드포인트에서 빠지는 쪽이 안전합니다.

    Startup Probe(스타트업 프로브, 초기 기동 확인)는 더 실무적입니다. 느리게 뜨는 앱에게 “운영 중 기준”을 너무 일찍 들이대지 않게 해 줍니다. JVM 앱, 대용량 캐시를 적재하는 API, 마이그레이션 이후에만 정상 동작하는 서비스는 startup probe가 없으면 정상 기동 중에도 liveness에 맞아 죽기 쉽습니다.

    현장에서 가장 자주 보는 오해는 이것입니다. “헬스 체크는 엄격할수록 좋다.” 실제 운영은 반대인 경우가 많습니다. 프로브는 엄격함보다 의미 분리가 더 중요합니다. Liveness는 재시작을 정당화할 수 있을 때만, Readiness는 트래픽 차단이 이득일 때, Startup은 초기화 변동 폭을 흡수할 때 써야 합니다.

    Kubernetes Liveness Probe와 Probe 3종 역할 구분표

    Probe 종류 실무 질문 실패 시 쿠버네티스 동작 넣어야 하는 경우 빼거나 약하게 둬야 하는 경우
    Liveness Probe 프로세스가 회복 불가능하게 멈췄는가 컨테이너 재시작 deadlock, event loop 정지, 내부 워커 hang 외부 의존성 장애에 자주 흔들리는 앱, 자체 hang 가능성이 낮은 단순 API
    Readiness Probe 지금 요청을 받아도 되는가 Service 엔드포인트에서 제외 DB 연결 전, 캐시 워밍업 중, 소비자 초기화 전, 배포 중 drain 필요 상태 판단에 무거운 쿼리나 외부 API 호출이 필요한 경우
    Startup Probe 초기 부팅이 아직 끝나지 않았는가 기동 구간 보호, 실패 지속 시 재시작 JVM, 모델 로딩, schema migration, 큰 플러그인 초기화 기동 시간이 매우 짧고 변동 폭이 작은 앱

    운영 판단을 더 거칠게 요약하면 아래처럼 보시면 됩니다.

    • Liveness: 계속 두면 더 나빠질 프로세스만 다시 띄웁니다.
    • Readiness: 살아 있어도 지금은 손님 받지 말자는 신호입니다.
    • Startup: 부팅 중인 앱을 운영 기준으로 성급하게 심판하지 않기 위한 완충 장치입니다.

    이 차이를 놓치면 증상이 묘해집니다. Pod는 Running인데 요청은 503이 쌓이거나, DB가 잠깐 느려졌을 뿐인데 liveness가 재시작 루프를 만들어 장애 반경을 넓혀 버리죠. 프로브 설계의 핵심은 “정상/비정상 판정”보다 실패 도메인을 어디서 끊을지에 있습니다.

    Kubernetes 헬스 체크 방식도 성격이 다릅니다

    Kubernetes에서는 보통 HTTP GET, TCP Socket, Exec 세 방식으로 검사합니다. 셋 다 쓸 수 있다고 해서 셋 다 좋은 건 아닙니다. 중요한 건 무엇을 검증하고 무엇을 버리는지를 알고 선택하는 겁니다.

    • HTTP GET: 가장 해석이 쉽습니다. <code>/live, /ready처럼 의미를 분리하기 좋고, 애플리케이션 레벨 판단이 가능합니다.
    • TCP Socket: 포트가 연결 가능한지만 확인합니다. 포트는 열려 있는데 실제 요청 처리는 불안정한 상황은 못 잡을 수 있습니다.
    • Exec: 컨테이너 안에서 명령을 실행하므로 유연하지만, 프로세스 생성 비용과 스크립트 실패 모드까지 같이 관리해야 합니다.

    웹 애플리케이션이라면 대체로 Readiness는 HTTP, Liveness는 아주 가볍고 보수적으로, Startup은 실제 기동 시간을 기준으로 별도 구성하는 쪽을 권합니다. 반대로 배치 워커처럼 HTTP 서버가 없고, 작업 큐 polling 상태나 pid 파일 확인이 더 직접적인 경우에는 exec probe가 더 맞을 수 있습니다.

    중요한 판단 하나만 더 짚으면, Liveness에서 외부 의존성을 확인하는 순간 프로브는 자가치유 장치가 아니라 장애 증폭 장치가 되기 쉽습니다. DB, Redis, Kafka, 외부 API는 readiness 쪽에서 다루고, liveness는 프로세스 내부 생존성만 보는 편이 운영 사고가 적었습니다.

    실전 구현: Deployment에 Liveness, Readiness, Startup Probe 넣기

    재현 가능한 예시로 가보겠습니다. 아래 YAML은 HTTP 엔드포인트를 분리한 애플리케이션 기준입니다. 데모용 구조지만 실무에서도 그대로 많이 씁니다. 포인트는 세 가지입니다. startup은 부팅 보호, readiness는 트래픽 허용 판단, liveness는 내부 고장 감지입니다.

    1. 애플리케이션에서 /live와 /ready를 분리합니다.
    2. /ready는 요청 처리에 필요한 최소 의존성이 준비됐을 때만 200을 반환하게 합니다.
    3. /live는 외부 의존성을 빼고, 프로세스가 hang 상태인지 중심으로 판단합니다.
    4. 기동 시간이 흔들리는 앱이면 startup probe로 운영 구간 판정을 늦춥니다.
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: probe-demo
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: probe-demo
      template:
        metadata:
          labels:
            app: probe-demo
        spec:
          containers:
            - name: app
              image: your-registry/your-app:1.0.0
              ports:
                - containerPort: 8080
              startupProbe:
                httpGet:
                  path: /live
                  port: 8080
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 24
              readinessProbe:
                httpGet:
                  path: /ready
                  port: 8080
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 3
                successThreshold: 1
              livenessProbe:
                httpGet:
                  path: /live
                  port: 8080
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3

    여기서 많이 놓치는 계산이 있습니다. startupProbe의 허용 시간은 보통 failureThreshold * periodSeconds로 먼저 읽으면 편합니다. 위 예시는 5초마다 검사하고 24번까지 실패를 허용하니, 대략 120초까지는 기동 보호 구간이라고 해석할 수 있습니다. 앱이 어떤 날은 15초, 어떤 날은 80초 걸린다면 initialDelaySeconds 하나로 버티는 것보다 startup probe가 훨씬 안전합니다.

    startupProbe, readinessProbe, livenessProbe가 YAML 안에서 어떤 식으로 연결되고 순서대로 동작하는지 보여주는 구성 이미지입니다.

    적용 후에는 아래 순서로 보는 게 제일 빠릅니다. 배포, 상태 변화, 이벤트, 직전 로그를 묶어서 확인해야 원인이 잘 보입니다.

    kubectl apply -f probe-demo.yaml
    kubectl rollout status deployment/probe-demo
    kubectl get pods -l app=probe-demo -w
    kubectl describe pods -l app=probe-demo

    kubectl get pods -w는 READY와 RESTARTS 변화를 실시간으로 보기 좋고, kubectl describe pods -l app=probe-demo는 probe 실패 메시지와 kubelet 이벤트를 확인할 때 특히 유용합니다. 앱 로그만 보고 있으면 왜 재시작됐는지 놓치는 경우가 많습니다.

    Kubernetes Liveness Probe 파라미터는 이렇게 읽으시면 됩니다

    • initialDelaySeconds: 검사 시작 전 대기 시간입니다. 다만 느린 앱 보호를 이 값 하나로 해결하려 들면 운영 단계의 장애 감지가 늦어집니다.
    • periodSeconds: 검사 주기입니다. 너무 짧으면 노이즈와 부하가 늘고, 너무 길면 장애 감지가 늦습니다.
    • timeoutSeconds: 응답 대기 시간입니다. readiness가 자주 timeout 난다면 엔드포인트 자체가 무겁다는 신호일 수 있습니다.
    • failureThreshold: 연속 실패 허용 횟수입니다. 일시적 지연과 실제 장애를 구분하는 완충 장치로 보면 이해가 쉽습니다.
    • successThreshold: 연속 성공 횟수입니다. readiness의 flap을 줄일 때 의미가 있고, liveness와 startup에서는 1이어야 합니다.

    제 경험상 기동이 느린 앱에 initialDelaySeconds만 크게 주는 방식은 나중에 운영 감지를 둔하게 만듭니다. 시작 구간과 정상 운영 구간은 실패 의미가 다르기 때문에, startup probe로 분리하는 편이 더 깔끔하더라고요.

    헬스 엔드포인트는 이렇게 나누는 편이 덜 사고 납니다

    문법보다 더 중요한 건 애플리케이션이 무엇을 200으로 돌려주느냐입니다. 같은 Spring Boot나 FastAPI라도 엔드포인트 설계가 엉성하면 probe는 멀쩡해 보이는데 운영은 계속 흔들립니다. 저는 아래처럼 역할을 나누는 편을 선호합니다.

    엔드포인트 포함할 것 빼야 할 것 이유
    /live 메인 루프 정상 동작, 내부 큐 정지 여부, 프로세스 핵심 스레드 상태 DB ping, 외부 API 호출, 느린 파일 시스템 검사 재시작이 의미 있는 내부 고장만 잡아야 하기 때문
    /ready DB 연결 풀 준비, 필수 캐시 준비, 메시지 소비자 초기화 완료 비핵심 외부 연동, 과도한 상세 진단, 고비용 쿼리 트래픽 수용 가능 여부만 빠르게 판단해야 하기 때문

    예를 들어 주문 API가 있고, 요청 한 건을 처리하려면 DB 연결과 내부 캐시 로드가 필수라고 해보겠습니다. 이 경우 /ready는 DB pool이 usable인지, 필수 캐시가 준비됐는지만 보면 됩니다. 반면 분석용 외부 API가 잠깐 느린 건 새 주문 수락과 직접 관계가 없다면 readiness에 넣지 않는 편이 낫습니다. 모든 연동을 readiness에 다 넣으면 안전해 보일 수는 있어도, 실제로는 트래픽을 과하게 차단하게 됩니다.

    실무에서 많이 하는 실수 4가지

    여기서부터가 설정 문법보다 훨씬 중요합니다. 대부분의 장애는 YAML 오타보다 잘못된 운영 가정에서 나옵니다.

    1. Liveness Probe에 외부 의존성을 넣는 경우

    예를 들어 /healthz가 DB 질의를 수행하고, DB 응답이 늦으면 liveness가 실패하도록 만들면 어떻게 될까요. 앱 프로세스는 멀쩡한데 외부 의존성 장애가 컨테이너 재시작으로 번집니다. 재시작 직후 연결 폭증까지 붙으면 DB는 더 버거워지고, 결국 앱과 DB가 같이 흔들립니다. 이 패턴은 진짜 오래 사람을 괴롭히더라고요. 재시작이 복구가 아니라 증폭이 되는 겁니다.

    분리 기준은 단순합니다.

    • Liveness: 프로세스 내부 상태만 확인
    • Readiness: 요청 처리에 반드시 필요한 의존성만 확인

    2. 느린 앱에 Startup Probe 없이 Liveness만 거는 경우

    JVM, 대형 모델 로딩, 플러그인 초기화, schema migration이 있는 앱은 부팅 시간이 환경마다 다릅니다. 이때 liveness가 먼저 붙으면 정상 기동 중인 프로세스를 죽입니다. 증상은 CrashLoopBackOff로 보이는데, 실제 원인은 코드 버그보다 프로브 타이밍인 경우가 꽤 많습니다.

    권하는 방식은 먼저 실제 기동 로그를 보고, 포트 오픈 시점이 아니라 요청 처리 준비 완료 시점을 기준으로 startup budget을 잡는 겁니다. 앱이 포트를 열었다고 준비가 끝난 건 아니니까요.

    3. Readiness 엔드포인트를 너무 무겁게 만든 경우

    Readiness는 자주 호출됩니다. 여기에 비싼 SQL, 다중 외부 API 확인, 긴 디스크 검사까지 넣으면 체크 자체가 시스템 부하가 됩니다. 더 나쁜 경우는 readiness endpoint가 느려서 timeout 나고, 그 timeout 때문에 엔드포인트에서 빠졌다가 붙었다가를 반복하는 겁니다. 서비스는 살아 있는데 트래픽 경로만 흔들리죠.

    실무에서는 readiness를 빠르고, 결정적이며, 요청 처리 가능 여부만 말하는 엔드포인트로 두는 게 안정적입니다.

    4. probe 실패 로그를 앱 로그만으로 해석하는 경우

    Pod가 재시작하면 애플리케이션 로그부터 보는 경우가 많습니다. 그런데 probe 실패의 1차 단서는 대개 kubelet 이벤트에 있습니다. “왜 죽였는지”는 kubectl describe pod에 더 잘 남습니다. 재시작된 후 현재 컨테이너 로그만 보면 이미 중요한 구간이 사라졌을 수도 있고요.

    kubectl describe pod <pod-name>
    kubectl logs <pod-name> --previous
    kubectl get events --sort-by=.metadata.creationTimestamp

    --previous는 직전 컨테이너 로그를 보기에 특히 중요합니다. probe 때문에 재시작된 직후에는 현재 로그가 너무 깨끗해서 오히려 단서가 안 남는 경우가 많습니다.

    트러블슈팅: CrashLoopBackOff가 probe 때문인지 확인하는 방법

    재현 가능한 시나리오 하나를 잡아보겠습니다. 앱이 시작하면서 DB migration을 수행하고, HTTP 포트는 먼저 뜨지만 실제 요청 처리는 migration 완료 후에만 가능하다고 해보죠. 이때 liveness가 너무 빨리 붙으면, Kubernetes는 애플리케이션이 아직 기동 중인데도 불안정하다고 판단해 재시작시킬 수 있습니다. 문제는 재시작하면 migration이 다시 시작된다는 점입니다. 이 루프가 반복되면 코드가 아니라 프로브가 장애 원인입니다.

    1. Pod 상태 확인: kubectl get pods -w에서 RESTARTS가 계속 증가하는지 봅니다.
    2. 이벤트 확인: kubectl describe pod에서 Liveness probe failed, Readiness probe failed, Startup probe failed 메시지를 찾습니다.
    3. 직전 로그 확인: kubectl logs --previous로 종료 직전 애플리케이션 상태를 봅니다.
    4. 기동 순서 분리: 포트 오픈 시점, migration 완료 시점, ready 응답 가능 시점을 로그로 나눠 읽습니다.
    5. Startup Probe 추가 또는 조정: 준비 완료 전까지 liveness 간섭을 차단합니다.

    이때 숫자를 감으로 찍지 말고, 실제 이벤트와 로그 타임라인을 맞춰 보셔야 합니다. 앱의 “실제 준비 완료”와 Kubernetes가 “준비됐다고 믿는 시점”이 어긋날 때 문제가 생깁니다. 먼저 그 간격을 맞춘 뒤 threshold를 조정해야 덜 헤맵니다. 순서가 반대면 결국 숫자 놀음이 되더라고요.

    kubectl get pod <pod-name> -o wide
    kubectl describe pod <pod-name> | sed -n '/Events:/,$p'
    kubectl logs <pod-name> --previous --timestamps
    kubectl logs <pod-name> --timestamps

    여기서 볼 포인트는 단순합니다. 이벤트 시각과 앱 로그 시각을 맞춰서, probe 실패가 먼저였는지, 앱 내부 오류가 먼저였는지를 분리하는 겁니다. 순서를 잘못 읽으면 원인과 결과를 바꿔 잡게 됩니다.

    검증: 설정 후 무엇을 보고 정상이라고 판단할까

    설정을 넣는 것과 운영이 안정화되는 것은 별개입니다. 배포 직후에는 아래 네 가지를 같이 보는 편이 좋습니다. 하나만 보면 착시가 생깁니다.

    • 배포 직후 READY 변화: Running보다 READY가 중요합니다. Running인데 READY가 0/1이면 서비스는 사실상 트래픽을 못 받습니다.
    • 엔드포인트 반영: readiness 실패 시 실제로 Service 엔드포인트에서 빠지는지 확인해야 합니다.
    • RESTARTS 안정화: 정상 상태에 들어간 뒤 RESTARTS가 더 늘지 않아야 합니다.
    • 이벤트 노이즈 여부: 간헐적 probe failure가 계속 쌓인다면, 배포 피크나 노드 부하 때 실제 장애로 이어질 수 있습니다.
    kubectl rollout status deployment/probe-demo
    kubectl get pods -l app=probe-demo -o wide
    kubectl get endpoints
    kubectl describe deployment probe-demo

    검증 기준을 더 분명하게 두자면 이렇습니다. READY는 기대 복제 수만큼 올라와야 하고, RESTARTS는 안정 구간에서 멈춰야 하며, endpoints에는 준비된 Pod만 남아야 합니다. Pod가 떠 있다는 사실만으로 정상 판정을 내리면 실제 장애를 놓치기 쉽습니다.

    Kubernetes Liveness Probe 검증과 Pod 상태 확인 이미지

    Pod 상태, 이벤트 로그, readiness 반영 여부를 함께 보는 검증 흐름 이미지입니다.

    운영 팁: 앱 유형별로 probe를 다르게 잡아야 합니다

    앱 유형 추천 구성 이유 보수적으로 볼 포인트
    가벼운 웹 API Readiness + 보수적 Liveness 기동이 빠르고 요청 처리 여부 판단이 단순함 Liveness가 굳이 필요 없는 서비스도 적지 않음
    기동이 느린 JVM 앱 Startup + Readiness + Liveness 초기화 시간 보호와 운영 구간 분리가 필요함 initialDelaySeconds 하나로 해결하려 하지 않기
    배치 워커 Exec 또는 최소 Liveness HTTP 엔드포인트가 없을 수 있음 작업 큐 적체를 liveness 실패로 바로 연결하지 않기
    외부 의존성이 많은 앱 Readiness 강화, Liveness 단순화 장애 전파를 재시작 루프로 만들지 않기 위함 핵심 의존성만 readiness에 포함하기

    혹시 “애플리케이션은 분명 떠 있는데 로드밸런서 뒤에서만 실패하는” 경험이 있었다면, 대체로 readiness가 너무 느슨하거나 너무 무거운 경우가 많습니다. 카프카 소비자가 붙은 앱에서도 비슷한 장면이 자주 나옵니다. HTTP 포트는 빨리 떠도 소비자 그룹 조인이 끝나기 전엔 실제 처리 품질이 불안정할 수 있거든요. 그때 /ready를 소비자 초기화 완료 이후에만 true로 바꾸면 배포 중 실패율이 꽤 줄어듭니다. 이거 실무에서 진짜 편하더라고요.

    자주 묻는 질문

    Readiness만 있으면 Liveness는 없어도 될까요?

    가능합니다. 이건 의외로 많은 팀이 과하게 쓰는 부분입니다. 애플리케이션이 hang 상태에 빠질 가능성이 낮고, 장애 시 프로세스가 명확하게 종료되며, readiness만으로도 트래픽 차단이 충분하다면 liveness를 아예 두지 않는 선택도 실무적으로 타당합니다. Liveness는 필수 옵션이 아니라 재시작이 실제 복구 수단일 때만 가치가 있습니다.

    TCP probe면 충분하지 않나요?

    정말 단순한 서비스라면 됩니다. 하지만 대부분의 웹 애플리케이션은 “포트가 열려 있다”와 “요청을 정상 처리할 수 있다”가 다릅니다. 그래서 운영 해석 가능성을 높이려면 HTTP 기반 엔드포인트 분리가 더 낫습니다.

    헬스 체크 엔드포인트는 하나로 합쳐도 될까요?

    작게 시작할 때는 가능합니다. 다만 운영 기간이 길어질수록 역할 분리가 이깁니다. /live, /ready를 분리하면 장애 원인과 후속 조치가 선명해집니다. 최소한 readiness와 liveness는 분리하는 쪽을 권합니다.

    Kubernetes Liveness Probe 선택 기준 요약 인포그래픽

    앱 유형별로 어떤 probe 조합을 선택하면 좋은지 한눈에 보는 요약 이미지입니다.

    마무리: Kubernetes Liveness Probe는 이렇게 고르면 덜 흔들립니다

    Kubernetes Liveness Probe는 만능 안정화 버튼이 아닙니다. 잘못 걸면 재시작이 복구가 아니라 장애 확산 경로가 됩니다. 반대로 Readiness Probe를 제대로 설계하면 준비 안 된 Pod로 트래픽이 들어가는 문제를 깔끔하게 줄일 수 있고, Startup Probe는 느린 기동 서비스를 불필요한 CrashLoopBackOff에서 지켜줍니다.

    기준은 꽤 분명합니다. 가벼운 API라면 Readiness를 먼저 정확히 만들고, Liveness는 꼭 필요한 경우에만 보수적으로 추가하세요. 느리게 뜨는 앱이라면 Startup Probe를 분리하세요. 외부 의존성이 많은 서비스라면 Liveness는 단순하게 두고 Readiness에서 트래픽 차단을 설계하세요. 운영 안정성은 “많이 검사하는 것”보다 무엇을 실패시켰을 때 어떤 후속 조치가 일어나는지를 정확히 설계할 때 올라갑니다.

  • [AI] AI 모델 클라우드 마이그레이션 체크리스트: 온프레미스 AI 전환 기준

    [AI] AI 모델 클라우드 마이그레이션 체크리스트: 온프레미스 AI 전환 기준

    AI 모델 클라우드 마이그레이션 체크리스트: 온프레미스 AI 전환 기준

    AI 모델 클라우드 마이그레이션을 검토할 때 많은 팀이 처음엔 GPU 인스턴스만 확보하면 된다고 생각합니다. 저도 예전엔 그렇게 봤는데, 실제 전환 프로젝트에 들어가면 GPU보다 먼저 터지는 문제는 데이터 경로, 모델 아티팩트 배포, 보안 경계, 준비 상태 검증이더라고요. 결국 이 작업은 서버 위치만 바꾸는 일이 아니라, 추론 요청이 들어와서 모델이 로드되고 결과가 저장되기까지의 흐름을 다시 설계하는 일에 가깝습니다.

    특히 AI 워크로드는 일반 웹 서비스보다 실패 양상이 더 복잡합니다. 애플리케이션 프로세스는 살아 있는데 모델 다운로드가 끝나지 않아 readiness가 실패할 수 있고, GPU는 한가한데 전처리 CPU가 막혀 지연이 튀는 경우도 흔합니다. 클라우드로 옮긴 뒤에도 입력 데이터가 온프레미스에 남아 있으면 왕복 지연과 전송 비용이 계속 쌓이죠. 그래서 이 글은 원론보다, 전환 회의와 전환 당일에 바로 써먹을 수 있는 체크 기준으로 정리해보겠습니다.

    AI 모델 클라우드 마이그레이션 전체 아키텍처 개요 이미지

    온프레미스 AI, 스토리지, 네트워크, 클라우드 추론 계층이 어떻게 연결되는지 한눈에 보여주는 개요 이미지입니다.

    1. 왜 AI 모델 클라우드 마이그레이션이 까다로운가

    일반 애플리케이션 이전은 보통 애플리케이션 바이너리와 데이터베이스를 중심으로 보면 됩니다. 그런데 AI 추론 서비스는 여기에 모델 파일, 토크나이저와 전처리 리소스, GPU 드라이버 호환성, 컨테이너 이미지 크기, 아티팩트 캐시, 배치와 실시간 트래픽의 공존이 더해집니다. 이 중 하나만 설계가 어긋나도 서비스는 떠 있지만 실제 요청은 느리거나 불안정해집니다.

    현장에서 자주 보는 실패는 크게 세 가지입니다. 첫째, 서비스는 클라우드에 올렸는데 입력 데이터와 피처 저장소는 사내망에 그대로 둬서 지연 시간 대부분을 네트워크 왕복에 써버리는 경우입니다. 둘째, 컨테이너 이미지는 잘 만들었는데 모델 아티팩트를 언제 어디서 받아오는지 기준이 없어 배포마다 시작 시간이 들쑥날쑥해지는 경우입니다. 셋째, 개발팀은 Kubernetes를 원하고 운영팀은 VM을 선호하는데 책임 경계를 정하지 않은 채 진행해서 장애가 나면 누구도 원인 구간을 단정하지 못하는 경우입니다.

    핵심은 기술 스택 이름이 아니라 실패 지점을 어디서 끊어 볼 수 있게 만들었느냐입니다. 모델 로딩 실패를 애플리케이션 로그에서만 보게 만들면 대응이 늦어집니다. 배포 시스템, 스토리지 접근, 네트워크 정책, 컨테이너 이벤트, readiness probe까지 각 단계에서 실패가 드러나야 운영이 됩니다. 제 경험상 마이그레이션이 잘된 팀은 최신 GPU를 쓰는 팀이 아니라, 실패를 빨리 국소화하는 팀이었습니다.

    2. AI 모델 클라우드 마이그레이션 전, 무엇을 옮기고 무엇을 남길까

    클라우드 AI 전환은 전부 이전하거나 전부 유지하는 식으로 가면 대개 비효율적입니다. 워크로드를 성격별로 나눠야 하거든요. 같은 모델이라도 실시간 API와 야간 배치의 최적 해법이 다를 수 있습니다.

    항목 온프레미스 유지가 유리한 경우 클라우드 이전이 유리한 경우 판단 포인트
    실시간 추론 API 내부 시스템 전용이고 입력 데이터가 사내망에서만 생성될 때 트래픽 변동이 크고 외부 서비스, CDN, API Gateway 연동이 많을 때 왕복 지연, 오토스케일링, 외부 노출 경로
    배치 추론 야간 고정 작업이고 유휴 GPU를 계획적으로 재활용할 수 있을 때 특정 기간에만 대량 실행이 몰리고 큐 길이가 자주 출렁일 때 실행 창 유연성, 큐 적체, 자원 점유 시간
    모델 학습 데이터 반출 제한이 강하고 장기 점유형 학습이 많을 때 짧은 기간 대규모 자원을 집중 투입해야 할 때 데이터 거버넌스, 체크포인트 저장 경로, 대역폭
    모델 저장소 폐쇄망 정책이 우선이고 외부 반출 심사가 까다로울 때 멀티 리전 배포, 버전 배포 자동화, 캐시 전략이 중요할 때 버전 전파 속도, 접근 통제, 캐시 일관성
    전처리/후처리 사내 DB, 파일서버, 내부 인증 체계와 강하게 결합돼 있을 때 API 기반으로 분리 가능하고 수평 확장이 자주 필요할 때 CPU 병목, 내부 연동 의존도, 서비스 분리 비용

    이 단계에서 꼭 문서화해두면 좋은 질문은 아래 다섯 가지입니다.

    • 모델 아티팩트는 이미지에 포함할지, 시작 시 다운로드할지, 볼륨으로 마운트할지
    • 입력 데이터는 클라우드에 복제할지, 전용 회선이나 프록시로 접근할지
    • GPU가 꼭 필요한 경로와 CPU로 충분한 경로를 분리했는지
    • 장애 시 온프레미스로 되돌리는 경로가 DNS 전환인지, 로드밸런서 가중치 조정인지, 배포 태그 롤백인지
    • 모델 버전 롤아웃과 인프라 롤아웃을 같은 절차로 묶을지, 분리할지

    실무적으로 정리하면 이렇습니다. 데이터가 사내망을 벗어나기 어렵고 지연 시간 요구가 빡빡하면 온프레미스나 하이브리드가 맞고, 트래픽 변동과 배포 빈도가 더 큰 문제라면 클라우드가 더 잘 맞습니다. 둘 다 애매하면 전체 이전보다 외부 노출 추론 API나 배치 작업부터 부분 이전이 안전합니다.

    3. AI 모델 클라우드 마이그레이션 사전 진단 체크리스트

    이 섹션은 실제 킥오프 회의에서 그대로 읽어도 됩니다. 저는 마이그레이션 전에 아래 항목이 하나라도 비어 있으면 일정을 바로 잡지 않습니다. 나중에 메꾸면 되겠지 싶어도, 보통은 막판에 가장 비싼 문제로 돌아오더라고요.

    1. 모델 목록 정리: 모델 이름, 버전, 프레임워크, 파일 크기, 로딩 경로, 토크나이저/전처리 리소스 위치, 의존 라이브러리 버전을 적습니다.
    2. 트래픽 패턴 확인: 실시간 API, 비동기 큐, 배치 실행을 분리해서 보고, 요청 급증 시점과 재시도 패턴이 있는지 확인합니다.
    3. 데이터 경로 파악: 요청 원천, 전처리 위치, 모델 호출 위치, 결과 저장소, 로그 전송 경로를 한 장의 흐름도로 그립니다.
    4. 보안 요구사항 정리: 네트워크 분리, TLS 종료 지점, Secret 주입 방식, 감사 로그 보존 위치를 정합니다.
    5. 운영 표준 확정: VM인지, 컨테이너인지, Kubernetes인지 결정하고, 배포 책임 팀을 명시합니다.
    6. 관측 지표 선정: p95 지연 시간, 오류율, 모델 로딩 시간, GPU 메모리 사용, Pod 재시작, 큐 적체, 스토리지 대기를 최소 기준으로 둡니다.
    7. 롤백 계획 수립: 어떤 이상 징후가 몇 분간 지속되면 되돌릴지, 누가 승인할지, 어떤 명령으로 되돌릴지 적습니다.
    8. 의존성 분리 검증: 코드 안에 절대 경로, 사내 DNS 이름, 로컬 마운트 경로, 특정 NIC 이름 같은 환경 종속값이 박혀 있지 않은지 확인합니다.

    제가 한 번 크게 헤맸던 사례도 딱 이 단계에서 걸렸어야 했습니다. 모델 파일은 컨테이너 시작 시 객체 저장소에서 내려받도록 바꿨는데, 토크나이저 사전 파일 경로는 예전 온프레미스 NFS 마운트 경로를 그대로 바라보고 있었거든요. 애플리케이션 로그에는 단순히 No such file or directory만 찍혀서 처음엔 이미지 문제로 봤고, 실제 원인은 설정 누락이었습니다. 모델 추론 실패는 모델 자체보다 주변 리소스 경로 누락에서 나는 경우가 생각보다 많습니다.

    사전 진단 단계에서 아래처럼 의존 파일을 한 번 훑어보면 이런 실수를 꽤 줄일 수 있습니다.

    find /opt/app -maxdepth 3 -type f \( -name "*.py" -o -name "*.yaml" -o -name "*.yml" -o -name "*.json" \) \
      -print0 | xargs -0 rg -n "/mnt/|/nfs/|/data/|\.internal|localhost|127\.0\.0\.1"

    이 명령은 코드와 설정 파일 안에 박혀 있는 환경 종속 경로, 내부 도메인, 로컬 참조를 빠르게 찾는 데 유용합니다. “이 정도는 나중에 고치면 되지” 하고 넘기면, 마이그레이션 막판에 가장 까다로운 형태로 돌아옵니다.

    4. 실전 구현 1: 현재 환경 계측과 병목 확인

    AI 모델 클라우드 마이그레이션 전에 먼저 해야 할 일은 현재 온프레미스 환경이 어디서 느린지 평균값이 아니라 병목 패턴으로 읽는 겁니다. 평균 CPU 사용률이 낮다고 안심하면 안 됩니다. 피크 시간에 CPU 전처리가 치솟는지, 디스크 대기가 늘어나는지, GPU 메모리 부족으로 컨테이너가 재시작하는지, 네트워크 인터페이스 하나에 트래픽이 몰리는지까지 봐야 합니다.

    초기에 많이 수집하는 명령은 아래 정도입니다.

    sar -u 1 5
    sar -n DEV 1 5
    iostat -x 1 5
    vmstat 1 5
    ss -ltnp
    df -h
    mount | rg -n "nfs|ceph|fuse|cifs"

    읽는 기준은 이렇게 가져가면 됩니다.

    • iostat -x에서 특정 디바이스의 await가 길고 대기열이 함께 늘면, 모델 파일 로딩이나 캐시 저장 과정에서 스토리지 병목이 있을 가능성이 큽니다. 다만 최신 SSD나 병렬 스토리지에선 %util만으로 포화 여부를 단정하긴 어렵습니다.
    • sar -n DEV에서 NIC 하나만 유난히 바쁘면 데이터 경로가 비대칭일 수 있습니다. 특히 모델 다운로드와 요청 처리 트래픽이 같은 인터페이스를 타면 전환 후 더 티가 납니다.
    • vmstat에서 swap 징후가 보이면, 모델 로딩 시 메모리 압박으로 초기화가 길어지거나 OOM 직전까지 가는 구조일 수 있습니다.
    • ss -ltnp로 실제 어떤 포트에 무엇이 바인딩되어 있는지 확인해두면, 클라우드 전환 후 readiness probe 경로와 포트 매핑 실수를 줄일 수 있습니다.
    • mount 결과에서 NFS나 원격 파일시스템 의존이 있으면, 그 경로를 클라우드에서 어떻게 대체할지 미리 정해야 합니다.

    GPU 워크로드라면 운영체제 지표만 보고 끝내면 아쉽습니다.

    nvidia-smi
    nvidia-smi dmon -s pucmem
    nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used,utilization.gpu,utilization.memory --format=csv

    여기서 특히 봐야 할 건 GPU 사용률 하나가 아니라 GPU 사용률과 지연 시간의 관계입니다. GPU 사용률이 낮은데 응답이 느리면 대개 GPU가 핵심 원인은 아닙니다. 전처리 CPU, 네트워크 왕복, 스토리지 다운로드, 직렬화 구간을 먼저 의심하는 편이 맞습니다. 반대로 GPU 사용률과 메모리 사용률이 함께 치솟고 배포 직후 실패가 늘면, 컨테이너 메모리 제한이나 모델 샤딩 전략을 다시 봐야 합니다.

    현업에서 자주 놓치는 또 하나는 모델 시작 시간입니다. 서비스 지연만 재고 배포 시간을 안 재면, 오토스케일링이 필요한 순간에 새 Pod가 제때 준비되지 않습니다. 저는 아래처럼 컨테이너 이벤트와 readiness를 같이 봅니다.

    kubectl get pods -n ml -w
    kubectl describe pod -n ml <pod-name>
    kubectl logs -n ml <pod-name> --since=10m

    만약 이벤트에 이미지 풀, 볼륨 마운트, readiness probe 실패가 순서대로 찍히면 모델 자체보다 시작 절차가 병목인 경우가 많습니다. 초기화가 긴 서비스라면 readiness와 liveness만 둘 게 아니라 startupProbe까지 함께 검토하는 편이 더 안전합니다. 이거 하나로 불필요한 재시작 루프를 꽤 줄일 수 있거든요.

    AI 모델 클라우드 마이그레이션 전 온프레미스 AI 병목 점검 이미지

    마이그레이션 전 병목을 확인하는 장면을 시각화한 이미지입니다. GPU와 디스크, 네트워크 지표를 함께 보는 느낌이면 좋습니다.

    5. 실전 구현 2: 컨테이너와 설정 분리부터 정리하기

    온프레미스 AI 환경에서 흔히 보이는 상태가 “서버 한 대에 파이썬 가상환경, 모델 파일, 인증서, 임시 스크립트가 다 엉켜 있는 구조”입니다. 이 상태로 클라우드에 가면 재현성이 떨어지고, 장애가 나도 어느 레이어 문제인지 분리하기 어려워집니다. 그래서 저는 이럴 때 가장 먼저 이미지, 설정, 시크릿, 모델 아티팩트를 분리합니다.

    원칙은 단순합니다. 이미지에는 코드와 런타임만 넣고, 환경별 차이는 환경 변수와 Secret으로 분리하고, 모델 아티팩트는 별도 경로에서 가져오게 만듭니다. 모델을 이미지에 굽는 방식은 작은 모델이나 배포 빈도가 낮을 때는 편하지만, 이미지가 비대해지고 버전 교체가 잦아지면 배포 속도를 늦춥니다. 반대로 매번 시작 시 다운로드하게 하면 시작 시간이 길어질 수 있으니, 배포 빈도와 모델 크기를 같이 봐야 합니다.

    배포 방식 언제 유리한가 피해야 할 상황 주요 리스크
    모델을 이미지에 포함 모델 크기가 작고 배포 빈도가 낮을 때 버전 교체가 잦고 이미지 전파 시간이 길 때 이미지 비대화, 롤백 지연
    시작 시 객체 저장소에서 다운로드 버전 교체가 잦고 중앙 관리가 중요할 때 시작 시간이 엄격하거나 네트워크 변동이 큰 환경 Cold start 증가, 다운로드 실패
    공유 볼륨/PVC 마운트 같은 노드나 클러스터에서 여러 Pod가 재사용할 때 스토리지 성능이 불안정하거나 락 경합이 있을 때 I/O 병목, 마운트 실패

    Kubernetes 배포를 예로 들면, 최소한 아래 정도까지는 분리해두는 편이 좋습니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: inference-api
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: inference-api
      template:
        metadata:
          labels:
            app: inference-api
        spec:
          containers:
            - name: api
              image: registry.example.com/inference-api:1.0.0
              imagePullPolicy: IfNotPresent
              ports:
                - containerPort: 8080
              env:
                - name: MODEL_PATH
                  value: /models/current
                - name: LOG_LEVEL
                  value: info
              envFrom:
                - secretRef:
                    name: inference-api-secret
              readinessProbe:
                httpGet:
                  path: /ready
                  port: 8080
                initialDelaySeconds: 10
                periodSeconds: 5
                failureThreshold: 6
              livenessProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                initialDelaySeconds: 30
                periodSeconds: 10
              startupProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                failureThreshold: 30
                periodSeconds: 10
              volumeMounts:
                - name: model-volume
                  mountPath: /models
          volumes:
            - name: model-volume
              persistentVolumeClaim:
                claimName: model-pvc

    이 설정에서 중요한 건 화려한 기능이 아니라 네 가지입니다. 모델 경로를 코드에 하드코딩하지 않는 것, readiness와 liveness를 분리하는 것, 초기화가 길면 startupProbe를 두는 것, Secret을 이미지 밖으로 빼는 것입니다. readiness는 “트래픽을 받을 준비가 됐는지”, liveness는 “프로세스가 비정상 상태인지”를 보는 기준입니다. 초기화가 오래 걸리는 서비스에서 startupProbe 없이 liveness를 너무 일찍 때리면 재시작 루프로 빠질 수 있습니다.

    배포 태그도 latest보다는 고정 버전을 쓰는 편이 낫습니다. 이건 취향 문제가 아니라 롤백 속도 문제에 가깝습니다.

    kubectl apply -f deployment.yaml
    kubectl rollout status deployment/inference-api
    kubectl get pods -l app=inference-api
    kubectl logs deploy/inference-api --tail=100
    kubectl rollout history deployment/inference-api

    검증할 때는 Pod가 뜨는지만 보면 부족합니다. 로그에서 모델 로딩 완료 메시지, 외부 저장소 접근 성공, 포트 바인딩, readiness 통과를 함께 확인해야 합니다. 특히 애플리케이션이 첫 요청 직전에 모델을 lazy load하도록 짜여 있으면, rollout status가 끝나도 실제 서비스 시점에만 장애가 날 수 있습니다. 이 경우엔 사전 검증 요청을 명시적으로 날려 워밍업까지 확인하는 편이 안전합니다.

    컨테이너 이미지, 환경 변수, 스토리지 볼륨, 서비스 경로가 어떻게 분리되는지 보여주는 구성 다이어그램입니다.

    6. 네트워크와 보안: 늦게 보면 일정이 무너집니다

    클라우드 AI 전환에서 일정이 가장 자주 밀리는 구간은 성능 튜닝보다 보안 승인입니다. 온프레미스에서는 그냥 되던 내부 호출이, 클라우드로 오면 Ingress, Egress, DNS, 인증서, 프록시, NAT, 감사 로그 요건을 통과해야 합니다. 이걸 배포 직전에 맞추려 하면 예외 규칙만 늘고, 운영 설명 가능성은 오히려 떨어집니다.

    • Ingress와 내부 서비스 경로를 분리합니다. 외부 공개 API와 내부 관리 API를 같은 진입점에 두지 않는 편이 낫습니다.
    • Secret은 이미지에 넣지 말고 Secret 관리 기능이나 외부 저장소에서 주입합니다.
    • Egress 허용 대상을 도메인, 포트, 목적별로 문서화합니다. 모델 다운로드, 인증, 로그 전송, 메트릭 전송을 분리해서 적어야 합니다.
    • 감사 로그는 누가 모델 버전을 배포했고, 누가 Secret을 변경했고, 누가 네트워크 정책을 열었는지 추적 가능해야 합니다.

    제가 실제로 겪었던 전형적인 사고는 이렇습니다. 개발 환경에서는 잘 되던 모델 API가 운영 클러스터에서만 외부 인증 토큰 발급에 실패했습니다. 앱 로그에는 단순한 timeout처럼 보여서 처음엔 코드 문제로 봤죠. 원인은 운영 네트워크 정책에서 인증 엔드포인트로 나가는 Egress가 막혀 있던 것이었습니다. 이런 문제는 “애플리케이션이 떠 있느냐”보다 의존 대상까지 실제로 연결되느냐를 따로 검증해야 잡힙니다.

    배포 전 연결성 테스트는 아래처럼 나눠서 보는 편이 좋습니다.

    curl -vk https://example.internal.health/ready
    curl -vk https://auth.example.com/
    nslookup auth.example.com
    dig auth.example.com +short
    openssl s_client -connect auth.example.com:443 -servername auth.example.com </dev/null

    이 조합이 좋은 이유는 실패 지점을 분리해주기 때문입니다. nslookup이나 dig에서 이름 해석이 안 되면 DNS 문제고, curl -vk에서 연결 자체가 안 되면 라우팅이나 방화벽 문제일 가능성이 큽니다. openssl s_client 단계에서 깨지면 인증서 체인이나 SNI 관련 문제를 의심할 수 있습니다. 보안팀이나 네트워크팀과 이야기할 때도 “안 돼요”보다 “DNS는 되고 TLS handshake에서 끊깁니다”가 훨씬 빠릅니다.

    추가로, AI 추론 서비스는 외부 모델 저장소나 내부 객체 저장소에서 큰 파일을 가져오는 경우가 많습니다. 그래서 Egress를 한 번 열어놓고 끝낼 게 아니라, 어떤 Pod가 어떤 목적지로 나가야 하는지를 좁혀 적는 편이 좋습니다. 운영팀 입장에서는 이게 정책 관리의 시작점입니다.

    7. AI 모델 클라우드 마이그레이션 전환 당일 체크리스트와 롤백 기준

    마이그레이션 전략은 기술보다 절차가 더 중요할 때가 많습니다. 전환 당일에는 “한 번에 바꾸고 보자”보다 단계적으로 바꾸는 편이 낫습니다. 제가 자주 쓰는 순서는 대체로 아래와 같습니다.

    1. 기존 온프레미스 서비스의 버전, 헬스 상태, 주요 로그 패턴을 기록합니다.
    2. 클라우드 환경에서 동일 입력으로 응답 형식과 로딩 시간을 사전 검증합니다.
    3. 읽기 전용 트래픽이나 일부 가중치만 새 환경으로 보냅니다.
    4. 오류 로그, 지연 시간, 모델 로딩 상태, 외부 연동 성공 여부를 집중 관찰합니다.
    5. 이상 징후가 없으면 트래픽 비율을 점진적으로 올립니다.
    6. 문제가 생기면 DNS, 로드밸런서, 서비스 라우팅 기준으로 즉시 롤백합니다.

    여기서 핵심은 “언제 되돌릴지”를 미리 적어두는 겁니다. 현장에서는 기술적 판단보다 심리적 지연이 더 무섭거든요. 그래서 애매한 표현보다 징후 중심 기준을 쓰는 편이 낫습니다.

    • readiness probe 실패가 연속으로 발생한다
    • 모델 로딩 실패 로그가 반복된다
    • 인증, 저장소 접근, 외부 API 연동 timeout이 지속된다
    • 온프레미스 대비 명확한 성능 저하가 보이는데 원인을 즉시 분리하지 못한다
    • Pod 재시작이나 노드 재스케줄링이 예상보다 잦다

    실무에서는 숫자 하나로 모든 걸 결정하기보다, 정상 로그와 비정상 로그의 차이를 미리 캡처해두는 편이 훨씬 도움이 됩니다. 예를 들어 정상일 때는 모델 로딩 완료, tokenizer 초기화 완료, readiness 성공이 순서대로 나오고, 비정상일 때는 다운로드 재시도나 mount 실패가 먼저 보인다면 전환 당일에 대시보드보다 로그 한 줄이 더 빨리 방향을 줍니다.

    가중치 기반 전환을 쓰는 환경이라면 전체 컷오버보다 부분 전환이 낫습니다. 이유는 단순합니다. AI 추론 서비스는 첫 요청 시 캐시가 비어 있는 경우가 많아서, 일부 트래픽으로 시작해야 cold path를 실제로 밟아볼 수 있기 때문입니다. 저는 이런 상황이면 전환 초반엔 새 환경을 정상 동작 확인용으로 보고, 안정화가 끝난 뒤에야 확장 수단으로 취급합니다.

    AI 모델 클라우드 마이그레이션 후 검증 대시보드 이미지

    전환 직후 검증 단계에서 운영자가 어떤 화면을 중점적으로 보는지 보여주는 대시보드형 이미지입니다.

    8. 검증과 결과 해석: 성공 여부는 이렇게 봅니다

    AI 모델 클라우드 마이그레이션이 끝났다고 판단하려면 서버가 켜져 있다는 사실만으로는 부족합니다. 적어도 아래 네 축이 같이 맞아야 합니다.

    • 기능 검증: 같은 입력에 대해 기대한 형식의 응답이 나오는가
    • 운영 검증: 로그 수집, 알림, 접근 제어, 배포 이력이 정상 동작하는가
    • 성능 검증: 병목이 GPU인지, CPU 전처리인지, 네트워크인지, 스토리지인지 구분 가능한가
    • 복구 검증: 재배포와 롤백이 문서대로 실제 수행되는가

    판단 기준도 조금 더 실무적으로 가져가야 합니다.

    • 응답은 정상이지만 시작 시간이 과하게 길다면 모델 아티팩트 다운로드 경로, 이미지 크기, PVC 마운트 지연을 먼저 봅니다.
    • GPU 사용률이 낮은데 지연이 높다면 CPU 전처리, 직렬화, 네트워크 왕복을 먼저 의심하는 편이 맞습니다.
    • Pod 재시작이 잦다면 메모리 제한, readiness 경로, 파일 마운트 실패, liveness 기준 과민 설정을 차례로 봅니다.
    • 특정 시간대에만 문제가 생기면 배치와 실시간 추론이 같은 노드 풀이나 같은 스토리지를 공유하는지 확인합니다.
    • 첫 요청만 느리고 이후는 괜찮다면 lazy load, 캐시 미스, DNS 캐시, 원격 다운로드를 먼저 의심합니다.

    여기서 중요한 건 수치 그 자체보다 설명 가능성입니다. 운영자가 “왜 느린지”를 두세 단계 안에 좁힐 수 있으면 성공에 가깝고, 장애가 나도 어느 레이어를 먼저 봐야 할지 모르면 아직 미완성입니다. 화려한 대시보드보다 원인 추적 경로가 짧은 구조가 더 값집니다.

    실제로 재현 가능한 시나리오를 하나 들면 이렇습니다. 평소엔 응답이 괜찮다가 배포 직후 몇 분 동안만 지연이 튀는 서비스가 있었습니다. GPU는 여유가 있었고 애플리케이션도 살아 있었습니다. 원인은 새 Pod가 올라올 때마다 모델 파일을 원격 저장소에서 다시 받아오고, 동시에 전처리 사전 파일도 초기화하느라 readiness 통과 직전까지 시간이 밀리던 구조였습니다. 이 경우 GPU 교체는 답이 아니고, 모델 캐시 전략과 readiness 기준을 손보는 게 답입니다.

    9. 자주 묻는 질문과 현업 권고

    온프레미스 AI를 전부 클라우드로 옮겨야 할까요?

    그럴 필요는 없습니다. 데이터 반출 제한이 강하거나 내부 시스템 전용이라면 일부는 남기는 게 맞습니다. 특히 학습 데이터, 민감 로그, 내부 전처리 파이프라인은 남기고, 외부 공개형 추론 API나 탄력성이 필요한 배치만 클라우드로 분리하는 구성이 현실적입니다.

    Kubernetes가 꼭 필요할까요?

    작은 팀이고 모델 수가 적고 변경 빈도가 낮다면 처음부터 복잡도를 올릴 필요는 없습니다. 다만 모델 버전이 자주 바뀌고, 환경이 늘고, 롤백 속도가 중요해지면 컨테이너 오케스트레이션이 운영 피로도를 줄여줍니다. 제 기준은 단순합니다. 서비스가 한두 개이고 담당자가 고정돼 있으면 VM도 가능하지만, 버전 수와 팀 수가 늘기 시작하면 Kubernetes 쪽이 결국 덜 아픕니다.

    비용보다 먼저 볼 것은 뭔가요?

    데이터 경로와 운영 절차입니다. 비용은 나중에 줄일 수 있어도, 데이터 왕복 구조와 롤백 체계가 꼬이면 되돌리기가 어렵습니다. 특히 모델은 클라우드에 있고 데이터는 온프레미스에 남아 있는 상태를 아무 생각 없이 만들면, 지연과 운영 복잡도가 같이 올라갑니다.

    이럴 땐 어떤 선택이 맞을까요?

    명확하게 정리하면 이렇습니다. 내부 데이터 의존이 강하고 지연에 민감하면 하이브리드가 맞고, 배포 속도와 탄력성이 더 중요하면 클라우드 이전이 더 잘 맞습니다. 작은 팀이 첫 전환을 한다면 전부 옮기기보다 배치 또는 외부 노출 API부터 시작하는 편이 안전합니다. 반대로 이미 모델 버전이 자주 바뀌고 롤백이 잦다면, 미루지 말고 컨테이너화와 설정 분리부터 정리한 뒤 클라우드로 가는 게 낫습니다.

    AI 모델 클라우드 마이그레이션 선택 기준 요약 이미지

    상황별 권장 전략을 빠르게 비교할 수 있는 요약 인포그래픽입니다.

    마무리: 이런 경우엔 이렇게 가시면 됩니다

    온프레미스 AI 환경이 이미 안정적이고 데이터가 사내망에 깊게 묶여 있다면, 무리해서 전부 옮기지 않는 편이 좋습니다. 이 경우엔 배치 추론이나 외부 공개 API부터 부분 이전이 잘 맞습니다. 반대로 트래픽 변동이 크고 모델 배포 주기가 빠르며, 운영팀이 버전 롤백과 오토스케일링에 자주 시달린다면 클라우드 쪽이 더 유리합니다.

    한 줄로 줄이면 이렇습니다. AI 모델 클라우드 마이그레이션은 GPU 이전 프로젝트가 아니라 운영 모델 재설계 프로젝트입니다. 데이터 경로가 복잡하면 하이브리드로 시작하고, 출시 속도가 우선이면 컨테이너와 설정 분리부터 끝내고 가는 편이 안전합니다. 무엇부터 할지 애매하다면 제일 먼저 계측과 의존성 탐색부터 해보세요. 이 순서가 생각보다 많이 살려줍니다.

    실무적으로는 이렇게 판단하면 됩니다. 이럴 땐 A: 사내 데이터 의존이 강하고 보안 경계가 복잡하면 하이브리드. 저럴 땐 B: 모델 버전 변경이 잦고 외부 트래픽 변동이 크면 클라우드 이전. 둘 다 애매하면 C: 전환보다 먼저 현재 병목과 숨은 의존성을 계측. 관련 글로 AI 인프라 구축 체크리스트나 모델 배포 전략 가이드를 함께 묶어두면 내부 링크 구조와 SEO 흐름도 더 좋아집니다.

  • [k8s] 온프레미스 Kubernetes 클라우드 마이그레이션: 결정 기준과 체크포인트

    [k8s] 온프레미스 Kubernetes 클라우드 마이그레이션: 결정 기준과 체크포인트

    [인프라] 온프레미스 Kubernetes 클라우드 마이그레이션: 결정 기준과 체크포인트

    온프레미스 Kubernetes 클라우드 마이그레이션 이야기가 나오면, 흔히 인프라 위치만 바꾸는 일처럼 들리죠. 그런데 실무에 들어가 보면 느낌이 꽤 다릅니다. 서버 몇 대 옮기는 문제가 아니라 운영 책임, 장애 양상, 배포 기준, 비용 구조가 한꺼번에 바뀌는 의사결정에 더 가깝거든요. 같은 Kubernetes라도 온프레미스와 클라우드 관리형 환경은 장애 지점이 다르고, 같은 YAML도 운영 의미가 달라지는 구간이 분명히 있습니다.

    이번 글에서는 온프레미스 Kubernetes 클라우드 마이그레이션을 검토할 때 먼저 봐야 할 판단 기준, 이전 전에 걸러야 하는 비호환 요소, 그리고 마이그레이션 당일보다 더 중요한 사전 검증 포인트를 실무 관점으로 정리해보겠습니다. 문서만 읽으면 잘 안 보이는 부분, 특히 "왜 여기서 일정이 자꾸 미끄러지는지"까지 같이 짚어보겠습니다.

    온프레미스 클러스터, 클라우드 관리형 Kubernetes, 하이브리드 연결 구조를 한눈에 보여주는 개요 이미지입니다.

    온프레미스 Kubernetes 클라우드 마이그레이션이 어려운 이유

    Kubernetes 자체는 이식성이 좋은 편입니다. 문제는 실제 운영 환경이 그렇지 않다는 데 있어요. 온프레미스에서는 익숙했던 L2/L3 네트워크, 방화벽 예외, 사내 DNS 포워더, LDAP, 사설 이미지 레지스트리, NFS나 Ceph 같은 스토리지, 고정 IP 화이트리스트가 클라우드로 들어가는 순간 전부 다시 검토 대상이 됩니다. 저는 이걸 클러스터 이동이라기보다 주변 의존성 노출 작업이라고 보는 편입니다.

    예를 들어 PVC가 모두 Bound 상태라고 해서 스토리지 이전 준비가 끝난 건 아니더라고요. 온프레미스에서 RWX로 쓰던 워크로드가 클라우드 블록 스토리지로 옮겨가면, 같은 YAML이어도 동시 마운트 전제가 깨질 수 있습니다. 반대로 stateless처럼 보이던 서비스가 실제로는 사내 DNS suffix, 내부 SMTP, 사설 인증서 체인에 강하게 묶여 있어서 클라우드에선 readinessProbe부터 실패하는 경우도 많습니다. 겉으로 보이는 리소스 종류보다 런타임에 어디로 붙는지가 훨씬 중요합니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 결정 기준

    방향을 잡을 때 저는 네 가지를 먼저 봅니다. 이 기준이 흐리면 설계가 아니라 희망사항만 남기 쉽습니다.

    • 운영 책임 경계: Control Plane, 업그레이드, 인증서, CNI, CSI, 장애 대응을 누가 맡을지
    • 데이터 중력: 데이터셋과 핵심 DB가 어디에 있어야 하는지, 애플리케이션이 그 데이터를 얼마나 자주 왕복하는지
    • 지연 시간과 네트워크 경로: 온프레미스 시스템, 레거시 장비, 사내 인증 체계와의 호출 빈도와 실패 허용 범위
    • 규제와 접근 통제: 로그 보존, 망 분리, 비밀정보 저장 위치, 감사 추적의 기준점

    실무에서는 기술 우수성보다 우선순위가 더 중요합니다. 운영 부담을 줄이는 게 1순위인지, 아니면 데이터를 가까이 두는 게 1순위인지부터 정해야 해요. 둘을 동시에 최대화하려 하면 하이브리드가 나오는데, 그 순간 인증, 라우팅, 관측성이 다중 경로가 됩니다. 하이브리드는 좋은 절충안이라기보다, 대개 명확한 제약 때문에 선택하는 구조에 가깝습니다.

    선택지 적합한 상황 피해야 하는 상황 성능/비용/안정성에 주는 영향 최종 판단 기준
    온프레미스 유지 공장 설비, 내부 장비, 초저지연 연동, 데이터 상주 요구가 강할 때 플랫폼 운영 인력이 이미 부족하고 업그레이드가 밀려 있을 때 지연 시간은 유리하지만 운영 복잡도와 업그레이드 리스크를 계속 짊어지게 됨 플랫폼 팀이 클러스터 수명주기를 끝까지 책임질 수 있는지
    관리형 Kubernetes로 이전 Control Plane 운영 부담을 줄이고 배포 표준화가 필요할 때 핵심 DB와 장비가 온프레미스에 있고 왕복 호출이 잦을 때 운영 안정성은 올라가지만 네트워크 경로와 egress 비용이 새로 생길 수 있음 장애 원인의 상당수를 플랫폼이 아니라 애플리케이션 쪽으로 좁히고 싶은지
    하이브리드 클라우드 데이터는 남겨두되 웹/API 계층만 단계적으로 이전해야 할 때 조직 합의가 안 돼서 임시 절충안으로 선택할 때 초기 전환은 유연하지만 네트워크, 인증, 모니터링 경로가 가장 복잡해짐 단계적 이전의 필요성이 복잡도 증가를 정당화하는지
    리플랫폼 후 이전 hostPath, 정적 IP, 특정 Ingress annotation, 온프레미스 스토리지 전제가 강할 때 시간이 없다는 이유로 구조 개선 없이 그대로 들고 가려 할 때 초기 일정은 늘어나도 장기적으로 운영 단순화와 재현성이 좋아짐 지금의 편의를 위해 미래 장애를 예약하는 구조인지

    제 권고는 비교적 분명합니다. 온프레미스 의존성이 데이터와 장비에 붙어 있으면 유지 또는 제한적 하이브리드, 문제의 본질이 플랫폼 운영 부담이면 관리형 Kubernetes가 맞습니다. 둘 다 애매하면 마이그레이션 설계부터 시작하지 말고, 먼저 의존성 인벤토리와 트래픽 흐름도를 만드는 게 훨씬 낫습니다.

    K8s 이전 전략, 이렇게 나누면 덜 꼬입니다

    마이그레이션은 세 단계로 나눠야 덜 꼬입니다. 이걸 한 번에 밀어붙이면 꼭 뒤에서 문제가 터지더라고요.

    1. 발견(Discovery): 현재 클러스터 리소스, 외부 의존성, 네트워크 경로, 보안 정책 수집
    2. 분리(Decoupling): 클라우드 비호환 요소 제거, 환경 차이 분리, 매니페스트 표준화
    3. 이전(Migration): 워크로드 배포, 데이터 동기화, 트래픽 전환, 롤백 절차 검증

    여기서 가장 자주 실패하는 건 발견 단계를 "대충 리소스 목록 뽑는 작업"으로 축소하는 경우입니다. 실제로는 이 단계에서 이전 난도 분류까지 끝내야 해요. 같은 Deployment라도 한쪽은 ConfigMap과 Secret만 있으면 바로 올라가고, 다른 한쪽은 내부 LDAP, SMB 마운트, 고정 IP 허용 목록, 사내 인증서 체인이 없으면 부팅조차 안 됩니다. 예전에 배치 잡 하나를 단순 API 보조 작업으로 봤다가, 내부 파일 서버 경로가 빠져 있다는 걸 뒤늦게 발견해 일정이 밀린 적이 있습니다. YAML은 얌전했는데 런타임 의존성은 전혀 얌전하지 않았던 케이스였죠.

    실전 구현 1: 현재 의존성부터 수집하기

    아래 명령은 단순 인벤토리 수집용이 아닙니다. "무엇이 이동을 어렵게 만드는지"를 분류하기 위한 출발점에 가깝습니다. 리소스 수보다 위험 신호를 읽는 용도로 보셔야 합니다.

    kubectl get nodes -o wide
    kubectl get ns
    kubectl get deploy,statefulset,daemonset -A -o wide
    kubectl get svc,ingress,endpointslices -A
    kubectl get pvc,pv -A
    kubectl get storageclass
    kubectl get networkpolicy -A
    kubectl get poddisruptionbudget -A
    kubectl get sa,role,rolebinding,clusterrole,clusterrolebinding -A
    kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
    kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\t"}{range .spec.volumes[*]}{.name}{":"}{.hostPath.path}{":"}{.persistentVolumeClaim.claimName}{" "}{end}{"\n"}{end}'

    이 결과를 볼 때 바로 표시해두면 좋은 위험 항목은 아래와 같습니다.

    • StatefulSet, PersistentVolumeClaim, PersistentVolume: 데이터 이전과 복구 절차 검증이 필요합니다.
    • hostPath: 관리형 환경이나 강화된 보안 정책에서는 제약을 받거나 의미가 달라질 수 있습니다.
    • DaemonSet: 노드 접근, 로그 수집, 보안 에이전트처럼 클라우드 노드 정책과 충돌하기 쉽습니다.
    • webhook: admission webhook가 필수 경로에 있고 failurePolicy: Fail로 운영되면 배포 자체가 막힐 수 있습니다.
    • serviceAccount와 RBAC: 클라우드 IAM 연계나 워크로드 아이덴티티 체계로 바뀌면 권한 모델이 달라집니다.
    • NetworkPolicy: CNI 구현 차이 때문에 같은 정책이라도 적용 범위와 디버깅 포인트가 달라질 수 있습니다.

    이 단계에서 꼭 같이 봐야 하는 게 파드 스펙에 박힌 온프레미스 전제입니다. 저는 아래처럼 한 번 더 필터링해서 눈에 띄는 항목을 추립니다.

    kubectl get deploy,statefulset,daemonset -A -o yaml | egrep 'hostPath:|nodeSelector:|tolerations:|topologySpreadConstraints:|storageClassName:|loadBalancerIP:|externalIPs:|dnsConfig:|dnsPolicy:|ingressClassName:'
    kubectl get ingress -A -o yaml | egrep 'kubernetes.io/ingress.class|nginx.ingress.kubernetes.io|alb.ingress.kubernetes.io|traefik.ingress.kubernetes.io'

    예를 들어 loadBalancerIP나 externalIPs에 기대는 매니페스트는 클라우드에서 그대로 동작하지 않는 경우가 많습니다. 또 nodeSelector가 온프레미스 물리 노드 이름이나 특정 랙 구성을 전제로 작성돼 있으면 스케줄링이 바로 깨질 수 있어요. 이런 항목은 나중에 급히 손보는 게 아니라, 발견 단계에서 리플랫폼 대상으로 태깅해두는 편이 훨씬 편합니다.

    판단 기준도 리소스 종류보다 서비스 특성에 두는 게 좋습니다. 예를 들어 PVC가 붙은 핵심 업무 워크로드는 단순 재배포 대상이 아니라 데이터 정합성 검증 대상입니다. 반대로 외부 상태 저장소를 이미 쓰는 API 서버는 선행 이전 후보가 됩니다. 이 분류만 제대로 해도 일정 산정이 훨씬 현실적으로 바뀝니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 의존성 매핑 이미지

    네임스페이스, 인그레스, 스토리지, 네트워크 정책을 기준으로 이전 대상과 위험 요소를 분류하는 이미지입니다.

    실전 구현 2: 매니페스트를 클라우드 친화적으로 정리하기

    온프레미스에서 잘 돌던 매니페스트를 그대로 들고 가면, 처음엔 배포가 되는 것처럼 보여도 운영 구간에서 문제가 납니다. 특히 StorageClass, Ingress, anti-affinity, 보안 컨텍스트, 서비스 어카운트 연계는 거의 항상 재검토 대상이에요. 이 단계의 목표는 단순히 배포 가능이 아니라 새 환경에서 설명 가능한 동작입니다. 이 기준을 잡아두면 나중에 장애가 나도 훨씬 빨리 좁혀집니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sample-api
      namespace: prod
    spec:
      replicas: 3
      revisionHistoryLimit: 5
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 0
          maxSurge: 1
      selector:
        matchLabels:
          app: sample-api
      template:
        metadata:
          labels:
            app: sample-api
        spec:
          serviceAccountName: sample-api
          terminationGracePeriodSeconds: 30
          securityContext:
            runAsNonRoot: true
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: sample-api
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
                - weight: 100
                  podAffinityTerm:
                    topologyKey: kubernetes.io/hostname
                    labelSelector:
                      matchLabels:
                        app: sample-api
          containers:
            - name: app
              image: registry.example.com/sample-api:1.0.0
              imagePullPolicy: IfNotPresent
              ports:
                - name: http
                  containerPort: 8080
              env:
                - name: TZ
                  value: Asia/Seoul
              readinessProbe:
                httpGet:
                  path: /ready
                  port: http
                initialDelaySeconds: 5
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3
              livenessProbe:
                httpGet:
                  path: /health
                  port: http
                initialDelaySeconds: 15
                periodSeconds: 20
                timeoutSeconds: 2
                failureThreshold: 3
              startupProbe:
                httpGet:
                  path: /ready
                  port: http
                failureThreshold: 30
                periodSeconds: 5
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"
                limits:
                  cpu: "1"
                  memory: "512Mi"
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: sample-api
      namespace: prod
    spec:
      selector:
        app: sample-api
      ports:
        - name: http
          port: 80
          targetPort: http
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: sample-api
      namespace: prod
    spec:
      ingressClassName: nginx
      rules:
        - host: sample-api.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: sample-api
                    port:
                      number: 80

    이 예시에서 실무적으로 중요한 포인트는 세 가지입니다. 첫째, readinessProbe와 startupProbe를 분리해서 초기 기동이 느린 애플리케이션이 롤링 업데이트 중 불필요하게 죽지 않게 해야 합니다. Kubernetes 공식 문서 기준으로도 startupProbe가 성공하기 전에는 readiness와 liveness가 동작하지 않기 때문에, 느린 기동 앱에서 꽤 유용하더라고요. 둘째, anti-affinity와 topologySpreadConstraints를 넣어 두지 않으면 이전 직후 일부 노드에 파드가 몰려 장애 복원력이 떨어질 수 있습니다. 셋째, 예전 annotation 중심 Ingress 설정 대신 가능하면 ingressClassName처럼 명시적 필드를 쓰는 편이 환경 이식성에 유리합니다.

    Helm이나 Kustomize를 쓸 때도 무조건 환경별 파일을 늘리기보다, 변해야 하는 축만 분리하는 쪽이 좋습니다. 예를 들어 온프레미스와 클라우드 차이가 스토리지 클래스, 인그레스 클래스, 서비스 타입뿐이라면 그 값만 오버레이로 빼는 식이죠. YAML 전체를 환경별로 복제하면 당장은 편하지만 6개월만 지나도 drift가 생깁니다. 운영팀이 제일 힘들어하는 건 복잡한 기술 자체보다, 서로 조금씩 다른 매니페스트인 경우가 많습니다.

    실전 구현 3: 이전 전 검증 체크리스트 만들기

    배포 전에 검증 항목을 코드화하지 않으면, 마이그레이션 당일 사람 기억력에 의존하게 됩니다. 이건 진짜 위험합니다. 최소한 아래 정도는 사전 검증 명령으로 굳혀두는 편이 좋습니다.

    kubectl diff -f k8s/
    kubectl apply --server-side --dry-run=server -f k8s/
    kubectl auth can-i create deployments --as system:serviceaccount:prod:deployer -n prod
    kubectl auth can-i get secrets --as system:serviceaccount:prod:deployer -n prod
    kubectl wait --for=condition=Available deployment/sample-api -n prod --timeout=180s
    kubectl top pod -A
    kubectl top node
    kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -n 50

    핵심은 명령 자체보다 해석 기준입니다.

    • kubectl diff에서 스토리지, 셀렉터, 서비스 타입 차이가 크면 단순 이전보다 구조 정리가 먼저입니다.
    • --dry-run=server가 실패하면 API 버전 문제만 보지 말고 admission policy, CRD 누락, webhook 응답 실패도 같이 의심해야 합니다.
    • auth can-i가 막히면 RBAC만의 문제가 아니라 클라우드 IAM 연동 방식 차이일 수 있습니다.
    • kubectl top는 평균값보다 편차를 봐야 합니다. 일부 노드에 몰리면 스케줄링 정책 문제일 가능성이 큽니다. 다만 이 명령은 Metrics Server가 구성돼 있어야 동작합니다.
    • events는 단발성 경고보다 반복 패턴이 중요합니다. 같은 Warning이 주기적으로 나오면 구조 문제일 때가 많습니다.

    실제 시나리오를 하나 들어보면 더 명확합니다. 온프레미스에 있던 API가 클라우드로 올라간 뒤 readiness는 통과하는데 사용자 응답만 느려지는 경우가 있습니다. 이때 많은 팀이 애플리케이션 성능 저하로 접근하는데, 제가 본 사례 대부분은 앱 코드보다 외부 DB 왕복 경로 증가, NAT 경유, 사내 DNS 포워딩 지연, 프록시 재시도가 원인이었습니다. 앱 로그만 보면 안 보이고, 네트워크 관점으로 봐야 잡히는 문제였죠.

    하이브리드 클라우드 기반 온프레미스 Kubernetes 클라우드 마이그레이션 네트워크 이미지

    온프레미스 DB, 클라우드 Kubernetes, VPN 또는 전용선 연결, Ingress와 egress 흐름을 설명하는 이미지입니다.

    자주 터지는 문제와 해결법

    이 구간은 문서보다 경험 차이가 크게 나는 부분입니다. 여기서 시간을 아끼려는 시도가 오히려 더 비싸게 돌아오는 경우를 많이 봤습니다.

    1. 스토리지 클래스 이름만 바꾸고 끝난 줄 아는 경우

    PVC가 Bound 되었다고 애플리케이션이 같은 성격의 스토리지를 얻은 건 아닙니다. ReadWriteOnce, ReadWriteMany, 파일시스템 특성, 스냅샷, 확장 방식, 마운트 지연이 전부 다를 수 있어요. 온프레미스에서 NFS나 CephFS 기반 RWX로 쓰던 구성이 클라우드 블록 스토리지로 바뀌면, 동일 노드 외 동시 접근 전제가 깨질 수 있습니다. 근본 원인은 Kubernetes라기보다 백엔드 스토리지 모델 차이에 있습니다.

    이럴 때는 YAML 수정 전에 두 가지를 먼저 확인하는 편이 낫습니다. 첫째, 애플리케이션이 정말 공유 파일시스템 semantics를 필요로 하는지. 둘째, 백업과 복구가 volume snapshot 중심인지 파일 복제 중심인지. 이걸 구분하지 않으면 이전 후 복구 절차가 오히려 후퇴할 수 있습니다.

    2. 서비스 디스커버리와 DNS 지연

    이전 후 "애플리케이션은 살아 있는데 호출이 이상하게 느리다"는 사례의 상당수가 DNS입니다. CoreDNS upstream, search domain, 사내 DNS 포워더, 외부 인증 서버 해석 경로가 바뀌면 앱은 멀쩡해도 응답 시간이 흔들립니다. 특히 내부 FQDN이 아닌 짧은 호스트명을 쓰던 서비스는 환경이 바뀌는 순간 문제가 확 드러납니다. 근본 원인은 앱 로직이 아니라 이름 해석 경로의 숨은 결합인 경우가 많습니다.

    kubectl -n kube-system get configmap coredns -o yaml
    kubectl run dns-debug --rm -it --image=busybox:1.36 --restart=Never -- nslookup internal.example.local
    kubectl exec -n prod deploy/sample-api -- cat /etc/resolv.conf
    kubectl exec -n prod deploy/sample-api -- wget -S -O- http://sample-api.prod.svc.cluster.local/health

    이 네 가지를 보면 search domain, nameserver, 내부 서비스 해석, 외부 도메인 질의가 어디서 막히는지 꽤 빨리 드러납니다. 저도 앱 팀이 성능 문제라고 가져오면 먼저 /etc/resolv.conf와 DNS 응답 경로부터 봅니다. 의외로 거기서 끝나는 경우가 정말 많더라고요.

    3. 보안 정책 충돌

    온프레미스에서는 관성적으로 허용되던 privileged 컨테이너, hostNetwork, hostPath, 루트 권한 실행이 관리형 환경이나 강화된 정책 환경에선 제약을 받기 쉽습니다. Pod Security Admission, admission webhook, 조직 보안 정책이 한꺼번에 걸리면 파드가 Pending도 아니고 아예 생성 거부될 수 있습니다. 이런 문제의 근본 원인은 "클라우드가 엄격해서"라기보다 기존 워크로드가 노드 내부 구조를 전제로 하고 있었기 때문인 경우가 많습니다.

    이 경우엔 예외를 늘리기보다 정말 필요한 권한인지 먼저 줄이는 편이 낫습니다. 이전 프로젝트에서 hostPath 의존 로그 수집기를 그대로 들고 가려다가 시간을 많이 썼는데, 결국 표준 로그 수집 방식으로 바꾸고 나서야 운영이 단순해졌습니다. 예외 허용은 빨라 보이지만 이후 감사와 장애 대응이 계속 무거워집니다.

    4. 관측성 누락

    마이그레이션 당일에는 "왜 느리지", "왜 일부만 실패하지", "왜 롤아웃이 안 끝나지" 같은 질문이 동시에 쏟아집니다. 이때 메트릭, 로그, 트레이스 중 하나라도 비어 있으면 원인 파악 속도가 급격히 떨어집니다. 특히 하이브리드에서는 온프레미스 구간과 클라우드 구간을 한 화면에서 연결해보지 못하면 책임 공방이 먼저 시작되기 쉽습니다. 근본 원인은 도구 부재보다 관측 기준점이 분산된 설계에 있습니다.

    기준은 단순합니다. 이전 전부터 적어도 애플리케이션 로그, Kubernetes 이벤트, 기본 자원 메트릭, 외부 의존성 호출 경로를 함께 볼 수 있어야 합니다. 배포만 성공하고 관측이 비어 있으면, 사실상 프로덕션 디버깅을 라이브로 시작하는 셈이거든요.

    비용보다 먼저 봐야 할 것: 네트워크와 운영 복잡도

    많은 팀이 클라우드 이전을 논의할 때 바로 인스턴스 비용부터 계산합니다. 물론 중요합니다. 다만 실제로 일정과 장애를 더 크게 흔드는 건 컴퓨트 단가보다 네트워크 경로와 운영 복잡도입니다. 온프레미스 DB를 계속 쓰면서 앱만 클라우드로 올리면, 성능 문제는 CPU가 아니라 왕복 지연에서 먼저 드러나는 경우가 많습니다. 그리고 하이브리드 구조에선 누가 어떤 구간을 책임지는지도 흐려지기 쉽습니다.

    질문 A를 택할 때 B를 택할 때 실무 추천
    DB를 어디에 둘 것인가 온프레미스 유지: 데이터 상주와 장비 연동에 유리 클라우드 이전: 앱과 DB를 가깝게 두기 쉬움 앱-DB 왕복이 잦으면 가능한 한 같은 쪽에 두는 편이 안전합니다.
    Control Plane을 누가 운영할 것인가 직접 운영: 유연성은 크지만 업그레이드 부담이 큼 관리형 사용: 표준화와 운영 단순화에 유리 플랫폼 운영 인력이 부족하면 관리형이 대체로 맞습니다.
    트래픽을 한 번에 바꿀 것인가 빅뱅 전환: 구조는 단순하지만 실패 비용이 큼 점진 전환: 검증은 쉬우나 경로 복잡도가 증가 stateless부터 점진 전환, 상태 저장 계층은 별도 계획이 현실적입니다.
    환경별 매니페스트를 어떻게 관리할 것인가 완전 분리: 빠르게 시작 가능 공통 베이스 + 차이만 오버레이: 관리 일관성 확보 장기 운영을 생각하면 차이만 분리하는 쪽이 drift를 줄입니다.

    검증과 결과 확인은 이렇게 보시면 됩니다

    이전 완료를 배포 성공으로만 판단하면 안 됩니다. 실제로 봐야 하는 건 애플리케이션이 새 환경에서 안정적으로 서비스되고, 장애 원인을 추적할 수 있는 상태인지입니다.

    1. 모든 파드가 Running인지보다 Ready 상태가 안정적으로 유지되는지
    2. 재시작, Pending, 이미지 풀 실패, 스케줄링 실패가 없는지
    3. Ingress를 통한 실제 사용자 경로와 내부 서비스 간 호출이 모두 정상인지
    4. 백업, 복구, 롤백 절차가 새 환경에서 재현 가능한지
    kubectl get pods -A
    kubectl get events -A --sort-by=.metadata.creationTimestamp
    kubectl rollout status deploy/sample-api -n prod
    kubectl logs deploy/sample-api -n prod --tail=100
    kubectl describe pod -n prod -l app=sample-api
    kubectl get endpoints -n prod sample-api -o yaml

    해석 기준은 조금 더 구체적이어야 합니다.

    • Pending가 남으면 리소스 부족보다 먼저 스토리지 바인딩, 노드 셀렉터, taint/toleration, zone 제약을 의심합니다.
    • CrashLoopBackOff면 이미지보다 환경 변수, Secret, 외부 시스템 연결 실패를 먼저 봅니다.
    • rollout status가 지연되면 readinessProbe 경로, 서비스 엔드포인트, Ingress 백엔드 연결을 함께 확인합니다.
    • describe pod에서 이벤트 순서를 보면 이미지 풀 실패인지, 볼륨 마운트 실패인지, probe 실패인지 구분이 빨라집니다.
    • endpoints가 비어 있으면 서비스 셀렉터와 파드 라벨 불일치부터 의심하는 게 빠릅니다.

    제가 자주 하는 질문도 있습니다. "지금 장애는 앱이 못 뜨는 문제인가, 떴는데 연결이 느린 문제인가, 연결은 되는데 권한이 막히는 문제인가?" 이 세 가지를 빨리 분리하면 대응 속도가 확 달라집니다. 마이그레이션 실패는 대개 기능 실패보다 분류 실패에서 길어집니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 검증 대시보드 이미지

    파드 상태, 이벤트, 에러 로그, 롤아웃 상태를 함께 보는 검증 단계 이미지입니다.

    하이브리드 클라우드가 맞는 경우, 아닌 경우

    하이브리드는 멋있어 보이지만, 운영팀 관점에선 가장 신중해야 하는 선택입니다. 두 환경의 장점을 동시에 쓰는 구조라기보다, 두 환경의 제약을 동시에 관리하는 구조가 되기 쉽기 때문이죠. 그래도 분명 맞는 경우는 있습니다.

    • 하이브리드가 맞는 경우: 데이터는 사내에 남겨야 하지만, 웹/API 계층의 배포 속도와 확장성이 더 시급할 때
    • 하이브리드가 피곤한 경우: 조직 합의가 안 돼서 임시 절충안으로 택할 때
    • 완전 이전이 맞는 경우: 플랫폼 운영 인력이 부족하고 관리형 Kubernetes 이점을 크게 볼 수 있을 때
    • 온프레미스 유지가 맞는 경우: 장비 연동, 내부 전용망, 초저지연 요구가 비즈니스 핵심일 때

    현업에서 내리는 권고는 이렇습니다. 하이브리드는 전환 과정 때문에 택하는 것이지, 영구 기본값으로 두면 운영 비용이 커질 가능성이 높습니다. 단계적 이전이 목적이라면 기간, 책임 경계, 최종 목표 상태를 미리 정해두는 편이 좋습니다. 목표 없는 하이브리드는 거의 항상 복잡도만 남깁니다.

    자주 묻는 질문

    무중단으로 꼭 옮겨야 하나요?

    항상 그렇진 않습니다. 세션 상태가 외부 저장소에 있고, 데이터 동기화와 트래픽 전환 절차가 준비돼 있으면 점진 전환이 가능합니다. 반대로 상태 저장 워크로드는 억지 무중단보다 짧고 통제된 점검 시간이 더 안전할 때가 많습니다. 저는 "무중단"보다 롤백 가능한 전환을 더 높은 우선순위로 둡니다.

    기존 YAML을 그대로 재사용해도 되나요?

    일부는 가능합니다. 다만 StorageClass, Ingress, 서비스 타입, 보안 컨텍스트, 노드 스케줄링, 아이덴티티 연계는 거의 항상 다시 봐야 했습니다. 같은 Kubernetes여도 스토리지와 네트워크, 인증 모델이 다르면 운영 의미가 달라지거든요.

    무엇부터 옮기는 게 좋을까요?

    보통은 stateless API, 내부 도구, 배치 워크로드부터 시작하는 편이 좋습니다. 반대로 공유 파일시스템 의존 서비스, 내부 장비와 강결합된 서비스, 핵심 DB 연동이 많은 서비스는 뒤로 미루는 게 안전합니다. 실패 비용이 낮은 워크로드에서 먼저 학습한 뒤 어려운 대상을 다루는 편이 전체 일정이 덜 흔들립니다.

    온프레미스 Kubernetes 클라우드 마이그레이션 선택 기준 요약 이미지

    각 선택지별 추천 상황과 주의사항을 짧게 비교하는 요약 이미지입니다.

    어떤 경우에 어떤 선택을 하면 되냐면

    온프레미스 Kubernetes 클라우드 마이그레이션의 정답은 하나가 아닙니다. 다만 선택 기준은 분명해야 합니다. 사내 데이터 의존과 장비 연동이 강하면 온프레미스 유지 또는 제한적 하이브리드가 맞습니다. 반대로 플랫폼 운영 부담이 이미 팀 생산성을 갉아먹고 있다면, 관리형 Kubernetes로 빨리 표준화하는 편이 낫습니다. 애매한 상태로 둘 다 잡으려 하면 실제로는 운영 복잡도만 늘어나는 경우가 많습니다.

    실무에서 권하는 판단 순서는 이렇습니다. 첫째, 데이터와 네트워크 경로를 기준으로 애플리케이션을 분류합니다. 둘째, 운영 책임을 줄이는 게 핵심이면 관리형으로 갑니다. 셋째, 하이브리드는 단계적 이전이 꼭 필요할 때만 씁니다. 넷째, hostPath, RWX 의존, 정적 네트워크 전제가 많다면 리플랫폼을 먼저 합니다. 이 기준이 서면 마이그레이션은 훨씬 덜 추상적이고, 일정과 위험도도 현실적으로 보이기 시작합니다.

    관련해서 다음 글에서는 Kubernetes Ingress와 Load Balancer를 클라우드 환경에서 어떻게 재설계할지를 더 깊게 다뤄보겠습니다. 결국 잘 된 이전은 화려한 아키텍처보다, 의존성 인벤토리, 검증 가능한 체크리스트, 단순한 운영 구조에서 나옵니다. 이 부분은 여러 번 해봐도 결국 같은 결론으로 돌아오더라고요.

  • [Kubernetes] 클라이언트 비교: Lens, K9s, kubectl 선택 가이드

    [Kubernetes] 클라이언트 비교: Lens, K9s, kubectl 선택 가이드

    [Kubernetes] 클라이언트 비교: Lens, K9s, kubectl 선택 가이드

    Kubernetes 클라이언트 비교가 왜 중요하냐고 물으시면, 운영 환경이 조금만 커져도 답이 바로 나옵니다. 파드(Pod, 컨테이너 실행 단위) 하나 상태 보는 건 쉬운데, 네임스페이스(Namespace, 리소스 격리 단위) 여러 개를 오가고 로그를 보고 이벤트를 확인하고 배포 상태까지 챙기려면 Kubernetes 관리 도구 선택이 꽤 중요하거든요. 저도 처음엔 kubectl만 붙잡고 버텼었는데, 실제로 써보니까 Lens, K9s, kubectl은 서로 경쟁 관계라기보다 역할이 다른 도구에 더 가깝더라고요. 오늘은 제가 홈랩과 실무에서 겪었던 흐름을 바탕으로, 어떤 상황에서 무엇을 고르면 덜 삽질하는지 정리해보겠습니다.

    특히 이 글은 단순한 스펙 나열이 아니라, Kubernetes 클라이언트 비교 관점에서 실제 운영 감각을 중심으로 풀어볼 생각입니다. 혹시 여러분도 'GUI가 편하긴 한데 결국 터미널로 돌아오게 되던데요?' 같은 경험 있으신가요? 저도 딱 그랬습니다 ㅎㅎ

    Lens, K9s, kubectl이 각각 어느 레이어에서 관리 경험을 제공하는지 한눈에 보여주는 개요 이미지입니다.

    Kubernetes 클라이언트 비교, 먼저 큰 그림부터 보겠습니다

    쉽게 말해 이렇습니다.

    • kubectl: Kubernetes 공식 CLI(Command Line Interface, 명령줄 도구)입니다.
    • K9s: 터미널 기반 TUI(Text User Interface, 텍스트 UI)로 클러스터 상태를 빠르게 탐색하게 도와줍니다.
    • Lens: GUI(Graphical User Interface, 그래픽 화면 기반) 중심으로 리소스를 시각적으로 관리하기 좋습니다.

    여기서 중요한 포인트가 하나 있습니다. 세 도구 모두 결국 kubeconfig(Kubernetes 접속 설정 파일)를 기반으로 클러스터에 접근하는 경우가 많다는 점입니다. 즉, 접근 권한과 컨텍스트(context, 현재 대상 클러스터/네임스페이스 설정)가 핵심이고, 겉모습만 다르다고 생각하시면 이해가 빠릅니다.

    Lens, K9s, kubectl 차이점은 어디서 체감될까요?

    1. kubectl 사용법이 기본기가 되는 이유

    kubectl은 결국 기준점입니다. Deployment(디플로이먼트, 배포 리소스), Service(서비스, 네트워크 노출 리소스), Ingress(인그레스, 외부 트래픽 진입점) 같은 객체를 가장 정확하게 다루기 좋거든요. 자동화 스크립트나 CI/CD에서도 거의 기본 전제로 깔립니다.

    제가 직접 해보니 장애 상황에서는 결국 kubectl로 돌아오게 되더라고요. 이유는 단순합니다. 로그, describe, events, yaml 출력까지 가장 세밀하게 볼 수 있기 때문입니다.

    2. K9s가 빛나는 순간

    K9s는 '터미널 안에서 빠르게 둘러보는 맛'이 있습니다. 방향키와 단축키로 파드 이동하고, 로그 보고, 리소스 상태를 훑는 흐름이 상당히 빠릅니다. SSH로 바로 들어가서 점검하는 환경에서는 특히 강합니다. GUI가 없는 서버 점검 환경에서는 진짜 편하더라고요.

    3. Lens가 편한 이유

    Lens는 시각화가 좋습니다. 여러 리소스를 화면에서 관계적으로 보기가 쉽고, 초보자도 진입 장벽이 낮은 편입니다. 특히 처음 Kubernetes를 접하는 분들은 YAML만 보는 것보다 상태를 한 화면에서 확인하는 방식이 훨씬 이해가 빠를 수 있습니다.

    다만 GUI가 편하다고 해서 운영의 본질이 쉬워지는 건 아닙니다. 여기서 많이 헷갈리시더라고요. 화면은 친절하지만, 권한 문제나 리소스 의미를 모르면 결국 판단은 사람이 해야 하거든요.

    쿠버네티스 관리 도구 비교 표로 정리해보겠습니다

    항목 kubectl K9s Lens
    형태 CLI TUI GUI
    학습 난이도 초반엔 높음 중간 낮음
    속도 익숙하면 매우 빠름 탐색이 빠름 화면 기반 확인이 편함
    자동화 적합성 매우 높음 낮음 낮음
    로그/이벤트 심층 분석 강함 강함 보통 이상
    원격 SSH 환경 매우 적합 매우 적합 제한적
    입문자 친화성 낮음 중간 높음
    추천 대상 운영자, 자동화 담당자 터미널 선호 운영자 초보자, 시각적 관리 선호자

    Lens K9s 비교만 놓고 보면, 초반 적응은 Lens가 쉽고 장기 운영 효율은 K9s가 더 손에 붙는 경우가 많습니다. 하지만 kubectl이 빠지면 결국 한계가 옵니다. 이건 꽤 중요합니다.

    실전 구현: kubectl, K9s, Lens를 같은 흐름에서 써보기

    이론만 보면 감이 잘 안 오죠. 그래서 간단한 점검 흐름으로 묶어보겠습니다. 예시는 특정 배포가 정상인지 확인하는 상황입니다.

    1. 현재 컨텍스트와 네임스페이스를 확인합니다.
    2. 배포와 파드 상태를 봅니다.
    3. 문제가 있는 파드 로그와 이벤트를 확인합니다.
    4. 필요하면 GUI나 TUI에서 리소스 관계를 함께 봅니다.

    1. kubectl로 기본 점검하기

    kubectl config get-contexts
    kubectl config current-context
    kubectl get ns
    kubectl get deploy -A
    kubectl get pods -A -o wide

    처음엔 이게 뭔가 싶었는데, 사실 운영자는 이 네 줄만 자주 써도 클러스터 현재 상태를 꽤 빨리 파악할 수 있습니다. 특히 current-context를 먼저 보는 습관은 꼭 들이시는 걸 추천합니다. 잘못된 클러스터에 명령 넣는 사고, 생각보다 자주 납니다.

    2. 특정 애플리케이션 문제 좁혀가기

    kubectl get pods -n production
    kubectl describe pod myapp-abc123 -n production
    kubectl logs myapp-abc123 -n production --tail=100
    kubectl get events -n production --sort-by=.metadata.creationTimestamp

    실제로 써보니까 장애 대응에서 제일 중요한 건 '무엇이 실패했는가'보다 '어디서부터 실패했는가'를 빠르게 찾는 거였습니다. describe 결과에서 이벤트를 보고, 로그에서 에러 패턴을 보고, 리소스 부족인지 이미지 풀(image pull) 실패인지 분리하는 게 핵심입니다.

    Kubernetes 클라이언트 비교에서 kubectl과 K9s 사용 장면 이미지

    운영자가 터미널 기반으로 파드 상태를 점검하고 로그를 추적하는 실전 흐름을 보여주는 이미지입니다.

    3. K9s로 빠르게 탐색하기

    k9s

    K9s는 실행 자체는 단순합니다. 다만 익숙해지려면 머릿속에 동선이 생겨야 합니다. 보통 저는 이런 순서로 봤습니다.

    1. 네임스페이스를 맞춥니다.
    2. Pods 화면에서 재시작 횟수와 상태를 봅니다.
    3. 문제 파드를 선택해 로그와 describe 성격의 정보를 확인합니다.
    4. 리소스 사용량이나 관련 객체로 이동합니다.

    SSH 세션 하나에서 다 처리되는 게 장점입니다. 홈랩에서 VM 여러 대 만질 때는 이 방식이 정말 편했어요. 마우스 손 안 대도 되니까 흐름이 끊기지 않거든요.

    4. Lens로 시각적으로 확인하기

    Lens는 클러스터를 등록한 뒤 Workloads(워크로드), Pods, Events, Config, Networking 같은 메뉴를 통해 탐색하는 방식입니다. 제가 초급자 분들 온보딩할 때는 Lens를 먼저 보여드린 적이 많았습니다. '아, Kubernetes 안에서 이런 객체들이 연결되는구나'가 빨리 보이기 때문입니다.

    예를 들어 서비스 장애가 났을 때, 파드만 보는 게 아니라 서비스와 엔드포인트(endpoint), 인그레스까지 같이 봐야 할 때가 있거든요. 이때 GUI의 장점이 분명히 있습니다.

    Kubernetes 클라이언트 비교를 위한 GUI 관리 도구 화면 이미지

    워크로드, 서비스, 인그레스 관계를 시각적으로 확인하는 GUI 관리 흐름을 표현한 이미지입니다.

    어떤 사람에게 어떤 도구가 맞을까요?

    kubectl이 맞는 경우

    • 운영 자동화와 스크립트 작업이 많습니다.
    • CI/CD 파이프라인과 연동해야 합니다.
    • 장애 원인을 깊게 파고드는 편입니다.
    • Kubernetes 클라이언트 도구의 기준 문법을 익히고 싶습니다.

    K9s가 맞는 경우

    • 터미널 작업이 익숙합니다.
    • 원격 서버나 SSH 환경에서 자주 일합니다.
    • 클러스터 탐색 속도를 중요하게 봅니다.
    • 마우스보다 키보드가 편합니다.

    Lens가 맞는 경우

    • Kubernetes 입문 단계입니다.
    • 팀원들과 화면 공유하며 상태를 설명해야 합니다.
    • 리소스 관계를 시각적으로 보는 게 편합니다.
    • 한눈에 파악되는 대시보드 느낌을 선호합니다.

    ⚠️ 실제로 자주 겪는 문제와 해결 방법

    여기서는 제가 삽질 좀 했던 부분을 솔직하게 적어보겠습니다. 이런 건 문서보다 경험에서 더 빨리 배우게 되더라고요.

    1. 컨텍스트(context) 착각

    가장 위험합니다. 개발 클러스터인 줄 알고 운영 클러스터를 보고 있던 적, 저도 있었습니다. 다행히 큰 사고는 안 났지만 식은땀이 나더라고요.

    kubectl config current-context
    kubectl config view --minify

    명령 전후로 현재 컨텍스트를 확인하는 습관이 중요합니다. 특히 여러 kubeconfig를 합쳐 쓰는 환경이라면 더 그렇습니다.

    2. 권한 문제(RBAC, Role-Based Access Control)

    Lens든 K9s든 kubectl이든, 권한이 없으면 안 보이거나 실패합니다. GUI가 예쁘게 보여도 백엔드는 결국 API 서버 권한 체크를 거치거든요.

    kubectl auth can-i get pods -n production
    kubectl auth can-i list secrets -n production

    화면 문제처럼 보이는데 사실은 권한 문제인 경우가 꽤 많습니다. 이거 진짜 자주 나옵니다.

    3. 로그만 보고 끝내는 실수

    처음엔 로그만 파봤었는데, 나중에 보니 이벤트(event) 쪽이 더 직접적인 경우가 많았습니다. 예를 들어 스케줄링 실패, 이미지 풀 실패, 프로브(probe) 실패 같은 건 이벤트에 힌트가 잘 남거든요.

    4. GUI 의존도가 너무 높아지는 문제

    Lens가 편해서 계속 쓰다 보면, 막상 터미널만 가능한 환경에서 당황할 수 있습니다. 그래서 제 추천은 이렇습니다. 입문은 Lens, 숙련은 K9s, 기준은 kubectl. 이 조합이 제일 덜 흔들렸습니다.

    검증: 실제로 무엇이 더 빨랐나?

    정량 벤치마크처럼 수치를 들이대고 싶진 않습니다. 확실하지 않은 숫자는 오히려 판단을 흐리거든요. 대신 체감 기준으로 말씀드리면 이렇습니다.

    • 처음 구조를 이해하는 속도는 Lens가 빨랐습니다.
    • 운영 중 다수 리소스를 훑는 속도는 K9s가 좋았습니다.
    • 정확한 원인 분석과 재현 가능한 절차 정리는 kubectl이 가장 강했습니다.

    제가 홈랩에서 여러 네임스페이스를 오가며 점검할 때도 비슷했습니다. 처음 상태 확인은 K9s나 Lens가 편했는데, 최종 확인 커맨드와 기록은 결국 kubectl로 남기게 되더라고요. 여기서 중요한 포인트! 빠른 확인 도구와 정확한 제어 도구는 다를 수 있습니다.

    Kubernetes 클라이언트 비교 결과를 정리한 운영 대시보드 이미지

    파드 상태, 이벤트, 로그 확인 결과가 정리된 운영 검증 화면을 묘사한 이미지입니다.

    정리: 제 선택은 하나가 아니라 조합입니다

    제 결론은 꽤 명확합니다. Kubernetes 클라이언트 비교를 할 때 하나만 고르려고 하면 오히려 답이 꼬입니다. kubectl은 반드시 익혀야 하고, 그 위에 K9s 또는 Lens를 얹는 방식이 가장 현실적입니다.

    • 초보자라면: Lens로 구조를 익히고 kubectl 사용법을 함께 배우세요.
    • 터미널 친화적 운영자라면: K9s와 kubectl 조합이 오래 갑니다.
    • 자동화까지 고려한다면: kubectl 중심 사고는 필수입니다.

    저도 처음엔 '어차피 GUI 있으면 되지 않나?' 싶었는데, 실제로 써보니까 클러스터를 깊게 이해할수록 kubectl 비중이 커졌습니다. 반대로 팀 협업이나 빠른 시각 확인은 Lens가 좋았고요. K9s는 그 사이에서 정말 실용적이었습니다. 이 셋은 배척할 대상이 아니라, 상황에 따라 꺼내 쓰는 공구함에 더 가깝습니다.

    세 도구의 장단점과 추천 대상, 사용 시나리오를 한 장으로 요약한 비교 인포그래픽 이미지입니다.

    FAQ: 많이 받는 질문

    Q1. Kubernetes 입문자는 무엇부터 시작하면 좋을까요?

    Lens로 객체 관계를 익히면서 kubectl 기본 명령을 같이 익히는 걸 추천합니다. 시각적 이해와 명령 기반 이해를 같이 가져가야 오래 갑니다.

    Q2. Lens K9s 비교에서 더 운영자다운 도구는 무엇인가요?

    터미널에 익숙하다면 K9s가 더 손에 붙을 가능성이 큽니다. 다만 팀 내 공유와 설명은 Lens가 더 편할 수 있습니다.

    Q3. kubectl만으로도 충분한가요?

    충분합니다. 다만 충분하다는 것과 편하다는 것은 다릅니다. 그래서 많은 분들이 K9s나 Lens를 보조 도구로 함께 씁니다.

    마무리

    오늘은 Kubernetes 클라이언트 비교라는 주제로 Lens, K9s, kubectl을 함께 봤습니다. 정답은 사람마다 조금 다르지만, 기준점은 분명합니다. 제어와 자동화는 kubectl, 빠른 탐색은 K9s, 시각적 이해는 Lens입니다. 이 프레임만 잡아도 도구 선택이 훨씬 쉬워집니다.

    다음 글에서는 kubectl 사용법을 조금 더 실전적으로 파서, 운영자가 자주 쓰는 조회 명령과 로그/이벤트 확인 루틴을 따로 정리해볼 예정입니다. 이전 글에서 다뤘던 홈랩 클러스터 구성 내용과 함께 보시면 더 이해가 잘 되실 겁니다. 혹시 지금 어떤 도구를 메인으로 쓰고 계신가요? 써보면 취향도 분명히 갈리더라고요.

  • [k8s] Karpenter 비용 최적화: 불필요한 노드 스케일링 방지 전략

    [k8s] Karpenter 비용 최적화: 불필요한 노드 스케일링 방지 전략

    [k8s] Karpenter 비용 최적화: 불필요한 노드 스케일링 방지 전략

    Karpenter 비용 최적화는 쿠버네티스 비용 절감에서 생각보다 훨씬 큰 비중을 차지하는데요. 클러스터를 한동안 운영해보면 CPU나 메모리를 거의 안 쓰는데도 노드가 계속 살아 있고, 짧은 스파이크 때문에 큰 인스턴스가 붙었다가 한참 뒤에야 내려가는 상황을 꼭 한 번쯤 겪게 되더라고요. 저도 홈랩과 실서비스 환경에서 Karpenter를 붙인 뒤 처음엔 “오토스케일링이면 자동으로 다 좋아지겠지”라고 생각했었는데, 실제로 써보니까 설정 하나 잘못 두면 Karpenter 노드 스케일링이 오히려 비용을 키우는 방향으로 움직이더라고요.

    특히 요청값(requests) 과대 설정, 파드 배치 밀도 부족, 비어 있지 않은 애매한 노드, 지나치게 넓은 인스턴스 선택 범위가 겹치면 노드가 쉽게 늘어나는데요. 여기서 중요한 포인트는 Karpenter가 문제의 원인이 아니라 현재 스케줄링 조건에 굉장히 충실하게 반응하는 엔진이라는 거더라고요. 쉽게 말해 입력이 거칠면 결과도 거칠게 나옵니다. 이번 글에서는 제가 직접 삽질하면서 정리한 Karpenter 비용 최적화 방법을, 현장에서 바로 적용할 수 있는 기준 위주로 풀어보겠습니다.

    Karpenter 비용 최적화 아키텍처를 설명하는 쿠버네티스 노드 스케일링 다이어그램

    Karpenter가 스케줄링 수요를 받아 노드를 추가하고, 한가한 노드를 통합하는 흐름을 보여주는 개요 이미지입니다.

    Karpenter 비용 최적화가 어려운 이유

    저도 처음엔 헷갈렸는데, 많은 분이 Cluster Autoscaler(클러스터 오토스케일러)와 Karpenter를 비슷하게만 보시더라고요. 그런데 운영 관점에서는 결이 조금 다르더라고요. Karpenter는 스케줄되지 못한 파드(Pod)를 보고 그때그때 더 적절한 노드를 만들려는 성향이 강합니다. 그래서 잘 맞추면 정말 시원하게 붙고 빠지는데, 반대로 조건이 지저분하면 필요 이상으로 세분화된 노드가 생기기 쉬워요.

    쉽게 말해, 아래 네 가지가 겹치면 비용이 올라갑니다.

    • 과한 requests: 애플리케이션이 실제보다 큰 CPU/메모리 요청을 잡고 있는 경우
    • 낮은 bin packing: 비슷한 파드끼리 한 노드에 촘촘히 못 모이는 경우
    • 느슨한 인스턴스 선택: 너무 큰 타입까지 후보에 열어둔 경우
    • 애매한 축소 정책: 비어 있지 않지만 옮길 수 있는 노드가 오래 살아남는 경우

    결국 핵심은 필요한 순간에는 빨리 늘리고, 불필요해진 순간에는 최대한 덜 남기기예요. 이 균형을 못 잡으면 오토스케일링 최적화가 아니라 오토 과금이 됩니다 ㅎㅎ

    Karpenter 노드 스케일링을 줄이는 핵심 개념

    제가 실무에서 가장 먼저 보는 건 세 가지입니다. 바로 requests, disruption(중단/통합 정책), 그리고 노드 풀의 제약조건이에요.

    항목 왜 중요한가 비용 영향
    resources.requests 스케줄러가 파드 배치 가능 여부를 판단하는 기준 과대 설정 시 필요 노드 수 증가
    NodePool 제약 어떤 인스턴스 계열과 용량 타입을 쓸지 결정 너무 넓으면 큰 노드가 쉽게 선택됨
    Consolidation(통합) 덜 효율적인 노드를 더 적은 수의 노드로 재배치 잔여 노드 제거에 직접적
    DaemonSet 오버헤드 모든 노드에 공통으로 올라가는 리소스 사용량 소형 노드 전략을 무너뜨릴 수 있음

    여기서 특히 놓치기 쉬운 게 DaemonSet(데몬셋, 모든 노드에 공통 배포되는 워크로드) 오버헤드인데요. 모니터링 에이전트, 로그 수집기, CNI(컨테이너 네트워크 인터페이스) 관련 파드가 생각보다 자리를 많이 차지하더라고요. 저도 처음엔 “왜 이렇게 작은 파드 몇 개 때문에 노드가 또 붙지?” 싶었는데, 계산해보니 공통 오버헤드가 누적돼 있더라고요.

    실전 1: requests부터 먼저 다이어트하기

    Karpenter 비용 최적화에서 가장 효과가 큰 건 의외로 Karpenter 설정이 아니라 워크로드 설정 정리더라고요. 실제로 써보니까 requests를 현실화하는 것만으로도 불필요한 노드 스케일링이 확 줄었거든요. HPA(Horizontal Pod Autoscaler, 수평 확장)나 VPA(Vertical Pod Autoscaler, 수직 조정)를 쓰더라도 requests가 터무니없이 크면 소용이 없더라고요.

    1. 상위 소비 워크로드를 찾습니다.
    2. 실사용량 대비 requests가 과한지 봅니다.
    3. 갑자기 줄이지 말고 단계적으로 낮춥니다.
    4. 배포 후 Pending(스케줄 불가)이나 OOMKilled를 꼭 확인합니다.
    kubectl top pod -A --containers
    kubectl get deploy -A -o yaml
    kubectl describe pod -n your-namespace your-pod
    

    예를 들어 웹 애플리케이션이 실제로는 CPU를 크게 쓰지 않는데도 requests를 넉넉하게 잡아두면, 스케줄러는 그 값을 진실로 믿고 더 많은 노드를 요구하게 돼요. 아래처럼 현실적인 범위로 정리하면 밀도가 확 좋아집니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: web-api
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: web-api
      template:
        metadata:
          labels:
            app: web-api
        spec:
          containers:
            - name: web-api
              image: nginx:stable
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"
                limits:
                  cpu: "500m"
                  memory: "512Mi"
    

    물론 숫자는 서비스마다 다르게 가져가야 해요. 여기서 중요한 건 정확한 절대값이 아니라, 실제 사용 패턴과 요청값의 간격을 줄이는 것입니다. 혹시 CPU는 낮은데 메모리만 높게 유지되는 워크로드가 있다면, 그 자체가 노드 분산의 원인이 될 수 있더라고요.

    실전 2: NodePool 제약으로 큰 인스턴스 남발 막기

    다음으로 많이 효과를 본 게 NodePool(노드풀, 노드 생성 정책 묶음) 제약이에요. Karpenter는 후보군이 넓을수록 순간적으로 커 보이는 선택을 할 수 있거든요. 특히 “아무 인스턴스나 가능”처럼 열어두면 비용 최적화보다 스케줄 성공률 쪽으로 기울 수 있더라고요. 그래서 저는 업무 성격별로 NodePool을 나눠 두는 편입니다.

    아래 예시는 범용 워크로드를 위한 보수적인 제약 패턴이에요. 인스턴스 계열과 세대, 아키텍처, 용량 타입(capacity type)을 좁혀서 운영 예측 가능성을 높이는 방식이거든요.

    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: general
    spec:
      template:
        spec:
          requirements:
            - key: kubernetes.io/arch
              operator: In
              values: ["amd64"]
            - key: kubernetes.io/os
              operator: In
              values: ["linux"]
            - key: karpenter.k8s.aws/instance-category
              operator: In
              values: ["c", "m"]
            - key: karpenter.k8s.aws/instance-generation
              operator: Gt
              values: ["3"]
            - key: karpenter.sh/capacity-type
              operator: In
              values: ["on-demand", "spot"]
      disruption:
        consolidationPolicy: WhenEmptyOrUnderutilized
        consolidateAfter: 5m
    

    여기서 중요한 포인트! 제약은 좁히되, 너무 빡빡하게는 두지 않는 것이 중요해요. 후보가 지나치게 적으면 스케줄 실패나 가격 변동 대응력 저하로 이어질 수 있더라고요. 제가 처음엔 계열을 너무 좁게 잡았다가 특정 시간대에 배치가 꼬여서 다시 완화했었습니다.

    Karpenter 비용 최적화를 위한 NodePool 요구사항 구성 이미지

    NodePool 제약조건을 통해 인스턴스 후보군을 제어하는 구성을 시각화한 이미지입니다.

    실전 3: Consolidation과 만료 정책으로 잔여 노드 줄이기

    오토스케일링 최적화에서 체감 차이가 큰 기능이 Consolidation(통합)이에요. 쉽게 말해 여러 노드에 어정쩡하게 흩어진 파드를 더 적은 노드로 옮길 수 있으면 정리하는 기능이거든요. 이걸 켜두지 않으면 트래픽이 빠진 뒤에도 애매하게 남은 노드가 오래 버텨요.

    제가 직접 해보니 아래 순서로 접근하는 게 안전했습니다.

    1. 먼저 underutilized(저활용) 통합 정책을 검토합니다.
    2. 바로 공격적으로 줄이지 말고, consolidateAfter 시간을 둡니다.
    3. PodDisruptionBudget(PDB, 파드 중단 예산) 때문에 이동이 막히는지 같이 봅니다.
    4. 배치 안정성이 확인되면 빈 노드 정리 시간을 조금 더 줄입니다.
    kubectl get nodepool
    kubectl get nodes
    kubectl get pods -A -o wide
    kubectl describe node <node-name>
    

    운영하다 보면 “왜 비어 보이는 노드가 안 내려가지?” 싶은 경우가 있는데, 대부분은 다음 중 하나더라고요.

    • PDB 때문에 파드 축출이 제한됨
    • anti-affinity(안티 어피니티, 분산 배치 규칙)가 강해서 재배치가 어려움
    • DaemonSet 오버헤드 때문에 다른 노드로 합치기 애매함
    • local storage(로컬 스토리지) 의존 파드가 있음

    즉, 통합 정책을 켠다고 끝이 아니라 파드가 정말 옮겨질 수 있는 상태인지를 같이 만들어야 해요. 이 부분에서 삽질 좀 했습니다 ㅎㅎ

    실전 4: Spot과 On-Demand를 역할별로 분리하기

    쿠버네티스 비용 절감을 노린다고 무조건 Spot(스팟, 유휴 용량 기반 저비용 인스턴스)을 늘리는 건 조금 위험해요. 비용은 줄 수 있어도 재시작 민감 워크로드가 섞이면 오히려 장애 대응 비용이 올라가더라고요. 그래서 저는 성격이 다른 워크로드를 섞지 않도록 taint/toleration(테인트/톨러레이션, 특정 노드 허용 규칙)과 NodePool 분리 전략을 권장해요.

    워크로드 유형 권장 용량 타입 이유
    배치성 잡(Job), 비동기 처리 Spot 중심 중단 허용 범위가 비교적 큼
    사용자 요청 직접 처리 API On-Demand 중심 지속성과 예측 가능성이 중요
    혼합형 워크로드 분리 운영 비용과 안정성 균형 확보

    이렇게 나누면 Karpenter가 필요한 곳에만 공격적인 비용 절감 전략을 적용하게 되는데요. 결과적으로 비용도 줄고, 왜 특정 노드가 붙었는지도 설명이 쉬워져요.

    ⚠️ 주의사항: 실제로 많이 막히는 포인트

    여기부터는 경험담에 가깝습니다. 저도 처음엔 “Karpenter가 똑똑하니까 알아서 정리해주겠지” 했었는데, 아래 네 가지에서 자주 막혔어요.

    1. requests는 줄였는데 HPA가 갑자기 민감해진 경우

    CPU requests를 낮추면 상대 사용률이 높아 보이면서 HPA가 더 빨리 반응할 수 있거든요. 즉 requests 다이어트와 HPA 목표값은 같이 봐야 합니다.

    2. PDB가 너무 보수적인 경우

    minAvailable이 높으면 consolidation이 생각보다 거의 안 움직여요. 서비스 특성을 해치지 않는 선에서 현실화가 필요합니다.

    3. anti-affinity를 습관처럼 강제한 경우

    고가용성을 위해 분산시키는 건 좋지만, 모든 워크로드에 강한 규칙을 걸면 노드가 잘 안 모여요. 꼭 필요한 곳에만 써야 합니다.

    4. DaemonSet 리소스를 잊은 경우

    노드를 잘게 쪼개 쓰려면 공통 오버헤드가 치명적이에요. 모니터링, 로깅, 보안 에이전트가 많을수록 소형 노드 전략은 불리해져요.

    이런 문제를 점검할 때는 이벤트와 노드 상태를 같이 보는 게 좋습니다.

    kubectl get events -A --sort-by=.lastTimestamp
    kubectl get pdb -A
    kubectl get daemonset -A
    kubectl top node
    
    Karpenter 비용 최적화 과정에서 노드 통합이 막히는 원인을 보여주는 대시보드 이미지

    불필요한 노드가 남아 있는 원인을 추적할 때 보는 지표와 병목 요소를 보여주는 이미지입니다.

    검증: Karpenter 비용 최적화가 잘 되었는지 확인하는 법

    설정을 바꿨으면 반드시 검증해야 해요. 저는 보통 아래 순서로 봅니다.

    1. 노드 수의 일중 변동 폭이 줄었는지 확인합니다.
    2. 트래픽 감소 후 빈 노드 정리 시간이 짧아졌는지 봅니다.
    3. 평균 노드 사용률이 올라갔는지 확인합니다.
    4. Pending 파드나 재스케줄 실패가 없는지 확인합니다.

    여기서 중요한 건 비용만 보면 안 된다는 점이에요. 비용은 줄었는데 배포 시간이 늘어나거나, 특정 시간대 지연이 늘었다면 진짜 최적화가 아니거든요. 제가 보는 체크리스트는 이렇습니다.

    • 노드당 파드 밀도 증가
    • 불필요한 대형 인스턴스 비중 감소
    • 저활용 노드 체류 시간 감소
    • 서비스 안정성 유지

    모니터링 시스템이 있다면 노드 생성/삭제 이벤트, CPU/메모리 요청 대비 사용량, Spot과 On-Demand 비중을 함께 보면 좋아요. 이거 진짜 편하더라고요. 원인과 결과가 한 화면에서 이어져 보이니까요.

    Karpenter 비용 최적화 전후 결과를 보여주는 노드 사용률 및 비용 비교 이미지

    Karpenter 비용 최적화 전후의 노드 개수와 활용률 변화를 비교하는 결과 이미지입니다.

    정리: 제가 권장하는 적용 순서

    마지막으로, 처음 적용하시는 분이라면 아래 순서가 가장 안전해요. 한 번에 다 건드리기보다 단계적으로 가는 게 좋습니다.

    1. 워크로드 requests를 현실화합니다.
    2. NodePool 제약으로 과도한 인스턴스 선택을 줄입니다.
    3. Consolidation 정책을 켜고 재배치 가능성을 점검합니다.
    4. Spot과 On-Demand를 워크로드별로 분리합니다.
    5. HPA, PDB, anti-affinity를 함께 조정합니다.

    Karpenter 비용 최적화는 설정 한 줄의 마법이라기보다, 스케줄링 입력값을 정리하는 운영 습관에 가깝습니다. 저도 처음엔 Karpenter만 만지면 될 줄 알았는데, 실제로는 애플리케이션 requests와 배치 정책이 훨씬 중요했어요. 그래서 이 주제는 결국 플랫폼 팀과 애플리케이션 팀이 같이 봐야 하더라고요.

    혹시 지금 클러스터에서 노드는 자꾸 늘어나는데 사용률은 낮은 상황이라면, 가장 먼저 requests와 PDB부터 점검해보세요. 체감 효과가 큽니다. 다음 글에서는 VPA와 HPA를 함께 사용할 때 어떤 순서로 튜닝하면 좋은지 다뤄볼 예정입니다. 이전 글에서 다룬 리소스 요청값 설계 글이 있다면 같이 참고하셔도 흐름이 잘 이어져요. 드디어 됐다 싶은 순간이 분명 옵니다 🎉

    Karpenter 비용 최적화 체크리스트와 적용 순서를 정리한 인포그래픽

    실무 적용 순서와 핵심 점검 항목을 한 장으로 정리한 요약 이미지입니다.

  • [k8s] Karpenter 오토스케일링, 성능 벤치마크: 다양한 워크로드 환경 테스트

    [k8s] Karpenter 오토스케일링, 성능 벤치마크: 다양한 워크로드 환경 테스트

    [Kubernetes] Karpenter 성능 벤치마크로 보는 오토스케일링 테스트

    Karpenter 성능 벤치마크 이야기를 해보려고 합니다. Kubernetes(쿠버네티스) 운영을 조금만 오래 해보면, 결국 병목은 애플리케이션이 아니라 노드가 언제, 얼마나 빠르게 붙느냐에서 터지는 경우가 많거든요. 저도 홈랩과 실서비스에 가까운 테스트 환경에서 이것저것 만져보면서 느낀 게, 오토스케일링은 단순히 “늘어난다”가 아니라 얼마나 빨리 반응하고, 얼마나 낭비 없이 붙고, 붙은 뒤 얼마나 안정적으로 정리되느냐까지 같이 봐야 한다는 점이었습니다.

    특히 이번 글은 Karpenter 성능 벤치마크라는 키워드에 맞춰서, 단순 사용기가 아니라 benchmark(벤치마크) 관점으로 정리했습니다. 수치만 던지는 글이 아니라, 어떤 워크로드에서 무엇을 관찰해야 하는지, 그리고 Kubernetes 오토스케일링 성능을 볼 때 어디서 실수하기 쉬운지까지 같이 묶어보겠습니다. 혹시 노드 확장 속도 테스트를 해보려고 하는데 어디서부터 봐야 할지 막막하셨다면, 이 글이 꽤 도움이 될 겁니다.

    Karpenter 성능 벤치마크를 위한 쿠버네티스 오토스케일링 아키텍처 개요 이미지

    Karpenter, 스케줄러, 워크로드, 신규 노드 프로비저닝 흐름을 한눈에 보여주는 아키텍처 개요 이미지입니다.

    Karpenter란 무엇이고, 벤치마크에서 왜 중요할까요?

    쉽게 말해 Karpenter는 Pending Pod(스케줄되지 못하고 대기 중인 파드)를 보고, 거기에 맞는 노드를 빠르게 띄우는 방식의 노드 프로비저너입니다. 예전에는 HPA(Horizontal Pod Autoscaler, 파드 수 자동 조절)와 Cluster Autoscaler(클러스터 오토스케일러, 노드 수 자동 조절)를 묶어서 보는 경우가 많았는데요. Karpenter는 여기서 조금 더 직접적으로 “어떤 노드가 필요한지”를 계산해서 띄우는 쪽에 가깝습니다.

    제가 처음 이걸 봤을 때는 “결국 노드 늘리는 거 아닌가?” 싶었는데, 실제로 써보니까 차이가 꽤 크더라고요. 특히 아래 같은 부분이 벤치마크 포인트였습니다.

    • 프로비저닝 결정 속도: Pending Pod가 생긴 뒤 새 노드가 뜨기까지의 흐름
    • bin packing(빈 패킹, 자원 밀집 배치) 효율: CPU와 메모리 요청량에 맞춰 얼마나 낭비 없이 배치되는지
    • 워크로드 적합성: 짧게 치고 빠지는 배치성 작업과, 지속적으로 부하가 유지되는 서비스형 워크로드에서 반응이 다른지
    • 정리 속도: 부하가 빠졌을 때 불필요한 노드를 얼마나 깔끔하게 줄이는지

    여기서 중요한 포인트! Karpenter 효율성 검증은 “몇 초 빨랐다”보다, 스케줄링 실패 원인이 어디서 사라졌는지를 보는 게 더 중요합니다. 이미지 풀링(Image Pulling), CNI(Container Network Interface) 초기화, 리소스 요청값 과대설정 같은 변수가 너무 많거든요.

    벤치마크 설계: 어떤 워크로드로 테스트해야 하나요?

    벤치마크는 결국 설계가 반입니다. 제가 직접 해보니, 하나의 워크로드만 보면 결론이 자꾸 왜곡되더라고요. 그래서 최소한 아래 3종류는 나눠서 보는 걸 추천합니다.

    워크로드 유형 특징 관찰 포인트
    Burst API 짧은 시간에 요청 급증 노드 확장 속도 테스트, Pending Pod 해소 시점
    Batch Job 대량 Job이 한 번에 생성 bin packing 효율, 스케줄링 밀집도
    Steady Service 지속적인 중간 부하 과잉 프로비저닝 여부, 축소 안정성

    이렇게 나누는 이유가 있습니다. Burst API는 순간 반응 속도를 보기 좋고, Batch Job은 Karpenter가 노드를 얼마나 촘촘하게 맞춰 띄우는지 보기 좋습니다. Steady Service는 오히려 쓸데없이 많이 띄우지 않는지를 확인하기 좋습니다. 사실 운영에서는 이 마지막이 비용에 제일 크게 영향을 주더라고요.

    테스트 환경 준비: 관측 지표부터 맞춰야 합니다

    벤치마크 전에 먼저 해야 할 건 화려한 부하툴이 아니라 관측 기준 통일입니다. Prometheus(프로메테우스, 메트릭 수집), Grafana(그라파나, 시각화), 그리고 kubectl 이벤트 확인 정도는 꼭 준비해두세요. 저는 처음에 부하부터 때렸다가, 나중에 보니 노드는 떴는데 이미지 풀 때문에 파드가 늦게 뜬 거라 삽질 좀 했습니다 ㅎㅎ

    1. Kubernetes 클러스터 준비
    2. Karpenter 설치 및 기본 프로비저닝 정책 정의
    3. 메트릭 수집 도구 연결
    4. 부하 생성 도구 배치
    5. 이벤트 타임라인 기록
    helm repo add karpenter https://charts.karpenter.sh
    helm repo update
    
    kubectl create namespace karpenter
    helm upgrade --install karpenter karpenter/karpenter \
      --namespace karpenter \
      --set settings.clusterName=my-cluster

    설치 방식은 환경마다 조금 다를 수 있지만, 핵심은 설치 자체보다 Provisioner/NodePool 성격의 정책과 워크로드 리소스 요청값을 맞추는 겁니다.

    apiVersion: karpenter.sh/v1beta1
    kind: NodePool
    metadata:
      name: default
    spec:
      template:
        spec:
          requirements:
            - key: kubernetes.io/arch
              operator: In
              values: ["amd64"]
          expireAfter: 720h
      disruption:
        consolidationPolicy: WhenUnderutilized

    위 예시는 개념 예시입니다. 실제 필드나 리소스 종류는 사용 중인 Karpenter 버전에 따라 다를 수 있으니, 운영 반영 전에는 현재 문서를 꼭 맞춰보셔야 합니다. 버전 차이 무시하고 복붙했다가 안 되는 경우가 진짜 많거든요.

    Karpenter 성능 벤치마크에서 NodePool과 리소스 요청값 구성을 설명하는 이미지

    NodePool 정책, 리소스 요청/제한, 스케줄링 조건이 어떻게 연결되는지 보여주는 구성 다이어그램입니다.

    실전 구현: 다양한 워크로드로 Karpenter 성능 벤치마크 진행하기

    1. Burst API 워크로드

    짧은 시간에 Deployment(디플로이먼트) 복제 수를 확 올리거나, HPA와 부하 생성기를 함께 써서 순간 부하를 만드는 방식입니다.

    kubectl scale deployment sample-api --replicas=50
    kubectl get pods -w

    여기서 보는 건 단순히 파드가 Running 되는 순간이 아닙니다.

    • 처음 Pending이 발생한 시점
    • Karpenter가 노드 생성 결정을 내린 시점
    • 노드 Ready 시점
    • 파드가 실제 서비스 가능 상태가 된 시점

    제가 직접 해보니 이 네 구간을 나눠서 봐야 병목이 보였습니다. 노드 생성은 빨랐는데 DaemonSet 초기화 때문에 실제 서비스 반영이 늦는 경우도 있더라고요.

    2. Batch Job 워크로드

    Job을 여러 개 한 번에 던져보면 bin packing 효율을 확인하기 좋습니다.

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: cpu-batch-sample
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: worker
              image: busybox
              command: ["sh", "-c", "sleep 120"]
              resources:
                requests:
                  cpu: "500m"
                  memory: "256Mi"
    for i in $(seq 1 30); do
      kubectl create -f job.yaml
    done

    이 테스트는 생각보다 중요합니다. 왜냐하면 실제 운영에서는 API 서버보다 배치 잡이 더 난폭하게 클러스터를 흔드는 경우가 많거든요. 여기서 Karpenter가 과도하게 큰 노드를 띄우는지, 아니면 필요한 만큼만 맞춰 띄우는지를 체크하면 Karpenter 효율성 검증에 큰 도움이 됩니다.

    3. Steady Service 워크로드

    이건 부하를 오래 유지하면서 축소 동작까지 보는 테스트입니다.

    kubectl top nodes
    kubectl top pods -A
    kubectl get events -A --sort-by=.lastTimestamp

    중요한 건 “늘어나는 속도”만 보지 말고, 줄어들 때 서비스에 영향 없이 정리되는지도 같이 보셔야 합니다. 노드가 줄면서 PodDisruptionBudget(PDB, 파드 중단 예산)이나 스케줄링 제약과 충돌하면 오히려 운영 피로도가 확 올라갑니다.

    ⚠️ 실제로 많이 겪는 문제와 트러블슈팅

    이 섹션은 꼭 넣고 싶었습니다. 벤치마크가 이상하게 나오면 대부분 Karpenter 자체 문제라기보다 주변 설정 때문이더라고요.

    • 리소스 요청값이 비현실적일 때: requests가 과하게 크면 노드가 과잉 생성됩니다.
    • 이미지 풀 지연: 새 노드가 떠도 컨테이너 이미지 다운로드 때문에 체감은 느립니다.
    • DaemonSet 오버헤드 누락: CNI, 로깅 에이전트, 모니터링 에이전트 자원이 계산에서 빠지면 예상보다 적게 들어갑니다.
    • 스케줄링 제약 과다: nodeSelector, affinity, taint/toleration이 많으면 Karpenter가 선택할 수 있는 노드 풀이 좁아집니다.

    저도 처음엔 “왜 노드를 띄웠는데 Pending이 안 없어지지?” 싶었는데, 알고 보니 topology spread constraints와 리소스 요청이 같이 꼬였던 적이 있었습니다. 이럴 때는 아래 순서로 보면 빨리 풀립니다.

    1. Pending Pod describe로 실패 이유 확인
    2. Karpenter 로그에서 provisioning decision 확인
    3. 노드 Ready 이후 CNI/DaemonSet 상태 확인
    4. 이미지 풀 시간과 애플리케이션 startup probe 확인
    kubectl describe pod <pending-pod-name>
    kubectl logs -n karpenter deploy/karpenter
    kubectl get daemonsets -A
    kubectl describe node <new-node-name>

    💡 팁 하나 드리면, 노드 확장 속도 테스트를 할 때는 테스트용 이미지 크기를 최대한 줄이세요. 이미지가 크면 오토스케일링 성능이 아니라 레지스트리/네트워크 성능을 재게 됩니다.

    검증과 결과 해석: 숫자보다 흐름을 보세요

    이제 결과를 봐야죠. 다만 여기서 조심할 게 있습니다. 제가 일부러 구체 벤치마크 수치를 박지 않는 이유는, 인스턴스 타입, 네트워크, 이미지 캐시, CNI 구성에 따라 결과가 너무 달라지기 때문입니다. 대신 재현 가능한 관찰 포맷을 드리는 게 더 낫습니다.

    측정 항목 어떻게 확인하나 좋게 봐야 할 신호
    Pending 해소 시간 pod 상태 변화, 이벤트 타임라인 증가 구간이 짧고 일관적임
    노드 준비 시간 node Ready 시점 워크로드 증가 시 급격한 편차가 적음
    자원 밀집도 node/pod 사용률 비교 유휴 자원이 과도하게 남지 않음
    축소 안정성 scale-in 이후 재스케줄 상태 서비스 영향 없이 정리됨

    실제로 써보니까, Kubernetes 오토스케일링 성능은 “최대 속도”보다 “예상 가능한 속도”가 더 중요했습니다. 갑자기 한 번 엄청 빠른 결과가 나오는 것보다, 비슷한 조건에서 비슷하게 반응하는 쪽이 운영에는 훨씬 유리하거든요.

    Karpenter 성능 벤치마크 결과로 워크로드별 Pending Pod와 노드 준비 시간을 비교한 이미지

    Burst, Batch, Steady 워크로드별로 Pending Pod 해소 흐름과 노드 Ready 전환을 비교한 대시보드 이미지입니다.

    비교 정리: 어떤 환경에서 Karpenter가 특히 잘 맞을까요?

    제 기준으로는 아래 환경에서 특히 체감이 좋았습니다.

    • 트래픽 변동폭이 큰 서비스
    • 짧은 배치 작업이 자주 몰리는 플랫폼
    • 팀이 노드 타입과 비용 최적화까지 같이 보고 싶은 환경

    반대로, 부하가 거의 고정이고 노드 풀도 단순한 환경이라면 체감 이점이 크지 않을 수도 있습니다. 그래서 벤치마크는 꼭 “남들도 좋다더라”가 아니라 내 워크로드 기준으로 봐야 합니다. 여기서 중요한 포인트, 다시 한 번 말씀드리면 Karpenter 성능 벤치마크는 제품 홍보 자료처럼 보면 안 됩니다. 운영 제약, 워크로드 패턴, 팀의 관측 역량이 같이 들어가야 의미가 생깁니다.

    Karpenter 성능 벤치마크와 기존 오토스케일링 방식을 비교한 요약 이미지

    프로비저닝 방식, 반응성, 자원 활용 관점에서 Karpenter와 기존 접근을 비교한 요약 인포그래픽입니다.

    자주 묻는 질문

    Karpenter만 있으면 HPA는 필요 없나요?

    아닙니다. HPA는 파드 수를 늘리고, Karpenter는 그 파드가 올라갈 노드를 준비하는 역할에 가깝습니다. 둘은 대체 관계라기보다 연동 관계로 보는 게 맞습니다.

    벤치마크에서 가장 먼저 봐야 할 로그는 뭔가요?

    Pending Pod 이벤트와 Karpenter 컨트롤러 로그입니다. 여기서 프로비저닝 판단이 있었는지 먼저 확인해야 합니다.

    비용 절감 효과도 바로 검증할 수 있나요?

    가능은 하지만, 하루 이틀 테스트로 단정하긴 어렵습니다. 최소한 여러 워크로드 패턴을 반복해보고, 축소 정책까지 함께 봐야 의미가 있습니다.

    마무리: 빠른 확장보다 중요한 건 예측 가능한 확장입니다

    이번 글에서는 Karpenter 성능 벤치마크를 워크로드 유형별로 어떻게 봐야 하는지 정리해봤습니다. 제가 직접 해보니, 제일 중요한 건 화려한 벤치마크 숫자가 아니라 Pending 발생 → 노드 생성 결정 → 노드 Ready → 서비스 가능 이 흐름을 끊김 없이 읽어내는 것이었습니다. 드디어 됐다! 싶은 순간도 있었지만, 사실 그 전에 로그 보면서 한참 헤맨 적도 많았거든요.

    정리하자면 이렇습니다.

    • ✅ Burst, Batch, Steady 워크로드를 나눠서 봐야 합니다.
    • ✅ 노드 생성 속도만 보지 말고 이미지 풀과 DaemonSet 초기화도 같이 봐야 합니다.
    • ✅ Karpenter 효율성 검증은 절대 수치보다 일관성과 자원 활용 흐름이 중요합니다.

    다음 글에서는 HPA와 함께 붙였을 때의 상호작용, 그리고 실제 운영에서 자주 쓰는 관측 대시보드 구성도 따로 다뤄볼 예정입니다. 이전 글에서 다뤘던 클러스터 리소스 요청값 설계 내용도 같이 보시면 훨씬 이해가 쉬우실 겁니다.

  • [Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석

    [Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석

    [Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석

    Karpenter 프로비저닝 실패 때문에 새 파드(Pod, 쿠버네티스에서 배포되는 최소 실행 단위)는 계속 Pending 상태인데, 오토스케일링(auto scaling, 부하에 따라 자동으로 늘고 줄어드는 기능)은 움직이지 않고, 로그만 한참 들여다보셨던 적 있으신가요? 저는 홈랩이든 업무 환경이든 이런 상황을 몇 번 겪었는데요. 처음엔 “분명 노드가 부족한데 왜 안 뜨지?” 싶었고, 실제로는 Karpenter 설정 자체보다 IAM(Role, 권한 역할), 서브넷(Subnet, 네트워크 구간) 태그, 인스턴스 제약 조건 같은 바깥 조건에서 막히는 경우가 많더라고요. 이번 글에서는 제가 실제로 디버깅할 때 밟는 순서대로, Karpenter 프로비저닝 실패를 어떻게 좁혀가는지 정리해보겠습니다.

    특히 Kubernetes 노드 문제 해결이나 Karpenter 디버깅이 익숙하지 않은 분들은, 무작정 로그부터 보는 것보다 “스케줄링 실패 → 프로비저닝 판단 → 클라우드 리소스 생성 → 노드 조인(join, 클러스터 참여)” 이 흐름으로 보면 훨씬 덜 헷갈리실 거예요. 저도 처음엔 이걸 한 덩어리로 봐서 삽질 좀 했습니다 ㅎㅎ

    Karpenter 프로비저닝 실패 흐름을 보여주는 Kubernetes 아키텍처 다이어그램

    Karpenter 프로비저닝 실패가 어느 단계에서 발생하는지 한눈에 보여주는 개요 이미지입니다.

    Karpenter 프로비저닝 실패를 볼 때 먼저 이해해야 할 흐름

    쉽게 말해 Karpenter는 “스케줄되지 못한 워크로드가 있네? 그럼 조건에 맞는 노드를 하나 띄워보자” 하고 판단하는 컴포넌트입니다. 여기서 중요한 포인트는, 단순히 노드를 많이 만드는 도구가 아니라 스케줄링 제약 조건을 해석해서 그에 맞는 노드를 계산한다는 점입니다.

    문제가 나는 지점은 보통 4군데입니다

    1. 파드 스케줄링 조건이 너무 빡빡한 경우
      예: nodeSelector, affinity, toleration, resource request가 충돌합니다.
    2. Karpenter가 적절한 노드 후보를 계산하지 못하는 경우
      예: 인스턴스 타입 제약, capacity type, 아키텍처 제약이 과합니다.
    3. 클라우드 리소스 생성 단계에서 실패하는 경우
      예: IAM 권한, 보안 그룹(Security Group), 서브넷 태그, 용량 부족 등이 걸립니다.
    4. 노드는 생성됐지만 클러스터에 조인하지 못하는 경우
      예: 부트스트랩(user data), 인증, 네트워크 경로 문제가 있습니다.

    이 네 구간만 분리해서 봐도 디버깅 난이도가 확 내려갑니다. 실제로 써보니까 “Karpenter가 고장났다”기보다는, 주변 조건 하나가 꼬여서 연쇄적으로 실패하는 경우가 더 많았습니다.

    Kubernetes 노드 문제 해결을 위한 기본 점검 순서

    제가 현장에서 가장 먼저 하는 건 거창한 분석이 아니라 증상 분리입니다. 파드가 문제인지, Karpenter 컨트롤러(controller, 제어 루프) 판단이 문제인지, 아니면 클라우드 API 호출이 막히는지부터 확인해야 하거든요.

    1. Pending 파드부터 확인

    kubectl get pods -A
    kubectl describe pod <pod-name> -n <namespace>

    여기서 꼭 보는 건 Events입니다. 예를 들어 다음 같은 메시지가 보이면 방향이 잡힙니다.

    • 0/3 nodes are available: 기존 노드에 스케줄이 안 되는 상황입니다.
    • Insufficient cpu 또는 Insufficient memory: 리소스 부족입니다.
    • node(s) had untolerated taint: 톨러레이션(toleration, 테인트 허용 설정) 문제입니다.
    • node affinity/selector mismatch: 파드 조건이 너무 좁습니다.

    혹시 여기서 이미 affinity나 nodeSelector 충돌이 보이면, Karpenter 이전에 워크로드 정의부터 손봐야 합니다. 이걸 놓치면 Karpenter 로그만 하루 종일 보게 되거든요.

    2. Karpenter 로그 확인

    kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=200
    kubectl get events -A --sort-by=.lastTimestamp

    로그에서는 이런 식의 단서를 찾습니다.

    • 프로비저닝 후보를 계산했는지
    • 요구사항(requirements) 때문에 제외된 인스턴스 타입이 있는지
    • 클라우드 제공자(provider) 호출에서 에러가 나는지
    • 생성 이후 노드 등록(register)이 안 되는지

    제가 직접 해보니, 로그를 길게 읽는 것보다 에러 키워드를 먼저 잡는 게 훨씬 빨랐습니다. 예를 들면 access denied, no subnets found, insufficient capacity, launch template 관련 문구 같은 것들이요.

    3. 노드 생성 여부와 조인 여부 분리

    kubectl get nodes
    kubectl get nodeclaims
    kubectl get nodepools
    kubectl describe nodeclaim <name>

    환경에 따라 리소스 이름은 다를 수 있지만, 핵심은 같습니다. 클라우드 인스턴스는 떴는데 노드가 안 보이는지, 아니면 인스턴스 자체가 생성되지 않았는지를 나눠서 봐야 합니다. 이 차이가 엄청 큽니다.

    Karpenter 디버깅을 위한 파드 이벤트와 로그 비교 이미지

    파드 이벤트와 Karpenter 로그를 같이 놓고 보면 어디서 막히는지 훨씬 빨리 찾을 수 있습니다.

    실전 사례: Karpenter 디버깅을 이렇게 풀었습니다

    이건 제가 자주 보는 전형적인 패턴입니다. 특정 워크로드가 배포된 뒤 Pending 상태가 길게 이어졌고, 클러스터 오토스케일링 오류 분석이 필요했던 상황이었습니다. 기존 노드는 여유가 없었고, Karpenter가 새 노드를 올려줘야 정상인데 아무 변화가 없었죠.

    증상

    • 파드는 Pending 상태 유지
    • Karpenter는 동작 중
    • 기존 노드는 리소스 부족
    • 새 노드는 추가되지 않음

    제가 먼저 확인한 파드 스펙

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sample-workload
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: sample-workload
      template:
        metadata:
          labels:
            app: sample-workload
        spec:
          nodeSelector:
            kubernetes.io/arch: amd64
          tolerations:
            - key: "workload-type"
              operator: "Equal"
              value: "batch"
              effect: "NoSchedule"
          containers:
            - name: app
              image: nginx:stable
              resources:
                requests:
                  cpu: "1"
                  memory: "1Gi"

    겉으로 보면 큰 문제 없어 보이죠. 저도 처음엔 워크로드 쪽은 괜찮다고 생각했었습니다. 근데 여기서 Karpenter 쪽 제약과 같이 봐야 했습니다.

    확인한 Karpenter 측 제약 예시

    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: general
    spec:
      template:
        spec:
          requirements:
            - key: kubernetes.io/arch
              operator: In
              values:
                - amd64
            - key: kubernetes.io/os
              operator: In
              values:
                - linux
            - key: karpenter.sh/capacity-type
              operator: In
              values:
                - spot

    여기서 문제는 단순 설정 문법이 아니었습니다. 워크로드는 특정 톨러레이션을 요구하고, NodePool은 spot만 허용하며, 실제 가용 영역이나 계정 조건상 적절한 인스턴스 풀이 너무 좁아진 상태였던 거죠. 게다가 서브넷 태그도 일부 누락돼 있었습니다. 결국 Karpenter 입장에서는 만들 수 있는 노드 후보가 사실상 거의 없었던 셈입니다.

    정리하면 원인은 두 개였습니다

    구분 문제 내용 영향
    네트워크 일부 서브넷 태그 누락 적절한 서브넷 탐색 실패
    용량 제약 spot만 허용하고 요구사항이 좁음 프로비저닝 후보 부족

    이런 경우 진짜 헷갈립니다. 로그에는 하나의 에러만 크게 보일 때도 있지만, 실제론 조건이 두세 개 겹쳐서 실패하는 경우가 많거든요.

    제가 적용했던 점검 명령어와 확인 포인트

    아래 명령어들은 특정 클라우드에 종속되지 않게, 쿠버네티스 관점에서 먼저 확인할 수 있는 것들입니다. 이 순서대로 보면 꽤 안정적으로 원인을 줄여갈 수 있었습니다.

    1. 파드 이벤트 확인
    2. Karpenter 로그에서 프로비저닝 시도 여부 확인
    3. NodePool/NodeClaim 조건 확인
    4. 생성된 노드 존재 여부 확인
    5. 클라우드 콘솔 또는 API에서 인스턴스 생성 실패 원인 확인
    kubectl describe pod <pod-name> -n <namespace>
    kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --since=30m
    kubectl get nodepools -o yaml
    kubectl get nodeclaims -o yaml
    kubectl get nodes -o wide

    여기서 중요한 포인트! Karpenter 디버깅은 “쿠버네티스 안쪽 정보”와 “클라우드 바깥 정보”를 같이 봐야 끝이 납니다. 쿠버네티스 쪽만 보면 “왜 안 되지?”에서 끝나고, 클라우드 쪽만 보면 “인스턴스가 왜 안 뜨지?”로 끝나더라고요.

    Karpenter 프로비저닝 실패 원인인 NodePool 제약 조건 다이어그램

    NodePool 제약 조건이 겹칠수록 프로비저닝 가능한 후보가 급격히 줄어드는 상황을 설명하는 이미지입니다.

    ⚠️ 실제로 많이 만나는 Karpenter 프로비저닝 실패 원인

    이 섹션은 제가 여러 번 겪으면서 “아, 이건 체크리스트로 빼야겠다” 싶었던 것들입니다. 운영 중인 분들이라면 특히 공감하실 거예요.

    1. 파드 요구사항이 너무 강한 경우

    • nodeSelector가 특정 아키텍처나 라벨만 강제
    • podAntiAffinity가 과도하게 설정됨
    • requests가 커서 맞는 노드가 매우 제한적
    • toleration이 없어서 tainted node에 못 올라감

    처음엔 Karpenter가 노드를 안 만드는 줄 알았는데, 실제론 만들어도 스케줄이 안 맞는 상태라 계산상 제외되는 경우가 있었습니다.

    2. NodePool 요구사항이 좁은 경우

    • 특정 인스턴스 계열만 허용
    • spot만 허용
    • 특정 zone만 허용
    • 아키텍처와 운영체제 조건이 불필요하게 제한됨

    이건 비용 최적화하려다 자주 생깁니다. 저도 spot 위주로 예쁘게 짜보려다가, 정작 트래픽이 몰릴 때 후보가 없어서 자동 확장이 안 된 적이 있었거든요. 그래서 운영 환경에서는 제약은 최소화하고, 꼭 필요한 정책만 남기는 쪽이 더 안전했습니다.

    3. 클라우드 리소스 검색 실패

    • 서브넷 태그 누락
    • 보안 그룹 태그 누락
    • IAM 권한 부족

    이 부분은 쿠버네티스 YAML만 아무리 봐도 안 나옵니다. 특히 태그 기반 디스커버리(discovery, 자동 탐색)를 쓰는 경우라면, 네트워크 리소스가 올바르게 식별되는지 꼭 확인하셔야 합니다.

    4. 노드 생성 후 조인 실패

    • 부트스트랩 스크립트 문제
    • 클러스터 API 엔드포인트 접근 문제
    • 노드 인증 관련 설정 누락

    이 경우는 인스턴스는 생성되는데 kubectl get nodes에 안 보입니다. 그래서 “Karpenter가 아무것도 안 했다”고 오해하기 쉬워요. 근데 사실 한 단계는 진행된 상태인 거죠.

    문제를 줄이기 위한 운영 팁

    실제로 써보니까 아래 네 가지가 제일 효과 있었습니다.

    • NodePool 제약을 처음부터 너무 촘촘하게 만들지 않기
    • 파드 requests/limits를 현실적으로 설정하기
    • 서브넷, 보안 그룹, IAM 같은 외부 조건을 체크리스트화하기
    • 이벤트와 로그를 함께 보기

    특히 Kubernetes 노드 문제 해결이 필요한 상황에서 오토스케일링 오류 분석은 재현이 어려운 경우가 많아서, 문제가 생겼을 때 바로 볼 수 있는 로그 수집 체계를 미리 만들어두는 게 좋습니다. 이전 글에서 다뤘던 클러스터 이벤트 정리 방식이 있다면 같이 묶어보셔도 좋고요. 이 부분은 다음 글에서 더 깊게 다뤄볼 예정입니다.

    검증: 수정 후 무엇을 확인했나

    저는 설정을 손본 뒤 항상 세 가지를 확인합니다. 단순히 노드가 떠도 끝이 아니거든요.

    1. Pending 파드가 실제로 Running으로 전환되는지
    2. 새 노드가 기대한 라벨과 용량으로 조인되는지
    3. 불필요하게 과한 스케일아웃이 일어나지 않는지
    kubectl get pods -A -w
    kubectl get nodes --show-labels
    kubectl top nodes

    문제가 해결되면 보통 흐름이 이렇게 바뀝니다.

    • Pending 파드 발생
    • Karpenter가 새 노드 계산 및 생성
    • 노드가 클러스터에 조인
    • 파드가 새 노드에 스케줄됨

    드디어 됐다! 하는 순간이 여기입니다. 특히 오래 Pending이던 워크로드가 새 노드에 바로 올라가는 걸 보면, 이거 진짜 편하더라고요. 다만 성공했다고 끝내지 말고, 왜 실패했는지 기록해두는 게 다음 장애 때 훨씬 큰 도움이 됩니다.

    Karpenter 프로비저닝 실패 해결 후 Pending 파드가 Running으로 전환된 결과 이미지

    수정 전후를 비교해 Pending 파드가 Running으로 바뀌는 결과 확인 이미지입니다.

    정리: Karpenter 디버깅은 순서가 전부입니다

    Karpenter 프로비저닝 실패를 잡을 때 가장 중요한 건, 증상을 한 번에 해석하려고 하지 않는 겁니다. 저도 처음엔 로그 한 줄에서 답을 찾으려 했는데, 실제론 파드 조건, NodePool 제약, 클라우드 리소스 검색, 노드 조인 이 네 단계를 분리해서 봐야 풀리더라고요.

    혹시 지금도 Kubernetes 노드 문제 해결 때문에 막혀 계시다면, 아래 체크리스트부터 차근차근 보시면 좋겠습니다.

    • 파드 Events에 스케줄링 실패 원인이 보이는가
    • Karpenter가 실제로 프로비저닝을 시도했는가
    • NodePool 제약이 과하지 않은가
    • 서브넷, 보안 그룹, IAM 조건이 맞는가
    • 생성된 노드가 클러스터에 정상 조인하는가

    이 순서만 익혀도 Karpenter 디버깅 속도가 꽤 빨라집니다. 저도 처음엔 헷갈렸는데, 몇 번 정리해두고 나니 장애 대응 시간이 확 줄었습니다. 마무리 전에 요약 하나 더 남겨볼게요.

    Karpenter 프로비저닝 실패 디버깅 체크리스트 인포그래픽

    Karpenter 프로비저닝 실패 원인과 대응 순서를 한 장으로 정리한 요약 이미지입니다.

    FAQ: 자주 헷갈리는 포인트

    Q1. 파드가 Pending이면 무조건 Karpenter 문제인가요?

    아닙니다. 파드 스펙의 affinity, toleration, resource request가 원인인 경우도 많습니다.

    Q2. 노드가 생성되지 않으면 어디부터 봐야 하나요?

    파드 이벤트, Karpenter 로그, NodePool 조건을 먼저 보고, 그 다음 클라우드 리소스 조건을 확인하는 게 좋습니다.

    Q3. spot만 허용해도 괜찮을까요?

    가능은 하지만, 워크로드 특성과 가용성 요구사항을 고려해야 합니다. 운영에선 제약을 너무 좁히면 Karpenter 프로비저닝 실패 가능성이 올라가더라고요.

    마무리

    이번 글은 특정 버전 문법을 깊게 파기보다는, 문제가 났을 때 어떤 순서로 생각해야 하는지에 집중해봤습니다. 사실 운영에서는 정답 YAML 하나보다, 실패를 분해해서 보는 관점이 더 오래 가거든요. 다음 글에서는 Karpenter와 Cluster Autoscaler를 어떤 기준으로 나눠서 볼지, 그리고 워크로드별로 어떤 제약을 어디까지 허용할지 이어서 정리해보겠습니다.