Skip to content

Kubernetes Runbook

donghoong2 edited this page Oct 28, 2026 · 2 revisions

Kubernetes 런북

develop 04c0e10 (2026-10-27) · 클러스터 기준 2026-10-14 기록

요약

  • dev 클러스터에서 자주 하는 확인(배포 버전, 파드·노드 상태, 시크릿 동기화, Argo CD)과 정기 점검(etcd 스냅샷, kubeadm 인증서, 업그레이드·재부팅 전후)을 모았다.
  • 운영하면서 실제로 겪은 장애를 증상별로 정리했다. 항목마다 영향 → 확인 → 조치 → 재발 방지 순서로 쓴다.
  • 클러스터를 왜 이렇게 구성했는지는 Kubernetes 설명 페이지에 있다.

Important

노드 접속은 SSM Session Manager로 하고, kubectl은 컨트롤 플레인에서 실행한다. 앱 Deployment는 Argo CD selfHeal이 켜져 있어서 kubectl로 바꾼 값은 몇 분 안에 Git 상태로 돌아간다. 상태를 바꾸는 조치는 매니페스트 레포 PR로 한다.

어디부터 볼까

flowchart TD
    S{무슨 증상인가}
    S -->|새 배포가 안 보인다| A[app.commit 확인] --> A2[Argo CD OutOfSync·OOM 확인]
    S -->|파드가 Pending이다| B[describe pod Events] --> B2{원인}
    B2 -->|볼륨 노드 어피니티| B3[Redis local-path PVC 항목]
    B2 -->|GPU 리소스 없음| B4[GPU 노드 꺼짐·device plugin 항목]
    S -->|파드는 Running인데 AWS 호출만 실패한다| C[ExternalSecret STATUS] --> C2[IMDS hop limit 항목]
    S -->|노드·네트워크 전체가 이상하다| D[CNI 파드 상태] --> D2[DNS·보안그룹 포트·커널 항목]
    S -->|노드 메모리가 꽉 찼다| E[Allocated resources] --> E2[롤아웃 중 파드 중복 항목]
Loading

자주 하는 확인

지금 dev에 떠 있는 버전 확인

curl -s https://<DEV_API_HOST>/actuator/info | jq -r .app.commit
curl -s https://<DEV_API_HOST>/actuator/health

app.commit이 머지한 커밋 해시와 같으면 새 이미지가 ALB·Traefik까지 거쳐 제대로 떠 있는 것이다. CI의 배포 확인 단계도 같은 값을 본다.

파드 상태와 로그

kubectl get pods -l app=backend
kubectl describe pod <POD_NAME>        # Pending·CrashLoop 사유는 Events에 있다
kubectl logs deploy/backend --tail=100
kubectl top pods                       # metrics-server가 필요하다

노드 여유

워커가 적어서 요청 메모리 합계가 빠듯하다. 롤링 업데이트 순간엔 파드가 하나 더 뜨므로 여유도 같이 본다.

kubectl get nodes -o wide
kubectl describe node <NODE_NAME> | grep -A8 "Allocated resources"

시크릿 동기화

kubectl get externalsecret                 # STATUS가 SecretSynced인지 본다
kubectl annotate externalsecret <NAME> force-sync="$(date +%s)" --overwrite

값은 출력하지 않고 키 이름만 확인한다. ESO는 폴링으로 동기화하므로, Secrets Manager 값을 바꾼 직후 급하면 위 어노테이션으로 바로 동기화한다. Secret이 바뀌면 Reloader가 파드를 재시작한다.

Argo CD 상태

UI는 외부에 노출하지 않고 SSM 포트포워딩으로만 연다. OutOfSync면 UI의 APP DIFF로 무엇이 다른지부터 본다.

Warning

되돌릴 때는 UI의 Rollback이 아니라 태그 커밋을 revert하는 PR을 머지한다. 자동 Sync 앱에서는 Rollback이 막혀 있고, 쓰더라도 Git과 클러스터가 어긋난다. Cilium과 Argo CD 자신은 수동 Sync라 변경을 머지한 뒤 직접 Sync해야 반영된다.

정기 점검

  • etcd 스냅샷: 매일 새벽 컨트롤 플레인에서 자동으로 뜨고 7일 보관한다. 복구 리허설은 아직 안 했다.

  • kubeadm 인증서: 기본 1년짜리다. 1년 안에 kubeadm upgrade를 하면 같이 갱신되지만, 안 하면 만료 전에 kubeadm certs renew all을 해야 한다. 놓치면 어느 날 클러스터 전체가 인증 실패로 멈추므로 만료 한 달 전에 확인 일정을 잡아 둔다.

    sudo kubeadm certs check-expiration
  • kubeadm upgrade 뒤: kube-proxy가 되살아나지 않았는지 확인한다. 지금은 Cilium이 대신하고 있고, kube-proxy DaemonSet은 롤백용으로 남겨 둔 채 스케줄만 막아 두었다.

  • 노드 재부팅 전: /boot에 현재 커널 파일이 실제로 있는지 확인한다(아래 "워커를 재부팅했더니…" 항목 참고).

  • 노드 추가 전: 현재 CNI 기준 노드 간 보안그룹 포트 목록(VXLAN, 헬스체크, 메트릭 포트)과 IMDS hop limit을 확인한다.

증상별 대응

겪은 문제 중 다시 만날 가능성이 높은 것만 추렸다. 장애에서 나온 항목은 끝에 사례 기록 링크를 단다.

Note

Calico를 쓰던 시기의 항목은 지금 CNI(Cilium)에서는 그대로 재현되지 않는다. 그래도 CNI를 다시 바꿀 때 같은 유형으로 돌아올 수 있어서 남겨 두었다.

컨트롤 플레인 주소(프라이빗 DNS 이름)가 VPC 안에서 해석되지 않는다

  • 영향: kube-proxy가 API 서버에 못 붙고, CNI·CoreDNS까지 줄줄이 막혀 클러스터 네트워크 전체가 동작하지 않는다.
  • 확인: VPC 안에서 엔드포인트 이름이 해석되는지 본다 → VPC 속성 enableDnsSupport와 enableDnsHostnames가 둘 다 켜져 있는지 본다. 프라이빗 호스티드 존은 둘 다 켜져야 해석된다. 이번 원인은 enableDnsHostnames가 꺼져 있던 것이었다.
  • 조치: VPC 속성을 켠다. 해결 전까지는 API 엔드포인트를 사설 IP로 통일해 둔다.
  • 재발 방지: 두 속성이 모두 켜져야 한다는 조건을 VPC 설정 확인 항목으로 남겼다.

Tip

호스트의 /etc/hosts로 우회해도 파드 안 프로세스는 그 파일을 보지 못한다. 노드에서 해석된다고 파드에서도 된다고 판단하면 안 된다.

Calico로 바꿨더니 calico-node가 계속 0/1이다 (Calico 시기)

  • 영향: 해당 노드의 파드 네트워크가 준비되지 않는다.
  • 확인: Typha 포트가 노드 간 보안그룹에 열려 있는지 본다 → operator 설정에서 BGP가 기본값(켜짐)인지 본다. BGP가 켜져 있으면 readiness가 BIRD를 요구한다. 이번엔 두 가지가 겹쳤다.
  • 조치: Typha 포트를 열고, operator 설정에 bgp: Disabled를 명시한다.
  • 재발 방지: CNI를 바꿀 때 보안그룹 포트 목록과 CNI 기본값을 먼저 확인한다.
  • 사례 기록: CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환

iptables에 규칙이 안 보이고, 노드에서 localhost로 NodePort에 접속이 안 된다 (eBPF 데이터플레인)

  • 영향: 서비스 자체는 정상이다. 진단하다 장애로 오해하기 쉽다.
  • 확인: eBPF 데이터플레인은 iptables 대신 자체 테이블과 BPF 맵을 쓰고, loopback 패킷은 가로채지 않는다. 같은 NodePort를 노드의 사설 IP로 호출해서 응답하면 정상이다.
  • 조치: 할 일은 없다. 노드에서 테스트할 때는 localhost 말고 사설 IP를 쓴다.
  • 재발 방지: 노드에서 하는 NodePort 테스트는 사설 IP로 한다는 것을 이 항목으로 남겨 둔다.

Cilium을 깔았더니 operator는 Pending이고 agent는 CRD를 기다리며 멈춘다

  • 영향: Cilium이 올라오지 않아 파드 네트워크가 준비되지 않는다.
  • 확인: operator 파드의 Events에서 스케줄 실패 사유를 본다 → 노드에 Calico가 남긴 NetworkUnavailable 컨디션의 taint가 있는지 본다. operator가 이 taint를 견디지(tolerate) 못해서 Pending이었고, 이 버전은 CRD 등록을 operator가 맡아서 agent도 같이 멈췄다.
  • 조치: operator에 해당 taint의 toleration을 추가한다.
  • 재발 방지: CNI를 바꿀 때는 이전 CNI가 남긴 노드 컨디션·taint도 같이 확인한다. Cilium은 수동 Sync 대상이라 변경 후 직접 Sync한다.
  • 사례 기록: CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환

워커를 재부팅했더니 인스턴스 체크가 실패하고 containerd가 컨테이너를 못 만든다

  • 영향: 해당 워커의 파드가 전부 뜨지 않는다. 일반 워커가 한 대라 앱 전체가 멈춘다.
  • 확인: 부팅된 커널 버전과 /boot에 있는 커널 파일을 비교한다 → 커널 모듈 로드 실패 로그를 본다. 이번 원인은 반쯤 진행된 커널 업그레이드였다. /boot에 커널 파일이 없어서 예전에 남아 있던 커널로 부팅됐고, 그래서 모듈 로드가 실패했다.
  • 조치: 커널을 다시 설치하고 initramfs를 재생성한 뒤 재부팅한다.
  • 재발 방지: 재부팅 전 /boot의 커널 파일 확인을 정기 점검 체크리스트에 넣었다.
  • 사례 기록: CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환 (Cilium 전환 중 함께 겪었고, 원인은 CNI와 무관했다)

노드·파드는 멀쩡한데 ExternalSecret이 며칠째 조용히 실패한다

  • 영향: Secrets Manager의 값 변경이 k8s Secret에 반영되지 않는다. 노드는 Ready, 파드는 Running이라 겉으로 드러나지 않고 AWS SDK 호출만 깨진다. 실제로 며칠 동안 수백 번 재시도하며 실패했는데 알림은 한 건도 없었다.
  • 확인: kubectl get externalsecret의 STATUS가 SecretSynced인지 본다 → ESO 로그에서 AWS 인증 실패를 본다 → 그 파드가 떠 있는 노드의 IMDS hop limit을 확인한다. 이번 원인은 CNI를 바꾸면서 파드 → 노드 → IMDS 경로의 홉이 늘어난 것이었다. 기본 hop limit으로는 파드가 IMDSv2 토큰을 받지 못했다.
  • 조치: 노드마다 hop limit을 올린다. 인스턴스 속성이라 노드마다 따로 맞춰야 한다.
  • 재발 방지: 노드 추가 전 hop limit 확인을 정기 점검에 넣었다.
  • 사례 기록: CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환 · 클러스터 상태가 레포 밖에 있어서 잃어버린 것들

Warning

한 대만 고치면 그 노드에 있는 파드만 살아나고, 다른 노드로 재스케줄되는 순간 다시 깨진다. 같은 역할의 노드는 전부 확인한다.

Redis 파드를 다른 노드로 옮겼더니 계속 Pending이다

  • 영향: Redis가 뜨지 않아 리프레시 토큰 재발급, 탈퇴 계정 토큰 블랙리스트 조회처럼 Redis를 쓰는 기능이 멈춘다.
  • 확인: kubectl describe pod의 Events에서 볼륨 노드 어피니티 충돌을 본다. local-path PVC는 처음 만들어진 노드에 묶여 있다.
  • 조치: PVC를 다시 만든다. 이때 데이터는 초기화된다(리프레시 토큰이 사라져 모든 사용자가 다시 로그인해야 할 수 있다).
  • 재발 방지: 운영 데이터를 담는다면 네트워크 스토리지를 검토해야 한다. 아직 남은 과제다.
  • 사례 기록: 클러스터 상태가 레포 밖에 있어서 잃어버린 것들

고장 난 앱에 롤아웃이 걸리자 노드 메모리가 두 배로 찬다

  • 영향: 같은 노드의 다른 파드까지 메모리 압박을 받는다. 일반 워커가 한 대라 노드 전체가 위험해진다.
  • 확인: kubectl get pods에서 새 파드가 Ready가 안 된 채 옛 파드와 같이 떠 있는지 본다 → 노드 Allocated resources를 본다. 새 파드가 Ready가 안 되니 옛 파드를 내리지 못하고, 둘 다 죽었다 살아나기를 반복하며 메모리를 차지한다.
  • 조치: 해당 앱의 replica를 0으로 내린다. selfHeal 때문에 kubectl scale은 되돌려지므로 매니페스트 레포 값을 바꿔서 머지한다.
  • 재발 방지: requests/limits를 꼭 지정하고, 롤링 업데이트 순간의 메모리 여유를 같이 본다. 워커 추가를 검토하고 있다.

Argo CD 컨트롤러가 뜨자마자 OOM으로 계속 재시작한다

  • 영향: Sync가 멈춰서 새 배포가 반영되지 않는다. 이미 떠 있는 앱은 그대로 돈다.
  • 확인: 컨트롤러 파드의 종료 사유가 OOMKilled인지 본다. 컨트롤러는 관리 대상 리소스를 전부 메모리에 올리므로, 관리 대상이 늘면 상한이 모자라진다.
  • 조치: 메모리 상한을 올린 뒤 파드를 직접 지운다. 컨트롤러는 StatefulSet이라 기존 파드가 Ready가 되길 기다리느라 교체가 막혀 있었다. Argo CD 자신은 수동 Sync 대상이다.
  • 재발 방지: 관리 대상 앱을 늘릴 때 컨트롤러 메모리 상한도 같이 본다.
  • 사례 기록: Helm 3과 4의 렌더링 차이를 리소스 단위 대조로 잡고 애드온을 Argo CD로 이전

큰 CRD가 apply되지 않는다

  • 영향: 그 CRD를 쓰는 컴포넌트를 설치하거나 업그레이드할 수 없다.
  • 확인: 에러 메시지가 어노테이션 크기 초과인지 본다. 일반 apply는 적용 내용을 last-applied 어노테이션에 남겨 256KB 한도에 걸린다.
  • 조치: Server-Side Apply로 적용한다.
  • 재발 방지: 큰 CRD를 가진 컴포넌트는 Server-Side Apply로 Sync하도록 설정한다.
  • 사례 기록: Helm 3과 4의 렌더링 차이를 리소스 단위 대조로 잡고 애드온을 Argo CD로 이전

Ollama가 에러 없이 CPU로 떠서 추론이 수십 배 느려지고, 결국 호스트가 OOM된다

  • 영향: 약관 ETL 처리가 사실상 멈추고, 호스트 OOM으로 GPU 노드의 다른 워크로드까지 죽는다.
  • 확인: 기동은 성공, 로그는 INFO, API는 200이라 겉으로는 정상이다. GPU를 실제로 쓰고 있는지(device plugin이 할당한 GPU를 프로세스가 쓰는지) 직접 본다. 이번 원인은 부팅 직후 GPU 드라이버가 준비되기 전에 떠서 GPU 탐지에 실패한 것이었다.
  • 조치: 파드를 재시작해 GPU가 준비된 뒤 다시 뜨게 한다.
  • 재발 방지: GPU를 안 쓰는 상태를 주기적으로 판정해 재시작하는 가드와, 클라이언트 쪽 연속 실패 차단기를 넣었다. "조용한 폴백"을 잡아내는 관측도 추가했다.

GPU 워크로드 파드가 Pending이다

  • 영향: GPU 인스턴스가 꺼져 있는 동안이라면 의도된 상태다.
  • 확인: GPU 인스턴스가 stop 상태인지 본다. GPU 노드는 상시 가동하지 않고 같은 인스턴스를 stop/start한다. 켜져 있는데도 Pending이면 노드에 nvidia.com/gpu 리소스가 잡혀 있는지(NVIDIA device plugin, containerd nvidia 런타임 등록)를 본다.
  • 조치: 꺼져 있어서라면 할 일은 없다. 켜져 있는데 리소스가 안 잡혔다면 device plugin과 런타임 등록을 점검한다.
  • 재발 방지: 꺼진 동안의 Pending은 정상으로 본다.

관련 사례 기록

관련 위키: Kubernetes

Clone this wiki locally