기본 콘텐츠로 건너뛰기

[Kubernetes - Operator] KUDO를 활용한 Galera Operator 단계별로 적용하기 - PART 3: Bootstrap 삭제 및 서비스 중단 없이 Scale Up/Down 처리

How to building real world sample step by step - Part 3

게시글 참고

이 게시글은 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 처리하고 동작을 검증하면서 정리한 내용입니다.

  • PART 1에서는 부트스트랩 노드 구성
  • PART 2에서는 클러스터에 노드들이 참여할 떄 사용할 서비스와 설정등을 구성하고 StatefulSet을 구성

이 문서는 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 해결하고 동작을 검증하면서 정리한 내용으로 구성된 Galera Cluster의 사용하지 않는 bootstrap 정보를 제거하고, 외부 연결을 위한 서비스 생성 및 서비스의 중단없이 Scale Up/Down 할 수 있도록 나머지 부분을 적용해 본다. 이 과정까지 완료되면 프로덕션 환경에 적용할 수 있는 정도가 된다.

Cleanup bootstrap node

PART 2 에서 모든 노드들을 클러스터에 참여시켰기 때문에 더 이상은 부트스트랩 노드가 필요하지 않다.

따라서 operator.yaml에 부트스트랩 노드와 관련된 리소스를 제거하는 Step과 Task를 추가하도록 한다.

...
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          ...
          - name: cleanup    # 추가
            tasks:
              - bootstrap_cleanup
tasks:
  ...
  - name: bootstrap_cleanup      # 추가
    kind: Delete
    spec:
      resources:
        - bootstrap_deploy.yaml
        - bootstrap_service.yaml
        - bootstrap_config.yaml

추가된 bootstrap_cleanup은 삭제 작업으로 PART 1에서 만들었던 bootstrap 관련 리소스들이 대상이며 이 작업이 실행되면 resources로 지정된 *.yaml 파일에 정의된 부트스트랩 리소스들 (ConfigMap, Service, Deploy)이 모두 삭제된다.

그러나 실제 Galera 노드들에서 사용하는 ConfigMap에 부트스트랩 노드 정보 (wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc)가 남아 있기 떄문에 Galera 인스턴스가 누락된 노드를 중심으로 작동하므로 반드시 문제가 되는 것은 아닐지라도 완전성을 위해 관련된 정보도 삭제해 주는 것이 좋다.

따라서 opreator.yaml파일에 ConfigMap을 수정할 수있는 Task를 추가하고 Step에서 이 Task를 이용하는 것으로 아래와 같이 변경한다.

...
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          ...
          - name: cleanup
            tasks:
              - bootstrap_cleanup
              - config      # 추가
tasks:
  ...
  - name: bootstrap_cleanup
    kind: Delete
    spec:
      resources:
        - bootstrap_deploy.yaml
        - bootstrap_service.yaml
        - bootstrap_config.yaml
  - name: config      # 추가
    kind: Apply
    spec:
      resources:
        - galera_config.yaml

이번에는 하나의 Step에서 두 개의 Task가 동작하도록 정의했다. deploy phase의 실행 전략을 strategy: serial로 지정했으므로 두 개의 Task는 순차적으로 처리된다.

참고

여기서 주의할 것은 PART 2에서 사용했던 galera_config.yaml을 다시 사용한다는 것이다. 즉, 노드를 설치할 때와 삭제할 때 설정 기능이 다른데 하나의 리소스를 재 사용하는 상황인 것이다. 이를 해결하기 위해서 템플릿에서 .StepName 변수를 활용해서 어떤 Step에서 호출되었는지를 식별해서 처리하는 방식을 사용한다.

StepName 정보를 기준으로 내용을 구성할 수 있도록 템플릿 리소스인 templates/galera_config.yaml 파일을 아래와 같이 수정한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Name }}-nodeconfig
  namespace: {{ .Namespace }}
data:
  galera.cnf: |
    [galera]
    wsrep_on = ON
    wsrep_provider = /usr/lib/galera/libgalera_smm.so
    wsrep_sst_method = mariabackup
    {{ if eq .StepName "firstboot_config" -}}
    wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc,{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
    {{ else -}}
    wsrep_cluster_address = gcomm://{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
    {{ end -}}
    wsrep_sst_auth = "{{ .Params.SST_USER }}:{{ .Params.SST_PASSWORD }}"
    binlog_format = ROW
  innodb.cnf: |
    [innodb]
    innodb_autoinc_lock_mode = 2
    innodb_flush_log_at_trx_commit = 0
    innodb_buffer_pool_size = 122M

wsrep_cluster_address 정보를 지정할 때 템플릿에 조건부 처리를 지정했다. 즉, firstboot_config Step일 경우는 부트스트랩 구성이 적용되고, 그 외는 클러스터에 참여할 노드 정보들만 구성되는 것이다.

{{ if eq .StepName "firstboot_config" -}}
  wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc,{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
{{ else -}}
  wsrep_cluster_address = gcomm://{{ $.Name }}-galera-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-galera-{{ $v }}.{{ $.Name }}-hs{{end}}
{{ end -}}

if, else, end 구문은 -}} 으로 종료시키고 있다. 이는 템플릿 엔진에게 해당 라인을 빈줄로 뇌두지 말고 삭제하도록 지정하는 것이다.

위와 같이 템플릿 내에 조건문을 사용해서 다른 여러 Task들에서 재 사용할 수 있다.

이제 Pod가 재 시작되면 post-bootstrap cluster에 대한 올바른 구성을 가지고 다시 백업을 시작할 수 있게 된다.

지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.

  • KUDO 설치 (using init)

    $ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리
  • Galera Operator 설치 (using install)

    $ kubectl kudo install ./
  • 설치된 Operator Instance 확인

    $ kubectl kudo plan status --instance=galera-operator-instance
  • 설치된 ConfigMap 및 Pod 확인

    $ kubectl describe configmap galera-operator-instance-nodeconfig
    
    Name:         galera-operator-instance-nodeconfig
    Namespace:    default
    Labels:       heritage=kudo
                  kudo.dev/instance=galera-operator-instance
                  kudo.dev/operator=galera-operator
    Annotations:  kudo.dev/last-applied-configuration:
                    {"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"galera-operator-instance-nodeconfig","namespace":"default","creationTimestamp":n...
                  kudo.dev/last-plan-execution-uid: 8cd36d4b-aeb5-473e-8997-1fad6090722a
                  kudo.dev/phase: deploy
                  kudo.dev/plan: deploy
                  kudo.dev/step: cleanup
    
    Data
    ====
    galera.cnf:
    ----
    [galera]
    wsrep_on = ON
    wsrep_provider = /usr/lib/galera/libgalera_smm.so
    wsrep_sst_method = mariabackup
    wsrep_cluster_address = gcomm://galera-operator-instance-galera-0.galera-operator-instance-hs,galera-operator-instance-galera-1.galera-operator-instance-hs,galera-operator-instance-galera-2.galera-operator-instance-hs
    wsrep_sst_auth = "root:admin"
    binlog_format = ROW
    
    innodb.cnf:
    ----
    [innodb]
    innodb_autoinc_lock_mode = 2
    innodb_flush_log_at_trx_commit = 0
    innodb_buffer_pool_size = 122M
    
    Events:  <none>

    정상적으로 ConfigMap이 조건에 따라서 wsrep_cluster_address 설정 값이 부트스트랩 정보없이 노드들의 정보만으로 구성된 것을 확인할 수 있다.

    $ kubectl get pods
    
    NAME                                         READY   STATUS    RESTARTS   AGE
    galera-operator-instance-galera-operator-0   1/1     Running   0          3m28s
    galera-operator-instance-galera-operator-1   1/1     Running   0          3m3s
    galera-operator-instance-galera-operator-2   1/1     Running   0          2m35s
    nfs-client-provisioner-b84668c6d-zxt8d       1/1     Running   0          91m

    부트스트랩 노드가 제거되고 나머지 노드들만 존재하는 것을 확인할 수 있다.

  • KUDO 및 Operator 삭제

    $ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -

Add service for connect from client

이제 클라이언트가 Galera Cluster에 연결할 수 있는 방법이 필요하다. Galera Cluster는 다중 마스터 구조이기 때문에 어디로 연결되더라도 안전하게 읽고 쓸 수 있다. 서비스는 어떤 의존성도 갖지않기 때문에 deploy plan의 어떤 단계에서 생성하던지 상관이 없지만 이미 클러스터 내부의 서비스를 생성한 단계가 있으므로 여기에 추가하도록 한다.

operator.yaml 파일의 이미 존재하는 cluster_service Task에 아래와 같이 추가한다.

...
  - name: cluster_services
    kind: Apply
    spec:
      resources:
        - hs-service.yaml
        - cs-service.yaml   # 추가
...

이제 사용한 서비스 리소스인 templates/cs-service.yaml 파일을 생성하고 아래와 같이 구성한다.

apiVersion: v1
kind: Service
metadata:
  name: {{ .Name }}-cs
  namespace: {{ .Namespace }}
  labels:
    app: galera
    galera: {{ .Name }} 
spec:
  ports:
    - port: {{ .Params.MYSQL_PORT }}
      name: mysql
  selector:
    app: galera
    instance: {{ .Name }}

이 서비스는 mySQL 포트만 필요하고 클라이언트가 Galera Clustr에 연결할 수 있도록 load balancing된 ClusterIP를 제공한다.

지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.

  • KUDO 설치 (using init)

    $ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리
  • Galera Operator 설치 (using install)

    $ kubectl kudo install ./
  • 설치된 Operator Instance 확인

    $ kubectl kudo plan status --instance=galera-operator-instance
  • 설치된 Service 확인

    $ kubectl get services
    
    NAME                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                               AGE
    galera-operator-instance-cs   ClusterIP   10.104.170.201   <none>        3306/TCP                              5m3s
    galera-operator-instance-hs   ClusterIP   None             <none>        3306/TCP,4444/TCP,4567/TCP,4568/TCP   5m3s

    실행 중인 클러스터에 모든 서비스가 생성된 것을 확인할 수 있다. 서비스의 상세 정보를 통해서 정상적으로 생성되었는지 확인한다.

    $ kubectl describe service galera-operator-instance-cs
    
    Name:              galera-operator-instance-cs
    Namespace:         default
    Labels:            app=galera
                      galera=galera-operator-instance
                      heritage=kudo
                      kudo.dev/instance=galera-operator-instance
                      kudo.dev/operator=galera-operator
    Annotations:       kudo.dev/last-applied-configuration:
                        {"kind":"Service","apiVersion":"v1","metadata":{"name":"galera-operator-instance-cs","namespace":"default","creationTimestamp":null,"label...
                      kudo.dev/last-plan-execution-uid: f5b71f8e-b9ef-49b0-81d1-8f82315e63d4
                      kudo.dev/phase: deploy
                      kudo.dev/plan: deploy
                      kudo.dev/step: cluster_services
    Selector:          app=galera,instance=galera-operator-instance
    Type:              ClusterIP
    IP:                10.104.170.201
    Port:              mysql  3306/TCP
    TargetPort:        3306/TCP
    Endpoints:         10.244.247.12:3306,10.244.84.132:3306
    Session Affinity:  None
    Events:
      Type     Reason                        Age    From                       Message
      ----     ------                        ----   ----                       -------
      Warning  FailedToUpdateEndpointSlices  4m37s  endpoint-slice-controller  Error updating Endpoint Slices for Service default/galera-operator-instance-cs: failed to update galera-operator-instance-cs-brmb9 EndpointSlice for Service default/galera-operator-instance-cs: Operation cannot be fulfilled on endpointslices.discovery.k8s.io "galera-operator-instance-cs-brmb9": the object has been modified; please apply your changes to the latest version and try again
    • 클라이언트 연결 확인

    생성한 서비스를 통해서 클라이언트가 Galera Cluster에 접속할 수 있다.

    $ kubectl run mysql-client --image=mysql:5.7 -it --rm --restart=Never -- mysql -u root -h galera-operator-instance-cs -p

    진행 중에 아래와 같은 메시지가 보이면 params.yaml에 정의했던 password를 입력하면 된다.

    If you don't see a command prompt, try pressing enter.

    mysql> 프롬프트가 보이면 정상적으로 연결이 된 것이다.

  • KUDO 및 Operator 삭제

    $ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -

Scale up/down without service interruption

KUDO는 파라미터의 값을 변경함으로써 연계된 Plan 이 구동된다. 파라미터로 정의한 NODE_COUNT를 변경하면 deploy plan이 구동될 것이기 때문에 ConfigMap을 변경하고 클러스터에 새로운 노드 수를 반영하기 위해서 StatefulSet이 재 시작될 필요가 있다.

이런 작업을 반영하기 위해 operator.yaml 파일에 새로운 plan을 아래와 같이 추가한다.

...
plans: 
  deploy:
    ...
  node_update:      # 추가
    strategy: serial
    phases:
      - name: deploy
        stratege: serial
        steps:
          - name: config
            tasks:
              - config
          - name: stateful
            tasks:
              - statefulset
...

이 Plan은 기존의 Task를 재 사용하는 것이기 때문에 별도의 Task 추가는 필요하지 않고 두 개의 Step이 순차적으로 실행되도록 정의한 것이다.

이제 NODE_COUNT 파라미터가 변경되면 위에 정의한 새로운 Plan이 구동될 수 있도록 params.yaml의 파라미터 정보를 아래와 같이 추가한다.

apiVersion: kudo.dev/v1beta1
parameters:
  ...
  - name: NODE_COUNT
    description: "Number of nodes to create in the cluster"
    default: "3"
    trigger: node_update      # 추가
  ...

PART 2 에서는 trigger 정보가 없었으며 이런 경우는 기본 값으로 deploy plan이 호출되게 된다. trigger를 지정해서 node_update plan을 호출하도록 한 것이다.

지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.

  • KUDO 설치 (using init)

    $ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리
  • Galera Operator 설치 (using install)

    $ kubectl kudo install ./
  • 설치된 Operator Instance 확인

    $ kubectl kudo plan status --instance=galera-operator-instance
  • NODE_COUNT 변경 검증

    $ kubectl kudo update --instance galera-operator-instance -p NODE_COUNT=3

    파라미터를 통한 변경이 적용되었기 때문에 변경에 따른 처리를 확인한다.

    $ kubectl kudo plan status --instance=galera-operator-instance

    Pod가 재 시작되는 시간이 있기 때문에 IN_PROGRESS 상태로 나타나며, 이 작업이 진행 중인 동안에 kubectl get pods 명령을 수행하면 추가된 수 만큼 노드가 배포가 되고, 이전에 동작하던 노드들은 한번에 하나씩 재 시작되는 것을 볼 수 있다. 반대로 NODE_COUNT 파라미터의 수를 줄이면 동작하던 노드들이 제거되는 것을 확인할 수 있다.

  • KUDO 및 Operator 삭제

    $ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -

Process syncronization before shuting down nodes

Scale up은 상관이 없지만 down의 경우는 노드가 제거될 때 Galera Cluster의 노드가 동기화되지 않으면 클러스터의 상태에 문제가 발생할 수 있으므로 노드가 종료되기 전에 동기화를 해야할 필요가 있다.

이를 위해서 상태를 검증하는 스크립트를 추가하고 StatefulSet의 spec에 preStop check를 수정해서 처리를 수행하도록 operator.yaml 파일의 deploy plan의 Statefulset Step 이전에 적용될 수 있도록 Step과 Task를 추가한다.

...
plans:
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          ...
          - name: node_scripts    # 추가
            tasks:
              - node_scripts
          ...
          - name: statefulset
          ...
  ...
tasks:
  ...
  - name: node_scripts    # 추가
    kind: Apply
    spec:
      resources:
        - node_scripts.yaml

상태를 검증하는 스크립트를 위한 ConfigMap 리소스인 templates/node_scripts.yaml 파일을 생성하고 아래와 같이 구성한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Name }}-node-scripts
  namespace: {{ .Namespace }}
  wait-for-sync.sh: |
    until mysql -u root -p{{ .Params.MYSQL_ROOT_PASSWORD }} -e "SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';" | grep -q Synced 
    do
        echo "Waiting for sync"
        sleep 5
    done

그리고 이 스크립트가 StatefulSet에 정상적으로 마운트될 수 있도록 templates/statefulset.yaml에 추가한다.

        volumeMounts:
        - name: {{ .Name }}-config
          mountPath: /etc/mysql/conf.d
        - name: {{ .Name }}-datadir
          mountPath: /var/lib/mysql
        - name: {{ .Name }}-node-scripts    # 추가
          mountPath: /etc/galera/wait-for-sync.sh
          subPath: wait-for-sync.sh
      volumes:
      - name: {{ .Name }}-config
        configMap:
          name: {{ .Name }}-nodeconfig
          items:
            - key: galera.cnf
              path: galera.cnf
            - key: innodb.cnf
              path: innodb.cnf
      - name: {{ .Name }}-node-scripts    # 추가
        configMap:
          name: {{ .Name }}-node-scripts
          defaultMode: 0755

ConfigMap에서 subPath로 스크립트을 직접 마운트했다. subPath를 사용할 때 제한들이 있지만 여기서는 큰 의미가 없다.

또한 Kubernetes 클러스터를 구성하는 노드들의 장애로 부터 보호하기 위해서 분리된 노드들로 배포되도록 스케줄되어 있는지에 대한 확인이 필요하다. 이 작업은 AntiAffinity (반 선호도) 규칙을 사용해서 처리가 가능하며 테스트를 위해서 이 규칙을 비활성화 시킬 수 있다. templates/statefulset.yaml 파일에 아래와 같이 조건 섹션을 구성한다.

...
spec:
  {{ if eq .Params.ANTI_AFFINITY "true" }}
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: "app"
                operator: In
                values:
                - galera
              - key: "instance"
                operator: In
                values:
                - {{ .Name }}
          topologyKey: "kubernetes.io/hostname"
  {{ end }}
...

참고

상기 예제의 Anti-Affinity는 반 선호도에 대한 Pod Selector 개념으로 topologyKey를 kubernetes.io/hostname으로 설정해서 Pod가 존재하는 동일한 Host가 아닌 다른 곳으로 배치하나는 의미가 된다.

사용된 ANTI_AFFINITY 파라미터를 params.yaml 파일에 추가한다.

...
  - name: ANTI_AFFINITY
    description: "Enforce pod anit-affinity"
    default: False
...

지금까지의 작업으로 Operator는 Node 선호도를 관리하고 안전하게 Scale up/down이 가능하고, 외부의 클라이언트가 접근할 수 있도록 구성되었다.

이 시점에서 몇 가지 벤치마크와 클러스터를 확장하고 축소하는 동시에 모든 것이 정상적으로 작동하는지를 확인해야 한다. how to measure mysql performance in kubernetes with sysbench를 이용해서 확인이 가능하다.

Conclusion

지금까지 Galera Cluster를 구성하고 운영하는 Operator를 단계별로 확인해 보았으며, 그 과정을 통해서 KUDO에서 제공하는 내장 변수들과 Sprig 템플릿 함수를 이용해서 조건부 템플릿을 적용해 리소스 파일들을 재 사용할 수 있다는 것도 확인했다.

향후에는 시간이 허락하고 관련된 작업을 진행하게 된다면 지금까지 확인한 설치관련 (Day 1 Operation) 뿐만 아니라 운영 중 (Day 2 Operation) 에 발생할 수 있는 상황들에 대한 부분을 추가로 검토해 볼 예정이다.

참고 자료

댓글