
지난 포스팅에서는 모든 노드에 파드를 하나씩 띄우는 데몬셋, 데이터를 영구적으로 유지하는 스테이트풀셋 등 다양한 특수 워크로드와 노드 관리 스킬에 대해 알아보았습니다.
지금까지는 "파드를 어떻게, 어디에 띄울 것인가"에 집중했다면, 이제부터는 "그렇게 띄운 파드들에 어떻게 접근하고 통신할 것인가" 에 대한 이야기를 해보려고 합니다.
파드는 언제든 죽고 다시 살아날 수 있는 일회성(Ephermeral) 자원이라 재시작될 때마다 IP 주소가 계속 변합니다.
따라서 파드의 IP를 직접 바라보고 통신하는 것은 매우 위험합니다. 변하지 않는 고정적인 접근점(Endpoint) 이 필요한데, 이 역할을 해주는 것이 바로 Service API입니다.
이번 포스팅에서는 쿠버네티스 네트워크의 기본 개념과 파드 간 내부 통신의 핵심인 ClusterIP, 그리고 Service Discovery에 대해 알아보겠습니다. 🚀
Service를 알아보기 전에 먼저 쿠버네티스 클러스터 내부의 네트워크가 어떻게 구성되는지 간단히 짚고 넘어가겠습니다.
동일한 파드 내의 통신: 같은 파드 안에 있는 컨테이너들은 동일한 IP 주소를 공유하므로 localhost로 서로 통신할 수 있습니다.
다른 파드와의 통신: 할당된 파드의 고유 IP를 이용해서 통신합니다.
클러스터를 생성하면 노드 상에 파드를 위한 내부 네트워크가 자동으로 구성됩니다. 사용하는 CNI(Container Network Interface) 플러그인에 따라 다르지만, 기본적으로 노드별로 다른 네트워크 세그먼트가 할당되고 노드 간 트래픽은 VXLAN이나 L2 라우팅 기술을 통해 전송됩니다. (이 대역은 쿠버네티스가 알아서 분할해 주므로 우리가 직접 설정할 필요는 없습니다!)
파드 간 통신은 IP만 알면 서비스 없이도 가능하지만, Service 리소스를 사용하면 다음과 같은 이점을 얻을 수 있습니다.
로드밸런싱(LoadBalancing): 여러 개의 파드로 트래픽을 고르게 분산시켜 줍니다.
서비스 디스커버리(Service Discovery): 파드의 IP가 수시로 바뀌어도, 서비스에 부여된 '이름(DNS)'을 통해 알아서 목적지를 찾아가게 해줍니다.
💡참고: 사용자가 직접 다루는 네트워크 리소스는 크게 L4 로드밸런싱을 제공하는 Service와 L7 로드밸런싱을 제공하는 Ingress로 나뉩니다. (Ingress는 추후 자세히 다뤄보겠습니다.)
ClusterIP는 쿠버네티스에서 가장 기본이 되는 서비스 타입입니다.
이 서비스를 생성하면 클러스터 내에서만 통신 가능한 내부 네트워크용 가상 IP가 할당됩니다. 외부에서 접근할 필요가 없는 내부 백엔드 서비스나 DB를 연결할 때 주로 사용합니다.
(기본적으로 떠 있는 kubernetes API 서버 서비스도 ClusterIP 타입입니다.)
-동작원리: Cluster IP로 요청을 보내면, 각 노드상에서 실행 중인 시스템 구성 요소인 kube-proxy가 이를 가로채서 연결된 파드들로 트래픽을 전송합니다.
백엔드가 될 Nginx 파드 3개 (Deployment)와 이를 묶어줄 Service(Cluster IP)를 함께 배포해보겠습니다.
👇 아래 매니페스트는 파드를 생성하고, 해당 파드들을 'sample-clusterip'라는 이름의 서비스로 묶어주는 설정입니다.
# sample-clusterip.yaml에 한 번에 작성
# 1. 타겟이 될 Nginx 파드를 3개 띄우는 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-deployment
spec:
replicas: 3
selector:
matchLabels:
app: sample-app # Service가 이 레이블을 보고 파드를 찾습니다.
template:
metadata:
labels:
app: sample-app
spec:
containers:
- name: nginx-container
image: amsy810/echo-nginx:v2.0
---
# 2. 위에서 만든 3개의 파드를 묶어줄 ClusterIP Service
apiVersion: v1
kind: Service
metadata:
name: sample-clusterip
spec:
type: ClusterIP # 기본값이므로 생략 가능합니다.
ports:
- name: "http-port"
protocol: "TCP"
port: 8080 # 서비스가 수신할 포트 (클라이언트가 접근하는 포트)
targetPort: 80 # 실제 트래픽을 전달할 목적지 컨테이너의 포트
selector:
app: sample-app # 앞서 Deployment에서 지정한 레이블과 매칭!
작성했으면 kubectl apply -f sample-clusterip.yaml로 실행해줍니다.
Service가 파드들을 제대로 찾았는지 확인하려면 Endpoints를 반드시 체크해야 합니다.
$ kubectl describe services sample-clusterip

...
Selector: app=sample-app
Type: ClusterIP
IP: 10.110.181.91
Port: http-port 8080/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.167:80,10.244.2.113:80,10.244.2.114:80 # 👈 핵심!
...
출력 결과를 보면 Selector를 통해 연결된 파드들을 찾고, Endpoints에 실제 접근할 파드들의 IP 리스트가 매핑된 것을 볼 수 있습니다.
클러스터 내에서 서비스 IP(10.110.181.91:8080)로 요청을 보내면, 이 3개의 Endpoints 중 하나로 로드밸런싱되어 실제 처리가 이루어집니다.
ClusterIP를 더 유연하게 사용할 수 있는 설정 팁 두 가지를 소개합니다.
포트 번호를 하드코딩하는 대신 컨테이너 포트에 name을 부여하고, Service에서 이 이름을 참조하게 만들 수 있습니다. 이렇게 하면 나중에 파드의 포트 번호가 바뀌어도 Service 설정을 건드릴 필요가 없어 유지보수가 편해집니다.
# Deployment 컨테이너 설정 부분
containers:
- name: nginx-container
image: amsy810/echo-nginx:v2.0
ports:
- name: http-web # 👈 컨테이너 포트에 'http-web'이라는 이름 부여
containerPort: 80
---
# Service 설정 부분
ports:
- name: "http-port"
protocol: "TCP"
port: 8080
targetPort: http-web # 👈 포트 번호(80) 대신 지정한 이름으로 연결
Service의 IP는 생성 시마다 랜덤하게 할당되지만, 레거시 시스템 연동 등의 이유로 고정된 IP가 필요할 때가 있습니다. 이때는 spec.clusterIP에 원하는 IP를 직접 명시하면 됩니다.
(단, IP는 생성 후에 변경할 수 없으므로, 수정하려면 기존 Service를 삭제하고 다시 생성해야 합니다.)
spec:
type: ClusterIP
clusterIP: 10.101.106.108 # 클러스터 IP 대역 내의 고정 IP 지정
컨테이너 환경에서 애플리케이션 설정 파일에 IP 주소를 명시적으로 하드코딩하는 것은 절대 피해야 합니다. 파드가 죽으면 IP가 무조건 바뀌기 때문입니다.
이 문제를 깔끔하게 해결해 주는 것이 바로 DNS를 사용한 서비스 디스커버리(Service Discovery) 입니다.
쿠버네티스 내부에는 DNS 서버(CoreDNS)가 기본적으로 동작하고 있습니다. Service를 생성하면 해당 서비스 이름으로 DNS 레코드가 자동 등록됩니다. 따라서 우리는 매번 바뀌는 IP 대신 "서비스 이름" 만 알고 있으면 통신이 가능합니다.
FQDN (정식 명칭) 구조: 서비스이름.네임스페이스이름.svc.cluster.local (예: sample-clusterip.default.svc.cluster.local)
같은 네임스페이스 안에서는 뒤의 꼬리표를 떼고 단순히 sample-clusterip라는 이름만으로도 통신할 수 있습니다.
실제로 파드안에 들어가서 DNS 기능이 잘 작동하는지 보겠습니다.
클러스터 내부에 있는 파드에 직접 접속해서, IP가 아닌 '서비스 이름' 으로 통신이 되는지 확인해볼 겁니다.
먼저 아까 띄워둔 파드 중 하나의 이름을 확인하고, 그 파드 내부의 터미널(sh)로 접속합니다. 그런 다음 서비스 이름으로 curl 요청을 보내보겠습니다.
# 1. 내 클러스터에 띄워진 파드의 정확한 이름 확인
$ kubectl get pods
# 2. 확인한 파드 내부의 셸(sh)로 접속
$ kubectl exec -it sample-deployment-5d9fcfcbb6-8g42x -- sh
# 3. 파드 내부에서 IP 대신 '서비스 이름'으로 HTTP 요청 전송
$ curl http://sample-clusterip:8080

/ # curl http://sample-clusterip:8080
Host=sample-clusterip Path=/ From=sample-deployment-5d9fcfcbb6-8g42x ClientIP=10.244.2.1 XFF=
일반적인 Nginx 웹 서버라면 "Welcome to nginx!" 같은 HTML 웹페이지 코드가 출력되었겠지만, 위와 같이 독특한 문자열이 나온 이유는 우리가 앞서 배포한 amsy810/echo-nginx 이미지 특성 때문입니다.
이 이미지는 '보낸 요청을 누가, 어떻게 받았는지' 를 echo 처럼 분석해서 화면에 직관적으로 보여주는 네트워크 테스트용 이미지입니다.
Host=sample-clusterip : DNS가 해석해 준 목적지 주소 (IP가 아닌 이름을 완벽하게 찾아가는 것을 알 수 있습니다.)
Path=/: 요청한 웹 경로 (루트)
From=sample-deployment-5d9fcfcbb6-8g42x : 이 요청을 실제로 넘겨받아 처리해 준 최종 파드의 이름
ClientIP=10.244.2.1 : 이 요청을 보낸 클라이언트 내부 IP
XFF: HTTP 프록시 통신 이력
💡Tip: 로드밸런싱 직접 눈으로 확인하기
터미널에서
curl http://sample-clusterip:8080명령어를 여러 번 연속 실행을 해봅시다.
결과값 중
From=뒷부분의 파드 이름이 3개의 파드로 계속 바뀌어 출력되는 것을 볼 수 있습니다. 이를 통해 Service가 뒤에 있는 파드들에게 트래픽을 골고루 나눠주는 '로드밸런싱' 역할을 하고 있다는 것을 눈으로 확인해볼 수 있습니다.🧐 왜 똑같은 파드 이름이 연속으로 나올까요?
스크린샷을 자세히 보면 1, 2, 3번 순서대로 트래픽이 가는 것이 아니라, 특정 파드(...-kmflf)에 3번 연속으로 트래픽이 가기도 합니다.
이는 쿠버네티스의 기본 트래픽 분배 방식(kube-proxy의 iptables 모드)이 엄격한 순번제(Round-Robin)가 아닌 랜덤(확률 기반) 으로 트래픽을 분배하기 때문입니다. 아주 정상적인 동작입니다.
파드는 일회성 자원이므로 새로 생성될 때마다 IP가 계속 바뀝니다. 따라서 변하지 않는 단일 진입점인 ClusterIP가 필수적입니다. 더 나아가, IP 주소 대신 '서비스 이름(name)' 을 사용하게 되면 파드의 IP나 포트 번호가 바뀌더라도 설정 변경 없이 안정적으로 통신할 수 있게 됩니다.
이번 시간에는 이렇게 쿠버네티스 네트워크의 가장 기본이 되는 ClusterIP와, 변하는 IP 대신 이름으로 서로를 찾게 해주는 Service Discovery(DNS) 에 대해 알아보았습니다.
하지만 ClusterIP는 말 그대로 '클러스터 내부용'입니다. 우리가 만든 웹 서비스를 외부 사용자가 인터넷을 통해 접속하게 하려면 어떻게 해야 할까요?
다음 포스팅에서는 클러스터 외부로 서비스를 노출하는 관문 역할을 하는 NodePort와 LoadBalancer (MetalLB) 에 대해 알아보겠습니다.