13년차의 서버실

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

[카테고리:] k8s

  • [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에서 트래픽 차단을 설계하세요. 운영 안정성은 “많이 검사하는 것”보다 무엇을 실패시켰을 때 어떤 후속 조치가 일어나는지를 정확히 설계할 때 올라갑니다.

  • [k8s] Kubernetes CPU Limit, 정말 사용해야 할까? 요청 중심 최적화 전략

    [k8s] Kubernetes CPU Limit, 정말 사용해야 할까? 요청 중심 최적화 전략

    Kubernetes CPU Limit, 정말 사용해야 할까?

    Kubernetes CPU Limit 얘기는 설정 한 줄처럼 보이지만, 실제 운영에선 지연 시간, 처리량, 오토스케일링, 멀티테넌시 정책이 한꺼번에 얽힙니다. 저도 예전엔 CPU limit를 일단 넣어두는 편이 더 안전하다고 봤거든요. 그런데 운영을 오래 해보면 패턴이 꽤 선명합니다. CPU limit는 성능 최적화 장치라기보다, 클러스터 통제 장치에 더 가깝더라고요.

    핵심은 메모리와 CPU를 같은 감각으로 다루면 안 된다는 점입니다. 메모리는 한계를 넘으면 OOMKill 같은 형태로 비교적 분명하게 드러납니다. 반면 CPU는 압축 가능한 자원이라서, limit를 넘는다고 바로 죽지 않고 리눅스 커널의 CFS quota 기반 throttling으로 서서히 막힙니다. 겉으로는 Pod가 멀쩡하고 readiness도 통과하는데, 실제 사용자 경험만 나빠지는 식이죠. 이게 운영에서 진짜 까다롭습니다.

    이번 글에서는 단순히 “limit를 빼세요” 같은 구호로 끝내지 않고, Kubernetes CPU Limit를 언제 유지해야 하는지, 언제 제거하는 편이 나은지, 판단 기준을 어디에 둬야 하는지 실무 기준으로 정리해보겠습니다. 특히 아래 세 가지는 꼭 분리해서 보셔야 합니다.

    • 노드가 진짜로 바쁜 상황: 클러스터 용량 부족 문제입니다.
    • 컨테이너 limit에 막힌 상황: 설정한 ceiling이 병목입니다.
    • request가 너무 낮아 경쟁에서 밀린 상황: throttling이 거의 없어도 성능이 흔들릴 수 있습니다.

    이 셋을 한데 묶어 보면 원인이 계속 엇나갑니다. 현업에서 CPU 문제를 오래 끄는 팀들은 대개 여기서 많이 헷갈리더라고요.

    CPU request와 CPU limit, 스케줄러와 kubelet, Linux cgroup이 어떻게 연결되는지 한눈에 보여주는 개요 이미지입니다.

    1. CPU request와 CPU limit, 정말 다르게 읽어야 합니다

    request는 스케줄링 기준이고, Linux 노드에서는 혼잡 시 상대적인 CPU 배분 기준에도 영향을 줍니다. 반면 limit는 커널이 강제로 거는 상한선입니다. 둘 다 resource 설정이지만 역할은 완전히 다릅니다. 스케줄러는 request를 보고 “이 Pod를 어느 노드에 올릴 수 있는가”를 판단하고, kubelet과 컨테이너 런타임은 limit를 받아 cgroup quota를 설정합니다. 즉, request는 배치 계획에 가깝고, limit는 실행 중 브레이크에 가깝습니다.

    이 차이가 중요한 이유는 운영상 신호가 완전히 다르게 나타나기 때문입니다.

    구분 CPU Request CPU Limit 운영에서 보이는 증상
    주요 역할 스케줄링 기준, 혼잡 시 상대적 배분 기준 절대 상한선 문제 원인이 다르게 드러납니다
    값이 너무 낮을 때 혼잡 시 경쟁에서 밀릴 수 있음 짧은 burst도 바로 잘릴 수 있음 전자는 처리량 저하, 후자는 지연 급증으로 자주 보입니다
    노드 CPU 여유와의 관계 여유가 있으면 영향이 완화될 수 있음 노드가 한가해도 limit에 걸리면 throttling 발생 노드 지표만 보면 놓치기 쉽습니다
    오토스케일링 영향 HPA의 utilization 계산 기준과 직접 연결됨 직접적인 스케일 기준은 아니지만 사용 패턴을 왜곡할 수 있음 request가 비현실적이면 HPA 판단도 흔들립니다
    정책적 의미 예약 통제 성능보다 거버넌스 목적일 때 limit가 더 설득력 있습니다

    그리고 의외로 자주 놓치는 사실이 하나 있습니다. 컨테이너에 limit만 주고 request를 생략하면, admission 단계에서 다른 기본값이 들어오지 않는 한 Kubernetes는 request를 limit와 동일하게 복사합니다. 여기에 namespace의 LimitRange까지 얹히면 의도하지 않은 request/limit 조합이 생깁니다. 그래서 manifest만 보고 판단하면 자꾸 틀립니다. 실제 Pod에 주입된 값을 봐야 합니다.

    2. Kubernetes CPU Limit가 안티패턴이 되는 순간

    제가 Kubernetes CPU Limit를 경계하는 이유는 단순합니다. 짧게 몰아서 끝낼 수 있는 일을, 굳이 길게 끌도록 만들기 때문입니다. 웹 API, gRPC 서버, 메시지 소비자, 배치 워커, 압축/암호화/직렬화가 섞인 애플리케이션은 CPU를 늘 높게 쓰는 게 아니라 순간적으로 확 씁니다. 이때 노드에 여유가 있어도 limit가 낮으면 그 순간만 잘립니다.

    현장에서 제일 많이 보는 패턴은 이렇습니다.

    1. 컨테이너에 requests.cpu: 250m, limits.cpu: 500m를 둡니다.
    2. 평소 평균 CPU 사용량은 낮아서 대시보드상 멀쩡해 보입니다.
    3. 트래픽 급증, TLS handshake 증가, JSON serialization, 압축, GC 같은 CPU 이벤트가 같은 시점에 겹칩니다.
    4. 잠깐 500m를 넘겨야 빨리 끝날 일을 quota가 막아버립니다.
    5. Pod 상태는 정상인데 P95, P99 latency와 queue lag가 튑니다.

    여기서 운영자들이 자주 잘못 읽는 지점이 있습니다. 노드 CPU 사용률이 낮다는 사실은 throttling이 없다는 뜻이 아닙니다. throttling은 노드 포화가 아니라, 내가 걸어둔 컨테이너 상한선 때문에 발생할 수 있기 때문입니다. 다시 말해, “클러스터가 부족해서 느린 것”과 “설정이 과하게 보수적이라 느린 것”은 완전히 다른 문제입니다.

    저는 CPU 이슈를 볼 때 아래 세 가지 실패 모드로 먼저 분류합니다.

    실패 모드 대표 신호 근본 원인 주된 처방
    Quota ceiling 노드 여유가 있는데 throttling 증가 CPU limit가 burst를 잘라냄 CPU limit 제거 또는 상향, request 재산정
    Share starvation throttling은 적은데 혼잡 시 처리량 저하 request가 너무 낮아 경쟁에서 밀림 request 상향, HPA 기준 재검토
    Actual node saturation 노드 전체 CPU도 높고 여러 Pod가 함께 흔들림 클러스터 용량 부족 또는 bin-packing 과밀 replica, node size, autoscaling, 배치 정책 조정

    실무에서 중요한 건 “CPU limit를 쓸지 말지”보다, 지금 내가 맞닥뜨린 문제가 셋 중 무엇인지 먼저 판별하는 것입니다. 이걸 건너뛰면 limit를 지워도 해결이 안 되고, request만 올려도 엉뚱한 방향으로 갑니다.

    3. 실전 구현 1: 먼저 현재 리소스 설정부터 확인합니다

    CPU는 감으로 만지면 거의 틀립니다. 제가 제일 먼저 보는 건 애플리케이션 코드가 아니라 실제 Pod에 주입된 request/limit와 namespace 정책입니다. 특히 헬름 차트, Kustomize, 정책 엔진, LimitRange가 섞여 있으면 manifest 원본과 실제 실행 상태가 다를 수 있습니다.

    kubectl get deploy -n prod api-server -o yaml
    kubectl describe pod -n prod api-server-xxxxx
    kubectl get limitrange -n prod -o yaml
    kubectl get resourcequota -n prod -o yaml

    kubectl describe pod의 Requests, Limits, QoS Class를 같이 보세요. 여기서 저는 세 가지를 체크합니다.

    • 의도하지 않은 기본값 주입: LimitRange가 CPU limit와 defaultRequest를 넣었는지
    • request=limit 강제 여부: Guaranteed QoS가 의도된 것인지
    • 컨테이너별 편차: 사이드카가 더 타이트한 limit를 가져 병목이 되는지

    사용량 확인은 kubectl top으로 시작해도 괜찮습니다. 다만 여기 값은 추세를 보는 용도지, throttling 자체를 증명하는 용도는 아닙니다. 저는 kubectl top을 “지금 누구부터 의심할지 정하는 1차 필터” 정도로 씁니다.

    kubectl top pod -n prod --containers
    kubectl top node
    kubectl get --raw /apis/metrics.k8s.io/v1/namespaces/prod/pods
    # 클러스터에 따라 아직 v1beta1만 열려 있을 수도 있습니다.
    # kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/prod/pods

    여기서 평균만 보고 끝내면 거의 반드시 놓칩니다. CPU limit 이슈는 평균 CPU가 아니라 짧은 burst의 절단 여부가 핵심이기 때문입니다. 1분 평균 그래프는 멀쩡한데 P99만 무너지는 서비스가 딱 이 부류예요.

    Prometheus가 있다면 저는 바로 throttling 비율도 같이 봅니다. 다만 아래 메트릭 이름은 보통 cAdvisor 계열 수집 기준이라, 배포 환경이나 런타임에 따라 이름이 다를 수 있습니다.

    sum by (namespace, pod, container) (
      rate(container_cpu_cfs_throttled_periods_total{namespace="prod",container!=""}[5m])
    )
    /
    sum by (namespace, pod, container) (
      rate(container_cpu_cfs_periods_total{namespace="prod",container!=""}[5m])
    )

    이 비율은 절대값 하나로 좋다 나쁘다를 단정하기보다, 배포 전후 추세와 latency 변화를 같이 보셔야 의미가 있습니다. 비율이 조금 보여도 성능에 영향이 없을 수 있고, 반대로 특정 서비스만 유독 증가하면서 응답 시간이 같이 튄다면 꽤 강한 신호입니다.

    Kubernetes CPU Limit 점검을 위해 Pod 리소스와 사용량을 확인하는 운영 화면

    실제 운영 점검 흐름처럼, Pod 사용량과 리소스 필드를 함께 보는 장면을 넣으면 이해가 훨씬 빨라집니다.

    4. Kubernetes CPU Limit를 줄이고 request 중심으로 바꾸는 방법

    지연 시간에 민감한 서비스라면 저는 보통 메모리 limit는 유지하고 CPU는 request 중심으로 먼저 바꿔봅니다. 여기서 중요한 건 limit만 지우고 끝내는 게 아니라, request를 실제 소비 패턴에 맞춰 다시 올리는 것입니다. request를 너무 낮게 두면 throttling은 사라져도 경쟁 상황에서 CPU 배분이 부족해질 수 있거든요. 그러면 병목이 모양만 바뀌어서 다시 돌아옵니다.

    예를 들어 기존 Deployment가 아래처럼 되어 있었다고 해보겠습니다.

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

    제가 지연 민감 서비스에서 먼저 시도하는 형태는 이런 쪽입니다.

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

    이때 request를 어떻게 잡느냐가 훨씬 중요합니다. 저는 보통 아래 순서로 판단합니다.

    1. 평시 사용량이 아니라 혼잡 시간대의 안정적 최소치를 봅니다.
    2. 짧은 burst는 request가 아니라 노드 여유가 흡수하게 둡니다.
    3. Pending 증가나 bin-packing 악화가 보이면 request를 다시 낮추거나 replica 전략을 손봅니다.
    4. HPA가 CPU utilization을 쓰고 있다면 request 변경이 스케일 판단에도 영향을 준다는 점을 같이 봅니다.

    즉, request는 단순한 “희망 사용량”이 아니라 스케줄링 비용과 오토스케일링 민감도를 동시에 결정하는 숫자입니다. 그래서 저는 limit보다 request를 더 신중하게 다룹니다.

    적용과 확인은 기본기가 중요합니다.

    kubectl apply -f deployment.yaml
    kubectl rollout status deployment/api-server -n prod
    kubectl describe deployment api-server -n prod
    kubectl get pod -n prod -l app=api-server -o wide

    그리고 namespace 정책상 CPU limit가 강제되는 환경이면 Deployment만 바꿔서는 소용이 없습니다. 아래처럼 LimitRange도 같이 봐야 합니다.

    apiVersion: v1
    kind: LimitRange
    metadata:
      name: default-cpu-memory
      namespace: prod
    spec:
      limits:
      - type: Container
        default:
          memory: "512Mi"
          cpu: "500m"
        defaultRequest:
          memory: "256Mi"
          cpu: "250m"

    이런 정책이 있으면 새 Pod가 뜰 때 CPU limit가 다시 들어갑니다. 운영에서는 이걸 배포 파이프라인 문제나 헬름 템플릿 버그로 오해하는 경우가 많습니다. 실제로는 admission 단계 정책 때문인 경우가 적지 않습니다.

    기존 설정과 개선 후 설정이 어떻게 달라지는지, request 중심 전략이 어떤 의미인지 보여주는 비교 이미지 자리입니다.

    5. Kubernetes CPU Limit와 CPU throttling을 어떻게 읽을까

    CPU 튜닝은 수치 하나만 보고 판정하면 자꾸 빗나갑니다. 저는 항상 애플리케이션 지표와 커널 신호를 묶어서 봅니다. 순서는 대체로 이렇습니다.

    1. 사용자 체감 지표: 응답 시간, 작업 처리 시간, 큐 적체, timeout 증가 여부
    2. throttling 지표: container_cpu_cfs_throttled_periods_total, container_cpu_cfs_periods_total, container_cpu_cfs_throttled_seconds_total
    3. 노드 여유: 실제로 클러스터가 바쁜지, 아니면 컨테이너만 막히는지
    4. request/limit 설계: 현재 값이 워크로드의 burst 패턴과 맞는지

    컨테이너 내부에서 cgroup 통계를 직접 확인할 수 있는 환경이라면 이런 식의 점검도 유용합니다. 다만 cgroup v1/v2에 따라 파일 경로는 달라질 수 있습니다.

    kubectl exec -n prod api-server-xxxxx -- sh -c 'cat /sys/fs/cgroup/cpu.stat 2>/dev/null || cat /sys/fs/cgroup/cpu/cpu.stat'
    kubectl exec -n prod api-server-xxxxx -- sh -c 'grep . /sys/fs/cgroup/cpu.max 2>/dev/null || grep . /sys/fs/cgroup/cpu/cpu.cfs_*'

    여기서 보고 싶은 건 멋진 숫자가 아니라 패턴의 일치입니다. 배포 이후 nr_throttled가 빠르게 늘고, 같은 시간대에 API latency가 같이 튄다면 limit가 유력한 원인입니다. 반대로 throttling은 거의 없는데 혼잡 시 처리량이 떨어진다면 request 과소설정이나 노드 포화 쪽을 의심하는 편이 맞습니다.

    제가 현장에서 자주 쓰는 실무 기준은 이렇습니다.

    • 노드 CPU는 한가한데 특정 Pod만 느리다: CPU limit부터 의심합니다.
    • 여러 Pod가 동시에 흔들리고 노드도 바쁘다: 클러스터 용량 또는 배치 문제일 가능성이 큽니다.
    • throttling은 낮은데 HPA가 늦게 반응한다: request 값과 HPA 목표치 조합을 다시 봅니다.
    • 배치 워커가 정시성 트래픽에만 흔들린다: 평균 CPU가 아니라 burst 패턴이 원인일 가능성이 높습니다.

    6. 자주 겪는 함정과 트러블슈팅

    운영에서 반복해서 나오는 함정은 대체로 비슷합니다. 그런데 겉으로 보이는 증상은 비슷해도 근본 원인은 꽤 다릅니다. 저는 아래 네 가지를 별개로 다룹니다.

    • LimitRange 자동 주입: manifest에서 CPU limit를 지워도 실제 Pod에는 다시 생깁니다.
    • limit만 넣었더니 request도 같이 올라감: 의도치 않게 request가 커져 스케줄링이 빡빡해집니다.
    • 평균값 함정: 짧은 burst형 워크로드는 1분 평균 CPU로는 거의 안 잡힙니다.
    • CPU와 메모리를 같은 정책으로 처리: 메모리 보호 논리를 CPU에 그대로 적용하면 성능 비용이 크게 생깁니다.

    제가 실제로 여러 번 본 재현 시나리오를 하나 들어보겠습니다. 백그라운드 워커가 평소에는 조용한데, 매시 정각에 누적된 메시지를 한꺼번에 소모하는 구조였습니다. 대시보드 평균 CPU는 높지 않았고 노드도 한가했습니다. 그런데 정각 직후에만 queue lag와 처리 지연이 치솟았어요. 겉으로 보면 외부 API 지연이나 브로커 문제처럼 보이는데, 실제로는 워커 컨테이너의 CPU limit가 burst를 잘라내고 있던 경우가 꽤 있었습니다.

    이럴 때 흔한 실수는 consumer 개수만 늘리거나 replica만 추가하는 겁니다. limit가 병목이면 replica를 늘려도 각 Pod가 똑같이 잘립니다. 결국 CPU limit 제거, request 상향, 워커 동시성 재조정을 함께 해야 풀리더라고요.

    또 하나, Guaranteed QoS가 항상 성능상 유리한 것은 아닙니다. 모든 컨테이너에 CPU와 메모리의 request/limit를 동일하게 맞추면 eviction 관점에서는 이점이 있을 수 있습니다. 하지만 일반 웹 서비스나 이벤트 소비자처럼 순간 burst가 필요한 워크로드에선 CPU burst 여지를 줄여 응답성에 손해가 날 수 있습니다. 반대로 정수 CPU request를 가진 Guaranteed Pod에 static CPU Manager를 조합하는 초저지연 워크로드는 예외입니다. 이런 경우는 아예 목표가 다릅니다. 멀티테넌시 유연성보다 코어 고정성과 지터 최소화가 우선이니까요.

    7. 검증: 바꾼 뒤 무엇이 좋아져야 정상인가

    설정을 바꿨다면 확인 항목도 분명해야 합니다. 저는 변경 후 아래 네 가지가 같이 움직이는지 봅니다.

    1. P95/P99 지연 시간 또는 작업 처리 시간이 개선되는가
    2. throttling 관련 메트릭 증가 속도가 눈에 띄게 낮아지는가
    3. 노드 CPU 사용량은 다소 올라가더라도 서비스 응답성이 좋아지는가
    4. Pending, FailedScheduling, HPA 이상 반응이 새로 생기지 않는가

    여기서 중요한 건 목표를 헷갈리지 않는 겁니다. CPU 사용률을 낮추는 것이 목적이 아니라, 필요한 순간에 일을 빨리 끝내게 만드는 것이 목적입니다. 그래서 CPU limit를 제거한 뒤 노드 CPU가 조금 더 올라가더라도, 응답 시간과 완료 시간이 좋아졌다면 그건 오히려 정상 반응일 수 있습니다.

    반대로 아래처럼 나오면 다시 조정해야 합니다.

    변경 후 관찰 결과 해석 다음 액션
    latency 개선, throttling 감소, 노드 CPU 소폭 상승 원인이 limit였을 가능성이 큼 현재 전략 유지, request만 미세 조정
    throttling 감소했는데 Pending 증가 request를 공격적으로 올린 상태 request 재산정, replica 또는 node capacity 검토
    latency 변화 거의 없음, 노드 CPU만 상승 limit가 핵심 병목이 아닐 수 있음 애플리케이션 lock, DB, 외부 API, GC 확인
    HPA 스케일 패턴이 갑자기 달라짐 request 변경이 autoscaling 기준에 영향 CPU target utilization과 min/max replica 재검토

    운영에서는 “좋아졌다”를 감각으로 말하면 안 됩니다. 변경 전후 1~2개 배포 주기라도 묶어서 보면서, 애플리케이션 지표와 throttling 신호가 같은 방향으로 개선되는지 확인하셔야 합니다.

    Kubernetes CPU Limit 조정 후 throttling 감소와 응답 시간 개선을 보여주는 대시보드 이미지

    변경 전후를 한눈에 보여주는 대시보드 이미지가 들어가면, 독자가 무엇을 검증해야 하는지 바로 이해할 수 있습니다.

    8. Kubernetes CPU Limit, 상황별 추천은 이렇게 보시면 됩니다

    이 주제는 열린 결말로 끝내면 현업에서 바로 쓰기 어렵습니다. 저는 아래처럼 꽤 명확하게 가져가는 편입니다.

    • 일반 웹 API, gRPC 서버, 이벤트 소비자, burst형 워커: CPU limit는 기본값처럼 넣지 마시고, request 중심으로 먼저 설계해 보세요. 메모리 limit는 유지하고, CPU는 관측 기반으로 조정하는 쪽이 대체로 덜 아픕니다.
    • 공유 클러스터에서 특정 팀이나 테넌트의 폭주를 강하게 막아야 하는 경우: CPU limit를 유지할 이유가 충분합니다. 다만 이때는 성능 비용을 인정하고, throttling 메트릭을 운영 기본 대시보드에 올려두는 편이 좋습니다.
    • HPA를 CPU utilization 기준으로 쓰는 경우: request가 곧 스케일 판단의 분모가 됩니다. request를 너무 높이거나 낮추면 autoscaling 동작이 왜곡될 수 있으니 limit보다 request 튜닝을 더 신중히 보셔야 합니다.
    • 초저지연, 코어 고정성이 중요한 특수 워크로드: request=limit, Guaranteed QoS, static CPU Manager 같은 전용 전략이 맞을 수 있습니다. 이건 일반 웹 서비스의 기본 전략과 분리해서 보셔야 합니다.

    제 판단 기준을 한 줄로 압축하면 이렇습니다. Kubernetes CPU Limit는 성능 향상을 위한 기본 옵션이 아니라, 통제 비용을 감수하고 쓰는 정책 옵션입니다. 서비스 응답성과 처리량이 우선이면 request를 먼저 제대로 잡고, CPU limit는 정말 필요한 경우에만 추가하는 편이 낫습니다. 반대로 클러스터 거버넌스와 테넌트 격리가 우선이라면 limit를 유지하되, 그 대가로 생기는 throttling을 보이는 지표로 관리해야 합니다.

    저는 실무에서 이걸 이렇게 정리합니다. 성능 문제를 풀 때는 CPU limit를 의심하고, 거버넌스 문제를 풀 때는 CPU limit를 설계한다. 이 구분만 분명해져도 불필요한 튜닝 시행착오가 꽤 줄어듭니다.

    HPA나 VPA, ResourceQuota까지 함께 보는 글도 이어서 읽어보시면 흐름이 더 잘 잡힙니다. 다음 글에서는 request 튜닝과 autoscaling을 어떻게 엮어야 하는지, 그리고 LimitRange와 ResourceQuota를 운영 정책으로 쓸 때 어디서 성능 비용이 생기는지 더 깊게 다뤄보겠습니다.

    마무리 직전에 들어갈 요약 인포그래픽 자리입니다. 독자가 상황별 권고안을 빠르게 다시 확인할 수 있게 해줍니다.

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

  • [k8s] Kubernetes 클러스터 비용 절감: Karpenter로 최적화하는 방법

    [k8s] Kubernetes 클러스터 비용 절감: Karpenter로 최적화하는 방법

    Kubernetes 클러스터 비용 절감: Karpenter로 최적화하는 방법

    Kubernetes 클러스터 비용 절감이 늘 화제가 되는 이유는 단순합니다. 청구서는 노드 기준으로 쌓이는데, 실제 낭비는 파드보다 노드 배치 방식에서 더 자주 생기거든요. 저도 EKS와 사내 테스트 클러스터를 오래 운영하면서 느낀 게 하나 있습니다. CPU나 메모리 사용률이 낮다고 해서 비용 구조가 좋은 건 아니더라고요. 진짜 문제는 빈 노드가 오래 남는 구조, 너무 좁은 인스턴스 선택 조건, 스케줄링 제약 때문에 비우지 못하는 노드였습니다. 이 지점에서 Karpenter는 단순한 오토스케일러라기보다, 비용 구조를 다시 설계하게 만드는 도구에 가깝습니다.

    특히 배치 잡, 이벤트성 트래픽, 개발·스테이징처럼 부하가 흔들리는 환경에서는 “스케일이 된다”와 “비용이 내려간다”가 같은 말이 아닙니다. 노드가 빨리 늘어나는 것보다 중요한 건 어떤 노드를 어떤 제약 아래 띄우고, 언제 합치고, 언제 지울지를 운영자가 의도적으로 설계하는 일입니다. Karpenter는 그 의도를 코드로 옮기는 데 강합니다. 다만 반대로 말하면, 의도가 흐리면 자동화도 같이 흐려집니다.

    이 글은 AWS EKS에서 self-managed Karpenter를 사용하는 시나리오를 기준으로 정리했습니다. 예시 리소스는 2026년 8월 기준 공식 문서에서 안내하는 NodePool(karpenter.sh/v1)과 EC2NodeClass(karpenter.k8s.aws/v1) 형식을 따릅니다.

    Kubernetes 클러스터 비용 절감 흐름을 한눈에 보여주는 아키텍처 이미지입니다. 워크로드, Karpenter, EC2 인스턴스 선택 관계를 이해하는 데 도움이 됩니다.

    Kubernetes 클러스터 비용 절감에서 Karpenter가 비용을 줄이는 방식

    Karpenter의 핵심은 “노드 그룹을 미리 정해놓고 그중 하나를 키운다”가 아니라, 현재 Pending 상태인 파드 집합에 맞춰 필요한 노드 모양을 계산한다는 데 있습니다. 그래서 같은 오토스케일링이라도 비용 최적화 포인트가 다릅니다. Cluster Autoscaler는 노드 그룹 설계가 결과를 크게 좌우하지만, Karpenter는 파드 요청값과 스케줄링 제약이 결과를 더 직접적으로 좌우합니다.

    이 차이가 실무에서 크게 드러나는 순간은 두 가지입니다. 첫째, 작은 파드 여러 개를 위해 큰 노드가 반복 생성되는 경우입니다. 둘째, 리소스는 남는데 스케줄링 제약 때문에 노드를 지우지 못하는 경우입니다. 전자는 과도한 requests와 좁은 인스턴스 필터가 원인이고, 후자는 PDB, anti-affinity, topology spread, DaemonSet, local storage가 원인인 경우가 많습니다. 비용 절감은 결국 스케일 업보다 스케일 다운이 더 어렵다는 현실을 인정하는 데서 시작합니다.

    비용이 새는 대표 패턴

    • requests가 실제 사용량보다 과하게 커서 작은 워크로드도 큰 노드를 요구하는 경우
    • 인스턴스 타입을 몇 개만 허용해 대체 가능성이 줄어든 경우
    • 팀별 성격이 다른 워크로드가 한 NodePool에 섞여 consolidation 여지가 줄어든 경우
    • Spot이 가능한 워크로드까지 전부 On-Demand로 남겨둔 경우
    • PDB, anti-affinity, topology spread 제약 때문에 비어 보이는 노드를 실제로는 비우지 못하는 경우
    • DaemonSet이나 startupTaints 설정이 맞지 않아 Karpenter가 노드 상태를 보수적으로 해석하는 경우

    Cluster Autoscaler와 Karpenter 비교

    항목 Cluster Autoscaler Karpenter
    증설 기준 기존 노드 그룹 중 하나를 확장 Pending 파드 요구사항에 맞는 새 노드를 계산
    비용 최적화 강점 단순하고 예측 가능함 인스턴스 다양화, consolidation, Spot 혼합에 강함
    설계 실수의 영향 노드 그룹 과설계로 고정 낭비가 생김 requests·제약 조건 오설계가 즉시 비용으로 이어짐
    잘 맞는 환경 정적이고 노드 종류가 거의 변하지 않는 환경 워크로드 변동성이 크고 비용 민감도가 높은 환경
    굳이 안 써도 되는 경우 이미 충분히 단순한 구조라면 유지 가치가 있음 클러스터가 작고 항상 비슷한 부하라면 도입 복잡도가 더 클 수 있음

    Kubernetes 클러스터 비용 절감을 시작하기 전 점검

    Karpenter를 붙이기 전에 먼저 봐야 할 건 “지금 어디서 돈이 새는가”입니다. 저는 이걸 노드 문제인지, 파드 설계 문제인지, 스케줄링 제약 문제인지로 나눠서 봅니다. 이 구분이 안 되면 Karpenter를 넣고도 체감 절감이 약합니다. 노드는 잘 늘고 줄어도, 정작 비우지 못하는 파드가 계속 남아 있으면 청구서는 생각만큼 안 내려갑니다.

    1. 노드 사용률보다 먼저 파드 requests와 실제 사용량의 간격을 확인합니다.
    2. 서비스형 워크로드와 배치형 워크로드를 분리할 수 있는지 봅니다.
    3. Spot 허용 범위를 애플리케이션 단위로 정합니다. 클러스터 단위로 뭉뚱그리면 보통 여기서부터 꼬입니다.
    4. PDB, affinity, topology spread, local storage, DaemonSet이 스케일 다운을 막는지 확인합니다.
    5. 서브넷 태그, 보안 그룹 태그, IAM 권한처럼 “노드를 못 띄우는 이유”를 미리 제거합니다.

    실무에서는 아래 정도만 봐도 현재 낭비 구조가 꽤 선명하게 드러납니다. 특히 노드가 비어 보이는데도 사라지지 않는 이유를 찾는 데 유용합니다.

    kubectl top nodes
    kubectl top pods -A --containers
    kubectl get pods -A -o wide
    kubectl get pdb -A
    kubectl get daemonset -A -o wide
    kubectl describe node <node-name>

    여기서 해석 기준이 중요합니다. kubectl top nodes가 한가한데 노드 수가 줄지 않으면 CPU보다 제약 조건을 먼저 봐야 합니다. 반대로 Pending 파드가 있는데 노드가 안 뜨면 NodePool 조건, EC2NodeClass 태그, IAM, 가용 용량 조건 중 하나가 원인인 경우가 많습니다. 저는 이때 kubectl describe pod와 kubectl describe nodeclaim을 바로 같이 봅니다. 파드가 원하는 조건과 Karpenter가 실제로 계산한 조건이 어긋나는지 금방 보이거든요.

    Kubernetes 클러스터 비용 절감을 위한 Karpenter 실전 구현

    AWS EKS 기준으로 보면 비용 절감 설계는 NodePool은 정책, EC2NodeClass는 인프라 속성으로 역할을 나누는 순간부터 쉬워집니다. 제 경험상 비용이 안 내려가는 팀은 대개 이 둘을 분리해서 생각하지 않습니다. 노드 생성 정책과 서브넷·보안 그룹·AMI 전략이 뒤섞이면, 나중에 원인 추적이 정말 번거로워집니다.

    아래 예시는 self-managed Karpenter의 v1 API 기준으로, 인스턴스 타입을 몇 개 박아두는 대신 카테고리와 세대로 폭을 넓혀 놓은 구성입니다. 비용 최적화만 놓고 보면 이 방식이 보통 더 낫습니다. 너무 구체적인 타입 화이트리스트는 예측 가능성은 올려도, 가격 선택권과 대체 가능성을 스스로 포기하는 셈이거든요.

    apiVersion: karpenter.k8s.aws/v1
    kind: EC2NodeClass
    metadata:
      name: default
    spec:
      role: KarpenterNodeRole-${CLUSTER_NAME}
      amiSelectorTerms:
        - alias: al2023@latest
      subnetSelectorTerms:
        - tags:
            karpenter.sh/discovery: ${CLUSTER_NAME}
      securityGroupSelectorTerms:
        - tags:
            karpenter.sh/discovery: ${CLUSTER_NAME}
      tags:
        team: platform
        intent: general
    ---
    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: general
    spec:
      template:
        metadata:
          labels:
            workload: general
        spec:
          nodeClassRef:
            group: karpenter.k8s.aws
            kind: EC2NodeClass
            name: default
          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", "on-demand"]
            - key: karpenter.k8s.aws/instance-category
              operator: In
              values: ["c", "m", "r"]
            - key: karpenter.k8s.aws/instance-generation
              operator: Gt
              values: ["5"]
          expireAfter: 720h
      disruption:
        consolidationPolicy: WhenEmptyOrUnderutilized
        consolidateAfter: 5m
        budgets:
          - nodes: "10%"
      limits:
        cpu: "200"

    이 설정에서 비용 관점으로 꼭 짚어야 할 포인트는 다섯 가지입니다.

    • requirements는 넓게 열고, 파드 쪽 제약으로 세밀하게 조정하는 편이 보통 유리합니다.
    • karpenter.sh/capacity-type에 spot과 on-demand를 함께 두면 선택지가 생기지만, 어떤 워크로드가 Spot으로 가도 되는지는 파드 수준에서 분리해줘야 합니다.
    • consolidationPolicy와 consolidateAfter는 비용 절감 체감에 직접 연결됩니다. 다만 너무 공격적으로 잡으면 짧은 변동에도 노드 교체가 잦아집니다.
    • budgets를 주지 않으면 Karpenter의 정리 속도가 운영 정책보다 앞설 수 있습니다. 특히 서비스형 워크로드에서는 노드 정리 속도를 제한하는 게 안전합니다.
    • expireAfter는 비용보다 운영 위생에 가깝습니다. 오래된 노드 청소, 드리프트 정리, 보안 패치 반영에는 유용하지만, 무조건 짧게 잡는다고 절감이 커지진 않습니다.

    적용 이후에는 리소스 생성 여부만 보지 말고, Karpenter가 왜 그 노드를 선택했는지를 확인해야 합니다.

    kubectl apply -f karpenter-nodeclass.yaml
    kubectl apply -f karpenter-nodepool.yaml
    kubectl get ec2nodeclass
    kubectl get nodepool
    kubectl describe nodepool general
    kubectl get nodeclaims
    kubectl describe nodeclaim <nodeclaim-name>

    describe nodeclaim에서 제가 먼저 보는 건 세 가지입니다. 요청 리소스 합계, 선택된 인스턴스 후보, 상태 조건입니다. 여기서 후보가 지나치게 좁으면 NodePool 요구사항이 과도한 거고, 상태가 Launched, Registered, Initialized 중 어디에서 막히는지 보면 인프라 문제인지 초기화 문제인지 구분이 빨라집니다. 이거 빨리 보이기 시작하면 운영 시간이 꽤 아껴집니다.

    Kubernetes 클러스터 비용 절감을 위한 NodePool과 EC2NodeClass 구성 이미지

    NodePool 정책과 EC2NodeClass 인프라 설정이 어떻게 연결되는지 설명하는 이미지입니다. 실전 구성에서 헷갈리는 지점을 줄여줍니다.

    워크로드를 섞지 말고 나누는 게 핵심입니다

    Karpenter 비용 최적화에서 제가 가장 크게 본 차이는 기능 자체보다 워크로드 분리였습니다. API 서버, 백그라운드 워커, 배치, CI 잡, 개발 환경을 한 NodePool에 몰아넣으면 겉보기엔 단순하지만 비용은 잘 안 내려갑니다. 이유는 간단합니다. 한쪽은 안정성을 위해 보수적인 배치를 원하고, 다른 한쪽은 싸고 빨리 비워지는 노드를 원하기 때문입니다. 요구가 다른 워크로드를 한 바구니에 넣으면 Karpenter는 결국 비싼 쪽 기준으로 움직이기 쉽습니다.

    실무에서는 보통 이렇게 나눕니다. 사용자 트래픽을 직접 받는 서비스는 On-Demand 중심, 중단 복구가 쉬운 배치나 비동기 작업은 Spot 중심입니다. 애플리케이션 설계가 재시도와 중단 복구를 갖추지 못했다면, Spot으로 절감하려는 시도는 비용 최적화가 아니라 장애 예고에 가깝습니다.

    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: batch-spot
    spec:
      template:
        metadata:
          labels:
            workload: batch
        spec:
          nodeClassRef:
            group: karpenter.k8s.aws
            kind: EC2NodeClass
            name: default
          taints:
            - key: workload
              value: batch
              effect: NoSchedule
          requirements:
            - key: karpenter.sh/capacity-type
              operator: In
              values: ["spot"]
            - key: kubernetes.io/arch
              operator: In
              values: ["amd64"]
      disruption:
        consolidationPolicy: WhenEmptyOrUnderutilized
        consolidateAfter: 2m
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: batch-worker
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: batch-worker
      template:
        metadata:
          labels:
            app: batch-worker
        spec:
          nodeSelector:
            workload: batch
          tolerations:
            - key: workload
              operator: Equal
              value: batch
              effect: NoSchedule
          containers:
            - name: worker
              image: public.ecr.aws/docker/library/busybox:latest
              command: ["sh", "-c", "sleep 3600"]
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"

    이렇게 분리하면 좋은 점이 명확합니다. Spot 전용 NodePool에서는 공격적으로 합치고 지워도 되고, 서비스형 NodePool에서는 보수적으로 운영하면 됩니다. 비용 절감은 결국 “전체 클러스터를 싸게”가 아니라 싼 방식으로 돌려도 되는 워크로드를 정확히 분리하는 데서 시작합니다.

    트러블슈팅: 노드는 안 줄고 비용만 나가던 실제 시나리오

    제가 실제로 자주 본 재현 가능한 시나리오를 하나 말씀드릴게요. 야간 배치가 끝났고 kubectl top nodes도 한가한데, 아침까지 노드가 그대로 남아 있는 경우입니다. 겉으로는 “Karpenter가 consolidation을 못 하나?” 싶지만, 실제 원인은 대부분 더 아래층에 있습니다.

    • PDB가 너무 보수적으로 잡혀 있어 파드 축출 자체가 불가능한 경우
    • strict anti-affinity 또는 topology spread 제약 때문에 파드를 한 노드로 모을 수 없는 경우
    • 모든 노드에 올라가는 DaemonSet이 예상보다 큰 requests를 차지하는 경우
    • startupTaints를 Karpenter 설정에 반영하지 않아 새 노드를 계속 필요한 것으로 판단하는 경우
    • emptyDir나 local storage를 쓰는 파드 때문에 안전한 축출 판단이 늦어지는 경우

    이럴 때 저는 순서를 거의 고정합니다. 원인을 바깥에서 안쪽으로 좁혀가야 시간을 덜 씁니다.

    1. kubectl get pdb -A로 중단 가능 범위를 확인합니다.
    2. kubectl describe pod <pod-name>로 affinity, topology spread, toleration, nodeSelector를 봅니다.
    3. kubectl get daemonset -A -o wide와 각 DaemonSet의 requests를 확인합니다.
    4. kubectl get nodeclaims와 kubectl describe nodeclaim <name>로 Karpenter 판단 근거를 확인합니다.
    5. Karpenter 로그에서 provision, disruption, consolidation 관련 메시지를 확인합니다.

    운영 중 점검할 때는 아래 명령이 빠릅니다.

    export KARPENTER_NAMESPACE="kube-system"
    kubectl get node -l karpenter.sh/nodepool
    kubectl get nodeclaims
    kubectl describe nodeclaim <nodeclaim-name>
    kubectl logs -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter --tail=200
    kubectl get events -A --sort-by=.lastTimestamp

    로그 해석 기준도 잡아두면 좋습니다. “found provisionable pod(s)”가 보이는데 nodeclaim 생성이 이어지지 않으면 NodePool 매칭 조건을 의심하세요. nodeclaim은 만들어졌는데 Ready까지 오래 걸리면 부트스트랩, CNI, IAM, 서브넷 라우팅처럼 노드 초기화 계층을 봐야 합니다. 노드가 한가한데 consolidation이 반복적으로 보류되면 대개 PDB나 스케줄링 제약 때문에 “비울 수 없는 상태”로 판단된 겁니다. 비용 최적화에서 중요한 건 낮은 사용률이 아니라 재배치 가능성입니다.

    운영 중 자주 쓰는 점검 명령

    kubectl get node
    kubectl get nodeclaims
    kubectl describe nodeclaim <nodeclaim-name>
    kubectl describe pod <pod-name>
    kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter --tail=200

    describe 출력에서는 Events와 Conditions를 먼저 보시면 됩니다. 생성은 됐는데 Registered 단계에서 머물면 인스턴스가 클러스터에 붙지 못하는 문제일 가능성이 크고, Initialized 단계에서 오래 걸리면 CNI나 kubelet 초기화 쪽을 봐야 합니다. 반대로 생성 시도 자체가 없으면, 그건 거의 항상 스케줄링 조건 문제입니다.

    Kubernetes 클러스터 비용 절감이 제대로 되는지 검증하는 법

    비용 절감은 노드 수만 보고 판단하면 자주 실패합니다. 저는 최소한 노드 수 변화, Pending 지속 시간, Spot 비중, 비어 있는 노드 유지 시간, 워크로드 재배치 가능성을 같이 봅니다. 노드 수가 줄어도 Pending이 늘어나면 절감이 아니라 가용성 훼손이고, Spot 비중이 늘어도 재시도 실패가 증가하면 운영비를 장애 비용으로 바꿔치기한 셈입니다.

    검증할 때 보는 체크포인트는 아래와 같습니다.

    • 배치 종료 후 불필요한 노드가 자연스럽게 정리되는지
    • 트래픽 급증 시 Pending 파드가 길게 남지 않는지
    • Spot 전환 대상 워크로드에서 재시도 실패가 늘지 않았는지
    • NodePool 분리로 조각화가 심해지지 않았는지
    • 인스턴스 타입 후보가 지나치게 좁아져 특정 시점에 노드 생성 실패가 반복되지 않는지

    실무적으로는 하나만 기억하셔도 됩니다. 비용 최적화는 평균 사용률 게임이 아니라, 배치 가능성과 축출 가능성 게임입니다. 파드를 잘 올리는 것만큼, 안전하게 옮기고 비울 수 있어야 청구서가 내려갑니다.

    Kubernetes 클러스터 비용 절감 결과를 보여주는 Karpenter 대시보드 이미지

    적용 전후 비교에 쓰기 좋은 대시보드 이미지입니다. 노드 수 변화, Spot 활용, 빈 노드 정리 흐름을 시각적으로 보여줍니다.

    선택 기준: 어떤 환경에 어떤 설정이 맞는가

    상황 추천 전략 이유 주의할 점
    운영 서비스 중심 On-Demand 우선, Spot은 보조, 보수적인 consolidation 가용성 손실 비용이 인프라 절감보다 크기 쉬움 PDB, zone spread, disruption budget을 먼저 설계해야 함
    배치·크론·비동기 워커 중심 Spot 적극 활용, 인스턴스 후보 넓게 허용 중단 허용성이 있으면 절감 여지가 큼 재시도, 체크포인트, idempotency가 없으면 오히려 위험
    개발·스테이징 환경 expireAfter와 consolidation을 적극 사용 유휴 시간이 길어 자동 정리 효과가 큼 야간 배포나 테스트 창과 충돌하지 않게 시간대 정책 필요
    멀티테넌트 클러스터 NodePool 분리, taint/label 정책 명확화 비용 책임과 워크로드 성격을 분리해야 최적화가 쉬움 과도한 분리는 조각화와 운영 복잡도를 키움
    항상 비슷한 부하의 소규모 클러스터 도입 전 단순한 고정 노드 구조와 비교 검토 Karpenter 운영 복잡도보다 절감 폭이 작을 수 있음 도구 도입 자체가 목적이 되지 않게 해야 함

    제 추천은 꽤 분명합니다. 서비스형 비중이 크면 안정형 NodePool부터 작게 시작하시고, 배치형 비중이 크면 Spot 전용 풀을 먼저 분리하시는 게 좋습니다. 둘을 동시에 크게 바꾸면 원인 추적이 어려워집니다. 비용 최적화는 한 번에 크게 먹히는 기술보다, 작은 정책을 분리해서 효과를 측정하는 운영 방식에 더 가깝습니다.

    자주 헷갈리는 포인트

    • Karpenter를 넣는다고 requests 오설계가 자동으로 해결되진 않습니다. 오히려 잘못된 requests를 더 빠르게 비용으로 바꿉니다.
    • 인스턴스 타입을 세세하게 고정하면 예측은 쉬워지지만 비용 선택권은 줄어듭니다. 특별한 이유가 없다면 계열·세대 중심 제약이 더 실용적입니다.
    • Spot 비중을 높일수록 애플리케이션의 중단 내성이 먼저 준비돼 있어야 합니다.
    • 노드 수가 줄어도 Pending과 축출 실패가 늘면 실패한 최적화입니다.
    • 조각화된 NodePool은 운영자가 통제하는 것 같아 보여도, 실제로는 consolidation 여지를 줄여 비용을 다시 올릴 수 있습니다.

    도입 전 점검 항목, 운영 체크포인트, 상황별 추천 전략을 요약한 인포그래픽 이미지입니다.

    마무리

    Kubernetes 클러스터 비용 절감은 노드를 적게 띄우는 기술이 아니라, 워크로드마다 다른 실패 허용 범위를 자원 정책으로 번역하는 일에 가깝습니다. Karpenter는 그 번역을 자동화해 주지만, 만능은 아닙니다. requests가 부정확하고, PDB가 과도하고, 스케줄링 제약이 뒤엉켜 있으면 자동화는 낭비를 더 빠르게 반복할 뿐입니다.

    그래서 저는 이렇게 권합니다. 운영 서비스가 중심이면 On-Demand 기반의 보수적 NodePool부터 시작하세요. 배치·개발 환경이 중심이면 Spot 전용 분리와 적극적인 consolidation부터 적용하시는 게 맞습니다. 둘 중 무엇이든 공통으로, 처음에는 NodePool 하나만 작게 도입해서 Pending 파드, NodeClaim 상태, 노드 정리 흐름을 관찰해 보세요. 그 단계에서 비용이 어디서 새는지, 그리고 Karpenter가 어디까지 해결해 주는지가 훨씬 또렷하게 보입니다.

    관련해서 이 블로그의 EKS 운영 가이드나 Kubernetes 스케줄링 글도 함께 읽어보시면, NodePool 분리와 requests 튜닝을 더 입체적으로 잡는 데 도움이 됩니다.

  • [홈랩] 라즈베리 파이 Kubernetes 클러스터 1년 운영 회고

    [홈랩] 라즈베리 파이 Kubernetes 클러스터 1년 운영 회고

    [홈랩] 라즈베리 파이 Kubernetes 클러스터 1년 운영 회고

    라즈베리 파이 Kubernetes 클러스터를 집에서 1년 정도 굴려보면, 처음 기대했던 그림과 실제 운영 현실이 꽤 다르다는 걸 알게 됩니다. 저도 처음엔 “작고 조용한 장비 몇 대로 Kubernetes(쿠버네티스, 컨테이너 오케스트레이션 플랫폼)까지 돌리면 완벽한 홈랩 아니겠나” 싶었거든요. 실제로 해보니까 분명 배운 건 많았고, 홈랩 Kubernetes 환경으로는 정말 훌륭했습니다. 다만 운영 관점에서는 생각보다 손이 많이 가고, ARM Kubernetes 환경 특유의 제약도 분명하더라고요.

    특히 k3s 라즈베리 파이 조합은 입문 장벽이 낮아서 많이들 시작하시는데, 막상 6개월, 1년 넘어가면 “왜 자꾸 스토리지가 문제지?”, “왜 어떤 이미지는 ARM에서 안 돌지?”, “왜 업그레이드가 무섭지?” 같은 현실적인 질문이 생깁니다. 오늘 글은 그런 부분을 미화하지 않고, 제가 직접 운영하면서 느낀 점을 정리한 회고입니다. 혹시 집에서 ARM Kubernetes 클러스터를 꾸려보려는 분이라면, 시행착오를 조금 덜 하실 수 있을 겁니다.

    라즈베리 파이 기반 홈랩 Kubernetes 클러스터 전체 구성 다이어그램

    라즈베리 파이 여러 대와 스위치, 스토리지, 라우터가 연결된 홈랩 Kubernetes 아키텍처 예시입니다.

    1. 왜 라즈베리 파이 Kubernetes 클러스터를 만들었나

    시작 이유는 단순했습니다. 클라우드에서만 Kubernetes를 보면 너무 추상적으로 느껴지거든요. 노드가 실제로 꺼지면 어떻게 되는지, 네트워크가 꼬이면 어디서부터 봐야 하는지, 스토리지가 왜 중요한지 같은 건 직접 만져봐야 감이 옵니다. 라즈베리 파이는 소비전력이 낮고, 크기도 작고, 무엇보다 “망가져도 다시 해보자” 할 수 있는 범위라서 홈랩에 잘 맞습니다.

    쉽게 말해, 홈랩 Kubernetes는 서비스 운영을 위한 정답이라기보다 분산 시스템의 감각을 몸으로 익히는 연습장에 가깝습니다. 이 포인트를 초반에 이해하면 만족도가 높아집니다. 반대로 “싼 비용으로 프로덕션 비슷하게 다 된다”에 초점을 맞추면 금방 피곤해질 수 있습니다.

    • 좋았던 점: 노드 추가/제거, 장애 테스트, 배포 자동화, Ingress(인그레스, 외부 트래픽 진입점), Persistent Volume(퍼시스턴트 볼륨, 영속 스토리지) 개념을 직접 익히기 좋았습니다.
    • 힘들었던 점: SD 카드 내구성, 전원 안정성, ARM 이미지 호환성, 발열, 스토리지 성능, 업그레이드 순서 같은 운영 이슈가 계속 따라왔습니다.

    2. 라즈베리 파이와 k3s, 무엇이 잘 맞았나

    제가 여러 선택지 중에서 k3s를 선호했던 이유는 명확합니다. k3s는 Kubernetes 경량 배포판으로 알려져 있고, 홈랩이나 에지 환경에서 많이 쓰입니다. 풀 사이즈 Kubernetes를 직접 구성하는 것보다 훨씬 빠르게 시작할 수 있었고, 필요한 핵심 개념은 대부분 그대로 가져갈 수 있었습니다. 처음엔 이게 뭔가 싶었는데, 막상 써보니 “학습용으로는 정말 적절하다”는 생각이 들더라고요.

    여기서 중요한 포인트는 k3s가 Kubernetes를 쉽게 만들어주지만, 운영 복잡도 자체를 없애주진 않는다는 점입니다. 설치는 쉬워도 운영은 결국 운영이죠.

    항목 라즈베리 파이 + k3s 일반 x86 미니 PC + Kubernetes
    진입 난이도 낮은 편 중간
    전력/소음 유리함 장비에 따라 다름
    ARM 호환성 검토 필수 상대적으로 부담 적음
    스토리지 성능 주의 필요 대체로 유리함
    학습 효과 매우 높음 매우 높음
    장기 운영 안정성 구성에 따라 편차 큼 대체로 유리함

    3. 홈랩 Kubernetes 구성은 어떻게 잡았나

    구성 자체는 복잡하게 시작하지 않는 게 좋습니다. 저도 초반에는 컨트롤 플레인(Control Plane, 클러스터 제어 영역)과 워커 노드(Worker Node, 실제 워크로드 실행 노드)를 나누는 개념부터 익혔고, 네트워크와 스토리지는 최대한 단순하게 가져갔습니다.

    3-1. 기본 구성 원칙

    1. 운영체제는 최소 구성으로 설치합니다.
    2. 노드 이름과 IP 대역을 미리 정리합니다.
    3. 시간 동기화와 고정 IP를 먼저 잡습니다.
    4. 클러스터 설치 전 컨테이너 이미지가 ARM을 지원하는지 확인합니다.
    5. 스토리지는 처음부터 분리할지, 로컬 디스크로 시작할지 결정합니다.

    제가 직접 해보니 가장 많이 꼬이는 건 Kubernetes 자체보다 주변부였습니다. DNS(도메인 네임 시스템), DHCP 예약, 공유기 설정, 스위치 품질, 전원 어댑터 같은 것들이요. 진짜 삽질은 여기서 많이 합니다.

    3-2. 예시 설치 흐름

    아래는 홈랩에서 많이 쓰는 형태의 아주 기본적인 흐름입니다.

    # 컨트롤 플레인 노드에서 k3s 설치
    curl -sfL https://get.k3s.io | sh -
    
    # 토큰 확인
    sudo cat /var/lib/rancher/k3s/server/node-token
    
    # 워커 노드에서 조인
    curl -sfL https://get.k3s.io | K3S_URL=https://CONTROL_PLANE_IP:6443 K3S_TOKEN=YOUR_TOKEN sh -

    설치는 생각보다 금방 끝납니다. 문제는 그 다음이죠. 노드가 붙었다고 끝이 아니라, 이제부터가 진짜 시작입니다.

    # 상태 확인
    sudo kubectl get nodes -o wide
    sudo kubectl get pods -A
    
    # 간단한 테스트 배포
    sudo kubectl create deployment hello --image=nginx
    sudo kubectl expose deployment hello --port=80 --type=ClusterIP

    3-3. 예시 매니페스트

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: demo-web
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: demo-web
      template:
        metadata:
          labels:
            app: demo-web
        spec:
          containers:
            - name: web
              image: nginx:stable
              ports:
                - containerPort: 80
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: demo-web
    spec:
      selector:
        app: demo-web
      ports:
        - port: 80
          targetPort: 80
    k3s 설치 후 컨트롤 플레인과 워커 노드가 연결된 구성도

    k3s 라즈베리 파이 환경에서 컨트롤 플레인과 워커 노드가 연결되는 기본 구조를 보여주는 다이어그램입니다.

    4. 1년 운영하면서 체감한 현실적인 문제들

    이 섹션이 아마 제일 중요할 겁니다. 블로그나 영상에서는 “설치 성공”까지가 잘 보이는데, 실제 운영은 그 뒤의 평범한 장애와 유지보수에서 갈립니다.

    4-1. 스토리지가 생각보다 훨씬 중요했습니다

    처음엔 SD 카드로도 되겠지 싶었는데, 오래 굴릴수록 로그, 컨테이너 레이어, 작은 쓰기 작업이 누적되면서 성능과 안정성 이슈가 슬슬 보였습니다. 특히 재부팅 후 상태가 애매해지거나, 특정 파드(Pod, Kubernetes의 최소 배포 단위)가 느리게 올라오는 상황을 몇 번 겪으니 금방 교훈이 생기더라고요.

    교훈: 홈랩이라도 스토리지는 가볍게 보면 안 됩니다. 가능하면 더 안정적인 저장장치 구성을 고민하는 게 좋습니다.

    4-2. ARM Kubernetes는 이미지 호환성을 항상 봐야 합니다

    이건 진짜 자주 나옵니다. 문서대로 배포했는데 안 뜹니다. 로그를 보면 이미지가 ARM 아키텍처를 제대로 지원하지 않거나, 특정 툴체인이 x86 중심으로 만들어진 경우가 있거든요. 저도 처음엔 네트워크 문제인가 싶어서 한참 봤는데, 결국 이미지 아키텍처 문제였던 적이 있었습니다.

    그래서 지금은 새 앱을 올리기 전에 아래처럼 확인하는 습관을 들였습니다.

    • 공식 이미지인지 확인
    • 멀티 아키텍처(multi-arch) 지원 여부 확인
    • ARM64 태그 또는 매니페스트 지원 여부 확인
    • 헬름 차트(Helm Chart, Kubernetes 패키지 템플릿)가 ARM에서 문제 사례가 없는지 확인

    4-3. 전원과 발열은 사소해 보여도 누적되면 문제입니다

    홈랩은 책상 위에서 끝나는 실험 같지만, 결국 24시간 켜두는 장비입니다. 전원이 순간적으로 불안정하면 노드가 이유 없이 사라진 것처럼 보일 수 있고, 발열이 누적되면 성능 저하나 불안정성으로 이어질 수 있습니다. 저는 한동안 Kubernetes 문제인 줄 알고 로그만 팠는데, 알고 보니 전원 쪽이 애매했던 적도 있었습니다. 이건 진짜 허탈하더라고요.

    5. ⚠️ 실제로 겪었던 트러블슈팅

    여기서는 조금 더 실전적으로 적어보겠습니다. 저도 처음엔 헷갈렸는데, 막상 패턴이 보이기 시작하면 접근 순서가 정리됩니다.

    5-1. 노드가 갑자기 NotReady가 될 때

    1. kubectl get nodes로 상태를 먼저 봅니다.
    2. kubectl describe node로 이벤트를 확인합니다.
    3. 해당 장비의 전원, 네트워크 링크, 디스크 상태를 OS 레벨에서 봅니다.
    4. 문제가 반복되면 Kubernetes 내부보다 하드웨어/OS 쪽을 먼저 의심합니다.
    sudo kubectl get nodes
    sudo kubectl describe node worker-01
    journalctl -u k3s -n 100
    journalctl -u k3s-agent -n 100

    5-2. 파드가 계속 재시작될 때

    처음엔 애플리케이션 버그라고만 생각했었는데, 실제로는 이미지 아키텍처 미지원, 환경 변수 누락, 볼륨 마운트 실패, 리소스 부족 등 원인이 다양했습니다. 여기서 중요한 건 로그와 이벤트를 같이 보는 것입니다.

    sudo kubectl get pods -A
    sudo kubectl describe pod POD_NAME -n NAMESPACE
    sudo kubectl logs POD_NAME -n NAMESPACE --previous

    5-3. Ingress가 안 열릴 때

    Ingress(인그레스, 외부 트래픽 진입점)는 홈랩에서 체감 난이도가 꽤 있습니다. 공유기 포트포워딩, 내부 DNS, 서비스 타입, Ingress Controller(인그레스 컨트롤러) 설정이 다 맞아야 하거든요. 한 군데만 틀려도 접속이 안 됩니다.

    • 서비스가 ClusterIP인지 NodePort인지 확인
    • Ingress 리소스의 호스트 이름이 DNS와 맞는지 확인
    • 공유기 포트포워딩 규칙 확인
    • 내부망 테스트와 외부망 테스트를 분리해서 확인

    이거 진짜 편하더라고요 싶은 구조는, 처음부터 도메인과 내부 DNS 전략을 정해두는 겁니다. 대충 시작하면 나중에 더 큰 삽질로 돌아옵니다.

    파드 재시작과 노드 상태 점검을 위한 운영 대시보드 장면

    노드 상태, 파드 상태, 리소스 사용량을 함께 점검하는 홈랩 Kubernetes 운영 화면을 표현한 이미지입니다.

    6. 검증은 어떻게 했나

    클러스터가 “떠 있다”와 “운영 가능하다”는 다릅니다. 저는 최소한 아래 기준으로 확인했습니다.

    1. 노드가 모두 Ready 상태인지
    2. 기본 시스템 파드가 안정적으로 유지되는지
    3. 테스트 애플리케이션이 재배포 후에도 정상 기동하는지
    4. 노드 한 대를 내렸을 때 서비스 복구 흐름이 보이는지
    5. Ingress를 통한 내부 접근이 되는지
    sudo kubectl get nodes
    sudo kubectl get pods -A
    sudo kubectl rollout restart deployment/demo-web
    sudo kubectl rollout status deployment/demo-web
    sudo kubectl get svc,ingress -A

    제가 실제로 만족했던 순간은, 뭔가 화려한 대시보드가 아니라 문제가 생겼을 때 원인을 좁혀갈 수 있게 된 시점이었습니다. “아, 이건 Kubernetes보다는 스토리지 쪽이네”, “이건 애플리케이션 이미지 문제네” 같은 판단이 빨라지더라고요. 그게 1년 운영의 가장 큰 수확이었습니다.

    7. 1년 운영 후 느낀 장점과 한계

    항목 장점 한계
    학습 Kubernetes 개념이 몸에 익음 실무형 대규모 운영과는 차이 있음
    비용 감각 집에서 반복 실험 가능 운영 시간 비용이 꽤 큼
    안정성 구성을 단순화하면 쓸 만함 하드웨어 편차와 스토리지 변수 큼
    확장성 노드 추가 경험에 좋음 성능 확장은 금방 한계가 옴
    재미 홈랩 감성이 확실함 문제 생기면 주말이 사라짐

    냉정하게 말하면, 장기 운영의 편안함만 놓고 보면 x86 미니 PC나 소형 서버가 더 나은 선택일 수 있습니다. 하지만 배움의 밀도는 라즈베리 파이 Kubernetes 클러스터가 정말 높았습니다. 장애 하나하나가 다 수업료였고, 그 덕분에 실무에서 로그를 보는 눈도 꽤 달라졌습니다.

    8. 이런 분께는 추천하고, 이런 경우는 말리고 싶습니다

    추천하는 경우

    • Kubernetes를 손으로 익히고 싶은 분
    • 홈랩 Kubernetes 환경에서 배포 자동화와 운영 흐름을 경험해보고 싶은 분
    • ARM Kubernetes 제약까지 포함해서 배우고 싶은 분

    말리고 싶은 경우

    • 처음부터 안정적인 장기 서비스 운영이 목표인 분
    • 트러블슈팅 자체를 즐기기 어려운 분
    • 하드웨어/네트워크까지 만지는 게 부담스러운 분

    사실 이 부분이 제일 현실적인 결론입니다. 라즈베리 파이 클러스터는 멋있습니다. 그런데 편한 건 아닙니다. 이 차이를 알고 시작하면 만족도가 높고, 모르고 시작하면 생각보다 금방 지칩니다.

    라즈베리 파이 Kubernetes 홈랩의 장점과 한계를 비교한 인포그래픽

    학습 효과, 전력, 안정성, 스토리지, 운영 난이도를 한눈에 비교하는 요약 인포그래픽입니다.

    9. 마무리: 홈랩의 현실과 교훈

    1년 운영 후 제 결론은 이렇습니다. 라즈베리 파이 Kubernetes는 최고의 학습 장비 중 하나지만, 최고의 운영 장비는 아닐 수 있습니다. 이 문장을 받아들이고 시작하면 훨씬 좋습니다. 저도 처음엔 “이걸로 다 되겠는데?” 싶었는데, 실제로 써보니까 홈랩의 현실은 꽤 다층적이더라고요.

    그래도 후회는 없습니다. 네트워크, 스토리지, 스케줄링, Ingress, 장애 대응, 로그 분석까지 한 번에 묶어서 배울 수 있었거든요. 특히 k3s 라즈베리 파이 조합은 작은 비용으로 큰 학습을 주는 도구였습니다. 다만 다음에 다시 구성한다면, 저는 초반부터 스토리지 전략과 전원 안정성, 그리고 ARM 이미지 호환성 점검 루틴을 더 엄격하게 가져갈 겁니다.

    혹시 지금 홈랩 Kubernetes를 고민 중이시라면, 제 추천은 이거예요. 처음엔 작게 시작하세요. 노드 수보다 운영 경험이 먼저입니다. 그리고 성공 기준을 “오래 켜두기”보다 “문제가 생겼을 때 이해할 수 있는가”에 두시면 훨씬 많이 남습니다.

    다음 글에서는 홈랩에서 자주 쓰는 Ingress(인그레스, 외부 트래픽 진입점) 구성이나, 모니터링 스택을 어떻게 가볍게 올릴지 다뤄볼 예정입니다. 이전 글이 있다면 함께 보셔도 흐름 잡는 데 도움이 되실 겁니다.

    정리 FAQ

    라즈베리 파이 Kubernetes 클러스터는 실사용에 적합한가요?

    가벼운 내부 서비스나 학습 목적에는 괜찮지만, 안정성이 아주 중요한 운영 환경이라면 더 여유 있는 하드웨어가 낫습니다.

    홈랩 Kubernetes에 k3s를 많이 쓰는 이유는 뭔가요?

    설치와 초기 구성이 비교적 단순하고, Kubernetes 핵심 개념을 익히기에 충분하기 때문입니다.

    ARM Kubernetes에서 가장 먼저 확인할 건 뭔가요?

    컨테이너 이미지의 ARM 지원 여부, 스토리지 구성, 전원 안정성 이 세 가지를 먼저 보시면 됩니다.

  • [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 비용 최적화 체크리스트와 적용 순서를 정리한 인포그래픽

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