LoadBalancer 서비스는 클러스터 외부에 L4 로드밸런서를 따로 두고, Kubernetes 에서 이 로드밸런서를 연동하여 사용하는 것이다. Amazon EKS, GKE, AKS 등의 퍼블릭 클라우드 프로바이더가 제공하는 AWS EKS, GKE 등의 플랫폼에서는 AWS NLB, GCP LB, Azure LB 등의 L4 로드 밸런서와의 연동을 기본 제공한다.(베어메탈 LoadBalancer 구현체인 MetalLB 등으로 온프레미스에서도 사용 가능함) 예를 들어, GKE 에서 LoadBalancer 서비스를 생성하면 GCP LB(Google Cloud Platform Load Balancer)가 자동 생성되고, 여기에 가상 IP가 할당되며, 쿠버네티스 클러스터 내의 노드들과 연결된다.

 

NodePort 나 ClusterIP - ExternalIPs 를 사용하면 진입점이 특정 노드가 되기 때문에, 해당 노드 장애시에는 서비스가 불가능해진다. 하지만 LoadBalancer 서비스를 사용하면 별도의 외부 로드밸런서가 진입점이 되기 때문에 안정성이 제고된다. 외부 로드 밸런서가 쿠버네티스 클러스터의 NodePort 서비스를 통해 여러 노드에 걸쳐 로드밸런싱을 하는 구조이다.

LoadBalancer 서비스를 생성하면, 내부적으로는 외부 로드밸런서에서 각 노드로 들어오는 트래픽을 수신하기 위해 NodePort 가 생성되고, 목적지 파드로 트래픽을 전송하기 위해 ClusterIP 도 생성된다.(실제 서비스 리소스가 생성되는 것은 아니고, NAT 규칙이 설정되는 개념)

 

1. LoadBalancer 생성

아래와 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Service
metadata:
  name: sample-lb
spec:
  type: LoadBalancer
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30082
  selector:
    app: sample-app

 

spec.ports[] 하위에 지정하는 포트 번호들은 아래와 같은 의미를 가진다.

항목 의미
spec.ports[].nodePort 모든 노드 IP에서 수신할 Port 번호
spec.ports[].port LoadBalancer 에 할당된 가상 IP 와 ClusterIP 에서 수신할 Port 번호
spec.ports[].targetPort 목적지 파드(컨테이너) 포트 번호

 

각각의 포트가 사용되는 구간을 그림으로 보면 아래와 같다.

 

생성 후에 아래 커맨드로 조회가 가능하다.

$ kubectl get services
NAME              TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
sample-lb         LoadBalancer   10.96.78.27   172.18.0.7    8080:30082/TCP   13m

 

sample-lb 서비스 이름으로 클러스터 내부에서 통신시 자동으로 설정된 ClusterIP 에서 DNS 해석을 진행하는 것을 볼 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -
- dig sample-lb.default.svc.cluster.local
...
;; QUESTION SECTION:
;sample-lb.default.svc.cluster.local. IN	A

;; ANSWER SECTION:
sample-lb.default.svc.cluster.local. 30	IN A	10.96.78.27
...
pod "testpod" deleted

 

2. 로드 밸런서 가상 IP 정적 지정 방법

실제 운영 서비스에서는 서비스의 엔드포인트가 되는 로드 밸런서의 주소를 IP 가 아닌 의미있는 도메인을 할당해서 사용하기 때문에 DNS 설정 등을 위해 고정 IP 를 할당하는 경우가 많다. 이때는 spec.loadBalancerIP 항목에 예약한 IP 주소를 입력한다.

apiVersion: v1
kind: Service
metadata:
  name: sample-lb
spec:
  type: LoadBalancer
  loadBalancerIP: xxx.xxx.xxx.xxx # 여기에 로드 밸런서의 IP로 사용할 주소를 입력
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

3. 로드 밸런서 방화벽 정책 설정

LoadBalancer 서비스를 생성하여 로드 밸런서를 사용하면 기본적으로 전세계에서 접속할 수 있는 상태가 된다. AWS EKS 나 GKE 에서는 spec.loadBalancerSourceRanges 에 접속을 허가할 IP 범위를 지정하면, 클라우드 프로바이더가 제공하는 방화벽 기능을 사용하여 접속을 제한할 수 있다. 기본값이 0.0.0.0/0 이기 때문에 제한이 없는 상태이다.

spec:
...
  loadBalancerSourceRanges:
  - 10.0.0.0/8
...

 

클라우드 프로바이더가 제공하는 방화벽이 없는 경우에 이 설정을 적용하면 쿠버네티스 노드의 iptables 를 사용하여 접속 제한이 이루어진다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

ClusterIP - ExternalIPs 서비스는 "지정한 노드"에서 수신한 트래픽만 특정 파드로 연결해준다고 하였다. NodePort 서비스는 약간 다르게, "전체 노드"에서 수신한 트래픽을 특정 파드로 연결해준다.

NodePort 서비스를 생성하면 내부적으로는 파드 네트워크에서 목적지 파드로 트래픽을 전달하는 ClusterIP 가 생성되며(실제 서비스가 생성되는 것은 아니고, NAT 규칙이 생성되는 개념임), kube-proxy 가 관리하는 NAT 규칙에 ClusterIP 를 거치도록 명시된다.

NodePort → ClusterIP → Pod

 

NodePort 생성시 아래와 같은 개념으로 clusterIP 속성이 지정된다.

Service A (type=NodePort)
    └─ spec.clusterIP: 10.x.x.x (자동 생성)

 

NodePort 는 지정한 포트를 Listen 하고있으며, 외부에서 Node 의 해당 포트로 접속하면 NodePort 가 트래픽을 ClusterIP 로 전달해주는 형태로 작동한다. ClusterIP 서비스가 받은 트래픽을 어떻게 목적지 파드로 분배하는지는 ClusterIP 서비스 에서 설명하였다.

 

1. 생성하기

아래 매니페스트로 NodePort 서비스를 생성한다.

apiVersion: v1
kind: Service
metadata:
  name: sample-nodeport
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30080
  selector:
    app: sample-app

 

spec.ports[] 의 각 항목은 아래와 같은 의미를 가진다.

항목 의미
spec.ports[].nodePort 모든 노드 IP에서 수신할 Port 번호
spec.ports[].port ClusterIP 에서 수신할 Port 번호
spec.ports[].targetPort 목적지 파드(컨테이너) 포트 번호

 

그림으로 보면 아래와 같은 구역에서 수신하는 포트이다.

 

아래 커맨드로 생성된 NodePort 서비스를 조회한다. CLUSTER-IP 가 자동으로 할당되는 것을 알 수 있다.

NAME              TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
sample-nodeport   NodePort    10.96.41.17   <none>        8080:30080/TCP   50m

 

실제 클러스터 내부의 파드에서 ClusterIP 가 서비스 디스커버리를 수행하는지 보려면 아래 커맨드를 실행한다.

# 클러스터 내부 DNS에서 sample-nodeport 라는 서비스 이름을 해석할 때 ClusterIP 의 주소(즉, 10.96.41.17)로 해석되는 것을 볼 수 있다.
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -
- dig sample-nodeport.default.svc.cluster.local
...
;; QUESTION SECTION:
;sample-nodeport.default.svc.cluster.local. IN A

;; ANSWER SECTION:
sample-nodeport.default.svc.cluster.local. 30 IN A 10.96.41.17
...
pod "testpod" deleted

 

2. nodePort 지정시 유의할 점

spec.ports[].nodePort 로 NodePort 서비스가 수신할 포트를 지정한다고 했는데, 대부분의 쿠버네티스 환경에서는 기본적으로 30000~32767 범위의 값을 허용한다. nodePort 를 직접 지정하지 않으면 자동으로 할당되는 값도 이 범위 안에서 결정된다.

범위를 벗어난 nodePort 를 지정하면 아래와 같은 에러가 출력된다.

The Service "sample-nodeport" is invalid: spec.ports[0].nodePort: Invalid value: 8888: provided port is not in the valid range. The range of valid ports is 30000-32767

 

또한, 모든 node 에서 Listen 할 포트를 지정하는 것이기 때문에, 이미 사용중인 포트를 지정하는 것도 불가능하다. 이미 다른 NodePort 서비스가 사용중인 포트를 사용하려고 할 때는 아래와 같은 에러가 출력된다.

The Service "sample-nodeport" is invalid: spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

ClusterIP 서비스에 externarlIPs 를 지정하면, 추가적인 IP 로도 ClusterIP 서비스로의 접근이 가능하게 된다. spec.type: ExternalIP 로 지정하는게 아닌, ClusterIP 로 지정하고, spec.externalIPs 를 지정한다. 단, externalIPs 에는 트래픽을 보내고자 하는 node 의 IP 를 지정해야 한다. externalIPs 를 지정하면, 쿠버네티스 클러스터 외부에서도 클러스터 내부 node IP 로 통신이 가능하며, 파드에 대한 요청도 분산되어 처리된다.

필드 설정값
spec.externalIPs 외부로 노출할 쿠버네티스 노드 IP 주소
spec.ports[].port ClusterIP 서비스에서 수신할 포트 번호
spec.ports[].targetPort 목적지 파드 포트 번호

 

서비스를 생성하기 위해서 먼저 node 의 IP 를 조회한다.

$ kubectl get nodes -o custom-columns='NAME:{.metadata.name},IP:{.status.addresses[?(@.type=="InternalIP")].address}'
NAME                    IP
desktop-control-plane   172.18.0.6,fc00:f853:ccd:e793::6
desktop-worker          172.18.0.4
desktop-worker2         172.18.0.3

 

아래 매니페스트를 사용하여 externalIPs 를 지정한 ClusterIP 서비스를 생성하자. externalIPs 에는 위에서 조회한 노드의 IP 중 클러스터 외부에서 접속 가능하게 할 IP를 기재한다.

apiVersion: v1
kind: Service
metadata:
  name: sample-externalip
spec:
  type: ClusterIP
  externalIPs:
    - 172.18.0.3 # node 의 IP
    - 172.18.0.4 # node 의 IP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

생성한 서비스를 조회해보자

$ kubectl get services
# sample-externalip 서비스의 EXTERNAL-IP 에 지정한 node IP 들이 출력됨을 확인한다.
NAME                TYPE        CLUSTER-IP     EXTERNAL-IP             PORT(S)    AGE
kubernetes          ClusterIP   10.96.0.1      <none>                  443/TCP    98d
sample-externalip   ClusterIP   10.96.37.106   172.18.0.3,172.18.0.4   8080/TCP   14s

$ kubectl describe service sample-externalip
Name:                     sample-externalip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.37.106
IPs:                      10.96.37.106
External IPs:             172.18.0.3,172.18.0.4
Port:                     http-port  8080/TCP
TargetPort:               80/TCP
Endpoints:                10.244.2.118:80,10.244.2.117:80,10.244.1.133:80
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
Events:                   <none>

 

클러스터 내부 파드에서 dig 를 이용해서 클러스터 내부 DNS 에서 반환하는 서비스의 IP 주소를 확인해보면, 위에서 생성한 서비스의 IP 주소임을 확인할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig sample-externalip.default.svc.cluster.local
...
;; QUESTION SECTION:
;sample-externalip.default.svc.cluster.local. IN	A

;; ANSWER SECTION:
sample-externalip.default.svc.cluster.local. 30	IN A 10.96.37.106
...
pod "testpod" deleted

 

이렇게 서비스를 생성하면 지정한 노드의 iptables 규칙에 kube-proxy 가 아래와 같은 규칙을 추가한다.(일부만 표시)

...
# 파드 IP 가 직접 등록되어있다.
Chain KUBE-SEP-LNFMA4JXKHGYU5GA (1 references)
    0     0 KUBE-MARK-MASQ  0    --  *      *       10.244.2.118         0.0.0.0/0            /* default/sample-externalip:http-port */
Chain KUBE-SEP-PIHNOKMVC3JPXE5V (1 references)
    0     0 KUBE-MARK-MASQ  0    --  *      *       10.244.2.117         0.0.0.0/0            /* default/sample-externalip:http-port */
...
Chain KUBE-SERVICES (2 references)
...
# ClusterIP 서비스 IP/Port 와 설정된 ExternalIPs(즉, 노드 IP)가 등록되어있다.
    0     0 KUBE-SVC-PCX4Y6UD55K35SPY  6    --  *      *       0.0.0.0/0            10.96.37.106         /* default/sample-externalip:http-port cluster IP */ tcp dpt:8080
    0     0 KUBE-EXT-PCX4Y6UD55K35SPY  6    --  *      *       0.0.0.0/0            172.18.0.3           /* default/sample-externalip:http-port external IP */ tcp dpt:8080
    0     0 KUBE-EXT-PCX4Y6UD55K35SPY  6    --  *      *       0.0.0.0/0            172.18.0.4           /* default/sample-externalip:http-port external IP */ tcp dpt:8080
...
# 파드로 로드밸런싱하는 방식이 지정되어있다.
Chain KUBE-SVC-PCX4Y6UD55K35SPY (2 references)
    0     0 KUBE-MARK-MASQ  6    --  *      *      !10.244.0.0/16        10.96.37.106         /* default/sample-externalip:http-port cluster IP */ tcp dpt:8080
    0     0 KUBE-SEP-HNM32ADNEKRGXVEK  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/sample-externalip:http-port -> 10.244.1.133:80 */ statistic mode random probability 0.33333333349
    0     0 KUBE-SEP-PIHNOKMVC3JPXE5V  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/sample-externalip:http-port -> 10.244.2.117:80 */ statistic mode random probability 0.50000000000
    0     0 KUBE-SEP-LNFMA4JXKHGYU5GA  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/sample-externalip:http-port -> 10.244.2.118:80 */
...

 

이 iptables 규칙은 해당 노드의 리눅스 커널이 "직접" 참조하며, 이를 바탕으로 패킷을 필터링하고 NAT 를 직접 수행한다. kube-proxy 파드가 NAT를 수행하는 것이 "아님"에 유의하자. kube-proxy 는 서비스 설정에 따라 노드에 iptables/IPVS 규칙을 생성할 뿐이다.

 

참고로, 각 노드에 실행중인 kube-proxy 파드는 아래 커맨드로 확인 가능하다.

# kube-proxy 파드 확인
$ kubectl get pods -n kube-system -o wide -l k8s-app=kube-proxy
NAME               READY   STATUS    RESTARTS      AGE    IP           NODE                    NOMINATED NODE   READINESS GATES
kube-proxy-6pn2g   1/1     Running   1 (52d ago)   126d   172.18.0.4   desktop-worker          <none>           <none>
kube-proxy-c8r4l   1/1     Running   1 (52d ago)   126d   172.18.0.3   desktop-worker2         <none>           <none>
kube-proxy-l52jn   1/1     Running   0             126d   172.18.0.3   desktop-control-plane   <none>           <none>

 

참고로, 노드의 iptables 를 아래 커맨드로 조회할 수 있다.

# 특정 노드의 kube-proxy 파드에 접속
$ kubectl -n kube-system exec -it kube-proxy-xxxxx -- sh

# 그 안에서 iptables 조회
$ iptables -t nat -L -n -v | grep KUBE

 

노드에 해당 포트로 패킷이 인입되면 노드의 리눅스 커널에서 iptables 에 명시된 NAT 규칙에 따라 목적지 Pod 로 NAT 를 수행한다. ClusterIP 서비스가 런타임(패킷이 들어오는 시점)에 직접 NAT 를 수행하는 것이 아니라는 점에 유의해야 한다. ClusterIP는 로드밸런싱 로직을 정의할 뿐이다. 즉, 실제 NAT는 커널이 수행하지만, "어느 파드로 NAT할지 결정하는" 서비스 로직을 ClusterIP 설정을 기반으로 kube-proxy 가 미리 ClusterIP 체인 로직으로 만들어둔다.(iptables 모드. selector에 매칭되는 각 파드가 대상임.)

 

정리하면, 아래와 같다.

[1] 외부 클라이언트 → 172.18.0.3:8080
       ↓
[2] 노드 172.18.0.3의 kube-proxy가 생성한 iptables/IPVS 규칙이 매칭됨
(노드 리눅스 커널 네트워크 계층에서 수행됨)
       ↓
[3] 해당 규칙이 패킷을 ClusterIP의 externalIPs 로 NAT 수행
(노드 리눅스 커널 네트워크 계층에서 수행됨)
       ↓
[4] ClusterIP 규칙에 따라 파드 중 하나(예: 10.244.1.133:80)로 로드밸런싱
(노드 리눅스 커널 네트워크 계층에서 수행됨)
       ↓
[5] 파드 내 컨테이너 프로세스가 80포트에서 응답

 

단, 무조건 클러스터 외부에서 접속할 수 있는건 아니고, 그 노드에 도달할 수 있는 외부 네트워크(예: 같은 사설망. LAN, VPC 등)에 한해서 접속가능하다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,