[쿠버네티스] 디플로이먼트 & 스테이트풀셋(stateful set)

신현식·2023년 2월 21일
1

구름_Kubernetes

목록 보기
7/25
post-thumbnail

디플로이먼트(deployment)

디플로이먼트(Deployment) 는 파드와 레플리카셋(ReplicaSet)에 대한 선언적 업데이트를 제공한다. 또한 디플로이먼트는 쿠버네티스에서 상태가 없는(stateless)앱을 배포할 때 사용하는 가장 기본적인 컨트롤러이다.

디플로이먼트에서 의도하는 상태를 설명하고, 디플로이먼트 컨트롤러(Controller)는 현재 상태에서 의도하는 상태로 비율을 조정하며 변경한다. 새 레플리카셋을 생성하는 디플로이먼트를 정의하거나 기존 디플로이먼트를 제거하고, 모든 리소스를 새 디플로이먼트에 적용할 수 있다.

kubectl api-resources | grep deployments
kubectl explain deploy.sepc
  • 대부분의 컨트롤러는 apps그룹에 속해있다.

  • 디플로이먼트가 소유하는 레플리카셋은 관리하지 말아야 한다. 사용자의 유스케이스가 다음에 포함되지 않는 경우 쿠버네티스 리포지터리에 이슈를 올릴 수 있다.


vi myapp-deploy-v1.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deploy
  labels:
    app: myapp-deploy
spec:
  strategy:         
    type: RollingUpdate   # default 값
    rollingUpdate:
      maxUnavailable: 1   
      maxSurge: 1         # 급증, 순간적으로 복제본의 수의 +1(25%)까지 가능
  minReadySeconds: 20
  replicas: 3
  selector:
    matchLabels:
      app: myapp-deploy
  template:
    metadata:
      labels:
        app: myapp-deploy
    spec:
      containers:
      - image: ghcr.io/c1t1d0s7/go-myweb:v1.0   
        name: myapp
        ports:
        - containerPort: 8080
        
kubrctl create -f myapp-deploy-v1.yaml
kubectl get deploy
kubectl get rs
kubectl get pods
  • 구조: deploy -> RS -> pod 여러개

  • 처음에는 v1의 복제본 3개로 구성되어있다 새로운 이미지(v2)로 바꾸니까 복제본이 이 버전으로 순차적으로 바뀐다. 새로운 버전을 배포할 수 있게 해주는 역할을 한다.

  • 이미 떠있는 파드를 수정하는 것이 아닌 파드의 템플릿을 수정한 것이다. 따라서 이는 파드를 새롭게 만들때 적용이 되는 것이다.

yaml 파일 수정 -> 
# 바꾼 내용의 파일로 deploy 대체
kubectl replace -f yaml파일 
kubectl apply -f yaml파일 

# vi로 수정
kubectl edit deploy <NAME>

# json 파일형식을 수정
kubectl patch deploy <NAME> {json} 

# 이미지만 수정
kubectl set image <TYPE> <NAME> <CONTAINER_NAME>=<NEW-IMAGE> 
  • 레플리카셋은 정상적인 방법으로 새로운 버전으로 변경을 할 수 없다. 변경을 위해선 템플릿을 변경한 이후 모든 파드를 제거한 이후 다시 만들어줘야한다. 반면에 디플로이먼트는 단순 복제본뿐만 아닌 새로운 이미지를 적용해서 바꿔주기 때문에 효율적이다.

디플로이먼트의 롤아웃 기록 확인

# 디플로이먼트의 myapp-deploy의 상태를 확인하는 명령
kubectl rollout status deployment myapp-deploy

# 이미지를 바꾸고 실시간으로 어떻게 변화하는지 확인해보기
kubectl set deployment myapp-deploy myapp=imageghcr.io/c1t1d0s7/go-myweb:v2.0  

# 개정판 정보를 확인, 업데이트를 할때마다 저장되고 과거의 버전으로 돌아갈 수도 있음
kubectl rollout history deployment myapp-deploy

# 과거 버전으로 돌아가는 명령
kubectl rollout undo deployment myapp-deploy --to-revision 2
# 확인
kubectl rollout status deploy myapp-deploy
kubectl rollout history deploy myapp-deploy



# From V2 to V3
kubectl apply -f myapp-deploy-v3.yaml

# 여기는 어노테이션이 들어가 있어서 history를 확인할 때 주석이 달린다.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deploy
  labels:
    app: myapp-deploy
  annotations:
    kubernetes.io/change-cause: My Golang Web App Version 3     # 주석
spec:

...

staragy

어플리케이션을 새로운 버전으로 배포
디플로이먼트은 레플리카셋과 큰 차이는 없다. 주로 차이는 staragy(배포 전략) 이 부분이며 레플리카셋의 확장버전이라고 보면 된다.
쿠버네티스에서는 Recreate, Ramped (also known as rolling-update or incremental) 이 2가지 방법을 지원한다. 기본은 rolling-update이다

  • maxUnavailable: 1 기본적으로 25%(1)로 세팅, 제거될 수 있는 수
  • maxSurge: 1 기본적으로 25%(1)로 세팅, 추가될 수 있는 수
    RSv1: 3 -> 3 -> 2 -> 1 -> 0
    RSv2: 0 -> 1 -> 2 -> 3 -> 3
# 2가지 방법 지원가능한 것 확인
kubectl explain deploy.spec.stragy

Recreate

재생성 전략은 버전 A를 종료한 다음 버전 A를 끈 후 버전 B를 배포하는 것으로 구성된 더미 배포이다. 이 기술은 응용 프로그램의 종료 및 부팅 시간에 따라 달라지는 서비스의 가동 중지 시간을 의미한다.

  • 장점: 설정하기 쉬움, 애플리케이션의 완전한 상태로 배포가능
  • 단점: 사용자에게 큰 영향을 미치며 애플리케이션의 종료 및 부팅 시간에 따라 다운타임이 예상
    recreate 동작과정

    다운타임

    planed: 웹사이트의 서비스 점검시간 등 , recreate는 이것만 사용가능
    unplaned: 천재지변으로 인해 서비스가 강제로 종료되는 등

Ramped (also known as rolling-update or incremental)

점진적 배포 전략은 모든 인스턴스가 롤아웃될 때까지 인스턴스를 차례로 교체하여 애플리케이션 버전을 천천히 롤아웃하는 것으로 구성됩니다. 순차적으로 업데이트하는 것을 의미한다.

일반적으로 다음 프로세스를 따릅니다. 로드 밸런서 뒤에 버전 A의 풀이 있으면 버전 B의 인스턴스 하나가 배포됩니다. 서비스가 트래픽을 수락할 준비가 되면 인스턴스가 풀에 추가됩니다. 그런 다음 버전 A의 한 인스턴스가 풀에서 제거되고 종료됩니다.
Ramped 동작과정

  • 장점: 설정하기 쉬움, 버전이 인스턴스 간에 천천히 릴리스, 데이터 재조정을 처리할 수 있는 상태 저장 애플리케이션에 편리
  • 단점: 롤아웃/롤백에는 시간이 걸림, 여러 API를 지원하는 것은 어려움, 트래픽에 대한 통제가 없음

데몬셋도 마차가지로 배포전략을 가지고 있다.

  • updateStrategy: rollingUpdate이기에 기본적으로 순차적으로 하나씩 바뀌게 된다. 여기서는 Ondelete를 사용할 수 있다.
    kubectl explain ds.spec.updateStrategy

스테이트풀셋(stateful set)

스테이트풀셋은 애플리케이션의 스테이트풀을 관리하는데 사용하는 워크로드 API 오브젝트이다. 스테이트풀셋도 kubectl explain stateful.spec.updateStrategy 배포전략이 존재한다.

파드 집합의 디플로이먼트와 스케일링을 관리하며, 파드들의 순서 및 고유성을 보장한다.

디플로이먼트와 유사하게, 스테이트풀셋은 동일한 컨테이너 스펙을 기반으로 둔 파드들을 관리한다. 디플로이먼트와는 다르게, 스테이트풀셋은 각 파드의 독자성을 유지한다. 이 파드들은 동일한 스팩으로 생성되었지만, 서로 교체는 불가능하다. 다시 말해, 각각은 재스케줄링 간에도 지속적으로 유지되는 식별자를 가진다.

스토리지 볼륨을 사용해서 워크로드에 지속성을 제공하려는 경우, 솔루션의 일부로 스테이트풀셋을 사용할 수 있다. 스테이트풀셋의 개별 파드는 장애에 취약하지만, 퍼시스턴트 파드 식별자는 기존 볼륨을 실패한 볼륨을 대체하는 새 파드에 더 쉽게 일치시킬 수 있다.

  • 스테이트풀셋은 다음 중 하나 또는 이상이 필요한 애플리케이션에 유용

    • 안정된, 고유한 네트워크 식별자.
    • 안정된, 지속성을 갖는 스토리지.
    • 순차적인, 정상 배포(graceful deployment)와 스케일링.
    • 순차적인, 자동 롤링 업데이트.
  • 상태가 없는 앱, 웹 같은 경우 디플로이먼트로 배포하고 상태가 있는 DB같은 경우 스테이트풀셋으로 배포한다.

  • 스토리지를 제공하는 목적은 데이터를 저장하기위한 목적이 아닌 앱, 웹 컨텐츠가 볼륨에 있고 이를 가져오기 위함이다. 만들어낸 데이터는 DB에 넣기에 따로 가지고 있지 않는다.
    볼륨이 있다고 해서 상태를 저장하는 용도로 쓰는 것이 아닌, 상태가 없는 것들은 이 정보를 가지고 클라이언트에게 제공해준다고 보면 된다. 같은 스토리지를 바라보고 있기 때문에 그것들은 상태가 같다.


vi 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:alpine
        ports:
        - containerPort: 8080
  • 스테이트풀셋은 클러스터 IP가 없는 헤드리스 서비스를 사용해야한다.
vi 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
    
kubectl create -f myapp-sts.yaml -f myapp-svc-headless.yaml
# 스테이트 풀셋을 보면 2개 준비완료
kubectl get sts,svc,ep

# 컨트롤러 뒤에 번호가 붙음 -> 고유성을 가지고 있음
kubectl get pod -o wide


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

# 호스트 질의 - 서비스의 이름지정
host myapp-svc-headless

# 스테이트풀셋 + 헤드리스, -> 파드의 이름으로 통신해 파드의 IP 반환
host myapp-sts-0.myapp-svc-headless
host myapp-sts-1.myapp-svc-headless
  • 스테이트풀셋인 sts-0을 지우면 0이 새롭게 생기고 sts-1을 지워도 1이 새롭게 생긴다. 즉 순서대로 0,1,2 순서로 무조건 생성된다는 것이다.

볼륨 설정

스테이트풀셋은 레플리카 셋과 동일하게 복제본을 제공하는 목적은 같지만 각 파드가 고유한 상태를 가져야 한다. 이를 위해 별도의 상태를 가져야 하기 때문에 스토리지를 따로 가지고 있어야 한다.

모든 파드가 같은 PVC를 보고 있으면 안된다.

vi 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:         # PVC의 템플릿
  - metadata:
      name: myapp-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
      storageClassName: nfs-client
 
# 위에서 만들어둔 헤드리스를 다시 만들어준다.

# 현재 pv와 pvc는 없는 상태

# 생성
kubectl create -f myapp-sts-vol.yaml
kubectl get po,svc

# pvc,pv가 생성됨
kubetctl get pv,pvc
  • 이는 파드가 2개 만들어지고, 각각 파드에 대한 PVC가 위 템플릿에 따라 따로 만들어진다. PVC가 스토리지 클래스인 nfs-client에 요청해 PV를 만들고 이로인해 서로 다른 스토리지를 가질 수 있고 고유한 상태가 되는 것이다.
  • 복제본을 늘리면 파드와 pvc,pv는 늘어난다. 하지만 이후 복제본의 수를 줄이면 파드는 줄어들지만 pvc,pv는 늘어난 개수를 유지하고 있다. 즉 한번 만들어진 pv와 pvc는 줄어들지 않는다는 것이다. 이는 스토리지가 고유한 상태인데 스케일링을 한다고 해서 볼륨이 삭제되어 데이터를 없애지는 않기 때문이다.
# 복제본 늘리기
kubectl scale sts myapp-sts-vol --replicas 4
kubetctl get pv,pvc
kubectl get po,sts

# 복제본 줄이기
kubectl scale sts myapp-sts-vol --replicas 2
kubetctl get pv,pvc
kubectl get po,sts
  • 또한 줄었다가 다시 늘리게 되었을때 기존에 있던 pv,pvc에 연결된다. 스테이트풀셋을 지운다고 해도 볼륨은 남아있고 새로 스테이트풀셋을 만들면 남아있는 볼륨에 그대로 다시 연결된다. 이를 원치 않으면 파드의 번호와 pv,pvc의 번호는 일치되기 때문에 필요없는 것을 미리 지워주면 된다.

DB 이중화

DB 이중화를 위해서 어떤 파드를 promary로 하고 어떤 것을 secondary를 할지 정하기 위해서 어디서 정보를 저장할지 알아야한다. 따라서 헤드리스 서비스를 통해 어떤 파드에 접근할수 있는지 알아야 하기때문에 스테이트풀셋으로 구성해야한다.

  • 볼륨을 분리시켰지만 동기화(sync)를 해줘야한다.

Database Sharding

  • 데이터를 세로나 가로로 나눈다. VP1, VP2를 파드의 스토리지라고 생각한다면 모든 파드가 다른 데이터를 가지고 있는 것이다.
vi mydb-cm-mysql.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: mydb-config
  labels:
    app: mydb
    app.kubernetes.io/name: mydb
data:
  primary.cnf: |
    [mysqld]
    log-bin    
  replica.cnf: |        # my.cnf 파일을 대체하기 위함
    [mysqld]
    super-read-only 


vi mydb-sts-mysql.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mydb
spec:
  selector:
    matchLabels:
      app: mydb
      app.kubernetes.io/name: mydb
  serviceName: mydb
  replicas: 2 
  template:
    metadata:
      labels:
        app: mydb
        app.kubernetes.io/name: mydb
    spec:
      initContainers:           # 초기화 컨테이너가 2개 있음
      - name: init-mysql               # 1번
        image: mysql:5.7
        command:
        - bash
        - "-c"
        - |
          set -ex
          [[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/primary.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/replica.cnf /mnt/conf.d/
          fi  
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map
      - name: clone-mysql              # 2번
        image: gcr.io/google-samples/xtrabackup:1.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          [[ -d /var/lib/mysql/mysql ]] && exit 0
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          [[ $ordinal -eq 0 ]] && exit 0
          ncat --recv-only mydb-$(($ordinal-1)).mydb 3307 | xbstream -x -C /var/lib/mysql
          xtrabackup --prepare --target-dir=/var/lib/mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ALLOW_EMPTY_PASSWORD
          value: "1"
        ports:
        - name: mysql
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping"]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
      - name: xtrabackup        # 실행되고 있는 중에 스토리지의 동기화를 해주는 역할
        image: gcr.io/google-samples/xtrabackup:1.0
        ports:
        - name: xtrabackup
          containerPort: 3307
        command:
        - bash
        - "-c"
        - |
          set -ex
          cd /var/lib/mysql
          if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
            cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
            rm -f xtrabackup_slave_info xtrabackup_binlog_info
          elif [[ -f xtrabackup_binlog_info ]]; then
            [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
            rm -f xtrabackup_binlog_info xtrabackup_slave_info
            echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
                  MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
          fi
          if [[ -f change_master_to.sql.in ]]; then
            echo "Waiting for mysqld to be ready (accepting connections)"
            until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done
            echo "Initializing replication from clone position"
            mysql -h 127.0.0.1 \
                  -e "$(<change_master_to.sql.in), \
                          MASTER_HOST='mydb-0.mydb', \
                          MASTER_USER='root', \
                          MASTER_PASSWORD='', \
                          MASTER_CONNECT_RETRY=10; \
                        START SLAVE;" || exit 1
            mv change_master_to.sql.in change_master_to.sql.orig
          fi
          exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
            "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mydb-config
  volumeClaimTemplates:       # 스토리지 클래스가 없기 때문에 default로 하나가 지정되어있어야함
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
  • init-mysql: 설정파일을 가져옴,

  • clone-mysql: 상대방의 DB에서 데이터를 동기화 해서 가져오는 역할을 함


 # 일반 서비스
mydb-svc-read.yaml

apiVersion: v1
kind: Service
metadata:
  name: mydb-read
  labels:
    app: mydb
    app.kubernetes.io/name: mydb
spec:
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mydb
    app.kubernetes.io/name: mysql

# 헤드리스, 쓰기작업이나 업데이트를 위함
mydb-svc-write.yaml

apiVersion: v1
kind: Service
metadata:
  name: mydb
  labels:
    app: mydb
    app.kubernetes.io/name: mydb
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None
  selector:
    app: mydb
    app.kubernetes.io/name: mydb
    
    
# 전체 실행
kubectl create -f .    

# 헤드리스와 일반 서비스 2개 생성
kubectl get svc

# 초기화 컨테이너가 존재하기때문에 시간이 좀 걸림
kubectl get pod,pv,pvc


# 테스트
kubectl run db-client --image ghcr.io/c1t1d0s7/network-multitool -it --rm

# 헤드리스
host mydb
# 파드 지정
host mtdb-0.mydb
# 일반 서비스 
host mydb-read
  • 쓰기가 가능한 DB
mysql -h mydb-0.mydb -u root -p
>show databases;
>CREATE DATABASE mydb
>CREATE TABLE mydb.mytb (message VARCHAR(100));
>INSERT INTO mydb.mytb VALUES ( "Hello world");
>SELECT * FROM mydb.mytb;

exit

  • 읽기 전용, 동기화가 되어있는 것을 확인, 여기서는 insert가 되지 않음
mysql -h mydb-1.mydb -u root -p
>show databases;
>SELECT * FROM mydb.mytb

# 명령형으로 보기
mysql -h mydb-1.mydb -u root -e 'SELECT * FROM mydb.mytb'
  • 스케일링 작업 실행, 새로 만들어진 파드도 동기화가 이미 되어있음(초기화 컨테이너에서 실행)
exit
# 2번 파드가 하나 새롭게 생겼음
kubectl scale sts mydb --replicas 3

# 작업
kubectl run db-client --image ghcr.io/c1t1d0s7/network-multitool -it --rm
mysql -h mydb-2.mydb -u root -e 'SELECT * FROM mydb.mytb'
profile
전공 소개

0개의 댓글