4.3 ReplicaSet과 Deployment
Part I. 시작하기의 첫 배포에서 Deployment를 만들자 ReplicaSet이 생기고 그 밑에 Pod가 생기는 것을 봤다. 이 장은 그 세 층이 서로를 어떻게 식별하는지, 그리고 이미지를 바꿨을 때 두 ReplicaSet 사이에서 벌어지는 교대가 정확히 어떤 순서로 진행되는지를 다룬다.
유지 루프와 소유 체인
ReplicaSet의 일은 단순하다. selector에 걸리는 Pod 수를 세고, replicas보다 적으면 template로 만들고 많으면 지운다. 그런데 selector는 label 기반이라 “내가 만든 Pod"와 “우연히 label이 같은 Pod"를 구분하지 못한다. 구분은 Pod의 metadata.ownerReferences가 맡는다. ReplicaSet이 만든 Pod에는 그 ReplicaSet을 가리키는 ownerReferences가 기록되고, ReplicaSet은 자신을 owner로 갖는 Pod만 자기 수에 넣는다.
# kubectl get pod nginx-86dcfdf4c6-w6k5b -o yaml 중 일부
metadata:
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: nginx-86dcfdf4c6
controller: true
blockOwnerDeletion: true같은 구조가 한 층 위에도 있다. Deployment가 ReplicaSet을 소유한다. Deployment를 삭제하면 garbage collector가 ownerReferences 체인을 따라 ReplicaSet과 Pod를 함께 정리하는 것도 이 체인 덕분이다. Deployment는 Pod template의 해시를 계산해 pod-template-hash label로 ReplicaSet과 Pod에 붙이므로, template가 다른 Pod들은 서로 다른 ReplicaSet에 소속된다. Pod 이름 중간의 임의 문자열처럼 보이던 부분이 이 해시다.
flowchart TD
D["Deployment nginx"] --> R1["ReplicaSet 86dcfdf4c6 (신규)"]
D --> R2["ReplicaSet 7d98b57f45 (구, replicas 0)"]
R1 --> P1["Pod ...-w6k5b"]
R1 --> P2["Pod ...-dv27x"]
R1 --> P3["Pod ...-hs8rt"]
ReplicaSet을 직접 만들 이유는 거의 없다. 수 유지만 필요해도 Deployment를 쓴다. 위 층이 하나 더 있다고 잃는 것은 없고, 업데이트가 필요해지는 순간 아래에서 보는 롤링 교대를 그대로 얻는다.
롤링 업데이트: 두 ReplicaSet의 교대
Pod template가 바뀌면(이미지, 환경 변수 등) 해시가 달라지므로 Deployment는 새 ReplicaSet을 만들고, 새것을 늘리면서 옛것을 줄이는 교대를 시작한다. 교대의 보폭을 정하는 파라미터가 둘이다. maxSurge(기본 25%)는 원하는 replica 수를 얼마나 초과해도 되는지, maxUnavailable(기본 25%)은 가용 Pod가 원하는 수에서 얼마나 모자라도 되는지를 정한다. 비율로 적으면 surge는 올림, unavailable은 내림으로 계산된다.
replicas 4에 기본값이면 surge 1, unavailable 1이다. 전체 Pod는 최대 5개까지 늘어나고 가용 Pod는 3개 아래로 내려가지 않는 범위에서, 새 ReplicaSet의 Pod를 늘리고 그것이 Ready가 되면 옛 ReplicaSet의 Pod를 줄이는 순서를 반복한다. 진행 조건이 readiness라는 점이 중요하다. 새 Pod가 readiness probe를 통과하지 못하면 교대는 그 자리에서 멈추고, 망가진 버전이 전체로 퍼지기 전에 정지한다. minReadySeconds를 지정하면 Ready 판정 후에도 그 시간 동안 지켜본 뒤에야 가용으로 인정한다.
멈춘 교대를 Deployment가 스스로 되돌리지는 않는다. progressDeadlineSeconds(기본 600초) 동안 진전이 없으면 status의 Progressing condition이 False로 바뀔 뿐, 자동 롤백은 없다. 알아차리고 조치하는 것은 바깥의 몫이다.
무중단이 필요하면 maxUnavailable: 0으로 두고 surge로만 교대한다. 반대로 여유 자원이 빠듯한 클러스터라면 maxSurge: 0으로 두고 가용 수를 일부 희생한다.
롤백과 revision history
교대가 끝난 옛 ReplicaSet은 삭제되지 않고 replicas 0으로 남는다. 이것이 revision 이력이다. revisionHistoryLimit(기본 10)만큼 보관되고 kubectl rollout history로 나열된다. 되돌리기는 그 template를 다시 적용하는 동작이다.
kubectl rollout undo deployment/nginx # 직전 revision으로
kubectl rollout undo deployment/nginx --to-revision=2 # 특정 revision으로undo는 시간을 되감는 것이 아니라 대상 revision의 template로 새 revision을 만들어 같은 롤링 교대를 한 번 더 실행한다. revision 1로 되돌리면 이력에는 revision 3이 생기고 1은 사라진다. 같은 template의 revision 번호가 최신으로 승계되는 것이다. 이력의 CHANGE-CAUSE 열은 자동으로 채워지지 않으며, kubernetes.io/change-cause annotation을 남긴 경우에만 표시된다.
Recreate가 필요한 경우
RollingUpdate의 전제는 옛 버전과 새 버전이 잠시 공존해도 된다는 것이다. 이 전제가 깨지는 워크로드가 있다. ReadWriteOnce 볼륨을 쓰는 애플리케이션은 옛 Pod가 볼륨을 반납하기 전에는 새 Pod가 attach하지 못해 교대가 교착된다. 두 버전이 호환되지 않는 데이터 포맷을 쓰는 경우, 외부 자원에 단독으로 접근해야 하는 singleton 프로세스도 마찬가지다. 이때는 strategy.type: Recreate로 바꾼다. 옛 Pod를 전부 종료한 뒤에 새 Pod를 기동하므로 그 사이의 downtime을 수용하는 선택이다. 상태를 가진 워크로드라면 애초에 Deployment가 맞는 자리인지, StatefulSet으로 가야 하는지부터 따져 본다.
정리
- ReplicaSet은 ownerReferences로 자기 Pod를 식별하며 수를 유지하고, Deployment는 pod-template-hash로 구분되는 ReplicaSet들을 관리한다.
- 롤링 업데이트는 maxSurge/maxUnavailable 범위 안에서 새 ReplicaSet을 늘리고 옛것을 줄이는 교대이며, 진행 조건은 새 Pod의 readiness다.
- 교대가 멈추면 Progressing condition이 바뀔 뿐 자동 롤백은 없다.
rollout undo는 옛 template로 새 revision을 만드는 동작이다. - 두 버전이 공존 불가능하면 Recreate 전략으로 downtime을 받아들인다.