
지난 포스팅에서는 외부 트래픽을 클러스터 내부로 안전하게 연결해주는 NodePort, 실무 표준인 LoadBalancer, 그리고 온프레미스 로드밸런서 MetalLB에 대해 알아보았습니다.
이제 외부에서 우리 서비스로 접속하는 '큰 길'은 뚫어두었는데, 실무에서는 단순히 길을 여는 것만으로는 부족할 때가 많습니다. 트래픽을 아주 디테일하게 조종하거나, K8s 외부의 레거시 시스템과 똑똑하게 연동해야 하는 등 네트워크 고급 기능들이 필요해지죠.
이번 포스팅에서는 쿠버네티스 네트워크를 내 마음대로 주무르기 위한 Service API의 고급 기능 6가지를 크게 두 가지 파트로 나누어 정리해 보겠습니다. 🚀
첫 번째 파트에서는 클러스터 내부로 들어온 트래픽을 목적에 맞게 분배하고 최적화하는 4가지 방법을 알아보겠습니다.
일반적으로 쿠버네티스 서비스는 요청이 들어올 때마다 여러 파드 중 하나로 무작위 분산(LoadBalancing)을 수행합니다.
하지만 로그인 세션이나 장바구니 데이터처럼 한 번 연결된 사용자는 다음 트래픽도 계속 동일한 파드로 전송되어야 하는 경우가 있습니다. 이때 사용하는 기능이 바로 Session Affinity 입니다.
기본값: None (들어올 때마다 다른 파드로 로드밸런싱)
설정 방법: 서비스 매니페스트의 spec.sessionAffinity를 ClientIP로 설정합니다.
유지 시간: spec.sessionAffinityConfig.clientIP.timeoutSeconds를 통해 최대 세션 고정 시간을 설정할 수 있습니다.
💡 원리: kube-proxy가 각 쿠버네티스 워커 노드의
iptables에 규칙을 기록하여 특정 IP의 트래픽을 기억하고 고정해 줍니다.
externalTrafficPolicy: Local) 🛑NodePort나 LoadBalancer 서비스로 외부 요청이 들어오면 기본적으로 2단계 로드밸런싱을 거칩니다. (클라우드 로드밸런서 ➔ 노드 A 도착 ➔ 다시 로드밸런싱 ➔ 노드 B 파드로 전송)
트래픽을 고르게 분산하기엔 좋지만, 다른 노드로 건너가면서 네트워크 지연(Latency)이 발생하고, 주소 변환(SNAT)이 일어나 클라이언트의 진짜 원본 IP를 알 수 없게 되는 치명적인 단점이 있습니다.
이 문제를 해결하는 옵션이 바로 externalTrafficPolicy: Local 입니다.
이 옵션을 켜면 노드에 트래픽이 도착한 후, 다른 노드로 2차 분산을 하지 않고 무조건 자신이 들어온 노드에 있는 파드에게만 통신을 시도합니다. 원본 클라이언트 IP 확보가 필수적이거나 데몬셋(DaemonSet) 구조에서 아주 유용합니다.
# sample-nodeport-local.yaml
apiVersion: v1
kind: Service
metadata:
name: sample-nodeport-local
spec:
type: NodePort
externalTrafficPolicy: Local # 핵심 설정!
ports:
- port: 8080
targetPort: 80
nodePort: 30081
selector:
app: sample-app
💡 실습 결과 요약 (기본값 vs Local 정책)
만약 기본값(
Cluster)이었다면 트래픽이 이리저리 튀면서 파드 로그에 K8s 내부 IP(10.244.x.x)가 찍혔겠지만,Local정책을 적용하면 다른 노드로 건너뛰지 않아 요청을 보낸 실제 클라이언트의 원본 IP가 그대로 보존됩니다.
클러스터 규모가 커져서 서울, 도쿄, 워싱턴 등 여러 리전(Region)이나 가용 영역(Zone)에 노드들이 흩어져 있다고 가정해 봅시다.
서울 노드에 있는 웹 파드가 무작위 로드밸런싱 때문에 미국에 있는 백엔드 파드로 트래픽을 던져버린다면 어떨까요? 응답 속도가 폭발적으로 늘어나고 네트워크 전송 비용도 엄청나게 발생할 것입니다.
쿠버네티스는 이를 막기 위해 "자기 자신과 물리적/논리적으로 가장 가까운 곳에 있는 파드에게 우선적으로 트래픽을 보내는" 토폴로지 고려 라우팅(Topology Aware Routing) 기능을 제공합니다. 노드의 위치 레이블(Zone)을 인식하여 굳이 멀리 있는 노드로 로드밸런싱 하지 않고 가까운 파드로 똑똑하게 라우팅합니다.
service.kubernetes.io/topology-mode: Auto를 추가해주면 끝입니다! (K8s 1.24+ 기준)일반적인 서비스(ClusterIP)는 콜센터의 '대표 번호' 와 같습니다. 대표 번호(가상 IP)로 전화하면 알아서 여유로운 상담원(파드)에게 연결해 주죠.
하지만 데이터베이스(DB) 클러스터처럼 로드밸런싱이 오히려 독이 되는 경우가 있습니다. DB의 Master(쓰기)와 Slave(읽기) 파드는 역할이 명확히 달라서, 콕 집어서 원하는 파드와 직접 통신해야 합니다.
이럴 때 사용하는 것이 '머리(가상 IP)'를 없애버린 Headless Service입니다.
설정 방법: spec.clusterIP: None 으로 설정합니다.
동작 방식: 이 서비스 이름으로 DNS 질의(nslookup)를 하면 가상 IP 대신 연결된 파드들의 실제 IP 주소 목록 전체를 반환합니다.
mysql-0, mysql-1처럼 파드의 고유한 이름이 보장되는 StatefulSet과 함께 자주 쓰입니다.
자, 지금까지는 K8s 내부의 트래픽을 다뤘습니다. 하지만 현업에서는 AWS RDS 같은 매니지드 DB나 외부 레거시 서버 등 쿠버네티스 외부 소프트웨어와 연동해야 할 일이 무척 많습니다. 이때 유용한 2가지 방법을 소개합니다.
애플리케이션 소스 코드에 외부 서비스의 긴 도메인 주소(예: my-db.ap-northeast-2.rds.amazonaws.com)를 직접 하드코딩하면, 나중에 DB 주소가 바뀔 때마다 코드를 수정하고 K8s 파드를 다시 배포해야 하는 대참사가 일어납니다.
이러한 '강한 결합'을 끊어내기 위해 ExternalName 서비스를 사용합니다.
apiVersion: v1
kind: Service
metadata:
name: google # 쿠버네티스 내부에서 사용할 짧은 별명
spec:
type: ExternalName
externalName: www.google.com # 실제 목적지 도메인
이렇게 설정하면 내부 파드들은 google이라는 짧은 서비스 이름만 호출해도 됩니다. 쿠버네티스 DNS 서버가 이를 가로채어 실제 외부 도메인(www.google.com)으로 연결해 줍니다.
목적지가 바뀌면 애플리케이션 코드는 놔두고 이 Service 매니페스트만 수정하면 되므로 느슨한 결합(Loose Coupling) 을 완벽하게 유지할 수 있습니다.
보통 Service를 만들 때는 selector를 지정하여 트래픽을 보낼 파드들을 결정하고, K8s는 이를 바탕으로 Endpoints 리소스를 '자동'으로 만들어 줍니다.
🤔 "그렇다면 만약 매니페스트에 Selector를 안 적으면 어떻게 될까요?"
정답은 'K8s의 자동화를 끄고, 우리가 직접 목적지(Endpoints)를 수동으로 지정할 수 있다' 입니다.
ExternalName이 외부 도메인 주소(CNAME)를 반환한다면, None-Selector 서비스는 K8s 내부 가상 IP(ClusterIP)를 유지한 채 트래픽을 외부망 IP로 분산(LoadBalancing) 시켜버립니다.
Service를 만들고, 완전히 똑같은 이름으로 목적지 외부 IP를 적어둔 Endpoints 리소스를 직접 생성합니다.# 1. Selector가 없는 서비스
apiVersion: v1
kind: Service
metadata:
name: sample-none-selector # 이름 일치 주의!
spec:
ports:
- port: 8080
---
# 2. 외부 IP를 가리키는 Endpoints 직접 생성
apiVersion: v1
kind: Endpoints
metadata:
name: sample-none-selector # 이름 일치 주의!
subsets:
- addresses:
- ip: 198.51.100.1 # 클러스터 외부의 목적지 IP 1
- ip: 198.51.100.2 # 클러스터 외부의 목적지 IP 2
ports:
- port: 80
이 방식을 쓰면 개발 환경에서는 K8s 내부 DB를 쳐다보고, 운영 환경에서는 외부 레거시 DB를 쳐다보게 만들더라도 애플리케이션은 똑같은 ClusterIP 서비스 하나만 호출하면 됩니다. 외부 리소스를 클러스터 내부 리소스인 것처럼 완벽하게 위장할 수 있는 강력한 기능입니다!