Kubernetes Service를 공부하다 보면 NodePort와 LoadBalancer가 자주 나온다. 그런데 실습을 하다 보면 둘이 너무 비슷해 보인다.
NodePort도 Service 뒤의 Pod로 보내고
LoadBalancer도 Service 뒤의 Pod로 보낸다
그럼 둘이 뭐가 다른 거지?
그리고 curl은 어디로 쳐야 하지?
이번 글에서는 Service 타입이 NodePort일 때와 LoadBalancer일 때 각각 어디로 접속해야 하는지를 정리해보려고 한다.
Kubernetes에서 실제 애플리케이션은 Pod 안에서 실행된다.
예를 들어 nginx Pod가 3개 있다고 하자.
nginx Pod 1
nginx Pod 2
nginx Pod 3
그런데 Pod는 언제든지 죽고 다시 만들어질 수 있다. 이때 Pod IP도 바뀔 수 있다.
그래서 외부에서 Pod IP를 직접 바라보는 것은 좋지 않다.
Pod IP 직접 접근
→ Pod가 재생성되면 IP가 바뀜
→ 접근 주소가 불안정함
그래서 Kubernetes에서는 중간에 Service를 둔다.
사용자
↓
Service
↓
Pod 1 / Pod 2 / Pod 3
Service는 고정된 입구 역할을 하고, 뒤에 있는 Pod들로 요청을 보내준다.
즉 Service의 핵심은 이것이다.
Service = Pod 앞에 세우는 고정된 접근 지점
Service는 selector를 보고 Pod를 찾는다.
예를 들어 이런 Service YAML이 있다고 하자.
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
여기서 중요한 부분은 이것이다.
selector:
app: nginx
뜻은 다음과 같다.
app=nginx 라벨을 가진 Pod를 찾아라
그래서 app=nginx 라벨을 가진 Pod가 3개 있으면, Service는 그 3개 Pod를 자신의 대상으로 잡는다.
이걸 확인할 때 보는 것이 Endpoints다.
kubectl describe svc nginx-svc
결과에 이런 줄이 나오면:
Endpoints: 10.42.1.8:80,10.42.2.7:80,10.42.0.8:80
이 뜻이다.
nginx-svc Service가
10.42.1.8:80
10.42.2.7:80
10.42.0.8:80
이 세 개의 Pod를 바라보고 있다
즉 Service 뒤에 Pod 3개가 정상적으로 연결된 것이다.
이 부분은 NodePort든 LoadBalancer든 똑같다.
NodePort와 LoadBalancer는 둘 다 Kubernetes Service다.
그래서 둘 다 결국 이런 구조를 가진다.
Service
↓
Endpoints
↓
Pod 1 / Pod 2 / Pod 3
그래서 kubectl describe svc를 했을 때 Endpoints가 잡히는 모습은 비슷하다.
NodePort여도:
Endpoints: 10.42.1.8:80,10.42.2.7:80,10.42.0.8:80
LoadBalancer여도:
Endpoints: 10.42.1.8:80,10.42.2.7:80,10.42.0.8:80
이렇게 보일 수 있다.
그래서 실습하다 보면 당연히 이런 생각이 든다.
둘 다 결국 Pod 3개로 보내는 거 아닌가?
맞다.
최종 목적지는 둘 다 Pod다.
다른 점은 외부에서 Service까지 들어오는 입구다.
NodePort Service를 만들면 kubectl get svc 결과가 이런 식으로 나온다.
kubectl get svc nginx-svc
NAME TYPE CLUSTER-IP PORT(S)
nginx-svc NodePort 10.43.188.207 80:30001/TCP
여기서 중요한 부분은 이것이다.
TYPE = NodePort
PORT(S) = 80:30001/TCP
이 의미는 다음과 같다.
Service 내부 포트 = 80
외부에서 들어갈 NodePort = 30001
즉, 외부에서는 30001번 포트로 들어가야 한다.
일반 VM이나 AWS EC2 기반 Kubernetes라면 Node가 실제 서버다.
그러면 외부에서는 Node IP와 NodePort로 접근한다.
curl http://<Node-IP>:30001
예를 들어 Node IP가 다음과 같다면:
master-1 172.26.8.74
worker-1 172.26.3.104
worker-2 172.26.9.60
이렇게 접속할 수 있다.
curl http://172.26.8.74:30001
curl http://172.26.3.104:30001
curl http://172.26.9.60:30001
AWS라면 Worker Node의 External IP로 접속할 수 있다.
curl http://<worker-node-external-ip>:30001
브라우저에서는 이렇게 들어간다.
http://<worker-node-external-ip>:30001
단, AWS에서는 보안그룹이나 방화벽에서 30001 포트가 열려 있어야 한다.
NodePort Service 생성
+ AWS 보안그룹 30001 인바운드 오픈
= 외부에서 NodeIP:30001 접근 가능
NodePort는 이렇게 외우면 된다.
NodePort = Node IP + NodePort 번호로 들어간다
k3d에서는 조금 다르다.
k3d의 Node는 실제 VM이 아니라 Docker 컨테이너다.
Mac
↓
Docker Desktop
↓
k3d node containers
그래서 kubectl get nodes -o wide에 보이는 Node IP가 이런 식이어도:
k3d-my-cluster-server-0 172.19.0.3
k3d-my-cluster-agent-0 172.19.0.4
k3d-my-cluster-agent-1 172.19.0.5
Mac에서 바로 다음처럼 접속이 안 될 수 있다.
curl http://172.19.0.3:30001
왜냐하면 저 IP는 Mac이 직접 접근하는 실제 VM IP가 아니라 Docker 내부 네트워크의 컨테이너 IP이기 때문이다.
그래서 k3d에서는 클러스터를 만들 때 포트 매핑을 해줘야 한다.
k3d cluster create my-cluster \
--servers 1 \
--agents 2 \
--port "30001:30001@loadbalancer"
이 의미는 다음과 같다.
Mac localhost:30001
↓
k3d loadbalancer container:30001
↓
Kubernetes NodePort 30001
↓
nginx-svc
↓
nginx Pod
그러면 Mac에서는 이렇게 접속한다.
curl http://localhost:30001
정리하면:
NodePort + 일반 VM/AWS
→ curl http://NodeIP:30001
NodePort + k3d 포트 매핑
→ curl http://localhost:30001
이번에는 Service 타입이 LoadBalancer라고 하자.
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
적용한다.
kubectl apply -f loabalancer-svc.yaml
Service를 확인한다.
kubectl get svc nginx-svc
출력 예시는 다음과 같다.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
nginx-svc LoadBalancer 10.43.188.207 <pending> 80:xxxxx/TCP
여기서 중요한 부분은 이것이다.
TYPE = LoadBalancer
port = 80
LoadBalancer는 원래 외부 LoadBalancer 주소로 들어가는 방식이다.
AWS EKS 같은 클라우드 환경에서는 LoadBalancer Service를 만들면 AWS Load Balancer가 생성될 수 있다.
구조는 이렇게 된다.
외부 사용자
↓
AWS Load Balancer
↓
Kubernetes Service
↓
Pod 1 / Pod 2 / Pod 3
이 경우 kubectl get svc를 했을 때 EXTERNAL-IP나 외부 DNS가 잡힐 수 있다.
NAME TYPE CLUSTER-IP EXTERNAL-IP
nginx-svc LoadBalancer 10.43.x.x abc.elb.amazonaws.com
그러면 접속은 이렇게 한다.
curl http://abc.elb.amazonaws.com
또는:
curl http://<LoadBalancer-External-IP>
브라우저에서는 이렇게 들어간다.
http://abc.elb.amazonaws.com
만약 Service가 80번 포트로 열려 있다면 뒤에 :80은 생략할 수 있다.
http://abc.elb.amazonaws.com
=
http://abc.elb.amazonaws.com:80
왜냐하면 HTTP의 기본 포트가 80이기 때문이다.
k3d는 AWS가 아니다.
즉, LoadBalancer Service를 만들었다고 해서 진짜 AWS ELB 같은 외부 로드밸런서가 생기는 것은 아니다.
k3d에서는 Docker/k3d loadbalancer 컨테이너와 포트 매핑을 이용해 외부 접근을 흉내 낸다.
그래서 LoadBalancer Service를 Mac에서 확인하려면 클러스터를 만들 때 포트 매핑을 해줘야 한다.
예를 들어 Service가 80번 포트를 사용한다면:
k3d cluster create my-cluster \
--servers 1 \
--agents 2 \
--port "80:80@loadbalancer"
이 의미는 다음과 같다.
Mac localhost:80
↓
k3d loadbalancer container:80
↓
nginx-svc LoadBalancer Service:80
↓
nginx Pod:80
그러면 Mac에서 이렇게 접속할 수 있다.
curl http://localhost
또는:
curl http://localhost:80
둘은 같은 의미다.
왜냐하면 HTTP의 기본 포트가 80이기 때문이다.
만약 클러스터를 이렇게 만들었다고 하자.
k3d cluster create my-cluster \
--servers 1 \
--agents 2 \
--port "8080:80@loadbalancer"
이건 이렇게 읽는다.
Mac localhost:8080
↓
k3d loadbalancer container:80
↓
nginx-svc Service:80
↓
nginx Pod:80
이때는 Mac에서 이렇게 접속해야 한다.
curl http://localhost:8080
curl http://localhost로 하면 안 맞다.
왜냐하면 http://localhost는 자동으로 80번 포트로 요청을 보내는데, 지금 Mac에서 열어둔 포트는 8080이기 때문이다.
정리하면:
--port "80:80@loadbalancer"
→ curl http://localhost
--port "8080:80@loadbalancer"
→ curl http://localhost:8080
--port "3000:80@loadbalancer"
→ curl http://localhost:3000
앞쪽 포트가 내가 접속하는 포트다.
이제 진짜 핵심만 모아서 비교해보자.
Service 예시:
nginx-svc NodePort 10.43.188.207 80:30001/TCP
일반 VM/AWS:
curl http://<Node-IP>:30001
k3d에서 포트 매핑한 경우:
curl http://localhost:30001
Service 예시:
nginx-svc LoadBalancer 10.43.188.207 EXTERNAL-IP 80/TCP
AWS/EKS:
curl http://<LoadBalancer-DNS>
또는:
curl http://<LoadBalancer-External-IP>
k3d에서 80:80 포트 매핑한 경우:
curl http://localhost
k3d에서 8080:80 포트 매핑한 경우:
curl http://localhost:8080
NodePort든 LoadBalancer든 Service는 ClusterIP를 가진다.
그래서 클러스터 내부에서는 Service 이름이나 ClusterIP로 접근할 수 있다.
curl 테스트 Pod를 띄운다.
kubectl run curl-test --image=curlimages/curl -it --rm -- sh
Pod 안에서 Service 이름으로 접근한다.
curl http://nginx-svc
또는 ClusterIP로 접근한다.
curl http://10.43.188.207
이건 NodePort든 LoadBalancer든 가능하다.
왜냐하면 둘 다 결국 Kubernetes Service이고, 내부에서는 Service 이름이나 ClusterIP로 접근할 수 있기 때문이다.
curl-test Pod
↓
nginx-svc
↓
Pod 1 / Pod 2 / Pod 3
| Service 타입 | 환경 | 접속 주소 |
|---|---|---|
| NodePort | 일반 VM/AWS | curl http://<Node-IP>:30001 |
| NodePort | k3d 포트 매핑 있음 | curl http://localhost:30001 |
| LoadBalancer | AWS/EKS | curl http://<LoadBalancer-DNS> |
| LoadBalancer | k3d 80:80 매핑 | curl http://localhost |
| LoadBalancer | k3d 8080:80 매핑 | curl http://localhost:8080 |
| 둘 다 내부 테스트 | curl-test Pod 안 | curl http://nginx-svc |
NodePort는 이렇게 외우면 된다.
NodePort
= Node IP와 NodePort 번호로 들어간다
= http://NodeIP:30001
LoadBalancer는 이렇게 외우면 된다.
LoadBalancer
= LoadBalancer 주소로 들어간다
= http://로드밸런서주소
k3d에서는 이렇게 외우면 된다.
k3d는 Node가 Docker 컨테이너다
그래서 Mac에서 localhost로 들어가려면 클러스터 생성 시 포트 매핑이 필요하다
포트 매핑은 이렇게 읽는다.
앞쪽 포트 : 뒤쪽 포트
앞쪽 포트 = 내가 curl이나 브라우저에서 치는 포트
뒤쪽 포트 = k3d 내부로 넘길 포트
예를 들어:
--port "8080:80@loadbalancer"
이면:
내가 접속하는 주소 = localhost:8080
k3d 내부로 넘기는 포트 = 80
그래서 접속은 이렇게 한다.
curl http://localhost:8080
NodePort와 LoadBalancer는 둘 다 결국 Service 뒤의 Pod들로 트래픽을 보낸다.
그래서 Endpoints를 보면 비슷해 보이는 것이 정상이다.
Service
↓
Endpoints
↓
Pod들
하지만 외부에서 Service까지 들어오는 입구가 다르다.
NodePort
= 사용자가 NodeIP:NodePort로 직접 들어감
LoadBalancer
= 외부 LoadBalancer 주소로 들어감
그리고 k3d에서는 진짜 클라우드 LoadBalancer가 있는 것이 아니기 때문에, Mac에서 테스트하려면 클러스터 생성 시 포트 매핑이 필요하다.
한 문장으로 정리하면 다음과 같다.
NodePort는 NodeIP:NodePort로 들어가고, LoadBalancer는 LoadBalancer 주소로 들어가며, k3d에서는 둘 다 localhost로 테스트하려면 미리 포트 매핑을 해줘야 한다.