* 이 글은 백엔드 개발자가 홈서버를 운영하며 공부하고 정리한 글입니다. 잘못 알고있는 점이 있다면 지적해주시면 감사하겠습니다.
이 글에서는 다음 항목을 다룹니다.
- RKE2로 k8s 환경 구성
- Longhorn 설치
- Longhorn 기본 설정을 오버라이드하는 StorageClass 구성
- MySQL 설치
- MySQL replicaiton 구성
- MetalLB 설치
- LoadBalnacer 구성
다이어그램으로 나타내면 다음과 같습니다.
Install RKE2
https://docs.rke2.io/install/quickstart
RKE2는 Rancher에서 개발한 k8s distro입니다. 가이드를 참고하여 RKE2를 설치합니다.
curl -sfL https://get.rke2.io | sudo sh -
설치가 완료되면 rke2-server.service를 enable 및 start 합니다.
start는 수 분의 시간이 소요될 수 있습니다.
sudo systemctl enable rke2-server.service
sudo systemctl start rke2-server.service
잘 설치되었는지 확인해봅니다.
systemctl status rke2-server.service
rke2-server.service가 enabled 및 active되었음을 확인할 수 있습니다.
이제 k8s 클러스터에 접근하기 위해 config 파일을 복사합니다.
mkdir -p ~/.kube
sudo cp /etc/rancher/rke2/rke2.yaml ~/.kube/config
해당 파일의 권한을 변경하여 sudo 없이 kubectl 커맨드를 실행할 수 있도록 합니다.
sudo chown $(id -u):$(id -g) ~/.kube/config
이제 shell에 RKE2 kubectl이 설치된 경로를 추가해주어야 합니다.
사용하는 shell 설정 파일을 열어줍니다. 저는 zsh를 사용하고 있으므로, ~/.zshrc 파일을 수정하겠습니다.
vi ~/.zshrc
RKE2 kubectl이 설치된 경로를 추가해줍니다.
export PATH=$PATH:/var/lib/rancher/rke2/bin
변경한 내용을 현재 shell에 반영합니다.
source ~/.zshrc
kubectl이 정상 동작하는지 확인해봅니다.
kubectl version
Install Helm
https://helm.sh/docs/intro/install/#from-apt-debianubuntu
Longhorn, MySQL을 Helm을 통해 설치할 예정이므로, 아직 Helm을 설치하지 않았다면 가이드를 참고하여 진행합니다.
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm
k8s storage
k8s에서 스토리지를 사용하는 방법은 크게 세 가지가 있습니다.
- PV/PVC를 직접 프로비저닝한다.
- NAS를 사용한다.
- 분산 스토리지를 사용한다.
1. PV/PVC를 직접 프로비저닝하는 경우
PV/PVC를 직접 프로비저닝해서 사용할 때 가장 큰 문제로 느꼈던 점은, 노드 지역성을 보장해야 한다는 점이었습니다. 특정 노드에 PV를 프로비저닝하면, 해당 PV에 접근하기 위해서 파드가 해당 노드 위에 올라가야만 했습니다.
예를 들어 다음 상황을 생각해보겠습니다.
- PV 생성 -> Node A
- MySQL Pod 스케쥴링 -> Node A
- MySQL Pod 재배포
- MySQL Pod 스케쥴링 -> Node B
하지만 Node B에는 PV가 존재하지 않으므로, MySQL 파드는 실패하게 됩니다.
해결법으로는 node affinity를 주는 방법이 있습니다. 예를 들어 host 네임으로 노드를 구별하여 PV 및 파드에 할당되는 노드를 제한하는 방법입니다. 하지만 node affinity를 통한 해결책은 특별한 노드가 생긴다는 점에서 k8s의 철학과 맞지 않는다고 느꼈습니다.
2. NAS를 사용하는 경우
NAS(Network Accessed Storage)는 네트워크를 통해 접근하는 스토리지를 말합니다. 따라서 NAS 안에서도 클라우드에서 제공하는 관리형 서비스, 혹은 다른 베어 메탈 머신에 설치한 MySQL 애플리케이션 등 여러 가지 종류가 있을 수 있습니다. 주어진 k8s 클러스터 외 환경에 스토리지를 운영한다는 점이 핵심입니다.
NAS를 사용하면 인프라 아키텍쳐적으로 깔끔하고, 고가용성을 고려하여 설계하기 용이해보여서 가장 마음이 가는 선택지입니다. 언젠가 구성해보고 싶지만 이번에 선택하지 않은 문제는 비용 때문입니다.
- 클라우드 서비스를 사용할 경우: 클라우드 사용료가 발생합니다.
- 다른 베어 메탈 서버 위 k8s에 구성: 추가 서버 구매 비용 및 전기세가 발생합니다.
- 현재 베어 메탈 서버 위 k8s 외 방식으로 구성: k8s 위에 MySQL을 올린다는 목적과 부합하지 않습니다.
3. 분산 스토리지를 사용하는 경우
분산 스토리지는 동일한 데이터의 카피를 여러 노드에서 저장하는 기술입니다. 따라서 데이터 유실 가능성이 적고, 하나의 노드 실패 시 레플리케이트된 다른 노드의 데이터에 접근함으로써 고가용성을 이룰 수 있습니다.
하지만 제가 분산 스토리지를 선택한 이유는 앞서 말한 장점보다는 다른 부분이 더 컸습니다. 바로 노드 지역성의 해소입니다. 분산 스토리지 서비스가 파드로 하여금 실제로 데이터가 저장된 위치와 무관하게 접근할 수 있도록 지원해주므로, PV/PVC를 직접 프로비저닝할 경우의 단점으로 지적한 노드 지역성을 해소할 수 있습니다.
Install Longhorn
https://longhorn.io/
Longhorn은 Rancher에서 개발한 분산 스토리지입니다. 설정이 간편하기도 하고, RKE2와 개발사가 같기도 하므로 호환성 걱정도 덜 수 있어 선택했습니다.
https://longhorn.io/docs/1.9.0/deploy/install/install-with-helm/
Helm을 이용해 Longhorn 1.0.0 버전을 longhorn-system 네임스페이스에 설치하겠습니다.
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace --version 1.9.0
Longhorn이 제대로 설치되었는지 다음 커맨드로 확인할 수 있습니다.
kubectl -n longhorn-system get pod
kubectl -n longhorn-system get svc
앞서 말씀드렸다시피 Longhorn을 가용성을 확보하기 위해 분산 스토리지로서 활용하기보다는, 데이터의 노드 지역성을 해결하기 위해 사용하는 것이 목적입니다. 그렇기 때문에 replica=1 설정이 필요합니다.
Longhorn replica 개수를 설정하는 방법은 세 가지가 있습니다.
- Longhorn storage의 default 설정을 수정한다.
- Longhorn storage를 사용하는 service에서 replica를 직접 지정한다.
- 커스텀 Longhorn storage를 생성한다.
1. Longhorn storage의 default 설정을 수정한다.
Longhorn storage의 default replica 개수를 1로 설정하는 방법은 간단하지만 몇 가지 단점을 가집니다.
- 기본값을 변경했다는 사실을 잊으면, 기본값대로 3개의 replica를 예상하고 운용하다가 의도치 않은 데이터 유실을 마주할 수 있습니다.
- 현재 MySQL 서비스에 대해서는 1개의 replica만 유지하고자 하지만, 다른 서비스의 경우 복수의 replica를 사용하고 싶을 수 있습니다. 이 경우에는 결국 default replica 개수 변경만으로는 해결할 수 없습니다.
2. Longhorn storage를 사용하는 service에서 replica를 직접 지정한다.
이것 역시 간단한 방법입니다. 예를 들어, MySQL service를 설치할 때 replica 수를 지정하는 방식입니다. 하지만 매번 서비스를 추가할 때마다 replica 수를 지정해주는 건 통일성이 부족하다고 느껴집니다. 하드코딩을 기피하는 것과 유사하게 손이 가지 않는 선택지입니다.
3. 커스텀 Longhorn storage class를 생성한다.
저는 커스텀 Longhorn storage class를 생성했습니다. 기본값을 바꾸지 않기 때문에 안전하고, 한 개의 replica 설정이 필요한 서비스들이 범용적으로 사용할 수 있기 때문입니다.
커스텀 Longhorn storage class 설정 파일을 longhorn-1-replica-storage-class.yaml 로 생성합니다.
vi longhorn-1-replica-storage-class.yaml
다음 내용을 붙여넣습니다.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-1-replica
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:
numberOfReplicas: "1"
reclaimPolicy: Retain
주요 파라미터에 대한 설명은 다음과 같습니다.
- provisioner: driver.longhorn.io - Longhorn으로 프로비저닝합니다.
- allowVolumeExpansion: true - 용량을 확장할 수 있도록 합니다.
- parameters. numberOfReplicas: "1" - replica를 1로 설정합니다.
- reclaimPolicy: Retain - storage를 삭제해도 pv를 자동으로 삭제하지 않습니다. 혹여 실수로 삭제하더라도 복구할 수 있습니다.
StorageClass를 생성합니다.
kubectl apply -f longhorn-1-replica-storage-class.yaml
StorageClass가 잘 생성되었는지 확인합니다.
kubectl describe sc longhorn-1-replica
Install MySQL
https://github.com/bitnami/charts/tree/main/bitnami/mysql
Helm 및 Bitnami로 MySQL을 설치하고, replication 설정을 해주겠습니다.
Replication의 목적
Replication하는 이유는 다음과 같습니다.
- 데이터 유실 가능성을 줄입니다.
- replica DB를 read-only DB로 활용하여 primary DB의 부하를 분산시킵니다.
replication 설정을 위해서 파라미터를 직접 입력하는 것도 가능합니다. 하지만 저는 파라미터 직접 입력은 1. 실수할 여지가 크고 2. 재수행이 어렵다는 단점 때문에 기피하는 편입니다.
대신 Helm values file을 활용하는 것을 선호합니다. 미리 정의한 yaml 파일에 파라미터를 입력하고, helm install/upgrade 시 전달하는 방식입니다.
Helm values file을 사용하여 MySQL을 설치하기 위해, 우선 helm-mysql-values.yaml 파일을 생성합니다.
vi helm-mysql-values.yaml
다음 내용을 붙여넣습니다.
image:
repository: bitnami/mysql
digest: sha256:7089d796fc9b4629a628bd445e4afabe607351ee665444c3197bdeaed812ea65
architecture: replication
auth:
rootPassword: "{root-password}"
username: "{username}"
password: "{password}"
replicationPassword: "{replication-password}"
global:
storageClass: "longhorn-1-replica"
primary:
persistence:
size: 50Gi
resources:
requests:
memory: "2Gi"
cpu: "2"
limits:
memory: "2Gi"
cpu: "2"
secondary:
replicaCount: 1
persistence:
size: 50Gi
resources:
requests:
memory: "1Gi"
cpu: "1"
limits:
memory: "1Gi"
cpu: "1"
주요 파라미터에 대한 설명은 다음과 같습니다.
- image.digest: ... - 위 예제의 digest는 MySQL 8.4.5 버전의 SHA256 digest입니다. tag로 지정하지 않은 이유는 동일 태그를 재사용하여 태그가 같더라도 실제로는 다른 이미지인 경우가 존재하기 때문입니다.
이는 image.tag로 지정할 경우 표시되는 메시지에서 경고하는 내용이기도 합니다.
따라서 동일 이미지를 보장하기 위해 digest로 지정해주도록 합니다.
digest는 이미지를 docker pull할 때 출력됩니다.
docker pull bitnami/mysql:{version}
# docker pull bitnami/mysql:8.4.5
이미 pull한 이미지라면 docker inspect 커맨드로 확인할 수 있습니다.
docker inspect bitnami/mysql:{version} --format='{{index .RepoDigests 0}}'
# docker inspect bitnami/mysql:8.4.5 --format='{{index .RepoDigests 0}}'
하지만 digest는 사람이 읽을 수 없는 값이기 때문에, yaml 파일만 봐서는 어떤 버전을 사용하는지 알 수 없습니다. 이 문제를 해결하기 위해 immutable tag를 사용하는 방법도 존재합니다.
설정한 digest와 동일한 이미지를 가리키는 immutable tag는 bitnami/mysql:8.4.5-debian-12-r0 입니다.
digest 대신 immutable tag를 사용하고자 한다면, digest 라인을 다음과 같이 수정해줍니다.
# digest: sha256:7089d796fc9b4629a628bd445e4afabe607351ee665444c3197bdeaed812ea65
tag: 8.4.5-debian-12-r0
- architecture: replication - source-replica 아키텍처를 지정해주는 핵심 설정입니다.
- auth: ... - source, replica의 유저/비밀번호를 설정합니다.
- global.storageClass: "longhorn-1-replica" - 앞서 생성한 커스텀 storage class로 설정함으로써 하나의 replica만 가지도록 합니다. 주의해야할 부분은, 여기서 말하는 replica는 Longhorn에 의한 볼륨 레벨 replica로, DB 레벨 replica와 별개라는 점입니다.
- primary.resources.... - k8s에서 얼마만큼의 리소스를 할당할 것인지 지정합니다.
resource를 지정하지 않을 경우 다음과 같은 경고 문구가 출력됩니다.
이때 주목할 점은, requests와 limits 값을 동일하게 유지했다는 점입니다. 이는 QoS(Quality of Service)를 고려한 선택입니다.
k8s는 파드를 요청한 리소스의 requests/limits에 따라 세 가지 QoS class로 구분합니다.
- Guaranteed
- Burstable
- BestEffort
Guaranteed는 해당 제약 조건을 초과하지 않는 한 pod가 kill되지 않는 것을 보장합니다. 그 외의 QoS(Burstable/BestEffort)는 클러스터 리소스 상황에 따라 evict될 수 있습니다.
따라서 MySQL과 같이 중요한 파드를 k8s가 kill할 수 없도록 하기 위해 Guaranteed로 만들어주는 것이 좋습니다. Guaranteed가 되기 위해서는 CPU 및 memory의 requests와 limits가 동일해야 하므로 그에 맞춰 설정했습니다. 물론 primary와 secondary의 requests와 limits를 통일시킬 필요는 없습니다.
- secondary.replicaCount: DB 레벨 replica의 개수를 설정합니다. 이 역시 Longhorn에 의한 볼륨 레벨 replica와 무관합니다.
이제 준비한 설정 파일로 MySQL을 설치합니다.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install mysql bitnami/mysql -f helm-mysql-values.yaml
APP VERSION: 9.3.0 이라는 문구 때문에 의도하지 않은 버전을 설치한 것으로 오해할 수 있지만, 해당 정보는 단순히 Helm chart의 메타데이터로, 실제로는 8.4.5 버전이 설치된 것이 맞습니다. 설치된 MySQL 버전은 뒤에서 확인해보겠습니다.
pod를 출력해보면 MySQL primary/replica pod를 확인할 수 있습니다.
kubectl get po
이제 MySQL에 접속해서 하고싶은 게 많습니다. 스키마도 만들고, 테이블도 만들고, 데이터도 추가하고 싶습니다. 그러기 위해서는 MySQL pod 안에 들어가도 되지만, DataGrip이나 MySQL Workbench 같은 DB 툴로 편하게 수정하고 싶습니다. 또, 어플리케이션에서 접근할 때 k8s CoreDNS를 통해 접근할 수도 있지만, 언젠가 MySQL을 k8s 클러스터 밖으로 옮기는 상황을 고민하다 보면 CoreDNS보다는 네트워크 기반으로 접근하고 싶다는 생각이 자꾸 듭니다. 또한, CoreDNS를 통해서만 접근 가능하다면, 로컬에서 개발할 때 MySQL에 접근할 수 없다는 치명적인 단점을 가지기도 합니다.
그러므로 외부에서 접근할 수 있도록 열어주도록 합니다. 여기에도 여러가지 방법이 있습니다.
- Port forwarding
- NodePort
- LoadBalancer
1. Port forwarding
간단하게 테스트할 때 좋은 방법이지만, 불안정한 방법입니다. session 유지가 필요하고, session 유지 제약을 회피하기 위해 background에서 실행시키더라도 pod가 내려갔다가 다시 올라오면 접속이 끊깁니다.
2. NodePort
하나의 노드만 운영한다면 특별한 단점은 없습니다. 하지만 특정 노드에만 접근하는 방식이므로 확장성이 부족하여 노드 추가 시 대응하기 어렵습니다. 예를 들어, 두 대의 노드가 존재할 때, 원하는 파드에 접근하기 위해서는 어떤 노드에 접근해야 하는지 어떻게 알 수 있을까요? 사람이야 직접 파드가 올라간 노드를 확인할 수 있지만, 어플리케이션에서는 어떻게 확인할 수 있을까요?
3. LoadBalancer
노드를 추가하는 경우를 고려하여 LoadBalancer를 선택했습니다. 이 선택은 저의 개인적인 경험과도 관계가 있습니다. 이전에 한 대의 Raspberry Pi 4B로 k8s 클러스터를 운영하다가, 메모리가 부족하여 노드를 하나 더 추가했었고, 그때 NodePort에서 LoadBalancer로 전환해야 했던 기억이 있기 때문입니다.
LoadBalancer Provisioning
LoadBalancer를 통해 MySQL primary DB에 접속할 수 있도록 설정하겠습니다.
바로 LoadBalancer를 설치할 수 있다면 좋겠지만, Bare metal 머신에서는 LoadBalancer를 설정하기 위해 신경써야할 부분이 있습니다.
우선 그냥 설치할 경우 어떤 문제가 발생하는지 보여드리겠습니다.
MySQL primary DB 파드에 접근하기 위한 LoadBalancer 설정 파일을 생성합니다.
vi mysql-primary-lb.yaml
다음 내용을 붙여넣습니다.
apiVersion: v1
kind: Service
metadata:
name: mysql-primary-lb
spec:
selector:
app.kubernetes.io/instance: mysql
app.kubernetes.io/component: primary
ports:
- protocol: TCP
port: 3306
targetPort: 3306
type: LoadBalancer
- port - 외부에서 접속하는 포트입니다. 원하는 값으로 설정합니다.
- targetPort - 컨테이너 포트입니다. MySQL 포트를 변경하지 않았으므로 기본값인 3306으로 설정합니다.
LoadBalancer를 생성합니다.
kubectl apply -f mysql-primary-lb.yaml
service를 확인해봅시다.
kubectl get services
mysql-primary-lb 서비스의 EXTERNAL-IP이 <pending>으로 표시되는 것을 확인할 수 있습니다. IP pool이 존재하지 않기 때문입니다. 이를 해결하기 위해 MetalLB를 사용합니다.
Install MetalLB
https://metallb.universe.tf/installation/
MetalLB는 베어 메탈 머신에서 로드밸런서 구현을 제공하는 오픈소스 프로젝트입니다.
Helm을 사용하여 MetalLB를 설치하도록 하겠습니다.
우선 가이드에 따라 MetalLB를 설치할 namespace에 권한을 주어야 합니다. metallb-system 네임스페이스에 설치하도록 하겠습니다.
권한 설정 파일을 생성합니다.
vi metallb-namespace.yaml
다음 내용을 붙여넣습니다.
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged
저장한 뒤에 privilege 설정을 적용합니다.
kubectl apply -f metallb-namespace.yaml
이제 Helm으로 MetalLB를 metallb-system 네임스페이스에 설치합니다.
helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm install metallb metallb/metallb --namespace metallb-system --create-namespace
MetalLB 설정 파일을 만들어줍니다.
vi metallb-config.yaml
다음 내용을 붙여넣습니다.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: metallb-ip-pool
namespace: metallb-system
spec:
addresses:
- {ip-pool-start}-{ip-pool-end}
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: my-l2-advertisement
namespace: metallb-system
- spec.addresses.... - IP pool을 설정해주는 부분입니다. 사용 가능한 IP pool이 있으시면 해당 IP pool을 등록해주셔도 되고, 없다면 현재 노드의 IP로 등록해주어도 동작합니다. 저는 {ip-pool-start}, {ip-pool-end} 둘다 현재 노드의 IP로 설정해주겠습니다.
불가피하게 하나의 IP만 등록할 경우 주의해야할 점이 몇 가지 있습니다.
- 하나의 LoadBalancer만 생성 가능합니다.
- 해당 IP의 노드가 실패할 경우 LoadBalancer를 통한 접근이 불가능해집니다.
- 개방할 port가 겹치지 않아야 합니다.
config 파일을 적용합니다.
kubectl apply -f metallb-config.yaml
이제 서비스를 다시 확인해보면, EXTERNAL-IP가 <pending>이 아닌 현재 노드의 IP로 설정된 것을 확인할 수 있습니다.
kubectl get services
긴 여정 끝에 드디어 외부에서 MySQL에 접속할 수 있게 되었습니다.
DataGrip에서 root 계정으로 접속해보겠습니다.
helm-mysql-values.yaml 파일에서 설정한 rootPassword로 정상적으로 접속되었습니다.
덤으로, 앞서 MySQL 설치 시 나오는 APP VERSION: 9.3.0 문구에 대해 설명드린 내용을 증명할 수 있는 시간이 왔습니다.
APP VERSION: 9.3.0 이라는 문구 때문에 의도하지 않은 버전을 설치한 것으로 오해할 수 있지만, 해당 정보는 단순히 Helm chart의 메타데이터로, 실제로는 8.4.5 버전이 설치된 것이 맞습니다.
SELECT VERSION();
말씀드린대로 8.4.5 버전으로 설치되어 있습니다.
남은 단계는 일반적인 MySQL 설정과 동일합니다.
앞으로 root 대신 다른 계정(ariadacapo)으로 DDL을 수행하기 위해 권한을 주겠습니다.
GRANT ALL PRIVILEGES ON *.* TO '{username}'@'%';
FLUSH PRIVILEGES;
이제 해당 계정으로 접속하면 스키마 생성, 테이블 생성 등 원하는 작업을 원하는 만큼 수행할 수 있습니다.
ariadacapo 계정으로 다시 접속하겠습니다.
제대로 권한을 받았는지 시험 삼아 hello_world 스키마를 생성해보겠습니다.
create schema hello_world;
정상적으로 생성되었습니다.
이제 외부에서 접속 가능한 MySQL이 준비되었으므로, 마음껏 활용할 수 있습니다.
단, 현재 구성에서는 MySQL primary pod에만 접근할 수 있습니다. secondary pod에도 접근할 수 있도록 구성하는 내용은 다음 포스트에서 진행하도록 하겠습니다.