[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을 위해 특정했다.

생성된 결과를 보면 각 속성이 원하는대로 잘 지정되었음을 알 수 있다.
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"이라는 계정에게 모든 권한을 주게 되어있다.

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
소중한 공감 감사합니다