퍼블릭 클라우드를 사용하면, 실제 물리 서버를 어떤 Region 과 Zone(Availability Zone) 의 것을 사용할지 결정하게 된다. Region 은 Zone 을 묶는 상위 개념이며, Region 과 Region 사이에는 도시나 국가 만큼의 큰 거리 차이가 존재한다. Zone 은 하나의 Region 안에 여러개 존재할 수 있으며, 흔히 생각하는 데이터센터라고 생각하면 좋다.

  • Region: ap-northeast-2 (서울)
    • Zone: ap-northeast-2a (데이터센터 a)
    • Zone: ap-northeast-2b (데이터센터 b)
    • Zone: ap-northeast-2c (데이터센터 c)

아래는 Region 과 Zone 의 차이를 비교한 표이다.

규모 도시/국가 단위 데이터센터 단위
물리적 거리 수십~수백 km 수백 m ~ 수 km
네트워크 레이턴시 높음 (10~200ms) 매우 낮음 (1~2ms)
장애 범위 전체 Region 장애 가능 Zone 단위 장애
비용 Region 간 트래픽 비쌈 Zone 간 트래픽 저렴 또는 무료
목적 DR(재해복구), 글로벌 배포 고가용성, 고성능 서비스

 

레이턴시를 고려하였을 때, 최대한 같은 zone 에 존재하는 node 의 pod 로 요청을 보내는 것이 유리하다. 이러한 설정을 하는 것을 Topology Aware Hint 라고 한다.

 

1. Topology Aware Hint 의 기능

같은 zone/region 으로 요청을 보내도록 우선순위를 설정할 수 있다. 기본 설정은 같은 zone 우선이다. 같은 node 로 우선순위를 설정할 수는 없다. 같은 node 단위에서 라우팅 하도록 하려면 클러스터 외부 트래픽에 대해 노드에서의 로드 밸런싱 제외 을 참고하자.(단 외부 요청에 한함)

 

2. 설정방법

Topology Aware Hint 는 Service 에 직접 설정하는 것이 아니라, EndpointSlice 컨트롤러가 node 의 zone/region 라벨을 보고 EndpointSlice에 hint를 자동으로 생성해준다.(hints.forZones) 각 노드에 존재하는 kube-proxy 는이 힌트를 읽고, client 노드와 같은 zone 의 Endpoints 를 우선으로 라우팅해준다.

실제로 사람이 설정해주어야 하는 것은 node 별로 label 을 붙여주는 것이다. 즉, 라벨만 붙여주면 클라이언트 node 와 동일한 zone/region 의 파드가 우선적으로 선택되게 된다. 참고로, 퍼블릭 클라우드를 사용하면 기본적으로 zone/region 라벨이 붙게 된다.

  • topology.kubernetes.io/region
  • topology.kubernetes.io/zone
$ kubectl label nodes node1 topology.kubernetes.io/region=ap-northeast-2
$ kubectl label nodes node1 topology.kubernetes.io/zone=ap-northeast-2-a

topology.kubernetes.io/region: ap-northeast-2
topology.kubernetes.io/zone: ap-northeast-2-a

 

3. 확인방법

아래의 커맨드로 EndpointSlice 를 조회해보면 fotZones 힌트가 생성된 것을 볼 수 있다.

$ kubectl get endpointslice -l kubernetes.io/service-name={ClusterIP 서비스명} -o yaml
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: sample-svc-abc12
addressType: IPv4
endpoints:
  - addresses:
      - 10.244.1.15
    zone: zone-a
    hints:
      forZones:
        - name: zone-a
  - addresses:
      - 10.244.2.19
    zone: zone-b
    hints:
      forZones:
        - name: zone-b
ports:
  - port: 80

 

https://kubernetes.io/ko/docs/concepts/services-networking/topology-aware-hints/ 참고

블로그 이미지

망원동똑똑이

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

,

NodePort 나 LoadBalancer 서비스에서는 외부 트래픽이 노드로 도착하면, 노드에서 쿠버네티스 클러스터 내의 적절한 파드로 로드밸런싱을 해준다. 즉, 트래픽을 받은 노드에 속한 파드로 전송되는 것이 보장되지 않는 것이다. 만약 LoadBalancer 서비스에서 외부 로드밸러서가 1차적으로 각 노드로 로드 밸런싱 중이라면, 노드에서 불필요한 2차 로드밸런싱이 이루어지는 형태가 된다.(NAT 가 발생해 발신측 IP 주소가 유실되는 문제도 있다.)

일반적인 파드의 경우는 각 노드에 1개 이상이 존재하는 것을 보장하지는 않기 때문에, 이러한 노드에서 파드로의 로드 밸런싱이 필요하다. 하지만, 데몬셋 같이 하나의 노드에 하나의 파드가 보장되는 경우에는 굳이 다른 노드의 파드로 로드 밸런싱 할 필요가 없어진다. 그런 경우에는 spec.externalTrafficPolicy 항목을 사용하여 외부 트래픽을 노드 내에서만 처리할지를 설정할 수 있다.

 

1. 노드 내에서만 로드 밸런싱 하도록 설정

아래의 매니페스트와 같이 spec.externalTrafficPolicy: Local 으로 생성한다. 외부로부터의 트래픽을 어떻게 로드밸런싱 할지에 대한 정책이기 때문에 당연히 NodePort, LoadBalancer 서비스에서 사용 가능하다.(ClusterIP 는 내부 트래픽이기 때문에 설정 불가능)

apiVersion: v1
kind: Service
metadata:
  name: nodeport-local
spec:
  type: NodePort
  externalTrafficPolicy: Local
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30081
  selector:
    app: sample-app
spec.externalTrafficPolicy 설정값 설명
Cluster 노드에 도착한 트래픽을 클러스터 내에 존재하는 다른 노드에 속한 파드까지 대상으로 하여 다시 로드밸런싱한다.(기본값)
Local 노드에 도착한 트래픽을 해당 노드 내의 파드 대상으로만 로드밸런싱한다.

 

이제 아래 커맨드로 파드가 1개 존재하는 desktop-worker 노드를 호출하면, 여러번 호출해도 동일한 파드로 요청이 가는 것을 볼 수 있다. client IP 주소도 유지된다.

# 서비스 IP, Port 확인
$ kubectl get svc nodeport-local
NAME                    TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
sample-nodeport-local   NodePort   10.96.25.136   <none>        8080:30081/TCP   76m

# 파드 확인
$ kubectl get pods -o custom-columns="NAME:{metadata.name},Node:{spec.nodeName},NodeIP:{status.hostIP}"
NAME                                 Node              NodeIP
sample-deployment-75c768d5fb-2lkwp   desktop-worker2   172.18.0.3
sample-deployment-75c768d5fb-9qdjm   desktop-worker    172.18.0.4
sample-deployment-75c768d5fb-g5vkv   desktop-worker2   172.18.0.3

# 아래 커맨드를 여러번 실행해본다.
$ curl -s http://172.18.0.4:30081

 

2. LoadBalancer 에서 노드 health check 하기

NodePort 서비스에서 spec.externalTrafficPolicy: Local 로 지정한 경우, 노드 내에 대상 파드가 하나도 없는 경우에는 TCP 수준의 Connection refused 에러가 발생한다. 목적지 포트를 Listen 하는 프로세스(파드)가 아예 없기 때문이다. 반면에 LoadBalancer 서비스에서는 spec.externalTrafficPolicy: Local, spec.healthCheckNodePort 를 지정하여(미지정시 랜덤 포트가 자동 지정됨) 외부 로드 밸런서가 해당 노드가 타겟 파드를 가지고 있는지 확인할 수 있게 한다.(헬스 체크) 해당 노드에 파드가 존재하지 않으면 http status 503 이 반환되고, 로드 밸런싱 대상에서 제외된다.

아래와 같은 매니페스트로 LoadBalancer 에 헬스 체크용 노드포트를 지정한다.(30086)

apiVersion: v1
kind: Service
metadata:
  name: lb-local
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  healthCheckNodePort: 30086
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30085
  selector:
    app: sample-app

 

spec.healthCheckNodePort 는 LoadBalancer 서비스 이면서 spec.externalTrafficPolicy: Local 인 경우만 지정할 수 있는 항목이다. 아래는 헬스 체크 엔드포인트가 응답하는 예시이다.

$ curl -s http://172.18.0.4:30086
{
  "service": {
    "namespace": "default",
    "name": "lb-local"
  },
  "localEndpoints": 1
}

 

블로그 이미지

망원동똑똑이

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

,

세션 어피니티란, 한번 연결된 타겟 파드로 다음번에도 연결되도록 하는 것이다. 즉, ClusterIP 서비스에 세션 어피니티를 적용한 경우(즉, 내부 네트워크에 적용), 파드끼리 통신할 때 한번 연결된 파드에 다음번 통신때도 연결된다.

 

1. 세션 어피니티 설정

아래와 같이 spec.sessionAffinity 항목과 spec.sessionAffinityConfig 항목을 설정한다.

spec.sessionAffinity: ClientIP 는 요청을 보낸 측을 식별할 때 client IP 주소로 식별하겠다는 것이고, spec.sessionAffinityConfig 의 설정은 세션 고정 시간을 의미한다.

apiVersion: v1
kind: Service
metadata:
  name: session-affinity
spec:
  type: LoadBalancer
  selector:
    app: sample-app
  ports:
  - name: http-port
    protocol: TCP
    port: 8080
    targetPort: 80
    nodePort: 30084
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10

 

sessionAffinityConfig.clientIP.timeoutSeconds: 10 을 설정했으므로, 요청 후 10초 이내의 요청은 같은 파드로 전송된다.

$ kubectl get pods
NAME                                 READY   STATUS    RESTARTS   AGE
sample-deployment-75c768d5fb-2lkwp   1/1     Running   0          33d
sample-deployment-75c768d5fb-9qdjm   1/1     Running   0          33d
sample-deployment-75c768d5fb-g5vkv   1/1     Running   0          33d

# 첫 요청
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://session-affinity.default.svc.cluster.local:8080
Host=session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.2.118  XFF=

# 첫 요청으로부터 10초 이내에 요청
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://session-affinity.default.svc.cluster.local:8080
Host=session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.2.118  XFF=

# 두번째 요청으로부터 10초 이내에 요청
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://session-affinity.default.svc.cluster.local:8080
Host=session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.2.118  XFF=

# 세번째 요청으로부터 10초 초과한 후 요청
# 다른 파드로 연결됨
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://session-affinity.default.svc.cluster.local:8080
Host=session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-9qdjm  ClientIP=10.244.2.118  XFF=

 

spec.sessionAffinity: None 으로 수정한 후 동일한 테스트를 진행해보면, 10초 이내에 재요청 하더라도 다른 파드에 전송되는 것을 볼 수 있다.

# 첫번째 요청
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://no-session-affinity.default.svc.cluster.local:8080
Host=no-session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.2.118  XFF=

# 첫번째 요청 후 10초 이내 두번째 요청 => 다른 파드로 전송됨
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://no-session-affinity.default.svc.cluster.local:8080
Host=no-session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-9qdjm  ClientIP=10.244.2.118  XFF=

# 두번째 요청 후 10초 이내 세번째 요청 => 다른 파드로 전송됨
$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://no-session-affinity.default.svc.cluster.local:8080
Host=no-session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.2.118  XFF=

# 세번째 요청 후 10초 이내 네번째 요청 => 다른 파드로 전송됨
kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- curl http://no-session-affinity.default.svc.cluster.local:8080
Host=no-session-affinity.default.svc.cluster.local  Path=/  From=sample-deployment-75c768d5fb-2lkwp  ClientIP=10.244.2.1  XFF=

 

위의 예제에서는 LoadBalancer 서비스를 생성할 때 생성된 ClusterIP 를 통해 1개의 노드 내에서 세션 어피니티가 적용되는 모습을 보았다. 그런데, 세션 어피니티는 실제로는 각 쿠버네티스 노드에 iptables 로 구현되어 있다. 따라서 NodePort 서비스에 세션 어피니티를 지정하면 동일한 client 의 재요청이라 하더라도 어느 노드에서 수신하느냐에 따라 다른 파드로 연결될 수 있음에 유의하자.(즉, 세션 어피니티는 노드 단위에서 작동한다.)

블로그 이미지

망원동똑똑이

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

,

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

블로그 이미지

망원동똑똑이

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

,

서비스에 할당 가능한 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 를 변경하려면 삭제 후 재생성 해야 함

 

블로그 이미지

망원동똑똑이

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

,

먼저, 클러스터 내부 DNS 가 무엇인지 알아야 한다.

클러스터 내부 DNS 는 쿠버네티스 파드에서 질의한 도메인이 최우선적으로 바라보는 쿠버네티스 DNS 이다. 도메인이 클러스터 내부 도메인 규칙을 준수한다면 외부 DNS 에 질의하지 않고 클러스터 내부에서 도메인 해석을 끝낸다. 클러스터 내부 DNS 는 kube-system 네임스페이스에 속한 kube-dns 라는 서비스가 그 실체이다.(ClusterIP 타입)

아래의 커맨드로 조회할 수 있다.

$ kubectl get services -n kube-system
# kube-dns 서비스의 CLUSTER-IP 확인. 이 주소가 클러스터 내부 DNS 서버 주소이다.

 

쿠버네티스에서 서비스 디스커버리란, 특정 서비스명으로부터 그 서비스의 엔드포인트 정보를 판별하는 것이다. 특정 조건의 대상이 되는 파드를 판별하는 것이라고 생각해도 좋다. 서비스 디스커버리를 수행하는 방법은 크게 아래 세가지가 있다.

  • 환경변수를 이용
  • DNS A 레코드를 이용
  • DNS SRV 레코드를 이용

1. 환경변수를 이용한 서비스 디스커버리

서비스와 연결된 Pod 에 대해 아래 커멘드처럼 env 를 조회하면, kubernetes 에서 관리하는 환경변수가 설정되어 있음을 알 수 있다. 이 환경변수 값들을 가지고 서비스 디스커버리를 수행하는 것이다. 파드 템플릿의 spec.enableServiceLinks: false 로 설정시에는 환경변수 추가가 비활성화된다.(기본값 true)

$ kubectl exec -it sample-deployment-75c768d5fb-2lkwp -- env | grep -i kubernetes
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

 

2. DNS A 레코드를 이용한 서비스 디스커버리

쿠버네티스에서 서비스 엔드포인트에 접속하기 위해서는 당연히 서비스에 할당된 IP 주소를 사용할 수 있지만, 클러스터 내부 DNS 에 자동 등록된 IP-DNS 레코드명을 사용하는 것이 관리 측면에서 바람직하다. IP 주소는 서비스를 생성할 때 마다 변경되기 때문에 미리 알 수 없기 때문이다. DNS명으로 주소를 사용하면 서비스 재생성에 따른 IP 주소 변경에 신경 쓸 필요가 없어진다.

ClusterIP 를 생성하면, 서비스의 name 값이 자동으로 DNS명으로 등록된다. 아래의 커맨드로 ClusterIP 서비스로 호출을 해보면 IP로 직접 호출하는것과 서비스 name 으로 호출하는 것이 동일한 것을 알 수 있다.

# 서비스의 IP로 호출
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- curl -s http://10.96.82.60:8080
Host=10.96.82.60  Path=/  From=sample-deployment-75c768d5fb-g5vkv  ClientIP=10.244.1.135  XFF=
pod "testpod" deleted

# 서비스의 name 으로 호출(DNS명으로 호출)
$ 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.134  XFF=
pod "testpod" deleted

 

실제로 등록된 FQDN 형식은 "{서비스명}.{네임스페이스명}.svc.cluster.local" 이다. 이를 확인하려면 클러스터 내의 컨테이너에서 dig(Domain Information Groper) 명령어를 사용해보면 된다. 아래 커맨드를 실행하여 클러스터 내부 DNS 에 등록된 레코드를 확인해보자. sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60 레코드가 등록된 것을 알 수 있다.

# dig 명령어로 DNS 정보 확인
$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig sample-clusterip.default.svc.cluster.local
If you don't see a command prompt, try pressing enter.
...
;; QUESTION SECTION:
;sample-clusterip.default.svc.cluster.local. IN A

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

 

QUESTION SECTION 에 출력된

;sample-clusterip.default.svc.cluster.local. IN A

내용을 해석하면 아래와 같다.

  • ; → dig에서 질의한 도메인 이름을 표시할 때 붙는 기호
  • sample-clusterip.default.svc.cluster.local. → 실제 DNS 서버에 요청한 도메인 전체 이름(FQDN)
  • IN → 인터넷 클래스
  • A → IPv4 주소를 요청했다는 뜻

ANSWER SECTION 에 출력된

sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60

내용을 해석하면 아래와 같다.

 

  • sample-clusterip.default.svc.cluster.local. → 응답한 도메인 이름
  • 30 → TTL(Time To Live): DNS 결과를 캐시해둘 수 있는 시간(초 단위)
  • IN → 인터넷 클래스
  • A → IPv4 주소를 요청했다는 뜻
  • 10.96.82.60 → 해당 서비스의 ClusterIP 주소

 

즉, 쿠버네티스 DNS(CoreDNS)가 sample-clusterip라는 Service를 찾아서, 그 서비스의 클러스터 내부 IP(10.96.82.60)를 응답했다는 뜻이다.

 

아래의 커맨드로 컨테이너 내의 DNS 설정파일을 확인하면, ndots:5 와 search: default.svc.cluster.local, svc.cluster.local, cluster.local 값이 명시되어 있다. 즉, 질의된 도메인의 dot 갯수가 5개 미만이면 질의된 도메인의 suffix 로 search 에 명시된 도메인들을 하나씩 붙여 질의해보는 것이다. 모든 질의에 실패하게 되면 최초 질의된 도메인으로 질의하게 된다.

예를 들어, sample-clusterip 도메인으로 질의하면 dot 이 5개 미만이라 FQDN 으로 인식하지 않기 때문에 아래 순서대로 질의에 성공할때까지 질의하게 된다.

  1. sample-clusterip.default.svc.cluster.local
  2. sample-clusterip.svc.cluster.local
  3. sample-clusterip.cluster.local
  4. sample-clusterip

예를 들어, test1.test2.sample.clusterip.com 도메인으로 질의하면 dot 이 5개 미만이라 마찬가지로 아래 순서대로 성공할때까지 질의함

  1. test1.test2.sample.clusterip.com.default.svc.cluster.local
  2. test1.test2.sample.clusterip.com.svc.cluster.local
  3. test1.test2.sample.clusterip.com.cluster.local
  4. test1.test2.sample.clusterip.com
$ 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
pod "testpod" deleted

 

 

출력 결과를 보면 알 수 있듯이 default 네임스페이스임을 명시하고 있다. 만약 test 네임스페이스라면 default 부분이 test 로 되어있을 것이다. 같은 네임스페이스에 속한 도메인으로 질의할 때는 네임스페이스를 생략하여도 되지만, 다른 네임스페이스로 질의할 때는 명시해야 하는 것이다.

 

아래 커맨드를 이용하면 역방향으로 IP -> 도메인명을 질의할 수 있다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -- dig -x 10.96.82.60
...
;; QUESTION SECTION:
;60.82.96.10.in-addr.arpa.	IN	PTR

;; ANSWER SECTION:
60.82.96.10.in-addr.arpa. 30	IN	PTR	sample-clusterip.default.svc.cluster.local.
...
pod "testpod" deleted

 

3. DNS SRV 레코드를 이용한 서비스 디스커버리

SRV 레코드란 특정 서비스가 어떤 호스트와 포트에서 제공되는지를 나타내는 DNS 레코드이다. 즉, SRV 레코드를 조회하면 특정 서비스가 어떤 포트에서 서비스되고 있는지 알 수 있다. 쿠버네티스에서 자동으로 생성하는 SRV 레코드 규칙은 다음과 같다.(port 명과 protocol 에는 접두어 "_" 가 포함됨)

 

"_{서비스 포트명}._{프로토콜}.{서비스명}.{네임스페이스명}.svc.cluster.local"

 

예를 들어 default 네임스페이스에서 아래와 같은 매니페스트로 생성된 서비스는

"_http-port.tcp.sample-clusterip.default.svc.cluster.local" 으로 SRV 레코드가 생성된다.

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

 

아래 커맨드로 컨테이너 내부에서 SRV 레코드를 조회할 수 있다.

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

;; ANSWER SECTION:
_http-port._tcp.sample-clusterip.default.svc.cluster.local. 30 IN SRV 0 100 8080 sample-clusterip.default.svc.cluster.local.

;; ADDITIONAL SECTION:
sample-clusterip.default.svc.cluster.local. 30 IN A 10.96.82.60
...
pod "testpod" deleted

 

ANSWER SECTION 에 출력된

_http-port._tcp.sample-clusterip.default.svc.cluster.local. 30 IN SRV 0 100 8080 sample-clusterip.default.svc.cluster.local.

내용을 해석하면 아래와 같다.

이름 _http-port._tcp.sample-clusterip.default.svc.cluster.local. 질의한 서비스 이름
TTL 30 30초 동안 캐시 가능
클래스 IN 인터넷 클래스
타입 SRV SRV 레코드
Priority 0 우선순위 (낮을수록 우선)
Weight 100 동일 우선순위 내에서의 부하분산 비율
Port 8080 서비스가 제공되는 포트
Target sample-clusterip.default.svc.cluster.local. 실제 트래픽을 전달할 대상 호스트명(서비스의 DNS 이름)

 

즉, sample-clusterip.default.svc.cluster.local 서비스의 _http-port라는 HTTP 서비스가 TCP 8080 포트에서 제공되고 있다는 것을 DNS 레코드로부터 해석할 수 있다.


 

위와 같이 쿠버네티스 클러스터 내부 DNS 의 서비스 디스커버리 방식을 알아보았다. 그렇다면, 클러스터 내부 DNS 가 아닌 클러스터 외부 DNS 를 사용하려면 어떻게 해야 할까?

[Kubernetes] 파드 DNS 설정 에서 다루었는데, 파드 매니페스트의 spec.dnsPolicy: None 를 지정하고 spec.dnsConfig 필드에 외부 DNS 서버를 지정해주면 된다. 이러한 설정을 해주지 않으면 기본적으로 spec.dnsPolicy: ClusterFirst 으로 적용되어 클러스터 내부 DNS 에 우선 질의한다.(단, spec.dnsPolicy: None 지정시에도 searchs, options 설정에 따라 클러스터 내부 DNS 를 우선적으로 질의하도록 설정할 수 있다.) 클러스터 내부 DNS 에는 "*.cluster.local" 레코드가 지정되어 있으므로 이를 서비스 디스커버리에 우선적으로 사용하고, 조건에 부합하지 않는 도메인 질의시에는 외부 DNS 에 다시 질의하게 된다.

 

DNS 질의 시간을 단축하기 위하여 클러스터 내부 DNS 인 kube-dns 를 사용하기 전에 각 노드에 DNS 캐시 파드를 따로 두고, 이를 우선적으로 사용하는 구조도 있다.(Node Local DNS Cache)

블로그 이미지

망원동똑똑이

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

,

1. Multi Port ClusterIP

하나의 ClusterIP 서비스에서 여러 Port 의 엔드포인트를 가지고, 각 Port마다 파드의 다른 Port 로 연결도 가능하다.

아래의 매니페스트처럼 spec.ports 배열필드에 여러 포트를 지정하면 된다. 여기서는 서비스의 8080 -> 파드의 80, 서비스의 8443 -> 파드의 443 포트로 연결된다.

apiVersion: v1
kind: Service
metadata:
  name: clusterip-multi
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
    - name: "https-port"
      protocol: "TCP"
      port: 8443
      targetPort: 443
  selector:
    app: sample-app

 

2. 포트 이름을 사용한 참조

Service 의 spec.ports[].targetPort 에 포트 번호를 지정하지 않고, 미리 정의해놓은 포트 이름으로 포트번호를 사용할 수도 있다. 먼저 서비스가 띄워질 파드를 정의할 때 spec.containers[].ports[] 에 포트 이름과 포트 번호를 지정하고, Service 에서는 targetPort 에 해당 포트 이름으로 설정해주면 된다. 이렇게 하면 Service 매니페스트에서는 동일한 변수명으로 포트를 지정하더라도, 실제 서비스가 띄워지는 Pod 마다 다른 포트로 연결해줄 수 있다.

 

아래의 매니페스트로 "http" 이름의 port 가 각각 80, 81 포트로 정의된 파드를 한개씩 띄우자

---
apiVersion: v1
kind: Pod
metadata:
  name: named-port-pod-80
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: amsy810/echo-nginx:v2.0
      ports:
        - name: http
          containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
  name: named-port-pod-81
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: amsy810/echo-nginx:v2.0
      env:
        - name: NGINX_PORT
          value: "81"
      ports:
        - name: http
          containerPort: 81

 

아래의 매니페스트로 "http" 이름을 통해 targetPort 를 지정한 Service 를 띄우자

apiVersion: v1
kind: Service
metadata:
  name: named-port-service
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: http
  selector:
    app: sample-app

 

아래의 커맨드로 서비스의 목적지 파드의 엔드포인트의 port 가 각각 80, 81 로 설정되었는지 확인할 수 있다.

# 파드의 IP/PORT 확인
$ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},IP:{status.podIP},PORT:{spec.containers[].ports[].containerPort}"
NAME                IP             PORT
named-port-pod-80   10.244.2.116   80
named-port-pod-81   10.244.1.132   81

# 서비스가 바라보는 파드의 port 확인
$ kubectl describe service named-port-service
Name:                     named-port-service
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=sample-app
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.134.44
IPs:                      10.96.134.44
Port:                     http-port  8080/TCP
TargetPort:               http/TCP
Endpoints:                10.244.1.132:81,10.244.2.116:80
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

 

블로그 이미지

망원동똑똑이

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

,