지난주 새벽의 작은 사고
지난주에 작은 사고가 있었다. 큰 장애는 아니었는데, 디버깅하면서 내가 너무 기본값을 신뢰하고 있었다는 걸 깨달았다. CronJob failedJobsHistoryLimit 얘기다.
상황은 이랬다. 데이터 동기화용 CronJob이 매 5분마다 도는데, 모니터링 대시보드에서 어느 새벽부터 실패 카운트가 슬슬 올라가고 있었다. 한 시간쯤 지나서 PagerDuty가 울렸고, 출근 전이라 자느라 못 봤다. 아침에 일어나서 보니 한 시간 동안 12번 실패한 상태였다.
로그를 보러 갔는데
당연히 kubectl logs부터 쳤다. Pod가 이미 사라진 상태. 그렇지, CronJob은 Job을 만들고, Job이 Pod를 만든다. Pod는 사라져도 Job 객체는 남아있어야 하니까 그쪽을 보자.
$ kubectl get jobs -n data-sync | grep sync-orders
sync-orders-29183455 0/1 2m 2m
딱 하나. 가장 최근에 실패한 거 하나만 남아있었다. 그 전 11번은? 다 지워졌다.
왜 그랬을까
CronJob 스펙을 다시 봤다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: sync-orders
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: sync
image: our-registry/sync:v2.3.1
failedJobsHistoryLimit을 명시 안 했다. 기본값이 뭔지 알고는 있었다 — 1. 근데 그게 실제로 뭘 의미하는지는 깊게 생각 안 해봤던 거다.
쿠버네티스는 새 Job이 끝날 때마다 정리 로직을 돈다. 실패한 Job이 limit를 넘으면 가장 오래된 것부터 삭제. 5분마다 새 Job이 만들어지고 모두 실패하니까, 5분 간격으로 직전 실패 Job이 깔끔하게 증발한 거다. 한 시간 동안 도는 동안 그 어떤 흔적도 남지 않았다.
사실 가장 화나는 건
실패 원인 자체는 별거 아니었다. 외부 API 키가 만료됐던 거. 그 자체는 5분이면 찾을 일이었는데, 첫 번째 실패 시점이 언제인지, 그때부터 뭐가 바뀌었는지를 추적하는 데 한 시간을 더 썼다. Argo Workflows나 Tekton이었으면 이력이 다 남았을 텐데, 가벼운 CronJob이니까 굳이 그럴 거 없다고 판단했던 게 부메랑이 됐다.
무엇을 바꿨나
일단 운영 중인 CronJob 전부에 다음을 적용했다.
spec:
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 7
성공은 굳이 많이 안 남겨도 된다. 디버깅에 필요한 건 실패 쪽이니까. 7로 한 건 우리 환경에서 가장 짧은 주기 CronJob이 5분이고, 30분 정도는 이력을 보고 싶어서 잡은 숫자. 더 짧은 주기를 쓴다면 더 늘려야 한다.
추가로 OpenSearch 쪽 로그 파이프라인을 점검했다. Pod가 사라져도 로그는 어쨌든 인덱스에 남아야 정상인데, 우리 환경은 Pod 종료가 너무 빠를 때 일부 로그가 누락되는 케이스가 있었다. 이쪽도 별도 이슈로 트래킹 중이다.
깨달은 것
기본값을 모르고 쓰는 것보다 무서운 건, 기본값을 안다고 착각하고 쓰는 거다. failedJobsHistoryLimit: 1이라는 숫자는 알고 있었지만, 그게 "주기적으로 실패하는 작업이라면 직전 실패 하나만 본다"는 의미라는 걸 사고 나서야 체감했다. 문서를 한 번 더 읽었으면 알 수 있었는데, "기본값이면 알아서 합리적으로 동작하겠지" 라는 안이함이 있었다.
비슷한 함정이 있는 필드들도 한 번씩 다시 봐야겠다. ttlSecondsAfterFinished, concurrencyPolicy, startingDeadlineSeconds. 다 자주 안 만지지만 한 번 잘못 설정하면 며칠 후에 후회한다.
혹시 비슷한 경험 있는 분 있으면 어떻게 운영하는지 궁금하다. 우리 팀에서는 이번 일을 계기로 모든 새 CronJob 매니페스트는 history limit를 명시하도록 PR 템플릿에 항목을 추가했다.
'IT > Kubernets' 카테고리의 다른 글
| Kubernetes graceful node shutdown, 안에서 도는 것들 (1) | 2026.05.15 |
|---|---|
| Karpenter 스케줄러는 어떻게 노드를 결정하는가 — 내부 동작 분석 (0) | 2026.05.14 |
| startupProbe 모르고 슬로우 스타트 앱 운영하지 마세요 (0) | 2026.05.13 |
| kube-proxy 내부 동작 - iptables, IPVS, nftables 모드는 패킷을 어떻게 처리하나 (0) | 2026.05.12 |
| 배포할 때마다 503이 잠깐씩 튀던 이유 — Pod 종료 흐름 삽질 노트 (0) | 2026.05.12 |