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 등)에 한해서 접속가능하다.

블로그 이미지

망원동똑똑이

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

,

서비스에 할당 가능한 IP 대역은 kube-apiserver 의 --service-cluster-ip-range 으로 지정되어있다.

아래와 같은 방법으로 조회한다.

$ kubectl get pods -n kube-system -l component=kube-apiserver -o name | xargs kubectl get -n kube-system -o yaml | grep service-cluster-ip-range
   - --service-cluster-ip-range=10.96.0.0/16

 

블로그 이미지

망원동똑똑이

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

,

클러스터 내부에서 가상 IP를 할당받고, 각 노드에 파드로 떠있는 kube-proxy 를 통해서 파드와 통신하는 내부용 로드 밸런서이다.

쿠버네티스 API 에 접속 가능하게 하기 위해 클러스터 생성시 기본적으로 기동되는 Kubernetes 서비스도 ClusterIP 이다.

kube-proxy 는 각 노드에 파드로 떠있으며, kube-system 네임스페이스에서 조회 가능하다.

$ kubectl get pods -o wide -n kube-system | grep proxy
kube-proxy-6pn2g                                1/1     Running       1 (45d ago)   119d   172.18.0.4   desktop-worker          <none>           <none>
kube-proxy-c8r4l                                1/1     Running       1 (45d ago)   119d   172.18.0.3   desktop-worker2         <none>           <none>
kube-proxy-l52jn                                1/1     Running       0             119d   172.18.0.3   desktop-control-plane   <none>           <none>

 

1. ClusterIP 생성

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

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

 

spec.ports[].port 는 ClusterIP 에서 수신하는 포트를 지정하고, spec.ports[].targetPort 는 목적지 파드의 포트번호를 지정한다.

 

이제 클러스터 내부 파드에서 방금 생성한 서비스명인 sample-clusterip 를 도메인명으로 호출하면, 서비스가 바라보고 있는 파드들(app=sample-app 라벨을 가지고 있는 파드들)로 로드밸런싱 된다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-9qdjm  ClientIP=10.244.1.158  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.1.159  XFF=
pod "testpod" deleted
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://sample-clusterip:8080
Host=sample-clusterip  Path=/  From=sample-deployment-75c768d5fb-2lkwp  ClientIP=10.244.1.163  XFF=
pod "testpod" deleted

 

2. ClusterIP 의 서비스 디스커버리

한가지 의문이 생긴다. 파드에서 sample-clusterip 도메인으로 호출했을 뿐인데 어떻게 도메인을 해석하고 목적지 파드까지 도달하게 되는 걸까? 아래와 같은 순서로 해석된다.

 

1) 파드 내에서 질의

파드 내에서 http://sample-clusterip:8080 질의

 

2) 파드 내 DNS 레코드 해석

파드 내 /etc/resolv.conf DNS 레코드에 따라 suffix 를 붙여 kube-dns 로 질의한다.

/etc/resolv.conf 는 아래 커맨드로 조회할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

이에 따라 10.96.0.10 DNS(kube-dns) 에서 해석이 가능 할 때까지 search 에 지정된 suffix 를 붙여 아래 순서대로 질의한다.

sample-clusterip.default.svc.cluster.local
sample-clusterip.svc.cluster.local
sample-clusterip.cluster.local
sample-clusterip

 

3) kube-dns 의 블록 매칭

kube-dns 에서는 coredns configMap 의 설정에 따라 kubernetes cluster.local 블록에 매칭되어 쿠버네티스 API 서버인 kube-apiserver 에 질의한다.
해당 설정은 아래 커맨드로 조회 할 수 있다.

$ kubectl get configmaps coredns -n kube-system -o yaml
...
kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
...

 

 

4) kube-apiserver 에서 네임스페이스와 서비스 명칭으로 서비스 IP 를 조회

쿠버네티스가 자동 생성하는 서비스의 DNS 이름 규칙에 따라 해당하는 서비스의 IP 를 조회한다.

규칙은 아래와 같다.

{서비스명}.{네임스페이스명}.svc.cluster.local

 

5) kube-dns 에서 이를 A 레코드로 응답하여 해석됨

실제로 dig 명령어를 통해 10.96.0.10 DNS 에서 도메인을 어떻게 해석하는지 질의 가능하다.

 

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig @10.96.0.10 sample-clusterip.default.svc.cluster.local
;; ANSWER SECTION:
sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60

 

10.96.82.60 서비스가 응답함을 의미한다.

 

6) 해당 서비스를 호출하여 각 파드로 로드밸런싱 됨

해석된 서비스 IP 인 10.96.82.60:8080 으로 호출한다.

 

3. ClusterIP 의 가상 IP 직접 지정

일반적으로 ClusterIP 를 생성할 때 IP 를 지정하지 않기 때문에 할당 가능한 대역 내에서 자동 할당된다. 하지만, 직접 IP 로 서비스를 참조하고자 하는 상황이 있을 수 있다.

  • DNS 캐시나 방화벽 정책을 고정 IP로 관리할 때(IP 기반 ACL)
  • 테스트 환경에서 동일한 IP 를 사용해야 할 때

직접 IP 를 지정하기 위해서는 spec.clusterIP 에 지정할 IP 를 설정한다.

apiVersion: v1
kind: Service
metadata:
  name: clusterip-vip
spec:
  type: ClusterIP
  clusterIP: 10.96.82.70
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

 

아래 커맨드로 직접 지정한 IP 가 설정되었는지 확인 가능하다.

$ kubectl get services clusterip-vip
NAME               TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
clusterip-vip      ClusterIP   10.96.82.70   <none>        8080/TCP   13m

 

다만, 지정하는 IP 는 아래와 같은 조건이 있으므로 주의하여 사용한다.

Service CIDR 대역 내 IP 로 지정해야 함 예: 10.96.0.0/12 범위 안이어야 함(kube-apiserver의 --service-cluster-ip-range 옵션으로 설정되는 범위)
이미 다른 Service 가 사용중이지 않아야 함 IP 충돌 발생 시 생성 실패 (The Service "X" is invalid: spec.clusterIP: Invalid value: "10.96.100.50": provided IP is already allocated)
한번 IP 를 지정하여 생성하면 변경할 수 없음(immutable) IP 를 변경하려면 삭제 후 재생성 해야 함

 

블로그 이미지

망원동똑똑이

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

,