여기서는 쿠버네티스의 레플리카셋(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.selectormatchLabels.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만 가지고도 모든 것을 할 수 있는 것이 아닌가? 라고 생각할 수 있는데, 그러면 다른 컨트롤러들이 도대체 어떤 일을 하며 레플리카셋과는 어떻게 다른 것일까?