새소식

Kubernetes

[Cloud Compute] Kubernetes 구축하기 - 5. mysql + xtrabackup 배포하기

  • -

 

지난 글에서 nginx 서버를 배포해보았으니 이번에는 데이터베이스를 배포해자.

 

nginx 같은 경우에는 stateless인 Deployment로 배포했는데 이번엔 다르게 진행되어야 한다.

기본적으로 데이터베이스는 데이터를 저장하는 것이고 이것은 날아가면 큰일나는 상황이다.

 

문제는 Deployment에서 Pod는 파리목숨과 같아서 날아가면 그 안의 데이터도 같이 날아간다.

이러한 경우에 사용하는 StatefulSet 이라는 것을 사용할 것이다. 그리고 PersistentVolume, PersistentVolumeClaim, Storage Class와 같이 뭔가 복잡해보이는 것까지 진행되기 때문에 내용이 많다.

 

갈길이 머니 서두르자.

 

1. 개념 

시리즈의 목표는 구축과 실습이다. 그러니 개념은 빠르게 훑는 정도로만 알아보자.

나머지공부는 친절한 블로그들이 많다!

 

1. Statefulset(STS)

이름에서부터 stateful 로 상태를 가지는 리소스로 Deployment와 달리 Pod의 이름이 순서대로 나열되어 데이터의 상태관리가 안전하게 이루어질 수 있다.

 

2. PersistentVolume(PV), PersistentVolumeClaim(PVC)

PV은 스토리지 리소스로 직접 또는 동적으로 할당된다. 
PVC은 스토리지를 요청하는 주문서로 필요한 스토리지의 양과 액세스 모드를 지정할 수 있다.

 

3. Storage Class(SC)

PV의 클래스를 말하는데 이 클래스에 따라 프로비저닝 방식이 달라진다. 클라우드 서비스에서 PVC로 PV를 요청하면 자체적으로 지원하는 클래스로 지정하여 PV를 제공하며, 직접 구축한 방식에서는 직접 세팅해야 한다. 여기서는 Local Class를 사용할 예정이다.

 

5. ConfigMap(CM)

Key-Value 쌍으로 일반적인 데이터를 저장하기 위한 리소스로 단순한 string부터 **.conf와 같이 설정파일도 저장하여 사용할 수 잇다.

 

6. Secret

ConfigMap과 유사하지만 보안목적으로 데이터를 저장하는데 사용된다. base64로 인코딩된 값을 저장한다.

공식문서에 따라 주의할 점은 꼭 살펴두는 것이 좋다.

여기서는 어디까지나 테스트목적으로 구축하고 다양한 리소스를 생성하고 접근해보는 것에 목적이 있기에 따로 적용하지는 않는다.

 

2. Storage Class 생성하기

먼저 생성을 진행해보자.

 

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

 

먼저 annotations를 보자.

 

storageclass.kubernetes.io/is-default-class: "true"

 

이 어노테이션의 true/false에 따라 생성되는 StorageClass를 기본으로 적용할 지를 결정한다. 

다음으로 provisioner 라는 항목이 있다. 이 속성은 어떤 방식으로 provisioning을 제공할지 지정하며

kubernetes.io/no-provisioner 은 Local을 의미한다.

우리가 사용할 Local은 동적 프로비저닝을 지원하지 않기 때문에 PVC만 지정해서 편하게 PV를 연결할 수 없다.

 

 

생성에 성공했고 NAME을 보면 (default) 라고 기본클래스로 사용됨을 알 수 있다.

 

 

3. PV(PersistentVolume) 생성하기

PVC를 통해 Volume이 연결될 수 있도록 PV를 생성해두자.

 

apiVersion: v1
kind: PersistentVolume
metadata:
  name: test-pv0
  labels:
    app: mysql
spec:
  storageClassName: local-storage
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /home/ubuntu/test0
    type: DirectoryOrCreate
  persistentVolumeReclaimPolicy: Retain
  claimRef:
    namespace: default
    name: test-mysql-sts-0

 

storageClassName : 어떤 스토리지 클래스를 이용할 것인지 지정하며 위에서 생성한 local-storage를 지정했다.

persistentvolumeReclaimPolicy : PV가 해제된후 volume을 관리하는 방식을 지정한다. 

Retain : Volume을 유지한다. 따라서 데이터도 남아있으며 수동으로 제거해야한다.
Delete : Volume을 폐기한다. 저장될 공간도 없기에 데이터도 폐기된다.

 

clainRef : PVC를 특정하여 바인딩되도록 한다. 여기서는 나중에 생성될 mysql을 위해 특정했다.

 

잘못 계산해서 하나를 더 만들었다...test-pv1은 지워버렸다

 

생성된 결과를 보면 각 속성이 원하는대로 잘 지정되었음을 알 수 있다.

 

4. ConfigMap 생성하기

mysql을 초기화하며 사용할 기본적인 설정값을 ConfigMap을 통해서 넣어보자.

 

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
  labels:
    app: mysql
data:
  master.cnf: |
    [mysqld]
    log-bin
    character-set-server=utf8mb4
    collation-server=utf8mb4_general_ci
    default-time-zone=Asia/Seoul
  slave.cnf: |
    [mysqld]
    super-read-only
    character-set-server=utf8mb4
    collation-server=utf8mb4_general_ci
    default-time-zone=Asia/Seoul
  initdb.sql: |
    grant all privileges on *.* to username@localhost identified by 'password';
    grant all privileges on *.* to username@'127.0.0.1' identified by 'password';

 

 

master.cnf 및 slave.cnf 에는 각각 MySQL 마스터 및 슬레이브 서버에 대한 설정이 포함되어 있다.
문자 집합과 콜레이션을 각각 utf8mb4와 utf8mb4_general_ci로 설정하고 기본 시간대를 Asia/Seoul로 설정했다.

master는 바이너리 로깅을 사용하도록 log-bin 을 추가했다.

initdb.sql 파일에는 MySQL 서버가 처음 초기화될 때 실행될 SQL 명령이 포함되어 있다. 로컬에서 접근할때 "username"이라는 계정에게 모든 권한을 주게 되어있다.

 

늦장부리다보니 글쓰기를 시작한지 45일이 지난 뒤에야 여기에 도달했다...

 

master.cnf, slave.cnf, initdb.sql 3가지 데이터가 저장되었음을 알 수 있다.

kube-root-ca.crt는 인증서로 Kubernetes 내부의 API서버의 TLS인증서를 서명하는데 사용되며 이 인증서로 검증된 클라이언트가 API서버에 접근하여 작업을 수행할 수 있게 한다.

 

5. Secret 생성하기

1-6에서 base64로 인코딩하여 저장해야한다고 적어놨다. 필수사항은 아니지만 혹시나 해당 데이터가 노출되었을 때 알아보기 어렵게 만드는 목적이 있어 강하게 권장되는 사항이다.

 

base64로 인코딩하는 방법은 여럿있지만 여기서는 다음 명령어를 사용할 것이다.

 

echo -n 'value' | base64

 

value부분에 원하는 데이터를 넣어 출력된 결과를 secret의 value로 사용하면 된다.

 

apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
type: Opaque
data:
  root-password: "cm9vdHBhc3N3b3Jk" # rootpassword
  password: "cGFzc3dvcmQ=" # password
stringData:
  root-username: "root" # root
  username: "username" # username

 

여기서 type : Opaque 로 되어있는 것을 볼 수 있다.

Opaque는 기본적용되는 타입으로 사용자가 직접 정의함을 의미하며, 그 외의 타입은 공식문서를 참조하자.

 

 

 

이제 StatefulSet으로 Pod를 배포하기 위한 준비단계가 끝났다.

 

6. Service 배포하기

이제 클러스터 내부/외부에서 접근할 수 있도록 Service를 생성할 순서가 되었다.

 

apiVersion: v1
kind: Service
metadata:
  name: mysql-svc
  labels:
    app: mysql
spec:
  selector:
    app: mysql
  clusterIP: None
  ports:
    - port: 3306
      name: mysql
    - port: 3307
      name: xtrabackup
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-np-svc
  labels:
    app: mysql
spec:
  selector:
    statefulset.kubernetes.io/pod-name: mysql-sts-0
  type: NodePort
  ports:
    - port: 3306
      targetPort: 3306
      nodePort: 30306
      name: mysql

 

이번엔 2개의 리소스가 담긴 yaml을 만들었다.

첫번째 Service는 내부에서 3306포트로 mysql에 접근할 수 있고, 3307포트로 xtrabackup에 접근할 수 있게 설정되었다.

한가지 눈여겨볼만한 속성은 clusterIP 이다.

해당속성이 none 으로 되어있는데 이것을 헤드리스(Headless) 서비스라고 한다.

ClusterIP가 할당되지 않고 각 Pod의 호스트 이름을 DNS 이름으로 사용하여 DNS 레코드를 생성한다.

ReplicaSet의 변동이 생겨도 해당 서비스와 연결된 Pod들의 집합을 받게되기 때문에 안정적이고 유연하게 Scale을 조정할 수 있게 된다.

 

두번째 Service는 NodePort를 사용하는 서비스로 Kubernetes 외부에 직접 포트를 개방하였다.가용포트 30000~32767 (어디서 나온가하면 공식문서를 확인하자) 중에 3306 비스무리하게 30306으로 개방했다.

 

 

첫번째 Service의 CLUSTER-IP가 None으로 된 것을 볼 수 있다.

두번재 Service의 Port(S)를 보면 내부포트:외부포트/프로토콜의 구조를 봐서 30306포트로 접근하면 3306포트로 연결해줌을 알 수 있다.

 

7. StatefuleSet 배포하기

이제 대망의 배포단계가 왔지만 여기서는 설명해야 할 것이 많다.

먼저 파일부터 보자.

 

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-sts
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql-svc
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
        - name: init-mysql
          image: mysql:5.7.34
          command:
            - bash
            - "-c"
            - |
              set -ex
              # Generate mysql server-id from pod ordinal index.
              # => 파드 서수 인덱스에서 mysql server-id를 생성합니다.
              [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
              ordinal=${BASH_REMATCH[1]}
              echo [mysqld] > /mnt/conf.d/server-id.cnf
              # Add an offset to avoid reserved server-id=0 value.
              # => 예약된 server-id=0 값을 피하기 위해 오프셋을 추가한다.
              echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
              # Copy appropriate conf.d files from config-map to emptyDir.
              # => config-map에서 emptyDir로 conf.d 파일을 복사한다.
              if [[ $ordinal -eq 0 ]]; then
                cp /mnt/config-map/master.cnf /mnt/conf.d/
              else
                cp /mnt/config-map/slave.cnf /mnt/conf.d/
              fi
          volumeMounts:
            - name: conf
              mountPath: /mnt/conf.d
            - name: config-map
              mountPath: /mnt/config-map
        - name: clone-mysql
          image: gcr.io/google-samples/xtrabackup:1.0
          command:
            - bash
            - "-c"
            - |
              set -ex

              # Skip the clone if data already exists.
              # => 데이터가 이미 있는 경우 복제를 건너뜁니다.
              [[ -d /var/lib/mysql/mysql ]] && exit 0

              # Skip the clone on primary (ordinal index 0).
              # => 서수 인덱스 0에서 복제 건너뛰기.
              [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
              ordinal=${BASH_REMATCH[1]}
              [[ $ordinal -eq 0 ]] && exit 0

              # Clone data from previous peer.
              # => 이전 피어에서 데이터를 복제합니다.
              ncat --recv-only mysql-sts-$(($ordinal-1)).mysql-svc 3307 | xbstream -x -C /var/lib/mysql

              # Prepare the backup.
              # => 백업을 준비합니다.
              xtrabackup --prepare --target-dir=/var/lib/mysql
          volumeMounts:
            - name: test
              mountPath: /var/lib/mysql
              subPath: mysql
            - name: conf
              mountPath: /etc/mysql/conf.d
      containers:
        - name: mysql
          image: mysql:5.7.34
          ports:
            - name: mysql
              containerPort: 3306
          volumeMounts:
            - name: test
              mountPath: /var/lib/mysql
              subPath: mysql
            - name: conf
              mountPath: /etc/mysql/conf.d
            - name: mysql-initdb
              mountPath: /docker-entrypoint-initdb.d
            - name: time-zone
              mountPath: /etc/localtime
          env:
            # - name: MYSQL_ALLOW_EMPTY_PASSWORD
            #   value: "1"
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef: # Get data from Secret
                  name: mysql-secret # Secret metaData name = mysql-secret
                  key: root-password # Secret data key = root-password
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: username
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: password
          livenessProbe:
            exec:
              command: ["mysqladmin", "ping"]
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
          readinessProbe:
            exec:
              #command: ["mysql", "-h", "127.0.0.1", "-u$MYSQL_USER", "-p$MYSQL_PASSWORD", "-e", "SELECT 1"]
              command:
                - bash
                - -c
                - mysql -h 127.0.0.1 -u$MYSQL_USER -p$MYSQL_PASSWORD -e "SELECT 1"
            initialDelaySeconds: 5
            periodSeconds: 2
            timeoutSeconds: 1
        - name: xtrabackup
          image: gcr.io/google-samples/xtrabackup:1.0
          ports:
            - name: xtrabackup
              containerPort: 3307
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef: # Get data from Secret
                  name: mysql-secret # Secret metaData name = mysql-secret
                  key: root-password # Secret data key = root-password
          command:
            - bash
            - "-c"
            - |
              set -ex
              cd /var/lib/mysql
              echo "current location : /var/lib/mysql"

              # Determine binlog position of cloned data, if any.
              # 복제된 데이터의 binlog 위치를 확인합니다(있는 경우).
              if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
                # XtraBackup already generated a partial "CHANGE MASTER TO" query
                # because we're cloning from an existing replica. (Need to remove the tailing semicolon!)
                # => XtraBackup은 이미 부분적인 "CHANGE MASTER TO" 쿼리를 생성했습니다.
                # => 기존 복제본에서 복제 중이기 때문입니다. (꼬리 세미콜론을 제거해야합니다!)
                echo "shell cat xtrabackup_slave_info sed change_master_to"
                cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in

                # Ignore xtrabackup_binlog_info in this case (it's useless).
                # => 이 경우 xtrabackup_binlog_info를 무시합니다(쓸모 없음).
                echo "shell rm xtrabackup_slave_info xtrabackup_binlog_info"
                rm -f xtrabackup_slave_info xtrabackup_binlog_info
              elif [[ -f xtrabackup_binlog_info ]]; then
                # We're cloning directly from primary. Parse binlog position.
                # => primary에서 직접 복제하고 있습니다. binlog 위치를 구문 분석합니다.
                [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
                echo "shell rm xtrabackup_binlog_info xtrabackup_slave_info"
                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

              # Check if we need to complete a clone by starting replication.
              # => 복제를 시작하여 복제를 완료해야 하는지 확인합니다.
              if [[ -f change_master_to.sql.in ]]; then
                # => echo mysqld가 준비될 때까지 기다리기(연결 수락)
                echo "Waiting for mysqld to be ready (accepting connections)"
                until mysql -h 127.0.0.1 -uroot -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1"; do sleep 1; done

                # => echo 클론 위치에서 복제 초기화
                echo "Initializing replication from clone position"
                mysql -h 127.0.0.1 -uroot -p${MYSQL_ROOT_PASSWORD} \
                      -e "$(<change_master_to.sql.in), \
                              MASTER_HOST='mysql-sts-0.mysql', \
                              MASTER_USER='root', \
                              MASTER_PASSWORD='${MYSQL_ROOT_PASSWORD}', \
                              MASTER_CONNECT_RETRY=10; \
                            START SLAVE;" || exit 1

                # In case of container restart, attempt this at-most-once.
                # => 컨테이너 재시작의 경우, 최대 한 번만 시도하십시오.
                mv change_master_to.sql.in change_master_to.sql.orig
              fi

              # Start a server to send backups when requested by peers.
              # => 서버를 시작하여 피어가 요청할 때 백업을 보냅니다.
              exec ncat --listen --keep-open --send-only --max-conns=2 3307 -c \
                "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root --password=${MYSQL_ROOT_PASSWORD}"

              # --backup : Make a backup and place it in xtrabackup `--target-dir`. ---> line 61
              # --slave-info : pints the binary log position of the master server. also writes this information to the xtrabackup_slave_info file as a CHANGE MASTER command.
              # --stream : Stream all backup files to the standard output in the specified format
              # --user : This option specifies the MySQL username used when connecting to the server.
              # --password : This option specifies the password to use when connecting to the database.
          volumeMounts:
            - name: test
              mountPath: /var/lib/mysql
              subPath: mysql
            - name: conf
              mountPath: /etc/mysql/conf.d
            - name: time-zone
              mountPath: /etc/localtime
      volumes:
        - name: conf
          emptyDir: {}
        - name: config-map
          configMap:
            name: mysql-config
        - name: mysql-initdb
          configMap:
            name: mysql-config
        - name: time-zone
          hostPath:
            path: /usr/share/zoneinfo/Asia/Seoul
  volumeClaimTemplates:
    - metadata:
        name: test
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

 

먼저 initContainers 란 Pod의 main container 의 초기화 작업을 위한 container 정의다.

설정파일이나 백업파일을 로딩하고 main container에게 전달하거나 직접 세팅하는 등의 작업을 진행할 수 있다.

여기서는 command와 volumeMounts 항목을 살펴보면 된다.

volumeMounts에서 name은 spec.volumes의 name을 가리킨다.

conf는 volumes[0]이고 emptyDir로 Pod간 공유되는 volume를 mount했다.

config-map은 4. ConfigMap 생성하기 를 통해 만들었던 mysql-config 를 volume으로 mount한 것이다.

 

어떤 정보 혹은 Volume이 어디에 mount되고 command에서 그 경로에 접근해서 어떻게 사용하는지 따라가다보면 이해가 될 것이다.

command는 여러 블로그에서 동일한 형태를 찾아볼 수 있으나 번역된 주석을 각 주석밑에 추가했으니 차례대로 읽어보길 추천한다.

 

 

StatefulSet을 생성하고 2번 Pod 목록을 확인해보았다.

initContainers 속성이 있기 때문에 STATUS 항목에서 Init:0/2 와 같은 정보를 확인할 수 있다.

여기서 READY 항목에서 nginx는 1/1 이고 mysql-sts-0은 2/2 로 적혀있다.

이유는 nginx는 Pod내에 containers가 한개이고 mysql-sts-0에서 containers는 2개 (mysql, xtrabackup)이기 때문이다.

 

추가로 PV도 잘 연결되었는지 확인해보자. 물론 READY가 2/2 라 잘 연결되었긴 하지만.

 

PVC에 의해 PV가 잘 붙었음을 확인할 수 있다.

 

8. 접속해보기

이렇게 배포가 마무리 되었으니 접속을 해보자.

먼저 mysql이 어떤 node에 배포되었는지를 확인해야 한다. Service의 NodePort는 Pod가 배포된 Node에 설정된 것이기 때문에 다음 명령어로 Pod가 어떤 node에 있는지 확인해보자.

 

kubectl get pod -o wide

 

node를 확인했다면 AWS에서 해당 node에게 주어진 public ip를 사용하면 된다.

나는 mysql관리프로그램으로 HeidiSQL을 사용하고 있어 스크린샷으로 보자.

 

 

 

이렇게 데이터베이스를 구축하는 것도 끝났다.

물론 이건 정말 설치만 한 수준이라는 것도 명심하자.

장애발생시 데이터관리, 권한관리, 스케일업 등등 많은 숙제들이 남아있다.

여기서는 거기까지 다루지는 않지만 시작하는데 있어 도움이 되길 바란다.

 

다음에는 Ingress에 대해서 알아보자

 

이전글

 

 

[Cloud Compute] Kubernetes 구축하기 - 4. nginx 서버 배포하기

지난 글에서 간단하게 어플리케이션을 배포하는 과정을 진행해보았다. 이번에는 nginx와 html을 배포해서 간단한 웹서버를 구축해보려고 한다. 어떻게 보면 지난글보다 이번글이 더 중요할 수 있

bateaux.tistory.com

 

다음글

 

[Cloud Compute] Kubernetes 구축하기 - 6. Ingress 적용하기

지난 시간 nginx를 배포했을 때, NodePort를 이용해 외부에서 접근할 수 있었다. 쉽게 접근할 수 있는 방법이지만 문제가 있다. 1. 가용포트범위가 한정적이다.(기본설정 기준) 알다시피 기본설정에

bateaux.tistory.com

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.