본문 바로가기

Technical/Cloud, Virtualization, Containers

[Kubernetes StatefulSet] 개요 & Nginx Web Cluster(1/5)

Kubernetes의 컨테이너  오케스트레이션을 이용한 어플리케이션의 구현 방식 또는 기술에서 중요하게 다뤄야 하는 것 중 하나가 StatefulSet 이다. 개념을 명확히 해야 하는 점에서, 아래의 두 가지 개념을 염두에 두고 상황에 따라 잘 활용해야 할 필요가 있어서 본 시리즈를 기획하였다.



Stateless Application

    • Web front-end 와 같이 디스크에 중요한 데이터가 없는 것들
    • 필요한 만큼의 여러 개의 똑같은 컨테이너를 시작/종료할 수 있는 것들
    • Ephemeral 특성 - 컨테이너가 죽으면 내부에 보관중인 데이터는 사라짐
    • Instance 별의 특별한 데이터가 없을 것 

Stateful Application

    • Container-specific 특성(주로 호스트/도메인명, 드물게는 IP 주소)
    • 암호, 인증키, 설정값 등의 Instance 개별 할당
    • 적용가능 Application(Clustered) 들은 다음과 같음
      : MySQL(Galera Cluster) Mongodb(Replicaset), Zookeeper ensemble, PostgreSQL cluster, Redis, ElasticSearch, etc.
   
 





Kubernetes 에서는 Stateful Application의 오케스트레이션을 위해서 StatefulSet(workload API Object, Kubernetes 1.9 버전부터 정식 지원) 형태로 구현되어 있는데,  본 5편으로 이루어진 시리즈에서는 다음의 내용들을 직접 구현하는 방법들을 정리해 두기로 한다. 


여기서 보여지거나 언급되는 각 솔루션들의 구현 형태는 여러 다양한 변형이 존재할 수 있으며, Kubernetes 1.9.x 버전에서 정상 작동하는 실제적 내용으로 수정/검증, 편집된 것이며, 레퍼런스가 있을 경우 아래에 별도 명시한다.


  • Nginx Web Cluster(본 편 ... 1/5)
  • Mongodb Replicaset by StatefulSet(2/5)
  • Mariadb Galera Cluster by StatefulSet and etcd(3/5)
  • Mariadb pxc-cluster(4/5)
  • Zookeeper Ensemble by StatefulSet(5/5)

Nginx Web Cluster 구현

주로 Kubernetes 공식 문서 내용을 기준으로 하여, StatefulSet의 주요 특징들을 확인하는 내용 위주로 정리한다

필요한 사항은 다음과 같다.
  • Running Kubernetes Cluster - v1.9 이상
  • Persistent Storage - GlusterFS(Hyper-converged 또는 External)

다음의 YAML 파일을 이용하여 ns-statefulset 네임스페이스 내에 Nginx 컨테이너 이미지로 StatefulSet 을 생성한다.

# kubectl create namespace ns-statefulset
namespace "ns-statefulset" created

# vi nginx-statefulset-test.yaml
# Service/endpoint for load balancing the client connection from outside
# By NodePort
---
apiVersion: v1
kind: Service
metadata:
  namespace: ns-statefulset
  name: nginx-svc
  labels:
    role: nginx
spec:
  type: NodePort
  ports:
    - port: 80
      name: client
      nodePort: 30080
  selector:
    role: nginxrs

# A headless service to create DNS records
---
apiVersion: v1
kind: Service
metadata:
  namespace: ns-statefulset
  name: nginx-hs
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
    targetPort: 80
  clusterIP: None
  selector:
    role: nginxrs

# StatefulSet for nginx cluster
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  namespace: ns-statefulset
  name: nginx-ss
spec:
  serviceName: nginx-hs
  replicas: 3
  template:
    metadata:
      labels:
        role: nginxrs
        environment: test
    spec:
      containers:
      - name: nginx
        image: gcr.io/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www-pvc
          mountPath: /usr/share/nginx/html
      terminationGracePeriodSeconds: 10
  volumeClaimTemplates:
  - metadata:
      name: www-pvc
      annotations:
        volume.beta.kubernetes.io/storage-class: glusterfs-storage
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 100Mi

# kubectl create -f nginx-statefulset-test.yaml
service "nginx-svc" created
service "nginx-hs" created
statefulset "nginx-ss" created


# kubectl get pods -n ns-statefulset -w
NAME         READY     STATUS              RESTARTS   AGE
nginx-ss-0   0/1       ContainerCreating   0          39s
nginx-ss-0   1/1       Running   0         1m
nginx-ss-1   0/1       Pending   0         0s
nginx-ss-1   0/1       Pending   0         0s
nginx-ss-1   0/1       ContainerCreating   0         0s
nginx-ss-1   1/1       Running   0         1m
nginx-ss-2   0/1       Pending   0         0s
nginx-ss-2   0/1       Pending   0         0s
nginx-ss-2   0/1       ContainerCreating   0         0s
nginx-ss-2   1/1       Running   0         1m
* 별도 설정을 하지 않으면 각 Pod는 순번(Ordinal Index)에 따라 하나씩 순차적으로 생성됨 

# kubectl get pods -n ns-statefulset 
NAME         READY     STATUS    RESTARTS   AGE
nginx-ss-0   1/1       Running   0          5m
nginx-ss-1   1/1       Running   0          4m
nginx-ss-2   1/1       Running   0          2m

# kubectl get service -n ns-statefulset
NAME                                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
glusterfs-dynamic-www-pvc-nginx-ss-0   ClusterIP   10.131.7.158     <none>        1/TCP          10d
glusterfs-dynamic-www-pvc-nginx-ss-1   ClusterIP   10.128.134.234   <none>        1/TCP          10d
glusterfs-dynamic-www-pvc-nginx-ss-2   ClusterIP   10.141.206.45    <none>        1/TCP          10d
nginx-hs                               ClusterIP   None             <none>        80/TCP         10d
nginx-svc                              NodePort    10.137.146.250   <none>        80:30080/TCP   10d

# kubectl get pvc,pv -n ns-statefulset 
NAME                     STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS        AGE
pvc/www-pvc-nginx-ss-0   Bound     pvc-a107f13f-57ed-11e8-8c49-080027f6d038   1G         RWO            glusterfs-storage   10d
pvc/www-pvc-nginx-ss-1   Bound     pvc-a5f52c7d-57ed-11e8-8c49-080027f6d038   1G         RWO            glusterfs-storage   10d
pvc/www-pvc-nginx-ss-2   Bound     pvc-aa78d9ba-57ed-11e8-8c49-080027f6d038   1G         RWO            glusterfs-storage   10d

NAME                                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                               STORAGECLASS        REASON    AGE
pv/pvc-a107f13f-57ed-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-0   glusterfs-storage             10d
pv/pvc-a5f52c7d-57ed-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-1   glusterfs-storage             10d
pv/pvc-aa78d9ba-57ed-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-2   glusterfs-storage             10d



StatefulSet 만의 특징적인 사항들의 확인


[Service와 Pod의 네트워크 ID 확인]


Busybox pod 를 통해 StatefulSet의 Headless Service(nginx-hs)와 Pod의 DNS 정보를 확인

# kubectl run -n ns-statefulset -it busybox --image=busybox

/ # nslookup -type=a nginx-hs.ns-statefulset.svc.cluster.local

Server: 10.128.0.10

Address: 10.128.0.10:53


Name: nginx-hs.ns-statefulset.svc.cluster.local

Address: 10.40.0.4

Name: nginx-hs.ns-statefulset.svc.cluster.local

Address: 10.32.0.12

Name: nginx-hs.ns-statefulset.svc.cluster.local

Address: 10.38.0.4


/ # nslookup -type=a nginx-ss-0.nginx-hs.ns-statefulset.svc.cluster.local

Server: 10.128.0.10

Address: 10.128.0.10:53


Name: nginx-ss-0.nginx-hs.ns-statefulset.svc.cluster.local

Address: 10.40.0.4

* Headless service는 StatefulSet의 도메인을 구성하는 단위이며, ${서비스명}.${네임스페이스}.svc.cluster.local 형식의 도메인이 만들어 짐

* 위의 yaml 파일 내용을 보면 headless service의 spec: 에서 clusterIp: None 으로 지정되어 있는 것이, 일반적인 kubernetes service와 극명하게 차이가 나는 부분이며, 이를 통해 headless service와 연결되는 특정한 Pod에 직접 access 가 가능하게 됨 

* 각 Pod의 이름은 ${StatefulSet 이름}-${순번} 형식으로 결정되며,Pod의 도메인 형식은 ${StatefulSet 이름}-${순번}.${서비스명}.${네임스페이스}.svc.cluster.local 이 됨



[각 Pod의 고유 정보를 설정하고 Scale-out, Self-healing 확인]


# for i in $(seq 0 2); \

  do kubectl exec nginx-ss-$i -n ns-statefulset \

    -- sh -c 'echo $(hostname -f) > /usr/share/nginx/html/index.html'; \

  done


# for i in $(seq 0 2); \

  do kubectl exec nginx-ss-$i -n ns-statefulset \

    -- sh -c 'curl -s localhost'; \

  done 

nginx-ss-0.nginx-hs.ns-statefulset.svc.cluster.local

nginx-ss-1.nginx-hs.ns-statefulset.svc.cluster.local

nginx-ss-2.nginx-hs.ns-statefulset.svc.cluster.local

# for i in $(seq 0 2); \

  do kubectl exec nginx-ss-$i -n ns-statefulset \

    -- sh -c 'cat /usr/share/nginx/html/index.html'; \

  done 

nginx-ss-0.nginx-hs.ns-statefulset.svc.cluster.local

nginx-ss-1.nginx-hs.ns-statefulset.svc.cluster.local

nginx-ss-2.nginx-hs.ns-statefulset.svc.cluster.local


# kubectl scale -n ns-statefulset statefulset nginx-ss --replicas=4

statefulset "nginx-ss" scaled


# kubectl get pods -n ns-statefulset -w

NAME                       READY     STATUS    RESTARTS   AGE

busybox-5f9469d8dc-cwjhf   1/1       Running   1          7h

nginx-ss-0                 1/1       Running   0          7h

nginx-ss-1                 1/1       Running   0          7h

nginx-ss-2                 1/1       Running   0          7h

nginx-ss-3                 1/1       Running   0          8m


# kubectl exec nginx-ss-3 -n ns-statefulset \

  -- sh -c 'echo $(hostname -f) > /usr/share/nginx/html/index.html'

* StatefulSet 의 replicas 를 4로 증가하고 nginx 의 index.html 생성


# curl -s http://10.255.10.170:30080

nginx-ss-3.nginx-hs.ns-statefulset.svc.cluster.local

# curl -s http://10.255.10.170:30080

nginx-ss-2.nginx-hs.ns-statefulset.svc.cluster.local

...

# curl -s http://10.255.10.170:30080

nginx-ss-1.nginx-hs.ns-statefulset.svc.cluster.local

...

# curl -s http://10.255.10.170:30080

nginx-ss-0.nginx-hs.ns-statefulset.svc.cluster.local

* 일반 service인 Nginx-svc 서비스(NodePort 30080으로 expose 됨)로 curl 접속, 각 Pod로 접속이 분배(로드밸런싱) 됨을 확인


# kubectl delete -n ns-statefulset pod nginx-ss-1

pod "nginx-ss-1" deleted


# kubectl get pods -n ns-statefulset -wNAME                       READY     STATUS    RESTARTS   AGE

busybox-5f9469d8dc-cwjhf   1/1       Running   1          7h

nginx-ss-0                 1/1       Running   0          7h

nginx-ss-1                 1/1       Running   0          5s

nginx-ss-2                 1/1       Running   0          7h

nginx-ss-3                 1/1       Running   0          16m


# kubectl exec -it -n ns-statefulset \

  busybox-5f9469d8dc-fv4jw \

  -- sh -c 'wget -qO- nginx-ss-1.nginx-hs.ns-statefulset.svc.cluster.local'

nginx-ss-1.nginx-hs.ns-statefulset.svc.cluster.local

* Pod nginx-ss-1를 삭제하였으나 self-healing이 동작하여 StatefulSet 의 향상은 유지되어, pod nginx-ss-1 에 원래의 Persistent Volume이 연결됨(해당 Pod 고유의 기존 데이터도 그대로 유지)

* Busybox 에서 pod nginx-ss-1의 웹서버에 wget으로 접속, 웹서비스가 정상 동작됨을 확인



[StatefulSet과 관여되는 서비스 Clear, Persistent Volume Clear]


# kubectl delete service -n ns-statefulset nginx-svc 

service "nginx-svc" deleted

# kubectl delete service -n ns-statefulset nginx-hs

service "nginx-hs" deleted

# kubectl delete statefulset -n ns-statefulset nginx-ss

statefulset "nginx-ss" deleted


# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                               STORAGECLASS        REASON    AGE
pvc-978bac5f-ae99-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-0   glusterfs-storage             7h
pvc-9c62652a-ae99-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-1   glusterfs-storage             7h
pvc-a0b1695a-ae99-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-2   glusterfs-storage             7h
pvc-cef812d4-aed3-11e8-8c49-080027f6d038   1G         RWO            Delete           Bound     ns-statefulset/www-pvc-nginx-ss-3   glusterfs-storage             50m

# for i in $(seq 0 3); do kubectl delete pvc -n ns-statefulset www-pvc-nginx-ss-$i; done
persistentvolumeclaim "www-pvc-nginx-ss-0" deleted
persistentvolumeclaim "www-pvc-nginx-ss-1" deleted
persistentvolumeclaim "www-pvc-nginx-ss-2" deleted
persistentvolumeclaim "www-pvc-nginx-ss-3" deleted

* StatefulSet에 연결된 Persistent Volume이 삭제되지 않을 경우, 확인 후 명시적으로 Clear 해 주어야 함. 이는 scale-in을 하여 Pod가 삭제되는 경우에도 동일하게 발생할 수 있으며, 글을 쓰기 시작한 시점에는 이러한 문제를 해결하고자 별도의 노력이 필요한 상황이며, 아래 2번째 레퍼런스를 참고해 볼 필요가 있음.



[References]

  • https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/
  • https://medium.com/@marko.luksa/graceful-scaledown-of-stateful-apps-in-kubernetes-2205fc556ba9



- Barracuda -