쿠버네티스 인 액션 - 스테이트풀셋: 복제된 스테이트풀 애플리케이션 배포하기 (9)

hyeokjin·2022년 9월 10일
0

kubernetes

목록 보기
9/9
post-thumbnail

스테이트풀 파드 복제하기

여러 개의 파드 레플리카를 실행하면서 개별 스토리지 볼륨을 사용하는 파드를 가지려면 어떻게 해야 할까? 스테이트풀셋은 애플리케이션의 인스턴스가 각각 안정적인 이름과 상태를 가지며 개별적으로 취급돼야 하는 애플리케이션에 알맞게 만들어졌다.

스테이트풀셋과 레플리카셋 비교

레플리카셋이나 레플리케이션컨트롤러 이들은 대부분 스테이트리스로 언제든지 완전히 새로운 파드로 교체될 수 있다. 스테이트풀 파드는 다른 접근이 필요하다. 스테이트풀 파드가 종료되면 새로운 파드 인스턴스는 교체되는 파드와 동일한 이름, 네트워크 아이덴티티, 상태 그대로 다른 노드에서 되살아나야 한다. 스테이트풀셋은 파드가 아이덴티티와 상태를 유지하면서 다시 스케줄링 되게 한다.

안정적인 네트워크 아이덴티티 제공하기

스테이트풀셋으로 생성된 파드는 서수 인덱스가 할당되고 파드의 이름과 호스트 이름, 안정적인 스토리지를 붙이는데 사용된다. 스테이트풀셋의 이름과 인스턴스의 서수 인덱스로부터 파생되므로 파드의 이름을 예측할 수 있다. 파드는 임의의 이름이 아닌 잘 정리된 이름을 갖는다.

거버닝 서비스

스테이트풀 파드는 때때로 호스트 이름을 통해 다뤄져야 할 필요가 있다.
스테이트풀 파드는 각각 서로 다르므로 그룹의 특정 파드에서 동작하기를 원할 것이다 이런 이유로 스테이풀셋은 거버닝 헤드리스 서비스를 생성해서 각 파드에게 실제 네트워크 아이덴티티를 제공해야 한다. 이 서비스를 통해 각 파드는 자체 DNS 엔트리를 가지며 클러스터의 피어 혹은 클러스터의 다른 클라이언트가 호스트 이름을 통해 파드의 주소를 지정할 수 있다.

스테이트풀셋 스케일링

스테이트풀셋을 스케일링하면 사용하지 않는 다음 서수 인덱스를 갖는 새로운 파드 인스턴스를 생성한다. 인스턴스 두 개에서 세 개로 스케일업하면 새로운 인스턴스는 인덱스 2를 부여받는다.

스테이트풀셋 스케일 다운에서는 어떤 파드가 제거될지 알 수 있다는 점이다 항상 가장 높은 서수 인덱스를 먼저 제거한다. 또한 데이터를 저장한 두 개의 노드가(복제된 데이터가 있는 상태) 동시에 다운된 경우 데이터를 읽을 수 있기 때문에 스케일 다운은 순차적으로 일어나며 분산 데이터 저장소는 손실된 복사본을 대체하기 위한 데이터 엔트리의 추가 복제본을 다른 곳에 생성할 시간을 갖게 된다.
이러한 이유로 스테이트풀셋은 인스터스 하나라도 비정상인 경우 스케일 다운 작업을 허용하지 않는다.

각 스테이트풀 인스턴스에 안정적인 전용 스토리지 제공

각 스테이트풀 파드 인스턴스는 자체 스토리지를 사용할 필요가 있고 스테이트풀 파드가 다시 스케줄링되면, 새로운 인스턴스는 동일한 스토리지에 연결돼야 한다. 어떻게 스테이트풀셋은 이를 달성할까?

볼륨 클레임 템플릿과 파드 템플릿을 같이 구성

스테이트풀셋이 파드를 생성하는 것과 같은 방식으로 퍼시스턴트볼륨클레임 또한 생성해야 한다. 이런 이유로 스테이트 풀셋은 각 파드의 함께하는 퍼시스턴트볼륨클레임을 복제하는 하나 이상의 볼륨 클레임 템플릿을 가질 수 있다.

퍼시스턴트볼륨클레임의 생성과 삭제의 이해

스테이트풀 파드는 스테이트풀 애플리케이션을 실행하기 위한 것으로 볼륨에 저장하는 데이터가 중요하고 스테이트풀셋의 스케일 다운에서 클레임이 삭제되면 결과는 치명적인 문제가 된다. 이런 이유로 기반 퍼시스턴트볼륨을 해제 하려면 퍼시스턴트볼륨클레임을 수동으로 삭제해야한다.

동일 파드의 새 인스턴스에 퍼시스턴트볼륨클레임 다시 붙이기

스케일 다운 이후 퍼시스턴트볼륨클레임이 남아 있다는 사실은 이후에 스케일 업을 하면 퍼시스턴트볼륨에 바인딩된 동일한 클레임을 다시 연결할 수 있고, 새로운 파드에 그 콘텐츠가 연결된다는 것을 의미한다. 실수로 스테이트풀셋을 스케일 다운하면 스케일 업으로 다시 되돌릴 수 있고 새 파드는 동일한 이름뿐만 아니라 동일한 지속된 상태를 다시 갖는다.

스테이트풀셋 보장 이해하기

스테이트풀 파드가 항상 동일한 아이덴티티를 가지는 파드로 교체되는 것을 확인했다. 쿠버네티스가 파드 상태를 확신할 수 없을 때는 어떨까? 쿠버네티스가 동일한 아이덴티티를 가지는 교체 파드를 생성하면 애플리케이션의 두 개 인스턴스가 동일한 아이덴티티로 시스템에서 실행할 수 있다. 두 인스턴스는 동일한 스토리지에 바인딩되고 두 프로세스가 동일한 아이덴티티로 같은 파일을 쓰려고 할 것이다.

쿠버네티스는 두 개의 스테이트풀 파드 인스턴스가 절대 동일한 아이덴티티로 실행되지 않고 동일한 퍼시스턴트볼륨클레임에 바인딩 되지 않도록 보장한다. 즉 스테이트풀셋은 교체 파드를 생성하기 전에 파드가 더 이상 실행 중이지 않는다는 점을 절대적으로 확신해야 한다는 뜻 이다.

스테이트풀셋 사용하기

각 파드 인스턴스에 단일 데이터 엔트리를 저장하고 검색할 수 있도록 확장 해보자.

app.js

const http = require('http');
const os = require('os');
const fs = require('fs');

const dataFile = "/var/data/kubia.txt";

function fileExists(file) {
  try {
    fs.statSync(file);
    return true;
  } catch (e) {
    return false;
  }
}

var handler = function(request, response) {
  if (request.method == 'POST') {		// POST 요청을 받으면 요청의 body를 데이터 파드에 저장한다.
    var file = fs.createWriteStream(dataFile);
    file.on('open', function (fd) {
      request.pipe(file);
      console.log("New data has been received and stored.");
      response.writeHead(200);
      response.end("Data stored on pod " + os.hostname() + "\n");
    });
  } else {		// GET 요청(그 외 다른요청)을 받으면 호스트 이름과 데이터 파일의 콘텐츠를 반환한다.
    var data = fileExists(dataFile) ? fs.readFileSync(dataFile, 'utf8') : "No data posted yet";
    response.writeHead(200);
    response.write("You've hit " + os.hostname() + "\n");
    response.end("Data stored on this pod: " + data + "\n");
  }
};

var www = http.createServer(handler);
www.listen(8080);

그런 다음 이미지를 빌드하거나, 이미 만들어둔 docker.io/luksa/kubia-pet 이미지를 사용하자

스테이트풀셋을 통한 애플리케이션 배포하기

애플리케이션을 배포하려면 다른 유형의 오브젝트가 두 가지 또는 세 가지를 생성해야한다.

  • 데이터 파일을 저장하기 위한 퍼시스턴트볼륨
  • 스테이트풀셋에 필요한 거버닝 서비스
  • 스테이트풀셋 자체

퍼시스턴트볼륨 생성

스테이트풀셋을 세 개의 레플리카까지 스케일링하려면 세 개의 퍼시스턴트볼륨이 필요하다

세 개의 퍼시스턴트볼륨을 생성한다.

persistent-volumes-gcepd.yaml

kind: List		# 이 파일은 세 개 퍼시스턴트볼륨의리스트를 설명한다.
apiVersion: v1
items:
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-a	# 퍼시스턴트볼륨의 이름은 pv-a, pv-b, pv-c 이다.
  spec:
    capacity:
      storage: 1Mi	# 각 퍼시스턴트볼륨의 용량은 1Mi이다.
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle	# 클레임에서 볼륨이 해제되면 다시 사용해 재사용된다.
    gcePersistentDisk:	# 볼륨은 GCE 퍼시스턴트 디스크를 기반 스토리지 메커니즘으로 사용한다.
      pdName: pv-a
      fsType: ext4
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-b
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    gcePersistentDisk:
      pdName: pv-b
      fsType: ext4
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-c
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    gcePersistentDisk:
      pdName: pv-c
      fsType: ext4

거버닝 서비스 생성하기

앞서 설명한 대로 스테이트풀셋을 배포하기 전에 먼저 헤드리스 서비스를 생성해야 한다. 스테이트풀 파드에 네트워크 아이덴티티를 제공하기 위해 사용된다.

kubia-service-headless.yaml

apiVersion: v1
kind: Service
metadata:
  name: kubia	# 서비스의 이름
spec:
  clusterIP: None	# 스테이트풀셋의 거버닝 서비스는 헤드리스여야 한다.
  selector:		# app=kubia 레이블을 가진 모든 파드는 이 서비스에 속한다.
    app: kubia
  ports:
  - name: http
    port: 80

clusterIP 필드를 None으로 설정하여 헤드리스 서비스가 된다. 이로써 피어 간 피어디스커버리를 사용할 수 있다.

스테이트풀셋 매니페스트 생성

kubia-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia
  replicas: 2
  selector:
    matchLabels:
      app: kubia # has to match .spec.template.metadata.labels
  template:
    metadata:
      labels:		# 스테이트풀셋으로 생성된 파드는 app=kubia 레이블을 갖는다
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data	# 파드 내부의 컨테이너는 pvc 볼륨을 이 경로에 마운트 한다.
          mountPath: /var/data
  volumeClaimTemplates:	# 이 템플릿으로 퍼시스턴트볼륨 클레임이 생성된다.
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

지금 까지 생성한 레플리카셋이나 디플로이먼트 매니페스트와 다르지 않다. 새로운 것은 volumeClaimTemplates 목록이다. volumeClaimTeplates에 각 파드를 위한 퍼시스턴트볼륨클레임을 생성하는 데 사용되는 data라고 부르는 볼륨클레임 템플릿을 정의한다.

스테이트풀셋 생성하기

$ kubectl create -f kubia-statefulset.yaml
statefulset.apps/kubia created
$ kubectl get po
NAME                       READY   STATUS              RESTARTS   AGE
kubia-0                    1/1     Running             0          32s
kubia-1                    0/1     ContainerCreating   0          7s

스테이트풀셋은 동시에 생성되지 않는다.
첫번째 파드가 생성되고 준비가 완료되면 두 번째 파드가 생성된다.

생성된 스테이트풀 파드 살펴보기

$ kubectl get po kubia-0 -o yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2022-09-11T16:53:31Z"
  generateName: kubia-
  labels:
    app: kubia
    controller-revision-hash: kubia-c94bcb69b
    statefulset.kubernetes.io/pod-name: kubia-0
  name: kubia-0
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: StatefulSet
    name: kubia
    uid: 27a8d361-d576-4766-8057-4d50506cce8b
  resourceVersion: "4361705"
  uid: 08545543-114e-4e2c-b591-5fc1e7507695
spec:
  containers:
  - image: luksa/kubia-pet
    imagePullPolicy: Always
    name: kubia
    ports:
    - containerPort: 8080
      name: http
      protocol: TCP
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:			# 매니페스트에 지정된 대로 볼륨이 마운트됐다.
    - mountPath: /var/data
      name: data
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-kcjwp
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  hostname: kubia-0
  nodeName: gke-kubia-default-pool-c2f41b01-3gd0
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  subdomain: kubia
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:			# 스테이트풀셋으로 볼륨이 생성됐다.
  - name: data
    persistentVolumeClaim:
      claimName: data-kubia-0	# 이 볼륨으로 클레임이 참조된다.
  - name: kube-api-access-kcjwp
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2022-09-11T16:53:41Z"
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: "2022-09-11T16:53:56Z"
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: "2022-09-11T16:53:56Z"
    status: "True"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: "2022-09-11T16:53:41Z"
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: containerd://c4ea29d656119583327b2cfa6a6c02b76ac6602ba9ac623848595693ddd43493
    image: docker.io/luksa/kubia-pet:latest
    imageID: docker.io/luksa/kubia-pet@sha256:4263bc375d3ae2f73fe7486818cab64c07f9cd4a645a7c71a07c1365a6e1a4d2
    lastState: {}
    name: kubia
    ready: true
    restartCount: 0
    started: true
    state:
      running:
        startedAt: "2022-09-11T16:53:56Z"
  hostIP: 10.138.0.4
  phase: Running
  podIP: 10.32.2.43
  podIPs:
  - ip: 10.32.2.43
  qosClass: BestEffort
  startTime: "2022-09-11T16:53:41Z"
  

생성된 퍼시스턴트볼륨클레임을 조회해 생성되었는지도 확인하자

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-kubia-0   Bound    pvc-37e8b9e6-2e8d-412a-aabf-7e58c1dc4353   1Gi        RWO            standard       6m32s
data-kubia-1   Bound    pvc-1f60adc8-f411-4eff-be1c-aac7fbaebb86   1Gi        RWO            standard       6m7s

API 서버를 통해 파드와 통신하기

API 서버의 유용한 기능 중 하나는 개별 파드에 직접 프록시 연결을 만들 수 있는 기능이다. kubia-0 파드에 요청을 보내고 싶다면 다음 URL을 호출해보자

<apiServerHost>:<port>/api/vi/namespaces/default/pods/kubia-0/proxy/<path>

API 서버는 보안이 강화돼 있으므로 API 서버를 통해 파드에 요청을 보내는 것은 번거롭다. 다행히 kubectl proxy를 사용해 인증과 SSL 인증서를 처리하지 않고 API 서버와 통신하는 방법을 배웠기 떄문에, 프록시를 다시 실행해보자

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

kubectl proxy를 통해 API 서버와 통신하므로 실제 API 서버의 호스트와 포트가 아닌 localhost:8001을 사용할 수 있다.

$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: No data posted yet

응답은 요청이 실제로 받아들여지고 kubia-0 파드에서 실행 중인 애플리케이션으로 처리되는 것을 보여준다.

파드에 전달한 요청은 GET 요청이지만 API 서버로 POST 요청을 보낼 수도 있다. 이것은 GET 요청을 보낸 것과 동일한 프록시 URL로 POST 요청을 전송해 수행된다. 애플리케이션 POST요청을 받으면 요청 본문에 있는 내용을 로컬 파일에 저장한다.

POST요청을 kubia-0 파드에 전송한다.

$ curl -X POST -d "Hey there! This greeting was submitted to kubia-0." localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
Data stored on pod kubia-0

이제 전송한 데이터가 파드에 저장돼야 한다. 다시 GET 요청을 수행해보자.

$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-1/proxy/
You've hit kubia-0
Data stored on this pod: Hey there! This greeting was submitted to kubia-0.

다른 클러스터 노드의 kubia-1 파드를 살펴보자

$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-1/proxy/
You've hit kubia-1
Data stored on this pod: No data posted yet

예상한 대로 노드는 자체의 상태를 가진다.

스테이트풀 파드를 삭제해 재스케줄링된 파드가 동일 스토리지에 연결되는지 확인하기

kubia-0 파드를 삭제하고 재스케줄링될 때 까지 기다리자. 이전과 동일한 데이터를 서비스 하는지 확인할 수 있다

$ kubectl delete po kubia-0
pod "kubia-0" deleted
$ kubectl get po
NAME                       READY   STATUS              RESTARTS   AGE
kubia-0                    0/1     ContainerCreating   0          1s
kubia-1                    1/1     Running             0          9m28s

새 파드가 생성되었다 이전 상태와 동일한 아이덴티티를 갖는지 확인해보자.
파드 이름은 같지만 호스트이름과 영구데이터는 어떨까?

$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: Hey there! This greeting was submitted to kubia-0.

파드의 응답이 호스트 이름과 데이터가 이전과 동일함을 나타낸다. 이로써 스테이트풀셋이 항상 삭제된 파드를 완전히 동일한 파드로 교체함을 확인했다.

스테이트풀셋 스케일링

스테이트풀셋의 스케일 다운과 오랜 시간 이후에 다시 스케일 업을 하는 것은 파드를 삭제 하고 스테이트풀셋이 즉시 재생성하는 것과 큰 차이가 없어야 한다. 스테이트풀셋의 스케일 다운은 파드를 삭제하지만 퍼시스턴트볼륨클레임은 변경되지 않은 상태로 유지된다.

스테이트풀 파드를 헤드리스가 아닌 일반적인 서비스로 노출하기

보통 클라이언트는 파드에 직접 연결하는 것보다 서비스를 통해 연결하므로 헤드리스가 아닌 서비스를 파드 앞에 추가해본다.

kubia-service-public.yaml

apiVersion: v1
kind: Service
metadata:
  name: kubia-public
spec:
  selector:
    app: kubia
  ports:
  - port: 80
    targetPort: 8080

이것이 외부에 서비스를 노출하지 않으므로 (노드포트나 로드밸런서 유형이 아닌 일반적인 ClusterIP이다) 클러스터 내부에만 접근 가능하다.

$ curl localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/
You've hit kubia-1
Data stored on this pod: No data posted yet

클라이언트 내부의 클라이언트는 클러스터된 데이터 저장소에 데이터를 저장하고 읽으려면 kubia-public 서비스를 사용할 수 있다. 물론 각 요청은 임의의 클러스터 노드에 전달되고 매 호출마다 임의의 노드에서 데이터를 가져온다.

스테이트풀셋의 피어 디스커버리

클러스터된 애플리케이션의 중요한 요구사항은 피어 디스커버리이다. 스테이트풀셋의 각 맴버는 모든 다른 맴버를 쉽게 찾을 수 있어야 한다.

SRV 레코드 소개

SRV 레코드는 특정 서비스를 제공하는 서버의 호스트 이름과 포트를 가리키는 데 사용된다. 쿠버네티스는 헤드리스 서비스를 뒷받침하는 파드의 호스트 이름을 가리키도록 SRV 레코드를 생성한다.

새 임시 파드 내부에서 DNS 룩업 도구인 dig를 실행해 스테이트풀 파드의 SRV 레코드를 조회할 수 있다

$ kubectl run -it srvlookup --image=tutum/dnsutils --rm --restart=Never -- dig SRV kubia.default.svc.cluster.local

이 명령은 srvlookup이라 부르는 일회용 파드를 실행하고 콘솔에 연결되며 종료되자마자 바로 삭제된다. 파드를 tutum/dnsutils 이미지의 단일 컨테이너를 실행하고 다음 명령을 수행한다.

dig SRV kubia.default.svc.cluster.local

...
;; ANSWER SECTION:
kubia.default.svc.cluster.local. 30 IN SRV    10 33 0 kubia-0.kubia.default.svc.cluster.local.
kubia.default.svc.cluster.local. 30 IN SRV    10 33 0 kubia-1.kubia.default.svc.cluster.local.

;; ADDITIONAL SECTION:
kubia-0.kubia.default.svc.cluster.local. 30 IN A 172.17.0.4
kubia-1.kubia.default.svc.cluster.local. 30 IN A 172.17.0.6
...

조회한 내용에서 ANSWER SECTION 에는 헤드리스 서비스를 뒷받침하는 두 개의 파드를 가리키는 두 개의 SRV 레코드를 보여준다. 또한 각 파드는 ADDITIONAL SECTION에 표시된 것처럼 자체 A 레코드를 가진다.

파드가 스테이트풀셋의 다른 모든 파드의 목록을 가져오려면 SRV DNS 룩업을 수행하기만 하면 된다.

dns.resolveSrv("kubia.default.svc.cluster.local", callBackFunction)

애플리케이션에서 이 명령어를 사용하면 각 파드가 피어를 디스커버리 할 수 있다.

DNS를 통한 피어 디스커버리

각 데이터 저장소 노드는 완전히 다른 저장소와 독립적으로 실행되고 서로 간에 커뮤니케이션이 존재하지 않는다. 이들을 통신하도록 해보자.
스테이트풀셋과 SRV 레코드를 사용할 것이다.

app.js 를 수정한다

app.js

const http = require('http');
const os = require('os');
const fs = require('fs');
const dns = require('dns');

const dataFile = "/var/data/kubia.txt";
const serviceName = "kubia.default.svc.cluster.local";
const port = 8080;


function fileExists(file) {
  try {
    fs.statSync(file);
    return true;
  } catch (e) {
    return false;
  }
}

function httpGet(reqOptions, callback) {
  return http.get(reqOptions, function(response) {
    var body = '';
    response.on('data', function(d) { body += d; });
    response.on('end', function() { callback(body); });
  }).on('error', function(e) {
    callback("Error: " + e.message);
  });
}

var handler = function(request, response) {
  if (request.method == 'POST') {
    var file = fs.createWriteStream(dataFile);
    file.on('open', function (fd) {
      request.pipe(file);
      response.writeHead(200);
      response.end("Data stored on pod " + os.hostname() + "\n");
    });
  } else {
    response.writeHead(200);
    if (request.url == '/data') {
      var data = fileExists(dataFile) ? fs.readFileSync(dataFile, 'utf8') : "No data posted yet";
      response.end(data);
    } else {
      response.write("You've hit " + os.hostname() + "\n");
      response.write("Data stored in the cluster:\n");
      dns.resolveSrv(serviceName, function (err, addresses) {	// 애플리케이션에서 SRV레코드를 얻기 위해 DNS 룩업을 수행한다.
        if (err) {
          response.end("Could not look up DNS SRV records: " + err);
          return;
        }
        var numResponses = 0;
        if (addresses.length == 0) {
          response.end("No peers discovered.");
        } else {
          addresses.forEach(function (item) {	// SRV 레코드가 가리키는 각 파드는 데이터를 가져오기 위해 연결된다.
            var requestOptions = {
              host: item.name,
              port: port,
              path: '/data'
            };
            httpGet(requestOptions, function (returnedData) {	// SRV 레코드가 가리키는 각 파드는 데이터를 가져오기 위해 연결된다.
              numResponses++;
              response.write("- " + item.name + ": " + returnedData + "\n");
              if (numResponses == addresses.length) {
                response.end();
              }
            });
          });
        }
      });
    }
  }
};

var www = http.createServer(handler);
www.listen(port);

스테이트풀셋 업데이트

스테이트풀셋이 이미 실행 중이므로 파드 템플릿을 업데이트해 파드가 새 이미지를 사용하는 방법을 살펴본다. 이와 함께 레플리카 수를 3으로 설정한다.

$ kubectl edit statefulset kubia
statefulset.apps/kubia edited

spec.replicas를 3으로 수정후, spec.template.spec.container.image 속성에 새 이미지를 가르키도록 변경한다.( 새 이미지는 luksa/kubia-pet-peers 로 미리 만들었다)

$ kubectl get po
NAME                       READY   STATUS              RESTARTS   AGE
kubia-0                    1/1     Running             0          11m
kubia-1                    1/1     Running             0          21m
kubia-2                    0/1     ContainerCreating   0          17s

새 파드 인스턴스가 새 이미지를 실행 중이다. 하지만 기존 두 개의 레플리카는 AGE로 판단해보면 최신으로 업데이트되지 못한 것 같다. 스테이트풀셋은 레플리카셋과 비슷하고 디플로이먼트와 비슷하지 않으므로 템플릿이 수정될 때 롤아웃을 수행하지 않기 때문이다. 레플리카를 수동으로 삭제하여 새 템플릿에 기반해 레플리카를 다시 생성하자

$ kubectl delete po kubia-0 kubia-1
pod "kubia-0" deleted
pod "kubia-1" deleted

클러스터된 데이터 저장소 사용하기

예상대로 동작하는지 확인해보자

$ curl -X POST -d "The sun is shinng" localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/
Data stored on pod kubia-0


$ curl -X POST -d "The weather is sweet" localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/
Data stored on pod kubia-1

이제 데이터 저장소에서 읽어보자

$ curl localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/
You've hit kubia-2
Data stored on each cluster node:
- kubia-0.kubia.default.svc.cluster.local: The weather is sweet
- kubia-1.kubia.default.svc.cluster.local: The sun is shining
- kubia-2.kubia.default.svc.cluster.local: No data posted yet

클라이언트 요청이 클러스터 노드 중 하나에 도달하면 모든 피어를 디스커버리해 데이터를 수집한 다음 모든 데이터를 다시 클라이언트로 보낸다. 스테이트풀셋을 스케일 업하거나 스케일 다운하더라도 클라이언트 요청을 서비스하는 파드는 항상 그 시점에 실행 중인 모든 피어를 찾을 수 있다.

스테이트풀 애플리케이션의 인스턴스가 피어를 디스커버리하고 수평 확장을 쉽게 처리할 수 있는지 보여주었다.

스테이트풀셋이 노드 실패를 처리하는 과정 이해하기

쿠버네티스는 새로운 대체 파드를 생성하기 전에 스테이트풀 파드가 더 이상 실행 중이지 않음을 절대적으로 확신해야 한다고 말했다. 노드가 갑자기 실패하면 쿠버네티스는 노드나 그 안의 파드의 상태를 알 수 없다.

스테이트풀셋은 노드가 실패한 경우 동일한 아이덴티티와 스토리지를 가진 두 개의 파드가 절대 실행되지 않는 것을 보장하므로, 스테이트풀셋은 파드가 더 이상 실행되지 않는다는 것을 확신할 때까지 대체 파드를 생성할 수 없으며, 생성해서도 안된다.

오직 클러스터 관리자가 알려줘야만 알 수 있다. 이를 위해 관리자는 파드를 삭제하거나 전체 노드를 삭제해야 한다.

노드의 네트워크 연결 해제 시뮬레이션

노드의 eth0 네트워크 인터페이스를 셧다운 시켜 노드의 네트워크를 끊어보자

노드 중 하나에 ssh 접속 후, 네트워크를 끊는다.

$ gcloud compute ssh gke-kubia-default-pool-c2f41b01-3gd0

$ sudo ifconfig eth0 down

쿠버네티스 마스터에서 본 노드 상태 확인

$ kubectl get node
NAME                                   STATUS     ROLES    AGE     VERSION
gke-kubia-default-pool-c2f41b01-37p5   Ready      <none>   7d11h   v1.22.11-gke.400
gke-kubia-default-pool-c2f41b01-3gd0   NotReady   <none>   7d11h   v1.22.11-gke.400
gke-kubia-default-pool-c2f41b01-wgt5   Ready      <none>   7d11h   v1.22.11-gke.400

컨트롤 플레인이 노드로부터 더 이상 상태 업데이트를 받을 수 없으므로 노드에 있는 모든 파드의 상태는 Terminating이다

$ kubectl get po
NAME                       READY   STATUS        RESTARTS   AGE
kubia-0                    1/1     Terminating   0          12m
kubia-1                    1/1     Running       0          12m
kubia-2                    1/1     Running       0          15m

앞에서 보듯이 파드가 네트워크 인터페이스를 셧다운한 노드에서 상태를 더이상 알 수 없다.

Terminating 상태인 파드에 무슨 일이 일어나는지 이해하기

노드가 다시 온라인 상태로 돌아와 파드의 상태를 다시 보고하면 다시 Running 으로 표시된다. 하지만 몇분이 지나도 파드의 상태가 Terminating으로 남아 있다면 파드는 자동으로 노드에서 제거된다. 쿠버네티스 컨트롤플레인(마스터)에서 이 동작을 수행한다.

현재 상황을 살펴보자. kubectl describe를 사용해 파드의 상세 정보를 표시하자

$ kubectl describe po kubia-0
Name:         kubia-0
Namespace:    default
Priority:     0
Node:         gke-kubia-default-pool-c2f41b01-3gd0/10.138.0.4
...
Status:       Terminating 
Reason:		  NodeLost

파드의 종료 이유에 NodeLost로 조회되면서 Terminating으로 표시된다. 메시지는 노드가 응답이 없기 때문에 노드가 손실됐다고 간주한다.

수동으로 파드 삭제

노드가 돌아오지 않았지만 클라이언트를 적절히 처리하려면 세 개의 파드가 실행중이어야 한다. 정상 노드로 다시 스케줄링해 kubia-? 파드를 가져와야 한다.

$ kubectl delete po kubia-0
pod "kubia-0" deleted

$ kubectl get po
NAME                       READY   STATUS        RESTARTS   AGE
kubia-0                    1/1     Terminating   0          18m

파드를 삭제하고 조회를 해보면 동일한 파드가 남아있다.
노드의 네트워크가 다운됐으므로 컨트롤 플레인이 이미 파드를 삭제했기 때문에다(노드를 제거하기 위해)

파드를 강제하는 방법을 알아보자

$ kubectl delete po kubia-0 --force --grace-period 0
warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "kubia-0" force deleted

--force와 --grace-period 0 옵션을 동시에 사용해야한다. kubectl 결과의 warning에 수행한 작업을 알려준다. 파드를 다시 조회해보면 비로소 kubia-0 파드가 다시 생성됐음을 알 수 있다.

$ kubectl get po
NAME                       READY   STATUS              RESTARTS   AGE
kubia-0                    0/1     ContainerCreating   0          13s

스테이트풀셋 예제는 여기까지다. 노드를 다시 온라인으로 만들자

$ gcloud compute instances reset gke-kubia-default-pool-c2f41b01-3gd0
Updated [https://www.googleapis.com/compute/v1/projects/bright-vision-360512/zones/us-west1-a/instances/gke-kubia-default-pool-c2f41b01-3gd0].
profile
노옵스를향해

0개의 댓글