Skip to content

Service는 정확히 무슨 일을 하나요? (타입 4개와 이름이 주소가 되는 과정) #30

Description

@sysnet4admin

4회차에 이런 질문이 나왔습니다.

service에서 kubernetes는 어떤 역할을 하나요?

수업에서는 두 가지를 다뤘습니다. Pod에 변하지 않는 이름을 준다는 것, 그리고 뒤에 있는 Pod들에 요청을 나눠 보낸다는 것입니다. 여기서는 그 위에 두 가지를 더합니다. 이름이 실제로 어떻게 주소가 되는지타입이 사실 네 개라는 것입니다.

Service가 하는 일은 셋입니다

하는 일 무엇이 안 바뀌게 되나
이름을 준다 backend-service라는 이름. Pod가 바뀌어도 그대로입니다
주소를 준다 ClusterIP. Pod IP는 바뀌지만 이 주소는 안 바뀝니다
뒤의 목록을 관리한다 라벨에 맞는 Pod 목록을 계속 갱신하고 그중 하나로 보냅니다

세 번째는 수업에서 kubectl describe svc backend-service로 보신 Endpoints입니다. Pod가 죽고 새로 생기면 그 목록이 알아서 바뀝니다.

앞의 두 가지가 이 글에서 더 볼 것입니다.


이름은 이렇게 주소가 됩니다

frontend의 nginx 설정에 backend-service라고만 적혀 있습니다. 그 짧은 이름이 어떻게 실제 주소가 되는지 보겠습니다. 아래는 제 실습 클러스터에서 Pod 안을 직접 들여다본 것입니다.

Pod 안의 /etc/resolv.conf입니다. 이름을 어디에 물어보라고 적힌 파일입니다.

search mcp-pilot.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

nameserver10.96.0.10입니다. 이게 무엇인지 보면 이렇습니다.

NAME       TYPE        CLUSTER-IP   PORT(S)
kube-dns   ClusterIP   10.96.0.10   53/UDP,53/TCP,9153/TCP

클러스터 안의 DNS도 Service입니다. Pod가 이름을 물어볼 곳이 또 하나의 Service인 셈입니다.

search 줄이 나머지를 설명합니다. 짧은 이름으로 물어보면 이 목록을 차례로 붙여서 다시 시도합니다. 실제로 조회해 보면 이렇게 나옵니다.

$ getent hosts a2a-echo
10.103.103.158  a2a-echo.mcp-pilot.svc.cluster.local

a2a-echo라고만 물었는데 전체 이름으로 바뀌어 답이 왔습니다. 그리고 그 주소가 Service의 ClusterIP와 같습니다.

$ kubectl get svc a2a-echo -o jsonpath='{.spec.clusterIP}'
10.103.103.158

정리하면 이런 순서입니다.

flowchart LR
  P["Pod 안의 앱<br/>backend-service 로 부름"]
  D["DNS Service<br/>10.96.0.10"]
  N["backend-service<br/>.default.svc.cluster.local"]
  C["ClusterIP<br/>10.x.x.x"]
  S["backend Pod<br/>둘 중 하나"]

  P -->|"1. 이 이름 어디야"| D
  D -->|"2. search 목록을 붙여 전체 이름으로"| N
  N -->|"3. 이 주소야"| C
  P -->|"4. 그 주소로 연결"| C
  C -->|"5. 뒤의 Pod 하나로"| S

  class D dns
  classDef dns fill:#854d0e,stroke:#fbbf24,color:#ffffff
Loading

이름의 전체 형태는 서비스이름.네임스페이스.svc.cluster.local입니다. 같은 네임스페이스 안이면 앞부분만 써도 되고, 다른 네임스페이스의 것을 부르려면 backend-service.other-namespace처럼 더 적으면 됩니다.

왜 이렇게 해 두었나. Pod IP는 바뀌지만 이 이름은 안 바뀝니다. 그래서 nginx 설정에 IP를 적지 않고 이름을 적어 둘 수 있습니다. 수업에서 "이름으로 부른다"고 한 것의 실제 내용이 이것입니다.


타입은 네 개입니다

수업에서는 둘만 봤습니다. 나머지 둘도 있습니다.

타입 하는 일 밖에서 접근
ClusterIP 클러스터 안에서만 쓰는 주소를 준다. 타입을 안 적으면 이것 안 됩니다
NodePort 위에 더해 모든 노드의 같은 포트를 연다 노드 IP로 됩니다
LoadBalancer 위에 더해 클라우드 로드밸런서를 붙인다 외부 IP로 됩니다
ExternalName 주소를 만들지 않고 다른 이름으로 넘긴다 해당 없음

앞의 세 개는 층층이 쌓입니다. 문서의 서술입니다.

The type field in the Service API is designed as nested functionality - each level adds to the previous.
(Service의 type 필드는 층층이 쌓이도록 설계되어 있습니다. 각 단계가 앞 단계에 더해집니다.)

쌓인다는 것은 바깥이 안을 품는다는 뜻입니다.

flowchart TB
  subgraph LB["LoadBalancer = 안의 것 전부 + 외부 로드밸런서"]
    subgraph NP["NodePort = 안의 것 + 노드마다 열리는 포트"]
      CI["ClusterIP<br/>클러스터 안에서 쓰는 주소"]
    end
  end

  class CI base
  classDef base fill:#166534,stroke:#4ade80,color:#ffffff
Loading

가장 안쪽이 ClusterIP입니다. NodePort는 그것을 그대로 두고 노드의 포트를 하나 더 얹은 것이고, LoadBalancer는 거기에 외부 로드밸런서를 또 얹은 것입니다.

주의할 것이 하나 있습니다. 이 그림은 기능이 쌓이는 순서이지 요청이 흐르는 순서가 아닙니다. 요청은 반대 방향으로 흐릅니다. 밖에서 로드밸런서로 들어와 노드 포트를 지나 ClusterIP를 거쳐 Pod에 닿습니다. 흐르는 그림은 #28에 있습니다.

그래서 LoadBalancer를 만들면 ClusterIP도 NodePort도 같이 생깁니다. kubectl get svc frontend-serviceCLUSTER-IP 값이 찍혀 있는 것이 그 증거입니다. 외부 공개용인데 안쪽 주소도 가지고 있습니다. 자세한 것은 #28에 있습니다.

그럼 NodePort와 LoadBalancer는 무엇이 다른가

쌓인다고만 하면 둘이 같은 것처럼 보입니다. 실제로는 앞에 무엇이 서느냐가 다릅니다.

기본 동작은 문서에 이렇게 적혀 있습니다.

To implement a Service of type: LoadBalancer, Kubernetes typically starts off by making the changes that are equivalent to you requesting a Service of type: NodePort. The cloud-controller-manager component then configures the external load balancer to forward traffic to that assigned node port.
(type: LoadBalancer를 구현할 때 쿠버네티스는 보통 type: NodePort를 요청한 것과 같은 변경을 먼저 합니다. 그다음 cloud-controller-manager가 외부 로드밸런서를 그 노드 포트로 트래픽을 넘기도록 설정합니다.)

즉 기본 동작에서는 로드밸런서가 노드 포트로 보냅니다. 앞의 포함 그림이 트래픽 관점으로도 맞는 경우입니다. 차이는 그 앞에 무엇이 하나 더 있느냐입니다.

NodePort LoadBalancer
밖에서 쓸 주소 노드 IP를 직접 알아야 합니다 클라우드가 IP 하나를 줍니다
포트 번호 30000에서 32767 사이 원하는 번호를 port에 적습니다
노드가 죽으면 그 노드 IP로 오던 요청은 실패합니다 로드밸런서가 살아 있는 노드로 보냅니다
노드를 늘리면 새 IP를 사용자에게 다시 알려야 합니다 로드밸런서가 알아서 포함시킵니다
비용 없습니다 클라우드 로드밸런서 요금이 붙습니다

정리하면 NodePort는 문을 열어 두기만 하고 그 뒤는 쓰는 쪽이 알아서 해야 합니다. 노드가 셋이면 IP도 셋이고 어느 것으로 갈지 사용자가 정해야 합니다. LoadBalancer는 그 앞에 창구를 하나 세워서 그 문제를 대신 처리합니다.

예외도 있습니다. 로드밸런서가 노드 포트를 거치지 않고 Pod로 바로 보내는 구현이 있습니다. 그럴 때는 노드 포트 할당을 끌 수 있습니다.

You can optionally disable node port allocation for a Service of type: LoadBalancer ... This should only be used for load balancer implementations that route traffic directly to pods as opposed to using node ports.
(type: LoadBalancer에서 노드 포트 할당을 끌 수 있습니다. 노드 포트를 쓰지 않고 Pod로 바로 보내는 로드밸런서 구현에서만 써야 합니다.)

그래서 정확히 말하면 이렇습니다. 가지고 있는 것은 항상 쌓여 있고, 그중 실제로 지나가는 경로는 로드밸런서 구현에 따라 다릅니다. 기본값에서는 노드 포트를 지나갑니다.

온프레미스에서는 또 다릅니다

위 설명은 클라우드를 전제로 한 것입니다. 클러스터 밖에 별도의 로드밸런서가 있고 그것이 노드로 보낸다는 그림입니다. 회사 서버실에 직접 올린 클러스터에는 그런 장비가 없습니다.

그래서 그 역할을 대신하는 것을 따로 설치합니다. 많이 쓰는 것이 MetalLB입니다. 제 실습 클러스터에 그것이 올라가 있어서 실제로 어떻게 되어 있는지 꺼내 봤습니다.

노드 IP                     LoadBalancer Service가 받은 IP
cp-k8s  192.168.2.60        192.168.2.100
w1-k8s  192.168.2.61        192.168.2.101
w2-k8s  192.168.2.62        192.168.2.102

외부 IP가 노드와 같은 대역입니다. 클라우드에서 받는 공인 IP와 성격이 다릅니다. 밖에 있는 장비가 준 주소가 아니라 같은 네트워크 안에서 비어 있는 주소를 가져다 쓴 것입니다.

동작 방식도 다릅니다. MetalLB 문서의 서술입니다.

In layer 2 mode, one node assumes the responsibility of advertising a service to the local network. ... MetalLB responds to ARP requests for IPv4 services.
(레이어 2 모드에서는 노드 하나가 그 서비스를 로컬 네트워크에 알리는 역할을 맡습니다. MetalLB가 그 IP에 대한 ARP 요청에 응답합니다.)

In that sense, layer 2 does not implement a load balancer. Rather, it implements a failover mechanism.
(그런 점에서 레이어 2 모드는 로드밸런서를 구현하는 것이 아닙니다. 장애 대비 전환을 구현하는 것입니다.)

All traffic for a service IP goes to one node. From there, kube-proxy spreads the traffic to all the service's pods.
(그 서비스 IP로 오는 모든 트래픽은 한 노드로 갑니다. 거기서 kube-proxy가 서비스의 모든 Pod로 나눠 보냅니다.)

정리하면 이렇습니다. 노드 하나가 그 IP를 자기 것처럼 응답해서 트래픽을 받고, 나누는 일은 그 노드 안에서 일어납니다. 로드밸런서가 밖에 서 있는 것이 아니라 노드 안에 있는 셈입니다. 그 노드가 죽으면 다른 노드가 그 IP를 넘겨받습니다.

그림으로 놓으면 이렇습니다. 먼저 클라우드입니다.

flowchart LR
  U["브라우저"] -->|"공인 IP : 80"| LB["클라우드 로드밸런서<br/>클러스터 밖에 있음"]
  LB --> N1["노드 1"]
  LB --> N2["노드 2"]
  LB --> N3["노드 3"]
  N1 --> P["Pod들"]
  N2 --> P
  N3 --> P

  class LB out
  classDef out fill:#854d0e,stroke:#fbbf24,color:#ffffff
Loading

노란 칸이 클러스터 밖에 있습니다. 거기서 이미 여러 노드로 나뉩니다.

다음은 온프레미스에서 MetalLB 레이어 2 모드입니다.

flowchart LR
  U["브라우저"] -->|"192.168.2.100 : 80"| N1["노드 1<br/>이 IP에 ARP로 응답<br/>모든 트래픽을 여기서 받음"]
  N1 -->|"kube-proxy가 나눔"| P["Pod들"]
  N2["노드 2<br/>대기"] -.->|"노드 1이 죽으면<br/>이 IP를 넘겨받음"| N1
  N3["노드 3<br/>대기"] -.-> N1

  class N1 out
  classDef out fill:#854d0e,stroke:#fbbf24,color:#ffffff
Loading

노란 칸이 노드 안으로 들어왔습니다. 밖에 따로 서 있는 것이 없습니다. 그리고 나뉘는 지점이 그 노드 안입니다. 점선은 장애가 났을 때 IP가 옮겨 가는 것을 뜻하고 평소에는 트래픽이 흐르지 않습니다.

두 그림의 차이는 두 가지입니다. 나뉘는 곳이 클러스터 밖이냐 노드 안이냐, 그리고 평소에 일하는 노드가 여럿이냐 하나냐입니다.

클라우드 온프레미스 (MetalLB 레이어 2)
로드밸런서가 있는 곳 클러스터 밖 노드 안
외부 IP 클라우드가 주는 공인 IP 노드와 같은 대역의 남는 IP
트래픽을 받는 노드 여러 노드에 나눠 보냄 한 노드가 다 받음
나누는 일은 어디서 로드밸런서가 받은 노드의 kube-proxy가
노드가 죽으면 로드밸런서가 다른 노드로 다른 노드가 그 IP를 넘겨받음

MetalLB에는 라우터에 경로를 알리는 BGP 모드도 있고 그때는 여러 노드로 나뉩니다. 다만 그 경우에도 로드밸런서 장비가 따로 서는 것은 아닙니다.

그래서 type: LoadBalancer는 "무엇을 원하는가"를 적는 것이지 "어떻게 할 것인가"를 적는 것이 아닙니다. 밖에서 접근할 주소 하나를 달라는 선언이고, 그것을 어떻게 만들지는 클러스터가 있는 환경이 정합니다. GKE에서는 구글이 만들고 회사 서버실에서는 MetalLB가 만듭니다. yaml은 같습니다.

ExternalName만 성격이 다릅니다. 이건 주소를 만들지 않습니다. 문서의 서술입니다.

Maps the Service to the contents of the externalName field ... The mapping configures your cluster's DNS server to return a CNAME record with that external hostname value. No proxying of any kind is set up.
(externalName 필드의 값으로 연결합니다. 클러스터 DNS가 그 외부 호스트 이름을 담은 CNAME 레코드를 돌려주도록 설정합니다. 어떤 종류의 중계도 하지 않습니다.)

쓰임새는 이렇습니다. 클러스터 밖에 있는 것을 클러스터 안 이름으로 부르고 싶을 때입니다. 예를 들어 데이터베이스가 클라우드의 관리형 서비스로 밖에 있는데, 앱 코드에는 db라고만 적고 싶은 경우입니다.

apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  type: ExternalName
  externalName: my-db.abc123.ap-northeast-2.rds.amazonaws.com

앱은 db라고 부르고 DNS가 실제 주소로 바꿔 줍니다. 나중에 데이터베이스를 클러스터 안으로 옮기면 이 Service만 ClusterIP로 바꾸면 되고 앱 코드는 안 고쳐도 됩니다.

우리 앱은 어디에 해당하나

frontend-service   LoadBalancer   외부 IP 있음    브라우저가 부름
backend-service    ClusterIP      외부 IP 없음    frontend의 nginx가 부름
kubernetes         ClusterIP      외부 IP 없음    Pod가 API 서버를 부를 때
kube-dns           ClusterIP      외부 IP 없음    모든 Pod가 이름을 물어볼 때

앞의 둘은 수업에서 만든 것이고 뒤의 둘은 클러스터에 원래 있는 것입니다. kubernetes#27에서 본 그 주소이고, kube-dns가 방금 본 이름 풀이를 맡습니다.

kubectl get svc -A를 치면 이 넷이 다 보입니다. 넷 중 셋이 ClusterIP입니다. 밖에 열 이유가 없으면 열지 않는 것이 기본이라는 뜻이기도 합니다.

직접 확인해 보시기 바랍니다

# 클러스터의 모든 Service와 타입
kubectl get svc -A

# LoadBalancer인데 ClusterIP도 있는지
kubectl get svc frontend-service -o jsonpath='{.spec.type}{"  "}{.spec.clusterIP}'

# Pod 안에서 이름이 어떻게 풀리는지
kubectl exec -it deploy/frontend -- cat /etc/resolv.conf
kubectl exec -it deploy/frontend -- getent hosts backend-service

# 그 결과가 Service의 ClusterIP와 같은지
kubectl get svc backend-service -o jsonpath='{.spec.clusterIP}'

마지막 두 줄의 값이 같으면 이름이 주소로 풀린 것입니다.

타입 선택 기준은 decision-guides/service-type.md에도 있습니다. 그쪽은 수업에서 쓰는 둘을 다루고 이 글은 나머지 둘을 더한 것입니다.

이 글은 참고용입니다. 읽지 않아도 수업 진행에는 지장이 없습니다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions