Kubernetes/05. 컨트롤러 종류 및 설명

[쿠버네티스] 쿠버네티스 스테이트풀셋 (StatefulSet) 소개 및 관리

Jaden Park 2021. 6. 25. 14:00

들어가며: 기존 컨트롤러의 문제점

스테이트풀셋(StatefulSet) 소개

  • 스테이트풀셋 이란?
  • 스테이트풀셋의 주의사항
  • 스테이트풀셋의 파드 이름 규칙
  • 스테이트풀셋의 파드 DNS 주소 규칙
  • 스테이트풀셋의 스토리지 볼륨
  • 스테이트풀셋의 스케일링

 

스테이트풀셋 관리

  • 스테이트풀셋 기본 살펴보기
  • 스테이트풀셋의 볼륨 클레임 템플릿

들어가며:

기존 컨트롤러라고 하면 레플리케이션 컨트롤러나 레플리카셋과 같은 본제본을 가지고 있는 컨트롤러를 의미합니다.

이후에서는 상징적으로 레플리카셋 컨트롤러로 대표해서 이야기 해보도록 하겠습니다.

 

레플리카셋은 파드 템플릿(replicaset.spec.template)에서 파드의 복제본을 생성합니다.

다음은 레플리카셋의 파드 템플릿의 예 입니다.

 

...
  template:
    metadata:
      labels:
        app: JadenPark
    spec:
      containers:
      - name: web-server
        image: nginx:alpine
        volumeMounts:
        - name: web-content
          mountPath: /usr/share/nginx/html
      volumes:
      - name: web-content
        hostPath:
          type: Directory
          path: /web_contents

레플리카셋이 파드 복제본을 생성할 때 파드의 이름 및 IP 주소만 다를 뿐 나머지는 모두 똑같은 파드를 생성하게 됩니다. 만약 파드가 PVC를 참조한다면, 이 역시 똑같은 PVC를 연결하게 됩니다. 해당 PVC는 특정한 하나의 PV에 연결되어 있습니다. 즉, 항상 똑같은 볼륨에 연결한다는 의미 입니다.

 

기존 컨트롤러에서 각 파드가 분리된 저장소 볼륨을 사용해야 한다면 어떻게 해야 할까요 ? (Ex. 데이터베이스 파드 및 스토리지)

 

 

 

1. 여러 파드를 각각 별도로 관리

모든 파드를 수동 생성 및 관리하는 방법이 있을 수 있습니다. 각각 파드를 다른 볼륨 및 PVC를 참조하는 파드를 생성하는 방법입니다.

파드 PVC PV
POD A PVC A PV A
POD B PVC B PV B
POD C PVC C PV C

그러나 노드나 파드의 문제 발생 시 모든 대응은 수동으로 해야합니다. 이 방법은 적절한 해결책이 될 수 없습니다.

 

 

2. 여러 컨트롤러 구성

첫 번째 방법은 문제를 해결하기 위해 각각 컨트롤러를 구성할 수 있습니다. 각각의 컨트롤러가 각각의 PVC를 사용하는 방식입니다.

RS POD PVC PV
RS A POD A PVC A PV A
RS B POD B PVC B PV B
RS C POD C PVC C PV C

각 레플리카셋의 복제본 개수를 1로 설정하고 하나의 파드만 운용할 수 있도록 구성할 수 있습니다. 각 파드는 각각의 PVC에 연결되어 있습니다.

 

이 방법을 이용하면 노드의 문제나 파드의 장애에 대비할 수는 있지만 각각의 레플리카셋은 별개의 리소스로 서로 연동되지 않는 레플리카셋을 운영해야 한다는 단점이 있습니다. 규모를 늘리려면 별도의 레플리카셋을 생성해야 합니다.

 

 

3. 하나의 컨트롤러와 볼륨 디렉토리 분리

두 번째 방법의 문제를 해결하기 위해 하나의 컨트롤러만 운영하고, 각 파드가 하나의 PVC를 사용하는 대신 별도의 디렉토리를 마운트하는 방법입니다.

 

RS POD PVC PV Dir
  POD A     /A
RS A POD B PVC A PV A /B
  POD C     /C

하나의 레플리카셋은 하나의 파드 템플릿을 사용하기 때문에 같은 볼륨을 연결합니다. 각 파드가 별도의 디렉토리에 각각의 데이터를 사용할 수 있도록 할 수 있습니다. 그러나 이는 일반적으로 가능하지 않을 뿐만 아니라, 가능하더라도 수동으로 관리해줘야 합니다.

 

 


스테이트풀셋 소개(StatefulSet)

 

스테이트풀셋 이란?

 

Stateless 애플리케이션을 예로 들면 아파치, nginx, IIS 가 있습니다.

 

해당 애플리케이션에서 서비스가 죽으면 단순 복제로 대체해주면 됩니다.

볼륨의 경우 같은 내용을 서비스하기 때문에 필요하다면 하나의 볼륨에 다수가 접근하면 됩니다.

 

네트워크 트래픽에서는 서비스에 접근하면 부하를 방지하기 위해 각각 분산을 하게 됩니다.

 

 

Stateful 애플리케이션은 각각의 역할이 있는데 Primary 메인 DB가 있고 Secondary로 Primary가 죽으면 대체할 DB이 존재하고 이를 감시하는 Arbiter가 있습니다.

 

각각의 역할이 있기 때문에 아비터가 죽으면 아비터 역할을 살려줘야 합니다.

또한, 각각의 역할마다의 볼륨을 사용하기 때문에 원래 사용하던 볼륨에 접근해야 해당 역할을 이어갈 수 있습니다.

 

네트워크 트래픽에서는 대체로 내부 시스템들이 데이터베이스에 사용되는데 각 앱에 특징에 맞게 들어가야 합니다.

App1에는 메인DB로 Read/Write가 가능하므로 내부 시스템들이 CRUD를 모두 하려면 이곳으로 접근해야하고

App2는 Read 권한만 있기 때문에 조회만 할 때 트래픽 분산을 위해 사용할 수 있으며

App3은 Primary와 Secondary를 감시하고 있어야 하기 때문에 App1,2에 연결이 되어야 합니다.

 

 

 

 

 

쿠버네티스에서 마이크로서비스 구조로 동작하는 애플리케이션은 대부분 상태를 갖지 않는 경우(Stateless) 가 많습니다. 그러한 경우에는 디플로이먼트,레플리카셋을 통해 쉽게 애플리케이션을 배포할 수 있습니다.

 

하지만, 레플리케이션 컨트롤러나 레플리카셋을 제공하는 파드는 각 별도의 볼륨을 사용할 수 있는 방법을 제공해주지 않아 모두 같은 볼륨으로 같은 상태를 가질수 밖에 없습니다. 또한 데이터베이스처럼 상태를 갖는(Stateful) 애플리케이션을 쿠버네티스에서 실행하는 것은 매우 복잡한 일 입니다. 왜냐하면 Pod 내부의 데이터를 어떻게 관리해야 할지, 상태를 갖는 Pod에는 어떻게 접근할 수 있을지 등을 꼼꼼히 고려해야 하기 때문입니다.

 

쿠버네티스가 이에 대한 해결책을 완벽하게 제공하는 것은 아니지만, 스테이트 풀셋이라는 쿠버네티스 오브젝트를 통해 어느정도 해결할 수 있도록 제공하고 있습니다.

즉, 파드마다 각각 다른 스토리지를 사용해 각각 다른 상태를 유지하기 위해서는 스테이트풀셋 (StatefulSet) 리소스를 사용하면 됩니다.

또한, 목적에 따라 해당  파드에 연결하기 위한 Headless Service 를 달아주면 됩니다.

 

 

  레플리카셋 스테이트풀셋
파드 생성시 이름 설정 Random 이름으로 설정
cf) Pod-ska25, Pod-dk15d ...
Ordinal index 이름으로 생성
cf) Pod-0, Pod-1, Pod-2 ...
파드 생성 시 순서 동시 생성  순차 생성. 0->1->2...
파드 Recreate 시 파드 이름 변경
cf) Pod-sdf34 -> Pod-vjng3
파드 이름 유지
cf) Pod-2 -> Pod-2
파드 삭제 시 순서 동시 삭제 인덱스 높은 순부터 순차 삭제 2->1->0
볼륨 생성 하는 방법 PVC를 직접 생성 volumeClaimTemplates 을 통한 동적 생성
파드의 수를 늘리면 PVC는? 1개의 PVC에 모두 연결 각각의 PV 를 생성한 뒤 연결
PVC 연결된 특정 파드를 죽으면? NodeSelector 가 설정 되어 있다면 해당 노드에 동일한 서비스로 랜덤한 파드이름 생성
(같은 노드에 PVC,파드가 생성되지 않으면 연결되지 않음)
특정 파드와 동일한 파드를 생성 후 기존 PVC와 연결 
PVC가 연결된 파드 수를 0으로 하면? PVC도 삭제함 PVC는 삭제하지 않음

 

 

 

cf ) 스테이트 풀셋은 애완동물로 비유가 됩니다. 가축에 비해 애완동물은 쉽게 대체될 수 없기 때문입니다.

 

레플리케이션 컨트롤러나 레플리카셋은 상태를 저장하지 않는 Stateless 이기 때문에 항상 실행하는 파드의 정보가 똑같을 필요는 없습니다. 병들어 죽은 가축처럼 얼마든지 교체할 수 있는 형태 입니다. 교체된 가축은 기존의 가축과 이름도 다르고(이름이 랜덤하게 붙음) 행동(IP)도 다를 수 있습니다.

 

스테이트풀셋이 관리하는 파드는 애완동물과 같아 애완동물이 죽으면 다른 애완동물로 대체할 수 없습니다.

애완동물을 대체하기 위해서는 (현실에서는 불가능하지만) 생김이나 행동 등 원래 있던 애완동물과 같아야 합니다.

스테이트 풀셋은 문제가 생긴 파드와 완벽하게 똑같은 파드로 대체합니다. 즉, 똑같은 이름과 똑같은 IP를 가진 파드로 교체한다는 의미입니다.

 

 

쿠버네티스에서 v1.4 까지 스테이트풀셋을 펫셋(PetSet)이라고 불렀습니다.

v1.5+ 부터 스테이트풀셋으로 변경

 

 

 

스테이트풀셋은 컨테이너 애플리케이션의 상태를 관리하는 데 사용하는 컨트롤러입니다.

 

디플로이먼트 컨트롤러와 같이 파드를 배포하고 복제본을 제공하며 스케일링을 관리할 수 있습니다. 그러나 디플로이먼트와 다른 점은 파드의 순서 및 파드의 고유성을 보장하며, 동일한 스펙으로 생성되지만 각각 고유한 볼륨을 가지고 있습니다.

  • 파드 배포
  • 파드의 복제본
  • 스케일링
  • 파드의 순서
  • 파드의 고유성 (이름, 네트워크, 스토리지)
  • 각 파드의 고유한 볼륨

 

 

스테이트풀셋의 주의사항

스테이트풀셋을 사용할 때 주의사항이 있습니다.

  • 파드에 사용할 스토리지는 PVC를 통해서만 가능합니다.
    • 미리 PV를 생성해놓거나
    • StorageClass를 사용해 동적 프로비저닝 사용

 

  • 스테이트풀셋을 삭제하거나 파드를 삭제하더라도 볼륨은 삭제되지 않습니다
    • 데이터의 안전을 보장하기 위함

 

  • 헤드리스 서비스가 필요합니다.
    • 파드의 고유한 네트워크 신원을 제공하기 위함
      • 예를 들어 일반적인 서비스라면, 서비스는 기본적으로 레이블 셀렉터가 일치하는 랜덤한 포드를 선택해 트래픽을 전달하기 때문에 스테이트풀셋의 랜덤한 포드들에게 요청을 분산될 것입니다.
      • 하지만, 이것은 스테이트풀셋이 원하는 동작이 아닙니다. 스테이트풀셋의 각 포드는 고유하게 식별되야하며, 포드에 접근할 때에도 '랜덤한 포드'가 아닌 '개별 포드'에 접근해야 합니다.
      • 이런 경우 헤드리스 서비스는 서비스의 이름으로 포드의 접근 위치를 알아내기 위해 사용되며, 서비스의 이름과 포드의 이름을 통해서 포드에 직접 접근할 수 있습니다.

 

 

스테이트풀셋의 파드 이름

스테이트풀셋의 각 파드 이름은 컨트롤러의 이름에 0부터 시작하는 순서 색인이 붙게 됩니다.

 

  • 파드의 이름 형식
{StatefulSet-Name}-{Order}

 

  • 파드 이름 예
mysts-0
mysts-1

 

 

스테이트풀셋의 파드 DNS 주소

헤드리스 서비스와 스테이트풀셋을 같이 사용하는 경우 파드의 DNS 주소는 다음과 같습니다.

  • 파드의 DNS 주소 형식
{Pod_Name}.{Governing_Service_Domain}.{Namespace}.svc.cluster.local

 

  • 파드의 DNS 주소 예
mysts-0.mysts.default.svc.cluster.local
mysts-1.mysts.default.svc.cluster.local

 

{Governing_Service_Domain}은 statefulset.spec.serviceName에 선언하며 해당 필드에 헤드리스 서비스의 이름을 지정합니다.

 

 

스테이트풀셋의 스토리지 볼륨

스테이트풀셋의 파드는 각각 고유한 PVC를 생성해 고유한 PV를 가집니다.

statefulset.spec.volumeClaimTemplates 필드에 선언하며, 미리 PV를 준비하거나, StorageClass를 통해 PV를 생성할 수 있습니다.

 

스테이트풀셋의 파드가 삭제되더라도 해당 볼륨은 안정적인 데이터 보존을 위해 자동으로 삭제되지 않습니다.

 

 

스테이트풀셋의 스케일링

  • 3개의 복제본이 있는 스테이트풀셋 파드는 0 -> 1 -> 2 순서대로 생성됩니다
  • 파드를 scale out 하기 전 기존 파드는 Running 및 Ready 상태여야 합니다
  • 파드가 scale in에 의해 삭제될 때는 역순을 진행됩니다

 

스테이트풀셋 관리

 

스테이트풀셋 기본 살펴보기

//myapp-svc-headless.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc-headless
  labels:
    app: myapp-svc-headless
spec:
  ports:
  - name: http
    port: 80
  clusterIP: None
  selector:
    app: myapp-sts

스테이트풀셋에 사용할 헤드리스 서비스 예제입니다.

서비스의 이름은 myapp-svc-headless이며, .spec.clusterIP를 None으로 설정해 헤드리스 서비스를 구성합니다. 파드 레이블 셀렉터는 app=myapp-sts 레이블을 가지고 있는 파드를 지정합니다.

 

 

//myapp-sts.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp-sts
spec:
  selector:
    matchLabels:
      app: myapp-sts
  serviceName: myapp-svc-headless
  replicas: 2
  template:
    metadata:
      labels:
        app: myapp-sts
    spec:
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb
        ports:
        - containerPort: 8080

스테이트풀셋 리소스의 기본 예제입니다.

 

.spec.serviceName: 스테이트풀셋에 사용할 헤드리스 서비스 이름 지정

앞서 선언한 헤드리스 서비스를 스테이트풀셋이 사용하도록 .spec.serviceName 필드에 지정합니다.

 

헤드리스 서비스와 스테이트풀셋 리소스를 생성해보겠습니다.

파드의 이름을 보면 0부터 시작해 인덱스가 순서대로 붙게 된 것을 확인할 수 있습니다.

 

 

스테이트풀셋의 복제본을 3개로 스케일링 해보겠습니다. Scale Out

$ kubectl scale statefulset myapp-sts --replicas=3

다음 순서인 2번 인덱스가 붙은 파드를 확인할 수 있습니다.

 

 

스테이트풀셋의 복제본을 2개로 스케일링하고 파드 목록을 확인해보겠습니다. Scale In

kubectl scale statefulset myapp-sts --replicas=2

가장 마지막 인덱스인 2번이 종료된 것을 확인할 수 있습니다.

 

 

헤드리스 서비스에 접근하기 전에 파드의 IP를 확인해보겠습니다.

파드의 IP는 192.168.9.100, 192.168.233.224 입니다.

 

 

임시 파드를 생성해서 헤드리스 서비스에 접근해보겠습니다.

$ kubectl run nettool -it --image=ghcr.io/c1t1d0s7/network-multitool --rm bash

 

헤드리스 서비스 이름으로 DNS에 질의하면 앞서 살펴본 대로, 서비스의 ClusterIP가 아닌(어차피 없음) 각 파드의 IP로 응답하는 것을 확인할 수 있습니다.

 

첫 번째 파드 이름에 서비스 이름을 붙여 질의하면, 첫 번째 파드 IP만 응답하는 것을 확인할 수 있습니다.

두 번째 파드도 동일합니다.

 

 

curl 로 접근도 해보겠습니다.

위와같이 스테이트풀셋 컨트롤러와 헤드리스 서비스를 구성하면, 파드의 FQDN을 사용하여 각 파드를 구분할 수 있습니다.

 

 

 

스테이트풀셋의 볼륨 클레임 템플릿

스테이트풀셋은 각 파드의 고유한 볼륨을 제공합니다.

 

다음은 볼륨 클레임 템플릿을 사용한 예제입니다.

//myapp-sts-vol.yaml

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp-sts-vol
spec:
  selector:
    matchLabels:
      app: myapp-sts-vol
  serviceName: myapp-svc-headless
  replicas: 2
  template:
    metadata:
      labels:
        app: myapp-sts-vol
    spec:
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: myapp-data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: myapp-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
      storageClassName: rook-ceph-block

 

.spec.template: 파드의 템플릿

.spec.volumeClaimTemplates: 영구 볼륨 클레임(PVC) 템플릿

 

다른 컨트롤러와 다르게 파드별 고유 상태를 가지기 위해 각 파드에 연결할 볼륨 클레임 템플릿을 선언합니다.

 

볼륨 클레임 템플릿을 사용하는 스테이트풀셋 리소스를 생성해보겠습니다.

$ kubectl create -f myapp-sts-vol.yaml
statefulset.apps/myapp-sts-vol created

 

스테이트풀셋 리소스와 파드 목록을 확인해보면 정상 흐름인것을 볼 수 있습니다.

 

 

 

파드의 상세정보에서 볼륨 관련 정보를 확인해보겠습니다.

myapp-data-myapp-sts-vol-0 이라는 PVC에 연결된 것을 확인할 수 있습니다.

 

 

PVC 리소스와 PV 리소스를 확인해보겠습니다.

스토리지 클래스에 의해 PVC에 제공할 PV 두 개가 생성된 것을 확인할 수 있습니다.

 

 

복제본 3개로 스케일링 해보겠습니다.

$ kubectl scale statefulset myapp-sts-vol --replicas=3
statefulset.apps/myapp-sts-vol scaled

 

myapp-sts-vol-2 파드가 사용할 PVC 및 PV 리소스가 생성된 것을 확인할 수 있습니다.

 

컨트롤러 및 서비스 리소스를 지워보겠습니다.

$ kubectl delete -f myapp-sts.yaml -f myapp-sts-vol.yaml -f myapp-svc-headless.yaml 

컨트롤러 및 파드가 삭제된 후에도 여전히 PV 및 PVC 리소스가 남아있는 것을 확인할 수 있습니다.

데이터의 안전을 보장하기 위한 조치로 불필요한 경우 수동으로 삭제해야 합니다.

 

 

회수 정책이 Delete로 PVC만 삭제하면 PV도 같이 삭제됩니다.