13년차의 서버실

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

[태그:] 컨테이너 운영

  • [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] GKE 운영 1년 회고: 성공 사례와 놓쳤던 실수들

    [k8s] GKE 운영 1년 회고: 성공 사례와 놓쳤던 실수들

    [k8s] GKE 운영 1년 회고: 성공 사례와 놓쳤던 실수들

    GKE 운영 회고를 한 번 정리해봐야겠다고 마음먹은 이유가 있습니다. Kubernetes(쿠버네티스) 자체는 이제 낯설지 않은 기술이 됐는데, 막상 Google Kubernetes Engine(구글 쿠버네티스 엔진, GKE)를 1년 정도 운영해보면 문서에서 보이던 장점과 실제 운영에서 체감하는 포인트가 꽤 다르거든요. 저도 처음엔 “관리형 Kubernetes면 운영 부담이 확 줄겠지?”라고 생각했었는데, 실제로 써보니까 줄어드는 부담도 분명 있었고, 대신 다른 종류의 실수가 새로 생기더라고요. 이번 글은 그런 관점에서 정리한 GKE 운영 회고입니다.

    특히 홈랩과 회사 환경을 오가며 느낀 점이 비슷했습니다. 클러스터를 만드는 건 빠른데, 클라우드 운영의 난이도는 배포 그 자체보다 권한, 네트워크, 비용, 관측성 같은 주변 요소에서 올라가더라고요. 혹시 지금 GKE를 막 도입하셨거나, 이미 운영 중인데 “왜 자꾸 예상 밖의 이슈가 생기지?” 싶으셨다면 이 글이 꽤 현실적인 체크리스트가 될 겁니다.

    GKE 운영 회고를 위한 전체 클러스터 아키텍처 개요 이미지

    GKE 클러스터, 로드밸런서, Ingress(인그레스, 외부 트래픽 진입점), 모니터링 구성까지 한눈에 보이는 아키텍처 예시입니다.

    1. 왜 GKE 운영 회고가 중요했는가

    제가 느낀 가장 큰 차이는 이겁니다. 쿠버네티스를 설치하는 능력과 쿠버네티스를 안정적으로 운영하는 능력은 완전히 다르다는 점입니다. 온프레미스에서는 제어권이 많아서 불편해도 원인을 깊게 파고들 수 있었는데, GKE 같은 관리형 서비스에서는 편한 대신 추상화가 한 겹 더 들어갑니다. 이게 장점이자 함정이더라고요.

    쉽게 말해, GKE는 컨트롤 플레인(control plane, 클러스터 제어 영역)을 많이 대신 관리해주니까 운영 진입 장벽은 낮아집니다. 근데 워크로드(workload, 실제 애플리케이션 실행 단위), 노드(node, 컨테이너가 올라가는 서버 자원), 네트워크 정책, 비용 구조는 결국 우리가 책임져야 합니다. 그래서 GKE 운영 경험은 “쿠버네티스를 얼마나 아느냐”보다 “어디까지 자동화에 기대고, 어디서부터 운영 원칙을 세우느냐”가 더 중요했습니다.

    2. GKE를 쉽게 설명하면 뭐가 좋은가

    처음 접하시는 분 기준으로 아주 단순하게 설명해보면 이렇습니다.

    • Kubernetes(쿠버네티스): 컨테이너를 여러 대 서버에 나눠 안정적으로 배포하고 관리하는 플랫폼입니다.
    • GKE: 그 Kubernetes를 Google Cloud에서 관리형으로 제공하는 서비스입니다.
    • Ingress(인그레스): 외부에서 들어오는 HTTP/HTTPS 요청을 내부 서비스로 연결하는 관문입니다.
    • Node Pool(노드 풀): 비슷한 성격의 노드들을 묶어서 운영하는 단위입니다.
    • HPA(Horizontal Pod Autoscaler, 수평 자동 확장): 트래픽이나 리소스 사용량에 따라 Pod(파드, 컨테이너 실행 단위) 수를 자동으로 늘리거나 줄입니다.

    제가 직접 해보니 GKE의 진짜 장점은 “초기 구축이 편하다”보다 기본기 있는 팀이 운영 표준을 빠르게 정착시키기 좋다는 데 있었어요. 클러스터 생성, 로드밸런서 연동, IAM(아이엠, 권한 관리), 모니터링 연계 같은 기본 골격이 잘 맞물리거든요. 반대로 기본 원칙 없이 시작하면 편한 만큼 더 빨리 꼬입니다. 이거 진짜 그렇더라고요.

    3. 1년 운영하면서 성공했던 선택들

    3-1. Node Pool을 역할별로 분리한 것

    처음엔 하나의 노드 풀로 다 돌려도 되겠지 싶었습니다. 근데 배치 작업(batch job)과 사용자 트래픽을 받는 웹 서비스가 한곳에 섞이니까 스케줄링이 생각보다 지저분해졌습니다. 이후엔 역할별로 나눴어요.

    • 웹 애플리케이션용 노드 풀
    • 배치 작업용 노드 풀
    • 운영 도구용 노드 풀

    이렇게 나누니까 자원 격리(resource isolation, 리소스 분리)가 쉬워졌고, 장애 분석도 빨라졌어요. “어디가 문제인지”가 훨씬 선명해집니다.

    3-2. 배포 파이프라인을 일찍 표준화한 것

    CI/CD(지속적 통합/지속적 배포) 흐름을 초기에 통일한 것도 컸습니다. 이미지 태그 규칙, 배포 순서, 롤백 기준을 정해두면 사람마다 다르게 배포하는 사고를 많이 줄일 수 있어요. 사실 장애 중 꽤 많은 비율이 기술 난이도보다 절차 부재에서 나오거든요.

    3-3. 관측성부터 깔고 간 것

    Logging(로깅, 로그 수집), Monitoring(모니터링, 지표 수집), Alerting(알림, 이상 징후 통지)을 초반에 정리해둔 게 운영 피로도를 크게 낮췄습니다. 처음엔 귀찮아도, 나중에 장애가 터지면 그 투자금이 그대로 돌아옵니다. 드디어 됐다 싶었던 순간이 이런 데서 나왔습니다.

    4. 실전 구현: 제가 정착시킨 기본 운영 방식

    여기서는 아주 복잡한 구조보다, 운영하면서 무난하게 오래 버틴 기본 틀을 예시로 보여드리겠습니다. 환경마다 다르겠지만 시작점으로는 충분합니다.

    1. 클러스터와 기본 네임스페이스(namespace, 리소스 논리 분리 공간)를 나눕니다.
    2. 워크로드 특성에 따라 노드 풀을 분리합니다.
    3. Ingress와 TLS(전송 구간 암호화)를 표준화합니다.
    4. 리소스 요청치(requests)와 제한치(limits)를 반드시 넣습니다.
    5. 배포 전에 헬스 체크와 롤링 업데이트 기준을 검증합니다.

    4-1. 클러스터 접속과 기본 점검

    gcloud container clusters get-credentials my-gke-cluster --region asia-northeast3 --project my-project
    kubectl get nodes
    kubectl get ns
    kubectl get pods -A

    이 단계는 별거 아닌 것 같아도 중요해요. 저도 초반엔 권한이 안 맞아서 접속은 되는데 일부 리소스가 안 보이는 상황을 겪었거든요. 접속 직후엔 노드 상태, 네임스페이스 목록, 전체 파드 상태를 한 번에 보는 습관을 들였습니다.

    4-2. 애플리케이션 배포 매니페스트 예시

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: web-app
      namespace: prod
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: web-app
      template:
        metadata:
          labels:
            app: web-app
        spec:
          containers:
            - name: web-app
              image: gcr.io/my-project/web-app:stable
              ports:
                - containerPort: 8080
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"
                limits:
                  cpu: "500m"
                  memory: "512Mi"
              readinessProbe:
                httpGet:
                  path: /health
                  port: 8080
                initialDelaySeconds: 5
                periodSeconds: 10
              livenessProbe:
                httpGet:
                  path: /health
                  port: 8080
                initialDelaySeconds: 15
                periodSeconds: 20
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: web-app
      namespace: prod
    spec:
      selector:
        app: web-app
      ports:
        - port: 80
          targetPort: 8080

    여기서 중요한 포인트! requests/limits를 빼먹으면 초반에는 멀쩡해 보여도 운영이 길어질수록 스케줄링 품질이 흔들립니다. 그리고 readiness probe(레디니스 프로브, 트래픽 받을 준비 확인)와 liveness probe(라이브니스 프로브, 살아있는지 확인)는 꼭 분리해서 생각하시는 게 좋습니다. 저도 처음엔 둘을 비슷하게 봤는데, 장애 대응할 때 차이가 꽤 커요.

    4-3. Ingress 구성 예시

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: web-app-ingress
      namespace: prod
      annotations:
        kubernetes.io/ingress.class: "gce"
    spec:
      rules:
        - host: app.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: web-app
                    port:
                      number: 80

    실제로 써보니까 Ingress는 단순히 외부 공개용 설정 파일이 아니라, 운영 책임 경계가 모이는 지점이더라고요. 인증서, 경로 라우팅, 헬스 체크, 방화벽, DNS까지 다 엮이거든요.

    GKE 운영 경험에서 본 배포 흐름과 Ingress 구성 이미지

    배포된 애플리케이션이 Service와 Ingress를 통해 외부 요청을 받는 구조를 시각화한 예시입니다.

    5. 놓쳤던 실수들: 문서에선 잘 안 보이던 운영 함정

    5-1. 리소스 요청치 없이 시작한 실수

    이건 정말 많이들 겪습니다. 초반 테스트에서는 다 잘 돼요. 그래서 “일단 배포부터 하자” 하고 requests/limits를 뒤로 미루기 쉽습니다. 저도 그랬습니다 ㅎㅎ 근데 운영 기간이 길어지면 특정 워크로드가 노드를 잠식하고, 다른 서비스의 응답성이 흔들립니다.

    해결 방법은 단순해요. 처음부터 보수적인 기본값이라도 넣고 시작하는 겁니다. 완벽한 값이 아니어도 괜찮습니다. 없는 것보다 훨씬 낫거든요.

    5-2. 권한 모델을 늦게 정리한 실수

    IAM과 Kubernetes RBAC(Role-Based Access Control, 역할 기반 접근 제어)를 뒤늦게 정리하면 운영자가 늘수록 위험이 커집니다. 특히 누가 클러스터 관리자 권한을 갖는지, 누가 배포만 할 수 있는지, 누가 시크릿(secret, 민감 정보)을 볼 수 있는지 기준이 애매하면 사고가 나더라고요.

    처음엔 저도 “팀이 작으니까 일단 넓게 열자”라고 생각했었는데, 이게 나중에 회수 비용이 더 커요. 최소 권한(least privilege, 필요한 최소 권한만 부여) 원칙은 빨리 적용할수록 편합니다.

    5-3. 로그는 쌓이는데 분석 기준이 없던 실수

    로그를 모으는 것과 로그를 운영에 쓰는 건 달라요. 에러 레벨 구분, 요청 ID(request ID, 요청 추적 식별자), 배포 버전 정보가 없으면 로그가 많아도 분석 속도가 안 나옵니다. 처음엔 이게 뭔가 싶었는데, 결국 로그 포맷 표준화가 먼저였습니다.

    6. ⚠️ 트러블슈팅: 실제로 시간을 잡아먹었던 문제들

    6-1. 파드는 정상인데 서비스가 안 열리던 문제

    가장 당황스러운 유형 중 하나입니다. kubectl get pods에서는 정상으로 보이는데, 외부에서는 접속이 안 됩니다. 이런 경우 저는 아래 순서로 확인했습니다.

    1. Pod readiness 상태 확인
    2. Service selector가 Deployment label과 일치하는지 확인
    3. Ingress backend 연결 상태 확인
    4. DNS 반영 여부 확인
    5. 방화벽/헬스 체크 정책 확인
    kubectl get pods -n prod
    kubectl describe service web-app -n prod
    kubectl describe ingress web-app-ingress -n prod
    kubectl get endpoints web-app -n prod

    실제로는 selector 오타나 readiness 실패가 생각보다 많았어요. 겉으로는 작은 실수인데 장애 체감은 크게 오더라고요.

    6-2. 오토스케일링이 기대처럼 안 움직이던 문제

    HPA를 걸어두면 자동으로 다 해결될 거라 기대하기 쉽습니다. 근데 메트릭(metric, 측정 지표) 기준이 애매하거나 애플리케이션이 스케일 아웃(scale-out, 인스턴스 수 확장)에 적합하지 않으면 기대만큼 반응하지 않더라고요. CPU만 보고 확장하면 실제 병목이 I/O나 DB 연결 수에 있을 수도 있거든요.

    그래서 저는 확장 정책을 신뢰하기 전에 병목 지점을 먼저 확인하는 쪽으로 바꿨어요. 이건 GKE라서가 아니라 Kubernetes 운영 전반의 교훈이었습니다.

    6-3. 비용이 천천히 새던 문제

    운영 초반엔 성능과 안정성에 집중하느라 비용은 뒤로 밀리기 쉽습니다. 저도 그랬고요. 그런데 사용하지 않는 로드밸런서, 과하게 큰 노드, 낮과 밤 차이가 큰 서비스의 고정 리소스가 비용을 서서히 끌어올립니다.

    해결 포인트는 세 가지였습니다.

    • 유휴 리소스 정기 점검
    • 환경별 리소스 상한선 정의
    • 노드 풀 목적 분리와 요청치 재조정

    비용은 어느 날 갑자기 터지지 않고 조용히 새는 경우가 많아요. 그래서 월말보다 주 단위 점검이 더 효과적이더라고요.

    7. 검증과 결과: 운영이 안정됐다고 느낀 기준

    제가 1년쯤 지나서 “이제 좀 운영 체계가 잡혔다”라고 느낀 기준은 화려한 대시보드가 아니었습니다. 아래 같은 변화가 보이면 꽤 건강한 상태예요.

    • 배포 실패 원인을 팀원들이 비슷한 순서로 추적할 수 있음
    • 장애가 나도 어느 레이어에서 막혔는지 빠르게 좁혀짐
    • 리소스 사용량과 트래픽 패턴의 관계가 보임
    • 온콜(on-call, 장애 대응 대기) 피로도가 줄어듦
    • 신규 서비스 온보딩 시간이 짧아짐

    결국 좋은 운영은 “문제가 아예 없는 상태”가 아니라 문제가 생겨도 예측 가능한 방식으로 다루는 상태에 가깝습니다. 이 부분은 GKE 운영 회고를 쓰면서도 다시 느꼈네요.

    GKE 운영 회고의 결과를 보여주는 모니터링 대시보드 이미지

    CPU, 메모리, 파드 수, 배포 상태를 함께 보는 운영 대시보드 예시입니다.

    7-1. 운영 방식 비교 표

    항목 초기 운영 방식 1년 후 정착 방식
    노드 구성 단일 노드 풀 중심 워크로드별 노드 풀 분리
    배포 기준 서비스별 제각각 공통 매니페스트와 파이프라인 사용
    권한 관리 넓은 권한 부여 최소 권한 원칙 적용
    관측성 로그 중심 확인 로그, 메트릭, 알림 기준 통합
    비용 관리 월말 점검 주 단위 점검과 리소스 재조정

    8. 자주 묻는 질문과 운영 팁

    8-1. GKE가 있으면 Kubernetes 운영이 쉬워지나요?

    네, 분명 쉬워지는 부분이 있어요. 다만 운영 책임이 사라지는 건 아닙니다. 제어 영역 일부를 대신 관리해줄 뿐, 애플리케이션 특성, 네트워크 설계, 비용 최적화는 여전히 팀의 몫입니다.

    8-2. 처음부터 완벽한 구조를 만들어야 하나요?

    아닙니다. 오히려 처음부터 너무 크게 설계하면 유지가 어려워요. 제가 추천하는 건 작지만 일관된 표준입니다. 네임스페이스 규칙, 배포 형식, 리소스 요청치, 로그 포맷 이 정도만 먼저 맞춰도 훨씬 낫습니다.

    8-3. 어떤 지표부터 봐야 하나요?

    가장 먼저는 CPU, 메모리, 재시작 횟수, readiness 실패, 배포 이벤트입니다. 그 다음에 애플리케이션 응답 시간과 에러율을 연결해서 보시면 돼요. 인프라 지표와 서비스 지표를 따로 보면 원인 파악이 늦어지더라고요.

    GKE 운영 회고 핵심 체크리스트와 실수 방지 포인트 요약 이미지

    운영 표준, 권한, 관측성, 비용 점검 항목을 한 장으로 요약한 체크리스트 이미지입니다.

    9. 마무리: GKE 운영 경험에서 남은 교훈

    이번 GKE 운영 회고를 정리하면서 다시 느낀 건, 결국 운영의 품질은 특정 기능 하나보다 작은 원칙들을 얼마나 꾸준히 지키느냐에 달려 있다는 점이었습니다. 클러스터를 잘 만드는 것보다, 문제를 빨리 좁히고 반복 실수를 줄이는 구조를 만드는 게 훨씬 중요했어요.

    제가 직접 해보니 성공 사례는 대부분 화려하지 않았습니다. 노드 풀을 분리하고, 리소스 요청치를 넣고, 권한을 조이고, 로그 기준을 맞추는 식의 기본기였어요. 반대로 놓쳤던 실수들도 대단한 기술적 난제가 아니라 “나중에 하자”라고 미뤘던 운영 습관에서 시작됐습니다. 사실 이게 제일 무섭거든요.

    혹시 지금 Google Kubernetes Engine을 도입하려고 하시거나, 이미 쓰고 있는데 운영이 어수선하다고 느끼신다면 먼저 화려한 도구보다 배포 표준화, 권한 모델, 관측성, 비용 점검 루틴부터 다시 보시는 걸 추천드립니다. 다음 글에서는 GKE 환경에서 Ingress와 내부 서비스 분리 전략, 그리고 운영용 대시보드 구성 기준도 이어서 다뤄볼 예정입니다. 이전 글에서 다룬 Kubernetes 기본 운영 원칙과 함께 보시면 더 이해가 쉬우실 겁니다.

    🎉 한 줄로 정리하면 이렇습니다. GKE는 편한 플랫폼이지만, 편하다고 해서 운영 원칙까지 대신 만들어주지는 않습니다. 이 부분만 초반에 잡아두시면 1년 뒤 회고가 훨씬 덜 아프실 겁니다.