레플리케이션과 그 밖의 컨트롤러
- 4.1 Pod를 건강하게 유지하기 — Liveness Probe
- 4.2 ReplicationController
- 4.3 ReplicaSet — RC의 후계자
- 4.4 DaemonSet — 노드마다 하나씩
- 4.5 Job — 한 번 완료되는 작업
- 4.6 CronJob — 주기적으로 돌리기
4장은 한 마디로 "Pod를 어떻게 계속 살려두는가"에 대한 장이야. 3장에서 Pod를 직접 만들어봤지만, 직접 만든 Pod는 죽으면 끝이거든. 실전에선 노드가 죽든 앱이 뻗든 자동으로 복구돼야 하고, 어떤 작업은 모든 노드에 하나씩 떠야 하고, 어떤 작업은 한 번만 돌고 끝나야 해. 이 요구들을 각각 담당하는 컨트롤러들이 있어 — ReplicationController, ReplicaSet, DaemonSet, Job, CronJob. 이 다섯이 4장의 주인공이야.
먼저 컨테이너가 죽으면 Kubelet이 알아서 재시작해줘. 근데 프로세스가 살아있긴 한데 응답을 못 하는 상태는 어떻게 감지할까? Java 앱이 OOM 직전에 GC만 돌리고 있다든가, 데드락이 걸렸다든가. 이건 OS가 알 수 없거든. 그래서 쿠버네티스는 liveness probe로 "이 컨테이너가 실제로 살아있는가"를 주기적으로 확인해. 세 종류가 있어. HTTP GET은 지정 경로에 HTTP 요청을 보내서 2xx/3xx면 성공, 나머지는 실패로 친다. TCP Socket은 지정 포트에 TCP 연결을 시도해보고. Exec는 컨테이너 안에서 명령을 실행해서 exit code 0이면 성공으로 봐.
YAML로는 livenessProbe 아래에 httpGet이나 tcpSocket, exec를 적고, 거기에 initialDelaySeconds를 거의 필수로 같이 넣어야 해. 앱이 시작하는 데 시간이 걸리는데 첫 프로브가 바로 들어와서 실패하면 컨테이너가 끝없이 재시작되거든. 책에서 저자가 여러 번 본 사고 패턴이야. 컨테이너가 exit code 137(128+9, SIGKILL)이나 143(128+15, SIGTERM)으로 죽어 있으면 외부에서 죽인 거고, events 로그를 보면 liveness probe 실패 때문이라는 게 보여. 좋은 liveness probe 설계 원칙 몇 가지를 짚어두면, 간단한 /health 엔드포인트를 두고 앱 내부의 필수 컴포넌트 상태만 확인하고, 외부 의존성은 절대 확인하지 마. 프론트엔드의 liveness probe가 백엔드 DB를 체크하면, DB 장애가 났을 때 프론트가 같이 재시작되는 참사가 나거든. 그리고 가볍게. 프로브는 1초 안에 끝나야 하고 CPU도 별로 안 써야 해. Java 앱에 Exec 프로브를 걸면 JVM을 매번 띄우게 돼서 비용이 커. HTTP GET을 권장하는 이유야. 그리고 retry loop를 직접 만들지 마. 쿠버네티스가 이미 failure threshold만큼 재시도해. 한 가지 중요한 한계가 있는데, liveness probe는 Kubelet이 실행해. 그 말은 노드 자체가 죽으면 Kubelet도 같이 죽어서 아무 일도 안 일어나. 다른 노드로 Pod를 옮기려면 Control Plane 레벨의 컨트롤러가 필요해. 그게 다음 절 주제야.
**ReplicationController(RC)**는 "이 라벨에 해당하는 Pod가 항상 N개 떠 있어야 해"라는 desired state를 보장하는 컨트롤러야. Pod가 하나 죽으면 새로 만들고, 노드가 죽으면 다른 노드에 새 Pod를 띄워. RC를 구성하는 세 부분이 있어. label selector는 관리할 Pod를 고르는 기준이고, replica count는 몇 개가 있어야 하는지, pod template은 새 Pod를 만들 때 쓰는 틀이야. 쿠키 커터 같은 거. 동작 방식은 리컨실레이션 루프라는 건데, 주기적으로 "라벨 셀렉터에 맞는 Pod가 몇 개?"를 세고, desired보다 적으면 템플릿으로 새로 만들고 많으면 일부를 지워. 이게 쿠버네티스 전체의 핵심 패턴이야. 명령형이 아니라 선언형, 원하는 상태를 쓰면 시스템이 알아서 맞춰주는 거.
여기서 짚어둘 디테일이 몇 개 있어. 먼저 Pod는 RC에 귀속되지 않아. Pod가 셀렉터와 맞으면 그 RC가 관리하는 거고, 라벨을 바꾸면 관리 대상에서 빠져. 이걸 이용하면 문제가 있는 Pod를 RC에서 떼어내 디버깅하고 RC가 대체 Pod를 띄우게 할 수 있어. 그리고 템플릿을 바꿔도 기존 Pod는 그대로야. 앞으로 만들어지는 Pod만 새 템플릿을 써. 그래서 "템플릿 바꾸고 기존 Pod 하나씩 지우기"가 원시적인 업데이트 방식이었지. 9장의 Deployment가 이걸 자동화해줘. 셀렉터는 절대 바꾸지 마. 셀렉터를 바꾸면 기존 Pod가 전부 고아가 되고 RC가 새 Pod를 만들어. 실수하기 쉬워. 마지막으로 kubectl delete rc kubia --cascade=false로 지우면 RC만 삭제되고 Pod는 살아남아. RC를 ReplicaSet으로 교체할 때 유용해. 스케일링은 kubectl scale rc kubia --replicas=10이나 kubectl edit rc kubia로 YAML의 spec.replicas를 바꾸면 돼. **"어떻게 10개로 늘릴까"가 아니라 "10개가 있었으면 좋겠다"**라고 선언하는 게 쿠버네티스식이야.
**ReplicaSet(RS)**은 RC의 업그레이드 버전이야. 동작은 거의 같은데 라벨 셀렉터가 더 표현력 있어. matchLabels는 단순 key=value(RC와 동일)고, matchExpressions는 In, NotIn, Exists, DoesNotExist 연산자를 지원해. 예를 들어 RC는 "env=production인 Pod"만 고를 수 있지만, RS는 "env 라벨이 있기만 하면" 또는 "env가 prod이거나 devel인 Pod"처럼 고를 수 있어. 결론은, 이제는 RC 대신 RS를 써. 그리고 실전에선 RS도 직접 만들기보다 Deployment가 내부적으로 RS를 만들어 쓰는 게 정석이야 (9장). RC는 deprecated 경로.
**DaemonSet(DS)**은 RC/RS와 비슷하지만 desired replica 개수 개념이 없어. 대신 "라벨 셀렉터에 맞는 모든 노드에 Pod가 하나씩 떠 있어야 한다"를 보장해. 노드가 추가되면 자동으로 그 노드에도 Pod가 뜨고 노드가 빠지면 같이 사라져. 용도는 로그 수집기(Fluentd, Filebeat) — 모든 노드의 로그를 수집해야 하니까, 리소스 모니터링(node-exporter), kube-proxy 자체도 DaemonSet으로 돌고, 스토리지 데몬(Ceph, GlusterFS) 같은 것들이야. 특정 노드 부분집합에만 띄우고 싶으면 Pod 템플릿에 nodeSelector를 걸어. 예를 들면 disk=ssd 라벨 붙은 노드에만. 한 가지 특이점이 있는데, DaemonSet은 스케줄러를 우회해. 어느 노드에 띄울지 스스로 결정하기 때문이야. 그래서 노드가 unschedulable 상태여도 DaemonSet Pod는 떠. 시스템 서비스 성격이라 이게 맞아.
RC/RS/DS의 Pod는 계속 돌아야 하는 프로세스야. 배치 작업처럼 한 번 끝나면 다시 안 떠도 되는 경우엔 Job을 써. Job 매니페스트의 핵심은 restartPolicy를 OnFailure나 Never로 해야 한다는 거야. 기본값 Always는 Job에서 쓸 수 없어. Job이 관리하는 Pod는 노드가 죽으면 다른 노드로 재스케줄되고, 프로세스가 실패(non-zero exit)하면 정책에 따라 재시도해. Job에서 두 개 스펙이 중요해. **completions**는 몇 번 성공해야 Job이 완료되는지야. 5로 하면 Pod를 하나씩 5번 돌려. **parallelism**은 동시에 몇 개까지 병렬로 돌릴지야. 2로 하면 동시에 2개씩 돌면서 총 5번 성공할 때까지 가. 실행 중에도 kubectl scale job ... --replicas=3로 parallelism을 바꿀 수 있어. 그리고 **activeDeadlineSeconds**는 Pod가 이 시간보다 오래 돌면 강제 종료시키는 설정이고, **backoffLimit**은 실패 시 최대 재시도 횟수야 (기본 6).
Job을 정해진 시간에 반복 실행하고 싶으면 CronJob을 써. 리눅스 crontab과 같은 포맷이고, schedule: "0,15,30,45 * * * *" 식으로 적어. 동작 방식은 CronJob이 스케줄에 맞춰 Job 리소스를 만들어내는 것이야. 그러면 그 Job이 Pod를 띄워. 즉 CronJob → Job → Pod의 2단계 중첩이야. 실전 주의사항 두 가지가 있어. 첫째, 정확히 한 번 실행된다고 믿지 마. 드물게 두 번 생성되거나 아예 안 생성될 수 있어. 그래서 작업은 idempotent해야 하고, 한 번 놓쳐도 다음 실행이 밀린 일을 처리할 수 있게 설계해야 해. 둘째, startingDeadlineSeconds — 스케줄된 시간에서 얼마나 지난 뒤까지 시작을 허용할지인데, 넘으면 Failed로 마킹돼. 컨트롤 플레인이 잠깐 멈췄을 때 밀린 실행이 우르르 터지는 걸 막는 안전장치야.
정리
4장 읽고 기억할 거 세 가지:
- Pod를 직접 만들지 마라. 직접 만든 Pod는 노드 장애 시 복구되지 않는다. 실전에선 항상 컨트롤러(RC/RS/DS/Job/CronJob)로 감싸고, 그게 desired state와 실제 상태를 맞춰주는 리컨실레이션 루프를 돌린다. 선언형 관리의 기반이 되는 패턴이다.
- Liveness probe는 Pod 내부 상태만 체크해야 한다. 외부 의존성(DB 등)을 확인하면 한쪽 장애가 전체 연쇄 재시작으로 번진다. 그리고
initialDelaySeconds를 반드시 설정해 앱 기동 시간을 감안해야 무한 재시작 지옥을 피한다. - 컨트롤러는 용도별로 다르다. 일반 서비스는 Deployment(내부적으로 RS 사용), 노드마다 하나 띄울 시스템 데몬은 DaemonSet, 배치성 작업은 Job, 주기 실행은 CronJob. RC는 deprecated이고 ReplicaSet도 보통 직접 쓰지 않는다 — Deployment 뒤에 숨어 있다.