10. 쿠버네티스 핵심 개념 (5)

10. 쿠버네티스 핵심 개념 (5)

반응형

이 문서는 인프런 강의 "데브옵스를 위한 쿠버네티스 마스터"을 듣고 작성되었습니다. 최대한 요약해서 강의 내용을 최소로 하는데 목표를 두고 있어서, 더 친절하고 정확한 내용을 원하신다면 강의를 구매하시는 것을 추천드립니다. => 강의 링크

네트워크 볼륨 (NFS)

Kubernetes 는 스토리지로 네트워크 파일 시스템( NFS , GlusterFS )을 이용할 수 있다. 이 절에서는 NFS 를 볼륨으로 이용하는 것에 대해서 다룬다. 이 절은 "VM"에서 진행한다. 먼저 모든 노드에 NFS 를 설치한다.

$ sudo -i # apt-get install nfs-common nfs-kernel-server portmap -y

그리고 노드 1개를 선택해서 다음 작업을 진행한다. 나는 "슬레이브2"에서 진행했다.

## nfs용 디렉토리 생성 # mkdir /home/nfs ## 권한 부여 # chmod 777 /home/nfs ## 모든 노드의 ip nfs 디렉토리 접근할 수 있도록 권한 부여 # tee /etc/exports <: <마운트 디렉토리> # mount -t nfs 10.0.2.5:/home/nfs /mnt ## 테스트 파일 생성 # echo test >> /home/nfs/test.txt ## /mnt에 공유되는지 확인 # cat /mnt/test.txt test

그 후 다음과 같이 volume-nfs.yaml 파일을 만든다.

src/ch10/k8s/volume-nfs.yaml

apiVersion: v1 kind: Pod metadata: name: mongodb spec: containers: - image: mongo name: mongodb volumeMounts: - mountPath: /data/db name: mongodb ports: - containerPort: 27017 volumes: - name: mongodb nfs: server: 10.0.2.5 path: /home/nfs

이제 리소스를 생성한다.

$ kubectl create -f volume-nfs.yaml

생성이 끝나면 mongodb 포드에 접속해서 mongo 명령어를 실행한다.

$ kubectl exec -it mongodb mongo ... >

이제 다음과 같이 mongodb 명령어를 사용해보자. 데이터베이스를 생성하고, 객체를 하나 저장하고 쿼리하는 내용이다.

# 데이터베이스 생성 및 선택 > use mydb switched to db mydb # 데이터 추가 > db.foo.insert({name: "test", value: 1234}) WriteResult({ "nInserted" : 1 }) # 데이터 쿼리 > db.foo.find() { "_id" : ObjectId("611280e2a9c5bd71d0b71690"), "name" : "test", "value" : 1234 } # 종료 > exit

이제 포드를 제거해보자.

$ kubectl delete -f volume-nfs.yaml

그 후 다시 생성한다.

$ kubectl create -f volume-nfs.yaml

정상적으로 영구 디스크를 볼륨으로 사용하고 있었다면, 포드를 삭제했더라도 데이터가 남아 있을 것이다. mongodb 에 접속해서 데이터베이스 선택, 데이터 쿼리를 진행해보자.

$ kubectl exec -it mongodb mongo ... # 데이터베이스 선택 > use mydb switched to db mydb # 데이터 쿼리 db.foo.find() { "_id" : ObjectId("611280e2a9c5bd71d0b71690"), "name" : "test", "value" : 1234 } # 종료 > exit

잘 수행이 된다 굳!

클라우드 네트워크 볼륨 (gcePersistentDisk)

이 절은 "GCP"에서 진행한다. 이전 절에서 했던 작업을 GCP 같은 클라우드엣거는 미리 제공을 한다. 여기서는 NFS 역할을 하는 영구 디스크인 gcePersistentDisk 를 볼륨으로 사용하는 것을 다룬다. 먼저 영구 디스크인 gcePersistentDisk 를 생성한다.

$ gcloud compute disks create --size=10GB --zone=asia-northeast3-a mongodb

그 후 다음과 같이 volume-nfs-gce.yaml 파일을 만든다.

src/ch10/k8s/volume-nfs-gce.yaml

apiVersion: v1 kind: Pod metadata: name: mongodb spec: containers: - image: mongo name: mongodb volumeMounts: - mountPath: /data/db name: mongodb ports: - containerPort: 27017 volumes: - name: mongodb gcePersistentDisk: pdName: mongodb fsType: ext4

이제 리소스를 생성한다.

$ kubectl create -f volume-nfs-gce.yaml

생성이 끝나면 mongodb 포드에 접속해서 mongo 명령어를 실행한다.

$ kubectl exec -it mongodb mongo ... >

이제 다음과 같이 mongodb 명령어를 사용해보자. 데이터베이스를 생성하고, 객체를 하나 저장하고 쿼리하는 내용이다.

# 데이터베이스 생성 및 선택 > use mydb switched to db mydb # 데이터 추가 > db.foo.insert({name: "test", value: 1234}) WriteResult({ "nInserted" : 1 }) # 데이터 쿼리 > db.foo.find() { "_id" : ObjectId("611280e2a9c5bd71d0b71690"), "name" : "test", "value" : 1234 } # 종료 > exit

이제 포드를 제거해보자.

$ kubectl delete -f volume-nfs-gce.yaml

그 후 다시 생성한다.

$ kubectl create -f volume-nfs-gce.yaml

정상적으로 영구 디스크를 볼륨으로 사용하고 있었다면, 포드를 삭제했더라도 데이터가 남아 있을 것이다. mongodb 에 접속해서 데이터베이스 선택, 데이터 쿼리를 진행해보자.

$ kubectl exec -it mongodb mongo ... # 데이터베이스 선택 > use mydb switched to db mydb # 데이터 쿼리 db.foo.find() { "_id" : ObjectId("611280e2a9c5bd71d0b71690"), "name" : "test", "value" : 1234 } # 종료 > exit

잘 수행이 된다 굳!

PV와 PVC (1) 정적 프로비저닝

이 절은 "GCP"에서 진행된다. 그리고 이전 절 "클라우드 네트워크 볼륨 (gcePersistentDisk)"을 진행하고 오길 바란다.

PersistentVolume(이하 pv) 는 Kubernetes 에서 유일하게 운영자가 관리하는 리소스이다. 실제적으로 스토리지 연결을 이 리소스로 한다고 생각하면 된다. PersistentVolumeClaim(이하 pvc) 는 추상 계층으로써 개발자가 Kubernetes 에서 스토리지 작업을 하지 않고도 작업된 스토리지 볼륨을 사용하는데 쓰이는 리소스이다. 대충 이런 느낌이랄까?

이제 다음과 같이 volume-pv.yaml 파일을 만든다.

src/ch10/k8s/volume-pv.yaml

apiVersion: v1 kind: PersistentVolume metadata: name: mongo-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce - ReadOnlyMany persistentVolumeReclaimPolicy: Retain gcePersistentDisk: pdName: mongodb fsType: ext4 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mongo-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: "" --- apiVersion: v1 kind: Pod metadata: name: volume-pv spec: containers: - image: mongo name: mongodb volumeMounts: - mountPath: /data/db name: mongodb ports: - containerPort: 27017 volumes: - name: mongodb persistentVolumeClaim: claimName: mongo-pvc

pod 에서 볼륨은 이제 pvc 로 지정하는 것을 볼 수 있다. 이 때 claimName 은 pvc 의 이름이어야 한다. 또한 pvc 에 작성된 spec 을 토대로 알맞는 pv 가 있다면 그것을 사용하게 된다. 그리고 pv 는 이전 절과 같이 gcePersistentDisk 를 사용한다. 이제 리소스를 생성한다.

$ kubectl create -f volume-pv.yaml

생성이 끝나면 다음 명령어들을 확인해보자.

$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE mongo-pv 10Gi RWO,ROX Retain Bound default/mongo-pvc 25s $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mongo-pvc Bound mongo-pv 10Gi RWO,ROX 28s

이때 pv 에서 CLAIM 에서 pvc 가 보이는지 STATUS 는 "Bound"인지 확인한다. 마찬가지로 pvc 에서는 STATUS 가 "Bound"인지 VOLUME 이 pv 의 이름이 설정됐는지 확인하면 된다. 이제 mongodb 포드에 접속해서 mongo 명령어를 실행한다.

$ kubectl exec -it mongodb mongo ... >

이제 다음과 같이 mongodb 명령어를 사용해보자. 이전 절에서 진행했다면 데이터베이스를 선택하고 바로 저장한 객체를 다음과 같이 불러올 수 있을 것이다.

# 데이터베이스 선택 > use mydb switched to db mydb # 데이터 쿼리 db.foo.find() { "_id" : ObjectId("611280e2a9c5bd71d0b71690"), "name" : "test", "value" : 1234 } # 종료 > exit

PV와 PVC (2) 동적 프로비저닝 StorageClass

이 절은 "GCP"에서 진행한다. pv 는 몇 가지 고려 사항이 몇 가지 있다.

개발 팀에 스토리지를 관리할 수 있는 자원이 있는가 디스크의 양이 충분한가

결국 pv 는 스토리지 영역을 컨트롤 할 수 있는 운영자가 절대적으로 필요하다. 만약 이런 부분이 미흡하다면 어떻게 해야 할까? StorageClass 를 이용하면 이 문제를 손쉽게 해결할 수 있다. StorageClass 는 pv 를 만들지 않고도 동적으로 pvc 가 요청한 크기만큼 스토리지를 볼륨으로 프로비저닝할 수가 있다. 다만 클라우드 프로바이더 GCP , AWS , Azure 등의 환경에서 사용하는 것이 권장된다.

다음과 같이 코드를 작성해보자. (이 때 pv , pvc 는 모두 삭제해두어야 한다.)

src/ch10/k8s/volume-storage-class.yaml

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: mongo-stroage-class provisioner: kubernetes.io/gce-pd parameters: type: pd-ssd --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mongo-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: mongo-stroage-class --- apiVersion: v1 kind: Pod metadata: name: mongodb spec: containers: - image: mongo name: mongodb volumeMounts: - mountPath: /data/db name: mongodb ports: - containerPort: 27017 volumes: - name: mongodb persistentVolumeClaim: claimName: mongo-pvc

이전 절에서 진행했던 코드에서 다음이 변경되었다.

pv는 storageClass로 대체한다. pvc의 storageClassName 필드에 storageClass의 이름을 할당한다.

이제 리소스를 생성한 후 다음 명령어들을 확인해보자.

# 리소스 생성 $ kubectl create -f volume-storage-class.yaml # pv 확인 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE mongo-pv 10Gi RWO,ROX Retain Released default/mongo-pvc 34m pvc-9a48f495-1af2-4174-824b-20c28d177615 1Gi RWO Delete Bound default/mongo-pvc mongo-stroage-class 4m18s # pvc 확인 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mongo-pvc Bound pvc-9a48f495-1af2-4174-824b-20c28d177615 1Gi RWO mongo-stroage-class 4m39s

pvc 에서 요청했던대로 1Gi 만큼의 볼륨이 생긴 것을 확인할 수 있다. 디스크를 확인해보면 ssd 형 디스크가 하나 생성한 것을 확인할 수 있다.

StatefulSet

StatefulSet 은 애플리케이션의 상태를 저장하고 관리하는데 사용되는 리소스이다. Deployment 와 상당히 유사한데, 차이점이라면 Deployment 는 포드가 무작위로 생성되고 배치되는 반면, StatefulSet 은 포드의 순서와 배치를 결정할 수 있다.

StatefulSet 은 다음과 같은 경우에 사용할 수 있다.

안정적이고 고유한 네티워크 식별자가 필요한 경우 안정적이고 지속적인 스토리지를 사용해야 하는 경우 질서 정연한 포드의 배치와 확장을 원하는 경우 포드의 자동 롤링 업데이트를 사용하기 원하는 경우

각 포드의 상태를 유지할 수 있는 장점이 생기는 반면 다음과 같은 단점도 생긴다.

StatefulSet 관련된 볼륨이 자동으로 삭제되지 않는다. Pod의 Storage는 PV 혹은 StorageClass로 프로비저닝을 해주어야 한다. 롤링 업데이트 수행 시 수동으로 복구해야 하는 경우가 생긴다. Pod 네트워크 ID를 유지하기 위해서 Headless Service가 필요하게 된다.

즉 상태를 유지할 수 있는 장점과 동시에 수동으로 관리의 필요성이 생긴다는 단점이 생긴다. 한 번 StatefulSet 을 만들어보자. 이 절은 "GCP"에서 진행한다. (로컬도 상관은 없다.) 다음과 같이 파일을 작성한다.

src/ch10/k8s/statefulset.yaml

apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None # Headless selector: app: nginx --- apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: selector: matchLabels: app: nginx serviceName: "nginx" replicas: 3 template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 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" ] storageClassName: "stateful-set-storage-class" resources: requests: storage: 1Gi --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: stateful-set-storage-class provisioner: kubernetes.io/gce-pd parameters: type: pd-ssd

작성 요령은 Deployment 와 거의 동일하다. 가장 먼저 차이점은 Service 작성 요령에 있다.

apiVersion: v1 kind: Service # ... spec: # ... clusterIP: None # Headless

spec.clusterIP 의 값이 "None"이다. 이렇게 작성된 리소스를 Headless Service 라고 한다. StatefulSet 이 관리하는 Pod 를 Service 로 연결해주려면 반드시 이렇게 만들어주어야 한다.

# ... apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: # ... spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: k8s.gcr.io/nginx-slim:0.8 ports: - containerPort: 80 name: web # ...

StatefulSet 관련해서 Deployment 와 가장 큰 차이점은 Pod 에 대해 작성할 때 다음 데이터가 반드시 필요하다.

spec.terminationGracePeriodSeconds

spec.containers[].ports.ports[].name

terminationGracePeriodSeconds 는 리소스를 종료할 때 대기해주는 시간을 의미한다. 순서대로 배치시키기 때문에 이 값은 필수적으로 들어간다. 또한, ports 관련 작성 시 이름이 필수적으로 들어가게 된다.

# ... apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: selector: # ... volumeClaimTemplates: - metadata: name: www spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "stateful-set-storage-class" resources: requests: storage: 1Gi

또한 volumeClaimTemplates 에서 pvc 와 연결해주어야 한다. 작성 요령은 Pod 의 volumes 와 유사하다. 이제 터미널에 다음을 입력하여 리소스를 생성한다.

$ kubectl create -f statefulset.yaml service/nginx created statefulset.apps/web created storageclass.storage.k8s.io/stateful-set-stroage-class created

리소스가 다 생성된다면 다음 명령어로 만들어진 리소스들을 확인해보자.

$ kubectl get pod -w # 순서대로 web-n (0, 1, 2...) 로 만들어지는 것을 확인할 수 있다. NAME READY STATUS RESTARTS AGE web-0 0/1 ContainerCreating 0 26s web-0 1/1 Running 0 32s web-1 0/1 Pending 0 0s web-1 0/1 Pending 0 0s web-1 0/1 Pending 0 7s web-1 0/1 ContainerCreating 0 7s web-1 1/1 Running 0 22s web-2 0/1 Pending 0 0s web-2 0/1 Pending 0 0s web-2 0/1 Pending 0 7s web-2 0/1 ContainerCreating 0 7s web-2 1/1 Running 0 17s $ kubectl get pvc # 역시 www-web-n (0, 1, 2..) 형식으로 출력된다. NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE www-web-0 Bound pvc-fc161b94-5c7d-4367-b15d-c148eab6cdea 1Gi RWO stateful-set-storage-class 2m16s www-web-1 Bound pvc-a49ba9cf-2b6d-48e7-b8c8-d78ec259d554 1Gi RWO stateful-set-storage-class 104s www-web-2 Bound pvc-7e940025-e320-4da5-a3ef-500b4e5134a3 1Gi RWO stateful-set-storage-class 82s $ kubectl get statefulset NAME READY AGE web 3/3 2m55s

이제 StatefulSet 을 스케일 아웃해보자. 3개에서 5개로 Pod 를 늘린다.

$ kubectl scale statefulset web --replicas=5 statefulset.apps/web scaled $ kubectl get pod -w NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 12m web-1 1/1 Running 0 11m web-2 1/1 Running 0 11m # 여기서부터 확인 가능하다. 역시 오름차순 형태로 pod가 생성된다. web-3 0/1 Pending 0 5s web-3 0/1 Pending 0 7s web-3 0/1 ContainerCreating 0 7s web-3 1/1 Running 0 17s web-4 0/1 Pending 0 0s web-4 0/1 Pending 0 0s web-4 0/1 Pending 0 7s web-4 0/1 ContainerCreating 0 7s web-4 1/1 Running 0 17s $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE www-web-0 Bound pvc-fc161b94-5c7d-4367-b15d-c148eab6cdea 1Gi RWO stateful-set-storage-class 13m www-web-1 Bound pvc-a49ba9cf-2b6d-48e7-b8c8-d78ec259d554 1Gi RWO stateful-set-storage-class 13m www-web-2 Bound pvc-7e940025-e320-4da5-a3ef-500b4e5134a3 1Gi RWO stateful-set-storage-class 12m www-web-3 Bound pvc-7565e8f0-7c9a-4f9f-b38f-e6f582e33187 1Gi RWO stateful-set-storage-class 86s www-web-4 Bound pvc-0ddd0bae-7aa9-4543-9dd0-0e176970ba09 1Gi RWO stateful-set-storage-class 69s

이제는 스케일 인해보자. 5개에서 1개로 줄인다.

$ kubectl scale statefulset web --replicas=1 $ kubectl get pod -w NAME READY STATUS RESTARTS AGE NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 14m web-1 1/1 Running 0 13m web-2 1/1 Running 0 13m # web-4는 순식간에 삭제되었다.. 역순으로 삭제된다. web-3 0/1 Terminating 0 118s web-3 0/1 Terminating 0 2m3s web-3 0/1 Terminating 0 2m3s web-2 1/1 Terminating 0 13m web-2 0/1 Terminating 0 13m web-2 0/1 Terminating 0 13m web-2 0/1 Terminating 0 13m web-1 1/1 Terminating 0 14m web-1 0/1 Terminating 0 14m web-1 0/1 Terminating 0 14m web-1 0/1 Terminating 0 14m $ kubectl get pvc www-web-0 Bound pvc-fc161b94-5c7d-4367-b15d-c148eab6cdea 1Gi RWO stateful-set-storage-class 15m www-web-1 Bound pvc-a49ba9cf-2b6d-48e7-b8c8-d78ec259d554 1Gi RWO stateful-set-storage-class 14m www-web-2 Bound pvc-7e940025-e320-4da5-a3ef-500b4e5134a3 1Gi RWO stateful-set-storage-class 14m www-web-3 Bound pvc-7565e8f0-7c9a-4f9f-b38f-e6f582e33187 1Gi RWO stateful-set-storage-class 3m5s www-web-4 Bound pvc-0ddd0bae-7aa9-4543-9dd0-0e176970ba09 1Gi RWO stateful-set-storage-class 2m48s

리소스가 오름차순으로 순차적으로 늘어나는것과 반대로 내림차순으로 삭제되는 것을 확인할 수 있다. 한 가지 이상한 점이 눈에 띈다. pvc 는 삭제되지 않았다는 것이다. 스토리지 볼륨의 경우 삭제하고 싶다면 수동으로 삭제를 진행해야 한다. 이번엔 롤링 업데이트를 해보자. StatefulSet 의 업데이트 전략은 2가지가 있다.

OnDelete : 모든 Pod를 수동으로 삭제 후 새로운 Pod가 업데이트 된다.

RollingUpdate : Pod가 내림차순으로 삭제 후

$ kubectl edit statefulset web

다음 처럼 수정한다. (replicas = 1 -> 3, image = 0.8 -> 0.9)

그 후 리소스 변화를 관찰해보자.

$ kubectl get pod -w NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 20m web-1 0/1 ContainerCreating 0 3s web-1 1/1 Running 0 24s web-2 0/1 Pending 0 0s web-2 0/1 Pending 0 0s web-2 0/1 ContainerCreating 0 0s web-2 1/1 Running 0 17s web-0 1/1 Terminating 0 21m web-0 0/1 Terminating 0 21m web-0 0/1 Terminating 0 21m web-0 0/1 Terminating 0 21m web-0 0/1 Pending 0 0s web-0 0/1 Pending 0 0s web-0 0/1 ContainerCreating 0 0s web-0 1/1 Running 0 10s

먼저 web-0 가 실행되는 상황에서 Pod 가 2개 더 필요하다. 그럼 web-1 , web-2 는 최신 버전인 "0.9"로 컨테이너가 실행된다. 그 후 web-0 가 삭제 후 다시 만들어진다. 다시 버전을 0.8로 복귀시켜보자.

$ kubectl edit statefulset web statefulset.apps/web edited $ kubectl get pod -w NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 2m28s web-1 1/1 Running 0 3m11s web-2 0/1 Terminating 0 2m47s web-2 0/1 Terminating 0 2m50s web-2 0/1 Terminating 0 2m50s web-2 0/1 Pending 0 0s web-2 0/1 Pending 0 0s web-2 0/1 ContainerCreating 0 0s web-2 1/1 Running 0 10s web-1 1/1 Terminating 0 3m24s web-1 0/1 Terminating 0 3m25s web-1 0/1 Terminating 0 3m34s web-1 0/1 Terminating 0 3m34s web-1 0/1 Pending 0 0s web-1 0/1 Pending 0 0s web-1 0/1 ContainerCreating 0 0s web-1 1/1 Running 0 10s web-0 1/1 Terminating 0 3m1s web-0 0/1 Terminating 0 3m2s web-0 0/1 Terminating 0 3m11s web-0 0/1 Terminating 0 3m11s web-0 0/1 Pending 0 0s web-0 0/1 Pending 0 0s web-0 0/1 ContainerCreating 0 0s web-0 1/1 Running 0 10s

역순으로 업데이트되는 것을 확인할 수 있다. 즉 레플리카 수에서 부족한 번호부터 채우되 기본적으로는 역순으로 업데이트하는 것을 확인할 수 있다.

from http://gurumee92.tistory.com/281 by ccl(A) rewrite - 2021-08-14 19:00:16