Search

statefulset-vs-deployment

쿠버네티스 문서의 스테이트풀셋 기본 실습을 따라해본다. 추가로, 기본적으로 스테이트리스의 파드셋을 관리하는, 디플로이먼트와 비교해본다. 하지만 스테이트풀셋과 디플로이먼트가 완전한 대척점에 있다고 보긴 어려워, 설명은 스테이트풀셋 위주로 할 것이다({스테이트풀셋의 기능} - {디플로이먼트의 기능}을 설명하게 된다).
스테이트풀셋은 공부하게 된 계기는 DOIK(Database Operator in Kubernetes)라는 스터디에 참여했기 때문이다. 스터디는 우연한 계기로 알게 됐는데, 스테이트풀셋이 실제 제품에서 사용 가능할까? 라는 의문이 있었기 때문에 참여했다. 스터디 이름에서 알 수 있듯, 실제론 오퍼레이터라는 설계 패턴으로 구현하는 것 같다. 하지만 DB를 컨테이너로 운영하는 기본은 스테이트풀셋이라 1주차엔 이것에 대한 스터디를 했다.
문서에서 말하듯 스테이트풀셋, 헤드리스 서비스, 영구 볼륨(요청), 동적 볼륨 프로비저닝의 개념을 알고 있어야 한다. 내 기준으론, 기본적인 쿠버네티스의 스테이트리스 파드셋 관리, 즉 디플로이먼트에 익숙한 사람이면 따라하기 쉬울 것 같다.
그 기준에서 조금 설명이 필요한 것은 동적 볼륨 프로비저닝인데, 실습에서 사용할 rancher/local-path-provisioner를 이야기한다. 그리고 스테이트풀과 비슷한 디플로이먼트와의 비교 실습은 다음 순서로 진행된다. 실습 환경은 이 레포이다:
네트워크 ID와 저장소
스케일링
업데이트
삭제
파드 관리 정책

rancher/local-path-provisioner

rancher/local-path-provisioner는 파드가 실행 중인 노드(호스트)에 동적 볼륨 프로비저닝을 하는 간단한 구현체이다. 쿠버네티스에서 동적 볼륨 프로비저닝은 저장소 클래스(StorageClass)를 생성하고 동적으로 영구 볼륨 요청(PVC)하여 영구 볼륨(PV)을 할당 받는 식이다. 따라서 rancher/local-path-provisioner는 저장소 클래스와 이 클래스로 생성하는 요청 시 영구 볼륨을 만드는 프로비저너로 구성돼 있다.
그 외 RBAC, 네임스페이스 등 추가적인 자원을 포함해 설치 파일은 한개이다(v0.22는 현재 stable 버전):
$ k apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.22/deploy/local-path-storage.yaml
Shell
복사
동적 볼륨 프로비저닝이 어떻게 동작하는지 보기 위해 RBAC 관련 그리고 네임스페이스는 제외하고 StorageClass, Deployment, ConfigMap만 살펴보자:
$ wget -qO- https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml | yq ea 'select(.kind=="ConfigMap" or .kind=="Deployment" or .kind=="StorageClass") | split_doc | [.] | sort_by(.kind) | reverse'- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-path provisioner: rancher.io/local-path volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete- apiVersion: apps/v1 kind: Deployment metadata: name: local-path-provisioner namespace: local-path-storage spec: replicas: 1 selector: matchLabels: app: local-path-provisioner template: metadata: labels: app: local-path-provisioner spec: serviceAccountName: local-path-provisioner-service-account containers: - name: local-path-provisioner image: rancher/local-path-provisioner:master-head imagePullPolicy: IfNotPresent command: - local-path-provisioner - --debug - start - --config - /etc/config/config.json volumeMounts: - name: config-volume mountPath: /etc/config/ env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace volumes: - name: config-volume configMap: name: local-path-config- kind: ConfigMap apiVersion: v1 metadata: name: local-path-config namespace: local-path-storage data: config.json: |- { "nodePathMap":[ { "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES", "paths":["/opt/local-path-provisioner"] } ] } setup: |- #!/bin/sh set -eu mkdir -m 0777 -p "$VOL_DIR" teardown: |- #!/bin/sh set -eu rm -rf "$VOL_DIR" helperPod.yaml: |- apiVersion: v1 kind: Pod metadata: name: helper-pod spec: containers: - name: helper-pod image: busybox imagePullPolicy: IfNotPresent
Shell
복사
yq 필터 설명 - select - 원하는 리소스 종류(kind)만 선택해 - split_doc | [.] - 각 파일의 YAML을 한 배열로 합침(eval-all(ea)와 같이 써야 함) - sort_by(.kind) | reverse - kind에 대해 역으로 정렬하면 설명하고 싶은 순서로 나온다.
1.
StorageClass - local-path
이어서 설명하는 디플로이먼트를 프로비저너로 쓴다. 영세한(?) 프로비저너인지 외부 라이브러리 목록에서 찾아볼 순 없다(실습을 하는데엔 오작동 없이 충분하다).
1.
Deployment - local-path-provisioner
프로비저너. 상세한 동작은 컨피그맵에 쓰여 있다.
1.
ConfigMap - local-path-config
config.json - hostPath를 백엔드로, 즉 노드 경로에 볼륨을 만드는데 상위 디렉토리는 /opt/local-path-provisioner 이다.
setup - hostPath에 디렉토리 경로를 만들어 볼륨을 프로비저닝하고
teardown - PVC가 삭제되면 만든 디렉토리를 삭제한다.
helperPod.yaml - busybox를 써서 프로비저닝(mkdir, rm -rf)을 한다.
실제 사용 예시는 스테이트풀셋을 만들어 보며 볼 수 있다.
설치가 잘 됐으면, 저장소 클래스가 추가됐을 것이다:
$ k apply -f local-path.yamlnamespace/local-path-storage createdserviceaccount/local-path-provisioner-service-account createdclusterrole.rbac.authorization.k8s.io/local-path-provisioner-role createdclusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind createddeployment.apps/local-path-provisioner createdstorageclass.storage.k8s.io/local-path createdconfigmap/local-path-config created$ k get scNAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGElocal-path rancher.io/local-path Delete WaitForFirstConsumer false 5s
Shell
복사
이를 기본으로 사용하기 위해 어노테이션을 추가하자:
$ k patch sc local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'storageclass.storage.k8s.io/local-path patched$ k get scNAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGElocal-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 21s
Shell
복사

파일 준비

스테이프풀셋과 비교할 디플로이먼트 정의 파일을 준비한다:
# web-sts.yamlapiVersion: v1kind: Servicemetadata: name: nginx-sts labels: app: nginx-stsspec: ports: - port: 80 name: web clusterIP: None selector: app: nginx-sts---apiVersion: apps/v1kind: StatefulSetmetadata: name: web-stsspec: serviceName: nginx-sts replicas: 2 selector: matchLabels: app: nginx-sts template: metadata: labels: app: nginx-sts spec: containers: - name: nginx image: k8s.gcr.io/nginx-slim:0.8 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi
YAML
복사
문서의 예제 파일을 참고해 이름과 레이블 -sts 접미사를 붙였다. 디플로이먼트와 구분하기 위함인데, PVC가 네임스페이스 리소스라 네임스페이스로 격리하는건 적절하지 않다고 생각했다. spec.volumeClaimTemplates에 storageClassName을 써주지 않으면 default인 local-path를 사용하게 될 것이다.
# web-dep.yamlapiVersion: v1kind: Servicemetadata: name: nginx-dep labels: app: nginx-depspec: ports: - port: 80 name: web clusterIP: None selector: app: nginx-dep---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: www-web-dep labels: app: nginx-depspec: storageClassName: local-path accessModes: - ReadWriteOnce resources: requests: storage: 1Gi---apiVersion: apps/v1kind: Deploymentmetadata: name: web-depspec: replicas: 2 selector: matchLabels: app: nginx-dep template: metadata: labels: app: nginx-dep spec: containers: - name: nginx image: k8s.gcr.io/nginx-slim:0.8 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumes: - name: www persistentVolumeClaim: claimName: www-web-dep
YAML
복사
스테이트풀셋과 비교할 디플로이먼트다. 스테이트풀셋과 마찬가지로 -dep 접미사로 객체 이름이나 레이블을 구분했다. 디플로이먼트는, 어차피 파드마다 네트워크를 식별할 필요가 없어, 헤드리스 서비스가 필수는 아니지만 비교를 위해 똑같이 만들었다.
디플로이먼트는 volumeClaimTemplates 즉, PVC를 템플릿 할 순 없다. 따라서 PVC를 명시적으로 한개만 만들었다(스테이트풀셋과 좋은 비교점이 되는진 모르겠다).

네트워크 ID와 저장소 확인

스테이트풀셋과 디플로이먼트의 큰 차이점은 파드 생성, 삭제에 순번을 주어 네트워크를 식별하고 저장소를 일정하게 붙인다. 파드의 생명주기는 자체는 동적이라 이 자원들을 고정적(static)이라 표현하지 않고 안정적(stable)이라고 한다.
실습을 하며 파드 생성, 삭제 순서를 보기 위해 다음 모니터 명령을 별도 터미널에 실행해 놓자:
$ k get pods -l app=nginx-sts # 스테이트풀셋$ k get pods -l app=nginx-dep # 디플로이먼트
Shell
복사
먼저 스테이트풀셋을 실행하면 파드 이름이 ’<스테이트풀셋>-<순번>’으로 실행된다:
$ k apply -f web-sts.yaml# 모니터web-sts-0 0/1 Pending 0 0sweb-sts-0 0/1 Pending 0 4sweb-sts-0 0/1 ContainerCreating 0 4sweb-sts-0 0/1 ContainerCreating 0 5sweb-sts-0 1/1 Running 0 5sweb-sts-1 0/1 Pending 0 0sweb-sts-1 0/1 Pending 0 5sweb-sts-1 0/1 ContainerCreating 0 5sweb-sts-1 0/1 ContainerCreating 0 6sweb-sts-1 1/1 Running 0 7s
Shell
복사
디플로이먼트는 파드 이름이 ’<디플로이먼트>-<디플로이먼트_다이제스트>-<파드_다이제스트>’로 식별된다:
$ k apply -f web-dep.yaml#모니터web-dep-74f5674789-2b7px 0/1 Pending 0 0sweb-dep-74f5674789-hlvgd 0/1 Pending 0 0sweb-dep-74f5674789-hlvgd 0/1 Pending 0 0sweb-dep-74f5674789-hlvgd 0/1 Pending 0 4sweb-dep-74f5674789-hlvgd 0/1 ContainerCreating 0 4sweb-dep-74f5674789-hlvgd 0/1 ContainerCreating 0 5sweb-dep-74f5674789-2b7px 0/1 Pending 0 5sweb-dep-74f5674789-2b7px 0/1 ContainerCreating 0 5sweb-dep-74f5674789-2b7px 0/1 ContainerCreating 0 6sweb-dep-74f5674789-hlvgd 1/1 Running 0 6sweb-dep-74f5674789-2b7px 1/1 Running 0 7s
Shell
복사
스테이트풀셋의 파드 생성은 상태가 Pending -> ContainerCreating -> Running(Ready)를 거친 후 다음 순서 파드를 생성하지만, 디플로이먼트는 다른 파드의 생성이나 순서 영향 없이 병렬로 동시에 생성한다.
스테이트풀셋에서 호스트 이름을 네트워크 ID로 쓰게 된다. 각 파드의 호스트 이름을 확인하고 DNS 질의 해보자:
$ for i in 0 1; do kubectl exec "web-sts-$i" -- sh -c 'hostname'; doneweb-sts-0web-sts-1$ k run --image busybox:1.28 dns-test --restart=Never --rm -it -- nslook^up web-sts-0.nginx-stsServer: 10.96.0.10Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localName: web-sts-0.nginx-stsAddress 1: 172.16.25.222 web-sts-0.nginx-sts.default.svc.cluster.localpod "dns-test" deleted$ k run --image busybox:1.28 dns-test --restart=Never --rm -it -- nslookup web-sts-1.nginx-stsServer: 10.96.0.10Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localName: web-sts-1.nginx-stsAddress 1: 172.16.40.87 web-sts-1.nginx-sts.default.svc.cluster.localpod "dns-test" deleted
Shell
복사
파드 이름의 DNS 레코드가 등록되어 있다. 하지만 디플로이먼트에선 몇번째 파드이느냐가 중요하지 않기 때문에 그러한 DNS 레코드는 등록하지 않는다:
$ k get pods -l app=nginx-dep --no-headers | awk '{print $1}' | xargs -I{} kubectl exec {} -- sh -c 'hostname'web-dep-74f5674789-2b7pxweb-dep-74f5674789-hlvgd$ k run --image busybox:1.28 dns-test --restart=Never --rm -it -- nslookup web-dep-74f5674789-2b7px.nginx-depServer: 10.96.0.10Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localnslookup: can't resolve 'web-dep-74f5674789-2b7px.nginx-dep'pod "dns-test" deletedpod default/dns-test terminated (Error)$ k run --image busybox:1.28 dns-test --restart=Never --rm -it -- nslookup web-dep-74f5674789-hlvgd.nginx-depServer: 10.96.0.10Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localnslookup: can't resolve 'web-dep-74f5674789-hlvgd.nginx-dep'pod "dns-test" deletedpod default/dns-test terminated (Error)
Shell
복사
대신 디플로이먼트의 (일반)파드는 자신의 IP를 dasherize한 레코드가 등록된다. 스테이트풀셋의 것은 그렇지 않다:
$ k get po -l app=nginx-dep -owideNAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATESweb-dep-74f5674789-2b7px 1/1 Running 0 12m 172.16.40.90 cluster1-worker1 <none> <none>web-dep-74f5674789-hlvgd 1/1 Running 0 12m 172.16.40.89 cluster1-worker1 <none> <none>$ k run --image busybox:1.28 dns-test --restart=Never --rm -it -- nslookup 172-16-40-90.nginx-depServer: 10.96.0.10Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localName: 172-16-40-90.nginx-depAddress 1: 172.16.40.90 172-16-40-90.nginx-dep.default.svc.cluster.localpod "dns-test" deleted$ k get po -l app=nginx-sts -owideNAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATESweb-sts-0 1/1 Running 0 14m 172.16.25.222 cluster1-worker2 <none> <none>web-sts-1 1/1 Running 0 14m 172.16.40.87 cluster1-worker1 <none> <none>$ k run --image busybox:1.28 dns-test --restart=Never --rm -it -- nslookup 172-16-25-222.nginx-depServer: 10.96.0.10Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localnslookup: can't resolve '172-16-25-222.nginx-dep'pod "dns-test" deletedpod default/dns-test terminated (Error)
Shell
복사
다음으로 스테이트풀셋의 동적으로 생성된 PVC와 디플로이먼트에서 선언한 것을 보자:
$ k get pvcNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEwww-web-dep Bound pvc-6758d381-cfcd-47b5-ac73-481123ee3a43 1Gi RWO local-path 23mwww-web-sts-0 Bound pvc-dc2a9ddd-8bdb-4064-8634-828e0479bbac 1Gi RWO local-path 25mwww-web-sts-1 Bound pvc-bb7b15ed-817f-42f0-af0f-375436f3ce2d 1Gi RWO local-path 25m
Shell
복사
스테이트풀셋의 파드는 순번 접미사로 구분되어 각각 PVC가 생겼지만, 디플로이먼트는 그렇지 않다. 파드가 2개임에도 하나의 PVC만 있는데 이는 위에서 그렇게 정의했기 때문에 그렇다.
파드 IP 확인하기 위해 본 파드 목록 wide 출력을 보면 디플로이먼트 파드는 한 노드에 몰려 있는 것을 볼 수 있다. 이따 디플로이먼트를 스케일 아웃하면 더 확실히 보일 것이다. PVC가 노드 종속적이기 때문이다(hostPath). 볼륨이 없는 노드엔 파드를 늘릴 수 없을 것이고, 같은 노드에 다른 컴퓨팅 리소스가 부족해도 파드를 늘릴 수 없을 것이다. 따라서 이런 상태(여기선 볼륨)가 있는 앱은 디플로이먼트가 적합하지 않다는 것을 알 수 있다(물론 하나의 PVC를 여러 디플로이먼트 파드가 쓰는 예제가 적절치 못한것도 있다).
앞서 설명한 local-path-provisioner가 hostPath로 볼륨을 프로비저닝하는 것을 확인해보자. 각 worker 노드에서 /opt/local-path-provisioner로 이동하면 파드의 PV를 볼 수 있다:
# cluster1-worker1$ ll /opt/local-path-provisioner/total 16drwxr-xr-x 4 root root 4096 May 26 06:57 ./drwxr-xr-x 5 root root 4096 May 26 01:30 ../drwxrwxrwx 2 root root 4096 May 26 06:57 pvc-6758d381-cfcd-47b5-ac73-481123ee3a43_default_www-web-dep/drwxrwxrwx 2 root root 4096 May 26 06:56 pvc-bb7b15ed-817f-42f0-af0f-375436f3ce2d_default_www-web-sts-1/# cluster1-worker2$ ll /opt/local-path-provisioner/total 12drwxr-xr-x 3 root root 4096 May 26 06:56 ./drwxr-xr-x 5 root root 4096 May 26 01:29 ../drwxrwxrwx 2 root root 4096 May 26 06:56 pvc-dc2a9ddd-8bdb-4064-8634-828e0479bbac_default_www-web-sts-0/
Shell
복사
스테이트풀셋이 안정적인 상태(PV)를 유지하는지 확인해보자. 각 파드 PV에 호스트 이름을 Nginx가 서빙할 수 있는 파일에 써 상태를 주입한다:
$ for i in 0 1; do k exec "web-sts-$i" -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'; done$ for i in 0 1; do k exec -it "web-sts-$i" -- curl localhost; doneweb-sts-0web-sts-1
Shell
복사
파드를 모두 삭제하고 재시작하길 기다렸다가 다시 요청하면, 같은 이름의 파드에 PV가 연결된 것을 확인할 수 있다:
$ k delete pod -l app=nginx-stspod "web-sts-0" deletedpod "web-sts-1" deleted$ for i in 0 1; do kubectl exec -i -t "web-sts-$i" -- curl http://localhost/; doneweb-sts-0web-sts-1
Shell
복사
디플로이먼트는 그렇지 않다:
$ k get pods -l app=nginx-dep --no-headers | awk '{print $1}' | xargs -I{} kubectl exec {} -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'$ k get pods -l app=nginx-dep --no-headers | awk '{print $1}' | xargs -I{} kubectl exec {} -- curl -s localhostweb-dep-74f5674789-hlvgdweb-dep-74f5674789-hlvgd$ k delete po -l app=nginx-deppod "web-dep-74f5674789-2b7px" deletedpod "web-dep-74f5674789-hlvgd" deleted$ k get pods -l app=nginx-dep --no-headers | awk '{print $1}' | xargs -I{} kubectl exec {} -- curl -s localhostweb-dep-74f5674789-hlvgdweb-dep-74f5674789-hlvgd
Shell
복사
하나의 PV를 쓰기 때문에 마지막에 실행됐을 파드의 이름(eb-dep-74f5674789-hlvgd)이 덮어 씌워졌고, 삭제 후 새로 생성된 파드 이름과 무관하게 이전에 PV에 쓰인 값이 서빙된다.

스케일링

스테이프풀셋은 파드 스케일링할 때도 순서를 따른다:
$ k scale sts web-sts --replicas=5statefulset.apps/web-sts scaled# 모니터web-sts-2 0/1 Pending 0 0sweb-sts-2 0/1 Pending 0 5sweb-sts-2 0/1 ContainerCreating 0 5sweb-sts-2 0/1 ContainerCreating 0 6sweb-sts-2 1/1 Running 0 7sweb-sts-3 0/1 Pending 0 0sweb-sts-3 0/1 Pending 0 5sweb-sts-3 0/1 ContainerCreating 0 5sweb-sts-3 0/1 ContainerCreating 0 5sweb-sts-3 1/1 Running 0 6sweb-sts-4 0/1 Pending 0 0sweb-sts-4 0/1 Pending 0 5sweb-sts-4 0/1 ContainerCreating 0 5sweb-sts-4 0/1 ContainerCreating 0 5sweb-sts-4 1/1 Running 0 6s$ k scale sts web-sts --replicas=3statefulset.apps/web-sts scaled# 모니터web-sts-4 1/1 Terminating 0 29sweb-sts-4 1/1 Terminating 0 29sweb-sts-4 0/1 Terminating 0 30sweb-sts-4 0/1 Terminating 0 30sweb-sts-4 0/1 Terminating 0 30sweb-sts-3 1/1 Terminating 0 36sweb-sts-3 1/1 Terminating 0 36sweb-sts-3 0/1 Terminating 0 37sweb-sts-3 0/1 Terminating 0 37sweb-sts-3 0/1 Terminating 0 37s
Shell
복사
스케일 아웃 시 순번이 순증하며 파드를 생성하고, 스케일 인 시엔 뒤에서부터 역순으로 파드를 제거한다(이 문서를 포함해 쿠버네티스 문서 전체에 스케일 업/다운이란 표현을 쓴다. 하지만 이것은 보통 스케일 아웃/인에 해당하는 동작을 설명하고 있어서 후자로 전부 대체한다). 현재 파드가 실행 중이 아니더라도 PV와 PVC는 남아 있게 된다(web-sts-3, web-sts-4):
$ k get pvc -l app=nginx-stsNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEwww-web-sts-0 Bound pvc-dc2a9ddd-8bdb-4064-8634-828e0479bbac 1Gi RWO local-path 101mwww-web-sts-1 Bound pvc-bb7b15ed-817f-42f0-af0f-375436f3ce2d 1Gi RWO local-path 101mwww-web-sts-2 Bound pvc-c3b54572-90e8-4947-bfd1-28e340505759 1Gi RWO local-path 3m51swww-web-sts-3 Bound pvc-d7011017-f3fc-4778-99db-d6e023f5cb57 1Gi RWO local-path 3m44swww-web-sts-4 Bound pvc-28952b79-94d9-4066-95de-b14102b822b1 1Gi RWO local-path 3m38s$ k get pv | grep stspvc-28952b79-94d9-4066-95de-b14102b822b1 1Gi RWO Delete Bound default/www-web-sts-4 local-path 2m52spvc-bb7b15ed-817f-42f0-af0f-375436f3ce2d 1Gi RWO Delete Bound default/www-web-sts-1 local-path 100mpvc-c3b54572-90e8-4947-bfd1-28e340505759 1Gi RWO Delete Bound default/www-web-sts-2 local-path 3m5spvc-d7011017-f3fc-4778-99db-d6e023f5cb57 1Gi RWO Delete Bound default/www-web-sts-3 local-path 2m58spvc-dc2a9ddd-8bdb-4064-8634-828e0479bbac 1Gi RWO Delete Bound default/www-web-sts-0 local-path 100m
Shell
복사
디플로이먼트는, 생성할 때와 마찬가지로, 스케일링 시 동시에 병렬적으로 진행한다:
$ k scale deploy web-dep --replicas=5# 모니터web-dep-74f5674789-7p2q4 0/1 Pending 0 0sweb-dep-74f5674789-7p2q4 0/1 Pending 0 0sweb-dep-74f5674789-jvgqm 0/1 Pending 0 0sweb-dep-74f5674789-zrl8m 0/1 Pending 0 0sweb-dep-74f5674789-jvgqm 0/1 Pending 0 0sweb-dep-74f5674789-zrl8m 0/1 Pending 0 0sweb-dep-74f5674789-7p2q4 0/1 ContainerCreating 0 0sweb-dep-74f5674789-jvgqm 0/1 ContainerCreating 0 0sweb-dep-74f5674789-zrl8m 0/1 ContainerCreating 0 0sweb-dep-74f5674789-7p2q4 0/1 ContainerCreating 0 1sweb-dep-74f5674789-jvgqm 0/1 ContainerCreating 0 1sweb-dep-74f5674789-zrl8m 0/1 ContainerCreating 0 1sweb-dep-74f5674789-jvgqm 1/1 Running 0 2sweb-dep-74f5674789-7p2q4 1/1 Running 0 2sweb-dep-74f5674789-zrl8m 1/1 Running 0 2s$ k scale deploy web-dep --replicas=3# 모니터web-dep-74f5674789-jvgqm 1/1 Terminating 0 2m17sweb-dep-74f5674789-7p2q4 1/1 Terminating 0 2m17sweb-dep-74f5674789-jvgqm 1/1 Terminating 0 2m17sweb-dep-74f5674789-7p2q4 1/1 Terminating 0 2m17sweb-dep-74f5674789-7p2q4 0/1 Terminating 0 2m17sweb-dep-74f5674789-7p2q4 0/1 Terminating 0 2m17sweb-dep-74f5674789-7p2q4 0/1 Terminating 0 2m18sweb-dep-74f5674789-jvgqm 0/1 Terminating 0 2m18sweb-dep-74f5674789-jvgqm 0/1 Terminating 0 2m18sweb-dep-74f5674789-jvgqm 0/1 Terminating 0 2m18s
Shell
복사

업데이트

스테이트풀셋의 업데이트 전략은 기본으로 RollingUpdate이며 디플로이먼트와 같다. 다만 순서는 뒤에서부터 역순으로 된다(파드를 삭제하고 새로 띄우는 작업이니 스케일 인과 연관해 생각해보면 되겠다). 디플로이먼트와 달리 스테이트풀셋의 RollingUpdate는 partition이라는 인자로 제어할 수 있다. partition은 업데이트가 적용될 파드 순번 이상(greater than or equal)을 뜻한다.
partition의 기본 값은 0이다. 만약 컨테이너 이미지를 바꾸는 업데이트를 한다면 뒷순번 파드부터 전체에 대해 이미지를 교체할 것이다:
$ k get sts web-sts -oyaml | yq .spec.updateStrategyrollingUpdate: partition: 0type: RollingUpdate$ k set image sts web-sts nginx=gcr.io/google_containers/nginx-slim:0.8statefulset.apps/web-sts image updated# 모니터web-sts-2 1/1 Terminating 0 13mweb-sts-2 1/1 Terminating 0 13mweb-sts-2 0/1 Terminating 0 13mweb-sts-2 0/1 Terminating 0 13mweb-sts-2 0/1 Terminating 0 13mweb-sts-2 0/1 Pending 0 0sweb-sts-2 0/1 Pending 0 0sweb-sts-2 0/1 ContainerCreating 0 0sweb-sts-2 0/1 ContainerCreating 0 1sweb-sts-2 1/1 Running 0 1sweb-sts-1 1/1 Terminating 0 69mweb-sts-1 1/1 Terminating 0 69mweb-sts-1 0/1 Terminating 0 69mweb-sts-1 0/1 Terminating 0 69mweb-sts-1 0/1 Terminating 0 69mweb-sts-1 0/1 Pending 0 0sweb-sts-1 0/1 Pending 0 0sweb-sts-1 0/1 ContainerCreating 0 0sweb-sts-1 0/1 ContainerCreating 0 1sweb-sts-1 1/1 Running 0 1sweb-sts-0 1/1 Terminating 0 69mweb-sts-0 1/1 Terminating 0 69mweb-sts-0 0/1 Terminating 0 69mweb-sts-0 0/1 Terminating 0 69mweb-sts-0 0/1 Terminating 0 69mweb-sts-0 0/1 Pending 0 0sweb-sts-0 0/1 Pending 0 0sweb-sts-0 0/1 ContainerCreating 0 0sweb-sts-0 0/1 ContainerCreating 0 1sweb-sts-0 1/1 Running 0 1s
Shell
복사
partition을 3으로 하고 또 다른 이미지(k8s.gcr.io/nginx-slim:0.7)로 업데이트하면 아무 일도 일어나지 않는다. 파드가 모두 파티션 내에 있기 때문이다:
$ k patch sts web-sts -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}'statefulset.apps/web-sts patched$ k get sts web-sts -oyaml | yq .spec.updateStrategyrollingUpdate: partition: 3type: RollingUpdate$ k set image sts web-sts nginx=k8s.gcr.io/nginx-slim:0.7statefulset.apps/web-sts image updated# 모니터에 변화가 없다# 컨테이너 이미지 출력$ for p in 0 1 2; do k get pod "web-sts-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; donegcr.io/google_containers/nginx-slim:0.8gcr.io/google_containers/nginx-slim:0.8gcr.io/google_containers/nginx-slim:0.8
Shell
복사
partition을 2로하면 web-sts-2 파드만 아까 업데이트한 새 이미지로 변경된다. 이런 식의 카나리 업데이트를 할 수 있다(모니터 로그가 길기 때문에 파드마다 컨테이너 이미지 출력으로 대체):
$ k patch sts web-sts -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'statefulset.apps/web-sts patched$ k get sts web-sts -oyaml | yq .spec.updateStrategyrollingUpdate: partition: 2type: RollingUpdate$ for p in 0 1 2; do k get pod "web-sts-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; donegcr.io/google_containers/nginx-slim:0.8gcr.io/google_containers/nginx-slim:0.8k8s.gcr.io/nginx-slim:0.7
Shell
복사
web-sts-2 파드에서 테스트가 충분히 됐으면, partition을 0으로 만들어 전체 업데이트하면 된다:
$ k patch sts web-sts -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'statefulset.apps/web-sts patched$ for p in 0 1 2; do k get pod "web-sts-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; donek8s.gcr.io/nginx-slim:0.7k8s.gcr.io/nginx-slim:0.7k8s.gcr.io/nginx-slim:0.7
Shell
복사
디플로이먼트는 partition 옵션이 없다. 디플로이먼트에서 카나리 업데이트를 레이블을 활용해 구현할 수 있지만, 스테이트풀셋처럼 인자로 제어되는 것이 아니라 여기선 다루지 않는다.

삭제

문서에 쓰인 Non-cascading/Cascading은 스테이트풀셋만의 기능은 아닌, API delete의 옵션이다. cascade 옵션 지정 없이(기본 background) 스테이트풀셋을 지우면 앞서 스케일 인에서 본 듯, 뒤의 파드부터 순서대로 지우고 스테이트풀셋 리소스도 삭제한다.
cascade를 orphan으로 하면 스테이트풀셋 같은 파드셋 리소스만 지우고 매칭된 파드는 지우지 않는다. 이렇게 한다면 더 이상 스테이트풀셋이 없으니 파드셋 순서와 무관하게 파드 삭제가 가능하다.
cascade 옵션을 쓰면 스테이트풀셋이 파드를 관리하는 특징과 무관하게 삭제할 수 있지만, cascade가 스테이트풀셋만 사용 가능한 옵션은 아니니, 헷갈리지 않도록, 실습은 하지 않는다.

파드 관리 정책

기본으로 스테이트풀셋은 파드 순번을 지키며 관리하지만, 디플로이먼트처럼 순서와 무관하게 관리할 수도 있다.
spec.podManagementPolicy는 기본 값이 OrderedReady이지만, Parallel로 설정할 수 있다. 꼭 순서가 중요하지 않은 스테이트풀 앱을 위한 옵션이라고 한다:
$ k get sts web-sts -oyaml | yq .spec.podManagementPolicyOrderedReady# spec.podManagementPolicy 는 패치할 수 없으므로 web-sts.yaml을 수정하고 다시 적용$ k delete sts web-sts$ k apply -f web-sts.yaml# 모니터web-sts-0 0/1 Pending 0 0sweb-sts-1 0/1 Pending 0 0sweb-sts-0 0/1 Pending 0 0sweb-sts-1 0/1 Pending 0 0sweb-sts-0 0/1 ContainerCreating 0 0sweb-sts-1 0/1 ContainerCreating 0 0sweb-sts-0 0/1 ContainerCreating 0 0sweb-sts-1 0/1 ContainerCreating 0 1sweb-sts-0 1/1 Running 0 1sweb-sts-1 1/1 Running 0 2s
Shell
복사

정리

스테이트풀셋을 사용하려면 헤드리스 서비스와 동적 볼륨 프로비저닝 가능한 저장소 클래스가 필요하다.
스테이트풀셋은 파드의 순번으로 관리한다.
순번 무관하게 관리하는 옵션도 있다 - spec.podManagementPolicy: Parallel
스테이트풀셋의 파드는 순번 접미사로 부여된 호스트 이름으로 네트워크 ID와 저장소를 안정적으로(stable) 연결한다.
스테이트풀셋 업데이트 전략 중 RollingUpdate는 partition 인자를 쓰면 카나리 업데이트가 가능하다.

참고

https://kubernetes.io/docs/home/
https://gasidaseo.notion.site/e49b329c833143d4a3b9715d75b5078d (스터디 참고 내용은 private 링크라 지난 모집 공고 링크로 대체)