이리저리 정보를 취합해 비교/토의한 결과, 결국 독립된 GPU 서버를 IDC 센터에 설치하는 형태로 마무리 되었다.
그냥 GPU 서버를 클라우드에 올려 준다면야 나는 매우 편하고 좋겠지만, 클라우드의 엄청난 비용 부담을 고려하면 이 방식이 가장 합리적인 판단이라고 생각한다.
RTX A6000 GPU의 성능이 지금 당장은 충분해 보이나, 향후 확장성을 고려하면 GPU 서버 역시 k8s 클러스터로 구성해야 한다.
하지만 실제 서비스를 사용자에게 제공할 때는 클라우드에 있는 k8s를 사용한다. GPU 클러스터는 GPU 중심의 워크로드만 담당하고 각종 인증, 데이터 처리, 웹 서비스 등은 클라우드에서 제공한다는 의미이다.
요컨대 클러스터 간의 통신을 구축할 필요성이 생겼다.
사실 이런 시나리오를 전혀 예측하지 못한 것은 아니다.
현대적인 인프라 아키텍처에서는 점차 여러 개의 k8s 클러스터를 운영하는 것이 일반적인 관습이 되어가고 있다. 처음에 k8s를 사용할 때는 하나의 클러스터로도 필요한 모든 서비스를 처리하는 게 가능해 보이지만, 확장성이나 장애 시 복원 탄력성 등 머지 않아 그 한계에 부딪히게 된다.
이번 GPU 케이스의 경우 “클라우드에 올리기에 너무 비싸 자체적으로 운영한다"라는 물리적인 확장성 이슈이다.
Istio는 이런 상황에 대응할 수 있도록 다수의 클러스터를 하나의 서비스 메시로 통합해 여러 클러스터에 걸친 워크로드 간 통신을 안전하고(mTLS, Authorization Policy), 일관된 방식(트래픽 제어)으로 구성할 수 있는 기능을 제공한다.
동일한 Service Mesh가 여러 k8s 클러스터에 걸쳐 뻗어있는 방식이다. (클러스터 클러스터) 여러 클러스터의 서비스들이 하나의 통합된 메시로 관리되며, 클러스터 간 자동 서비스 발견 및 로드 밸런싱이 가능하다.
Multiple Clusters 모델의 주요 특징:
서비스 발견 (Service Discovery): 여러 클러스터의 서비스 엔드포인트를 자동으로 통합
크로스 클러스터 로드 밸런싱: 요청을 여러 클러스터의 엔드포인트에 자동 분산
네트워크 구성: 단일 네트워크 또는 다중 네트워크(지리적 분산, 보안 격리) 구성 가능
제어 평면 (Control Plane): 단일 제어 평면으로 여러 클러스터 관리 혹은 각 클러스터에 독립적으로 배치
클러스터 간 엔드포인트 발견을 위해서는 각 제어 평면에 remote secret을 배포하여 다른 클러스터의 API 서버에 접근할 수 있도록 구성해야 한다.
하나의 국가 내에 존재하는 여러 도시들을 생각해보자, 예를 들면 대한민국의 서울/부산/대구.
지리적 위치는 물론 수도, 가스, 전기, 하다못해 지하철 노선 등등 분명 독립된 인프라가 존재한다. 하지만 모든 도시는 “대한민국"이라는 국가 내에서 동일한 법률, 정부, 화폐, 신분증 체계를 공유한다. 도시 간 이동은 상당히 자유로우며, 다른 도시에 있는 사람을 같은 국가의 시민으로 인식한다.
내 경우 연결할 두 클러스터의 네트워크가 물리적으로 완전히 분리된 상황이므로 3번 혹은 4번만 보면 된다.
Multi-Primary와 Primary-Remote의 차이는 제어 평면을 각 클러스터마다 배포 할 것인지(Multi-Primary), 아니면 하나의 제어 평면만 두고(Primary), 다른 클러스터를(Remote) 관리만 할 것인지(Primary-Remote)이다.
앞서 비유를 이어가면 다음과 같다.
Multi-Primary (개별 제어 평면): 연방제 국가의 각 주(州)가 독립적인 정부를(주 정부) 가지는 방식.
예를 들어, 미국의 캘리포니아, 텍사스, 뉴욕은 각각 독립적인 주 정부를 가지고 있다. 한 주의 정부가 마비되어도 다른 주는 정상적으로 운영되지만, 각 주마다 정부 조직을 유지해야 하므로 비용과 운영 부담이 크다.
Primary-Remote (단일 제어 평면): 단일 국가의 중앙 정부가 여러 지역을 관리하는 방식.
예를 들어, 우리나라는 중앙정부(서울)가 서울, 부산, 대구 등 모든 도시를 관리한다. 하나의 정부로 통합 관리하므로 운영이 간단하지만, 중앙정부가 마비되면 전국이 위험해진다. (물론 우리나라도 지방 자치가 있긴 한데, 미국이나 독일 등의 주 정부에 비하면 권한이 매우 약하다.)
개별 제어 평면(Multi-Primary)
단일 제어 평면(Primary-Remote)
장점
고 가용성, 장애 격리
구성과 운영 난이도가 낮음
단점
리소스 소모가 많아지고 운영 난이도가 높음
Primary의 Istiod 다운 시 전체 메시가 위험해짐
2가지 시나리오를 모두 경험해보고 싶어서, 내가 개인적으로 운영하는 k8s는 단일 제어 평면, 회사의 k8s는 개별 제어 평면으로 구성하려고 한다.
이번 포스트에선 개인 k8s에 멀티 클러스터 서비스 메시를 Primary-Remote on different network 시나리오에 맞춰 구성한 내용을 담는다.
멀티 클러스터 서비스 메시 구현
실습한 시나리오는 Primary-Remote이다.
Primary Cluster는 Oracle Cloud Infrastructure에 OKE Cluster, Remote Cluster는 집에 있는 On-Premise Cluster이다.
Primary 클러스터에 단일 제어 평면(Istio Control Plane)을 설치하고, Remote 클러스터는 Primary가 만든 서비스 메시에 클라이언트로서 참여한다.
전체 클러스터를 아우르는 Mesh ID 값을 정해야 한다. 내 경우 apex-captain-mesh로 지정했다.
각 클러스터에 Istio 리소스가 배포되는 Namespace는 istio-system이다. (기본값)
각 클러스터의 이름과 네트워크명을 할당했다. Primary Cluster는 oke, Remote Cluster는 workstation이다.
⚠️ 두 클러스터를 왔다갔다 하면서 작업해야 해서 헷갈릴 수 있다.
각 섹션 앞에는 어느 Context에서 진행하는 내용인지 명시 해두었으니 참고하자.
Primary 기본 네트워크 설정
ㅤ⚠️ Context : Primaryㅤ
Primary 클러스터의 istio-system Namespace에 Network 값을 Label로 달아준다.
global.externalIstiod를 true로 설정하는 것이 중요하다. 이는 OKE 클러스터에 설치되는 Istiod가 외부 제어 평면으로 기능 할 수 있도록 하는 플래그이다.
Primary에 east-west gateway 설치
ㅤ⚠️ Context : Primaryㅤ
Istio에는 north-south, east-west 게이트웨이라는 개념이 있다. 남북이니, 동서니 하는데 이게 Istio에서만 쓰는 이상한 용어는 아니고, 트래픽의 방향에 대한 얘기이다.
위 그림의 Data Center를 각각 하나의 k8s라고 생각해 보자.
External Network, 즉 “인터넷"을 북쪽, “k8s"를 남쪽에 두면 north-south 트래픽이란 것은 인터넷과 서비스 간, 즉 외부 사용자와의 통신을 의미한다. 일반적으로 많이 쓰는 “Nginx Ingress Controller"가 담당하는 것이 north-south 트래픽인 것이다.
반면, 두 k8s를 나란히 옆으로 두면 두 k8s 간의 통신은 동-서 간, 즉 east-west 간의 통신이 된다. east-west 게이트웨이는 k8s 간의 istio mesh 구성을 위해 통신하는 출입문이다. 멀티 클러스터 서비스 메시를 구성하는 모든 k8s에 하나씩 필요하다.
우선 Primary 클러스터에 east-west gateway를 설치해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Primary-Istio-East-West-Gateway.ymlname:istio-eastwestgatewaynetworkGateway:okeservice:type:LoadBalancer# Reserved IP를 생성해서 고정된 값을 넣는 것을 추천한다loadBalancerIP:<할당할 LB의 IP> # 기타 추가할 Annotations# 아래는 OCI Free Tier에 부합하는 로드밸런서 설정이다.# 사용하는 클라우드 혹은 LB 공급자에 맞춰 설정하자.# annotations: # 'service.beta.kubernetes.io/oci-load-balancer-security-list-management-mode':'None',# 'service.beta.kubernetes.io/oci-load-balancer-shape':'flexible',# 'service.beta.kubernetes.io/oci-load-balancer-shape-flex-max':'10',# 'service.beta.kubernetes.io/oci-load-balancer-shape-flex-min':'10',
kubectl get svc -n istio-system -l app=istio-eastwestgateway
다음과 같이 나오면 정상이다.
1
2
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-eastwestgateway LoadBalancer <클러스터 내부 IP> <LB의 IP> 15021:30358/TCP,15443:30461/TCP,15012:30494/TCP,15017:32510/TCP 2d4h
NAME AGE
gateway.networking.istio.io/istiod-gateway 2d4h
NAME GATEWAYS HOSTS AGE
virtualservice.networking.istio.io/istiod-vs ["istiod-gateway"]["*"] 2d4h
Remote Cluster 제어 평면 설정
ㅤ⚠️ Context : Remoteㅤ
Remote 클러스터의 istio-system 네임스페이스에 Primary의 제어 평면을 사용할 것임을 지정해주자.
위에서 생성한 remote-workstation.yml 파일을 그대로 Primary 클러스터에 배포해준다.
1
kubectl apply -f remote-workstation.yml
Remote에 east-west gateway 설치
ㅤ⚠️ Context : Remoteㅤ
1
2
3
4
5
6
7
8
9
10
11
12
# Remote-Istio-East-West-Gateway.ymlname:istio-eastwestgatewaynetworkGateway:workstationservice:type:LoadBalancerloadBalancerIP:<할당할 LB의 IP> # 이 부분이 좀 특이한데,# 내가 운영 중인 Workstation Cluster의 경우 Iptime Router 뒤에서 동작한다.# 단순 LB IP만 가지고는 Primary가 접근할 수 없으므로# 실제 Router의 IP를 넣어줘야 한다.externalIPs:- <Router의 외부 IP>
kubectl get svc -n istio-system -l app=istio-eastwestgateway
다음과 같이 나오면 정상이다.
1
2
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-eastwestgateway LoadBalancer <클러스터 내부 IP> <LB의 IP>,<Router의 IP> 15021:30358/TCP,15443:30461/TCP,15012:30494/TCP,15017:32510/TCP 35h
istioEastWestGatewayServicePortPatch=this.provide(Resource,'istioEastWestGatewayServicePortPatch',()=>{constkubeConfigPath=this.k8sOkeEndpointStack.okeEndpointSource.shared.kubeConfigFilePath;constproxyUrl=this.k8sOkeEndpointStack.okeEndpointSource.shared.proxyUrl.socks5;constserviceName=this.istioEastWestGatewayRelease.shared.name;constnamespace=this.namespace.element.metadata.name;constadditionalPorts:{port: number;name: string;targetPort: number;protocol: string;present: boolean;}[]=[{port: 80,name:'http',targetPort: 80,protocol:'TCP',present: true,},{port: 443,name:'https',targetPort: 443,protocol:'TCP',present: true,},];constprovisioners=additionalPorts.map<LocalExecProvisioner>(({port,name,targetPort,protocol,present})=>{if(present){// 포트 추가
return{type:'local-exec',command: dedent`
port_exists=$(kubectl get svc ${serviceName} -n ${namespace}\ -o jsonpath='{.spec.ports[?(@.port==${port})].port}' 2>/dev/null || echo '')
if [ -z "$port_exists" ]; then
kubectl patch svc ${serviceName} -n ${namespace}\ --type='json' \ -p='[{"op": "add", "path": "/spec/ports/-", "value": {"name": "${name}", "port": ${port}, "protocol": "${protocol}", "targetPort": ${targetPort}}}]' || true
fi
`,environment:{KUBECONFIG: kubeConfigPath,HTTPS_PROXY: proxyUrl,},};}else{return{type:'local-exec',command: dedent`
port_exists=$(kubectl get svc ${serviceName} -n ${namespace}\ -o jsonpath="{.spec.ports[?(@.port==${port})].port}" 2>/dev/null || echo '')
if [ -z "$port_exists" ]; then
exit 0
fi
ports_list=$(kubectl get svc ${serviceName} -n ${namespace}\ -o jsonpath='{.spec.ports[*].port}' 2>/dev/null || echo '')
idx=0
for port_item in $ports_list; do
if [ "$port_item" = "${port}" ]; then
kubectl patch svc ${serviceName} -n ${namespace}\ --type='json' \ -p="[{\"op\": \"remove\", \"path\": \"/spec/ports/$idx\"}]" || exit 1
exit 0
fi
idx=$((idx + 1))
done
exit 1
`,environment:{KUBECONFIG: kubeConfigPath,HTTPS_PROXY: proxyUrl,},};}},);return{triggers:{releaseId: this.istioEastWestGatewayRelease.element.id,ports: JSON.stringify(additionalPorts),},dependsOn:[this.istioEastWestGatewayRelease.element],provisioners,};},);
OAuth2 Proxy
위에서 이어지는 문제이다.
기존 Nginx Ingress를 썼을 때는 GitHub OAuth2 앱을 기반으로 OAuth2 Proxy를 Nginx Auth 레이어에 추가해 보호 했었는데, Istio Gateway + VirtualService에서는 GitHub OAuth를 사용하는 것이 불가능했다.
엄밀히 말하면 가능은 한데, 여러 도메인을 동시에 보호하는 게 너무 힘들었다.
어차피 언젠가 옮길 생각도 있었어서, 이참에 별도의 인증 앱을 배포하기로 했다.
본래 Keycloak을 쓸 생각이었으나, Keycloak Helm Chart에도 적혀 있길, Bitnami가 2025년 8월 28일부로 기존 무료 컨테이너 이미지 대부분을 레거시 저장소로 옮기고 업데이트를 중단했다. (…)
이미지가 모두 내려가 있다
여기에 대해선 이리저리 말이 많은데, Bitnami가 Broadcom에 인수된 이후 Keycloak을 유료로 바꾸려는 게 아닌가 하는 의견이 지배적이다.
해결책:
다행히 Keycloak을 사용 중이던 것은 아니어서 그냥 대체 앱을 설치해서 쓰고 있다. 선택한 앱은 Authentik. 이거에 대해서도 추후 포스팅 하도록 하겠다.
다음은 실제 시연 영상이다.
Authentik으로 인증 거쳐서 Torrent 서비스에 접속
마치며
이번 작업을 통해 클라우드와 온프레미스 환경에 있는 두 개의 Kubernetes 클러스터를 하나의 통합된 서비스 메시로 연결할 수 있게 되었다.
멀티 클러스터 서비스 메시 구성 자체는 공식 문서를 따라하면 그렇게 어렵지 않았지만, 실제 운영 환경에서 마주한 문제들 — 특히 Ingress와 인증 레이어의 재구성 — 은 예상보다 많은 시간을 소모했다.
다만 이러한 과정을 통해 Istio의 Gateway/VirtualService 아키텍처에 대해 더 깊이 이해할 수 있었고, 결국 Nginx Ingress Controller에서 Istio로의 마이그레이션까지 완료할 수 있었다.
앞서 언급했듯이, 다음 단계로는 회사에서 운영 중인 Kubernetes 클러스터에 Multi-Primary 모델을 적용해볼 계획이다. 이렇게 하면 더 높은 가용성과 장애 격리를 확보할 수 있을 것이다.
또한 이번에 도입한 Authentik과 Istio Gateway 기반의 인증/라우팅 구조에 대해서도 별도의 포스팅으로 정리할 예정이다. 이 부분은 멀티 클러스터 구성보다는 일반적인 Istio 사용 사례에 가깝지만,
실제 운영 환경에서 겪은 경험들을 공유하면 누군가에게는 도움이 되지 않을까 싶다.
현대적인 마이크로서비스 아키텍처에서는 단일 클러스터의 한계를 넘어 여러 클러스터를 운영하는 것이 점차 표준이 되어가고 있다.
Istio는 이러한 트렌드에 부합하는 강력한 도구이며, 잘 활용한다면 복잡한 인프라를 일관된 방식으로 관리할 수 있다.
혹시 이 글을 읽는 누군가가 비슷한 작업을 계획하고 있다면, 이 글이 조금이나마 도움이 되기를 바란다.