2025년 9월 21일 일요일

IP 하나로 k8s 클러스터 내 여러 개 서비스에 접근하기

* 이 글은 백엔드 개발자가 홈서버를 구성하며 겪은 시행착오를 공부하고 정리한 글입니다. 잘못 알고있는 점이 있다면 지적해주시면 감사하겠습니다.


이 글에서는 다음 항목을 다룹니다.

  • MetalLB 설치
  • LoadBalnacer 구성
  • ClusterIP 구성
  • NGINX ingress controller 구성


다이어그램으로 나타내면 다음과 같습니다.


앞선 포스트에서 물리 서버 위에 RKE2로 k8s 클러스터를 구성한 후 MySQL을 설치했습니다. 이제 외부에서 접근할 수 있도록 만들어보겠습니다.


Install MetalLB

https://metallb.universe.tf/installation/

MetalLB는 베어 메탈 머신에서 로드밸런서 구현을 제공하는 오픈소스 프로젝트입니다.


Helm을 사용하여 MetalLB를 설치하도록 하겠습니다.

우선 가이드에 따라 MetalLB를 설치할 namespace에 권한을 주어야 합니다. metallb-system 네임스페이스에 설치하도록 하겠습니다.

https://metallb.universe.tf/installation/#installation-with-helm


권한 설정 파일을 생성합니다.

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


Install NGINX

LoadBalnacer에서 원하는 서비스로 트래픽을 전달할 수 있는데, NGINX를 추가로 설치해주는 이유가 궁금할 수 있습니다. 그 이유는 환경적 제약사항을 우회하기 위해서입니다.

저는 하나의 IP만 가용한 환경에서 k8s 클러스터를 구성하고 있습니다. LoadBalancer 서비스는 포트를 분리하더라도 여러 개의 서비스로 트래픽을 보낼 수 없기 때문에, NGINX ingress controller에서 여러 개의 서비스로 트래픽을 분배하는 역할을 수행하도록 구상했습니다.


helm으로 nginx ingress를 설치하기 위해 repo에 ingress-nginx를 추가합니다.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update


31000 port로 MySQL primary pod에 접근하고, 32000 port로 MySQL secondary pods에 접근하도록 하겠습니다.

helm-ingress-values.yaml 파일을 생성하고, 아래 내용을 붙여넣은 후 저장합니다.

tcp:
  31000: "default/mysql-primary:3306"
  32000: "default/mysql-secondary:3306"

controller:
  service:
    extraPorts:
      - name: mysql-primary
        port: 31000
        targetPort: 31000
        protocol: TCP
      - name: mysql-secondary
        port: 32000
        targetPort: 32000
        protocol: TCP

helm-ingress-values.yaml 파일을 사용하여 커스텀한 nginx ingress controller를 설치합니다.

helm install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  -f helm-ingress-values.yaml

정상적으로 설치되었다면 아래와 같이 안내 문구가 나타납니다.



주의

저와 같이 RKE2로 k8s를 설치하셨다면 다음과 같은 오류 문구를 마주할 수 있습니다.


이미 RKE2에서 자동적으로 구성한 nginx ingress가 존재하기 때문에 발생하는 오류입니다.
kubectl get all -n kube-system | grep ingress



저는 기존에 존재하는 NGINX ingress를 제거해주도록 하겠습니다.


/etc/rancher/rke2/config.yaml 파일을 생성해줍니다.

sudo vi /etc/rancher/rke2/config.yaml

아래 내용을 붙여넣고 저장합니다.
disable:
  - rke2-ingress-nginx
RKE2를 재시작합니다.
sudo systemctl restart rke2-server

이제 RKE2 nginx ingress가 존재하지 않는 것을 확인할 수 있습니다.
kubectl get all -n kube-system | grep ingress



ClusterIP 생성

MySQL primary, secondary pod에 접근하기 위해 ClusterIP를 생성하겠습니다.

* 앞선 포스트에서 안내한 방식대로, helm + bitnami 을 통해 설치할 경우 자동적으로 생성되기 때문에 생략할 수 있습니다.


아래 두 파일을 생성합니다.
mysql-primary-cluster-ip.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-primary
spec:
  type: ClusterIP
  ports:
    - port: 3306
      targetPort: 3306
      protocol: TCP
      name: mysql
  selector:
    statefulset.kubernetes.io/pod-name: mysql-primary-0
mysql-secondary-cluster-ip.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-secondary
spec:
  type: ClusterIP
  ports:
    - port: 3306
      targetPort: 3306
      protocol: TCP
      name: mysql
  selector:
    app.kubernetes.io/instance: mysql
    app.kubernetes.io/component: secondary

테스트

이제 외부에서 접근할 수 있습니다.
실제로 DB에 연결한 후 hostname을 출력해보면 접근 포트에 따라 다른 hostname을 출력하는 것이 확인됩니다.

hostname: mysql-primary-0
hostname: mysql-secondary-0