kubernetes: ReplicaSet
여기서는 쿠버네티스의 레플리카셋(ReplicaSet)에 대해서 이야기해 보자. 아 그리고, ReplicationController
는 안 봐도 된다. 이제 없어질 것이기도 하고, replicaset이 더 나은 최신 버전이고 개념이 비슷하므로 그냥 replicaset
만 보자…
Kubernetes의 최소 배포 단위
우리가 거대한 쿠버네티스 팜을 가지고 있다고 생각해 보자. 어떤 어플리케이션을 간단히 배포하기 위해 팟 한 개를 배포한 경우 쿠버네티스 팜 전체에 팟이 하나 생성된다. 이중화를 위해 다른 하나를 더 생성한 경우 전체 팜에 어플리케이션이 두 개 동작할 것이다. 이렇게 반 년 정도 둔 경우, 아직도 팟 두 개가 이 쿠버네티스 팜에서 돌아간다고 확신할 수 있는가?
레플리카셋은 팟의 복제 단위이다. 그리고 우리는 레플리카셋을 이용해서 쿠버네티스 팜에 우리가 원하는 수의 팟이 언제나 실행됨을 보장할 수 있다.
템플릿으로 보는 ReplicaSet의 동작
레플리카셋은 개념이 매우 간단하다. 팟을 쿠버네티스 팜에 replicas
만큼 유지하겠다는 뜻이다. 따라서 yaml 템플릿 또한 매우 간단하다. 리소스의 spec에 replicas
의 수를 기록하면 끝나며, 내용은 팟과 같다. 예를 들어 공식 홈페이지에 나온 예를 들어 보면, 아래와 같이 YAML을 통해 레플리카셋을 생성할 수 있다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
# modify replicas according to your case
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
labels:
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google_samples/gb-frontend:v3
spec.replicas
위 템플릿의 spec.replicas
를 보면 3으로 되어 있으므로, 팟은 이 팜에서 항상 3개가 유지될 것이다.
$ kubectl get replicaset
NAME DESIRED CURRENT READY AGE
frontend 3 3 3 6s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-9si5l 1/1 Running 0 1m
frontend-dnjpy 1/1 Running 0 1m
frontend-qhloh 1/1 Running 0 1m
왜 3개를 유지하는가? 쿠버네티스의 컨트롤러가 하는 일은, 선언된 조건을 맞추기 위해 최선을 다한다. 그래서 쿠버네티스 위에서의 리소스들은 선언적이다. 3이라고 기록했다면 팜의 총량에서 3을 유지하도록 최선을 다한다. 레플리카셋의 팟 하나가 문제가 생겨 정상 동작하지 않는 경우 쿠버네티스는 3개를 유지하기 위해 새로운 팟을 생성한다. 그래서 다시 총량이 3이 된다.
selector
그러면 어떻게 선언적인 리소스를 유지할 수 있을까? 무슨 팟이 실행중인지를 어떻게 알고 이를 유지한다는 것일까. 내가 그냥 같은 이름으로 팟을 만들면 그건 같이 카운트가 되는 건가? 뭐 이런 궁금증이 들 수가 있는데, 쿠버네티스에서는 선택자(selector)
가 이런 역할을 한다.
선택자(selector)는 이름 그대로 선택하는 역할을 한다. 어떤 것을 선택하는가 하면 Pod의 Label을 선택한다. 레플리카셋이 어떻게 팟의 수를 카운트하고 유지시키는가? 바로 팟의 Label을 선택하여 그 수를 세기 때문이다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-replicaset
labels:
app: nginx
purpose: replicaset-test
spec:
replicas: 3
selector:
matchLabels:
purpose: replicaset-test
template:
metadata:
labels:
purpose: replicaset-test
spec:
containers:
- name: nginx
image: nginx
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-replicaset-j49hc 0/1 ContainerCreating 0 4s
nginx-replicaset-m4hjt 0/1 ContainerCreating 0 4s
nginx-replicaset-zs8tz 0/1 ContainerCreating 0 4s
위 템플릿의 spec.selector
는 matchLabels.purpose == replicaset-test
를 선택하도록 되어 있다. 이 purpose는 어디서 가져오는가? spec.template.metadata
에서 가져오는 것이다. 따라서 정확히 말한다면 레플리카셋의 하위 컴포넌트를 계산하여 숫자를 맞추는 것이 아니라, label만을 주기적으로 세어 이 숫자대로 맞춘다는 것.
그렇다면 여기서 spec.replicas
의 수를 6으로 변경한다면?
$ kubectl apply -f test.yaml
replicaset.apps/nginx-replicaset configured
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-replicaset-m4hjt 1/1 Running 0 4m7s
nginx-replicaset-j49hc 1/1 Running 0 4m7s
nginx-replicaset-zs8tz 1/1 Running 0 4m7s
nginx-replicaset-flgbk 0/1 ContainerCreating 0 7s
nginx-replicaset-s5twd 0/1 ContainerCreating 0 7s
nginx-replicaset-bx8s5 0/1 ContainerCreating 0 7s
6이라는 수를 지키기 위해 바로 3개의 팟이 추가적으로 뜨는 것을 확인할 수 있다.
label을 이리저리 조작하는 경우
결국 replicaset
에서 라벨을 사용하여 그 수를 컨트롤한다는 것을 알겠는데, 그러면 그 라벨을 변경하면 어떻게 될까?. 우선 replicaset은 팟을 생성하도록 되어 있으므로, 팟을 한번 변경해 보자.
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-replicaset-m4hjt 1/1 Running 0 6m44s
nginx-replicaset-j49hc 1/1 Running 0 6m44s
nginx-replicaset-zs8tz 1/1 Running 0 6m44s
nginx-replicaset-s5twd 1/1 Running 0 2m44s
nginx-replicaset-flgbk 1/1 Running 0 2m44s
nginx-replicaset-bx8s5 1/1 Running 0 2m44s
$ kubectl get pod nginx-replicaset-m4hjt -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2020-06-20T09:54:48Z"
generateName: nginx-replicaset-
labels:
purpose: replicaset-test
managedFields:
...
...
이 label.purpose를 변경해 보자. kubectl edit
든, yaml
을 출력해서 저장하든, 컨테이너를 하나 선택해서 라벨을 변경해 보자. 변경한 라벨은 nginx-replicaset-m4hjt
이다.
$ kubectl edit pod nginx-replicaset-m4hjt
pod/nginx-replicaset-m4hjt edited
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-replicaset-j49hc 1/1 Running 0 7m31s
nginx-replicaset-zs8tz 1/1 Running 0 7m31s
nginx-replicaset-s5twd 1/1 Running 0 3m31s
nginx-replicaset-flgbk 1/1 Running 0 3m31s
nginx-replicaset-bx8s5 1/1 Running 0 3m31s
nginx-replicaset-m4hjt 1/1 Running 0 7m31s
nginx-replicaset-mztv6 0/1 ContainerCreating 0 3s
바로 하나가 추가됨을 알 수 있다. 선언된 것을 지키기 위해 최선을 다한다는 것을 생각해 보면 당연한 것이기도 하다. 어쨌든 nginx-replicaset-mztv6
이라는 팟이 새로 생성된다. 그럼 다시 nginx-replicaset-m4hjt
를 원래 라벨로 변경하게 되면?
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-replicaset-j49hc 1/1 Running 0 10m
nginx-replicaset-zs8tz 1/1 Running 0 10m
nginx-replicaset-s5twd 1/1 Running 0 6m40s
nginx-replicaset-flgbk 1/1 Running 0 6m40s
nginx-replicaset-bx8s5 1/1 Running 0 6m40s
nginx-replicaset-m4hjt 1/1 Running 0 10m
nginx-replicaset-mztv6 0/1 Terminating 0 3m12s
다시 하나가 삭제됨을 알 수 있다. 여기서 특이한 것은 가장 나중에 생성된 nginx-replicaset-mztv6
팟이 삭제되는데, 이것에 대해서는 추후에 설명할 일이 있을 것 같다..
이번에는 팟을 한번 생성해 보자.
apiVersion: v1
kind: Pod
metadata:
generateName: nginx-pod
labels:
app: nginx
purpose: replicaset-pod
spec:
containers:
- name: nginx
image: nginx
여러 팟을 생성할 것이므로 generateName
을 통해 랜덤한 이름을 만들도록 했다. 생성하면 당연히 다음과 같이 나타날 것이다.
$ kubectl create -f pod.yaml
pod/nginx-podrf5jj created
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-podrf5jj 0/1 ContainerCreating 0 6s
5개의 팟을 실행해 보자.
$ kubectl create -f pod.yaml
pod/nginx-podrcv5g created
$ kubectl create -f pod.yaml
pod/nginx-pod6hzn6 created
$ kubectl create -f pod.yaml
pod/nginx-podw6ddq created
$ kubectl create -f pod.yaml
pod/nginx-pod92xxh created
$ kubectl create -f pod.yaml
pod/nginx-podrhwtr created
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-podrf5jj 1/1 Running 0 3m13s
nginx-podw6ddq 0/1 ContainerCreating 0 5s
nginx-pod92xxh 0/1 ContainerCreating 0 4s
nginx-podrhwtr 0/1 ContainerCreating 0 3s
nginx-podrcv5g 1/1 Running 0 7s
nginx-pod6hzn6 1/1 Running 0 6s
이러면 이제 서로 아무런 연관이 없는, 동일한 역할을 하는 팟이 생성되었다. 라벨은 purpose=replicaset-pod
인데, 이를 레플리카셋으로 묶을 수 있을까?
우선 아까의 레플리카셋 템플릿을 사용해서 셀렉터만 살짝 바꾼 후 만들어보면,
$ cat grouping-pod.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-replicaset
labels:
app: nginx
purpose: replicaset-test
spec:
replicas: 6
selector:
matchLabels:
purpose: replicaset-pod
template:
metadata:
labels:
purpose: replicaset-pod
spec:
containers:
- name: nginx
image: nginx
$ kubectl create -f grouping-pod.yaml
replicaset.apps/nginx-replicaset created
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-pod92xxh 1/1 Running 0 4m18s
nginx-podrhwtr 1/1 Running 0 4m17s
nginx-podrf5jj 1/1 Running 0 7m27s
nginx-podrcv5g 1/1 Running 0 4m21s
nginx-pod6hzn6 1/1 Running 0 4m20s
nginx-podw6ddq 1/1 Running 0 4m19s
...
...
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-pod92xxh 1/1 Running 0 5m33s
nginx-podrhwtr 1/1 Running 0 5m32s
nginx-podrf5jj 1/1 Running 0 8m42s
nginx-podrcv5g 1/1 Running 0 5m36s
nginx-pod6hzn6 1/1 Running 0 5m35s
nginx-podw6ddq 1/1 Running 0 5m34s
앗, 변화가 없다! 생각해 보면 숫자만 지키면 되기 때문에 컨트롤러가 생성된 순간 숫자를 맞추기 위해 노력할 것이고, 이미 목표가 달성된 상태이므로 당연히 새로운 팟을 만들지 않을 것 같다. 이걸 보면 깨달을 수 있는 것이, spec.replicas
에 값을 명시한다고 해서 그 팟을 바로 생성하는 것이 아니라는 것이다. 컨트롤러가 생성된 후 그 spec.replicas
를 달성하기 위해 노력을 한다 뭐 이런 것이 아닐까 싶다.
아니 그러면 팟은 그대로 두고, 반대로 replicaset만 지울 수도 있나?
$ kubectl delete rs nginx-replicaset --cascade=false
replicaset.apps "nginx-replicaset" deleted
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-podw6ddq 1/1 Running 0 9m31s
nginx-podrcv5g 1/1 Running 0 9m33s
nginx-pod6hzn6 1/1 Running 0 9m32s
nginx-pod92xxh 1/1 Running 0 9m30s
nginx-podrhwtr 1/1 Running 0 9m29s
nginx-podrf5jj 1/1 Running 0 12m
...
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-podw6ddq 1/1 Running 0 10m
nginx-podrcv5g 1/1 Running 0 10m
nginx-pod6hzn6 1/1 Running 0 10m
nginx-pod92xxh 1/1 Running 0 10m
nginx-podrhwtr 1/1 Running 0 10m
nginx-podrf5jj 1/1 Running 0 13m
$ kubectl get rs
No resources found in playground namespace.
물론 가능하다. --cascade=false
를 사용하면 된다.
그냥 지운다면 다음과 같이 모든 팟이 삭제될 것이다.
$ kubectl create -f grouping-pod.yaml
replicaset.apps/nginx-replicaset created
$ kubectl delete -f grouping-pod.yaml
replicaset.apps "nginx-replicaset" deleted
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-pod6hzn6 0/1 Terminating 0 28m
nginx-pod92xxh 0/1 Terminating 0 28m
nginx-podw6ddq 0/1 Terminating 0 28m
nginx-podrhwtr 0/1 Terminating 0 28m
nginx-podrcv5g 0/1 Terminating 0 28m
nginx-podrf5jj 0/1 Terminating 0 31m
팟과 레플리카셋
결국 팟이 컨테이너의 그룹화를 이룬다면, 레플리카셋은 팟의 그룹화를 이룬다는 것이다. 컨테이너는 어플리케이션 단위라고 하면 팟은 작업 단위라고 볼 수 있으므로 결국 레플리카셋은 배포 단위라고 볼 수 있겠다. 이 배포 단위를 항상 달성할 수 있도록 라벨을 사용해서 자원을 카운트하며, 팟의 수를 조절하므로 가용성을 챙길 수 있고, 장애 발생 시 그 수를 최대한 유지하려는 정책 덕분에 이런 면을 self-healing
과 같은 개념으로 자주 설명한다.
그러면… 이렇게 하면 다 끝난거 아닌가? 배포도 되고 셀프 힐링도 달성이 되면, replicaset만 가지고도 모든 것을 할 수 있는 것이 아닌가? 라고 생각할 수 있는데, 그러면 다른 컨트롤러들이 도대체 어떤 일을 하며 레플리카셋과는 어떻게 다른 것일까?