Service 란?
Pod을 생성하게 되면 자체 IP가 Pod에 할당됩니다. 그러나 이렇게 할당받은 IP는 랜덤하게 지정이되고 리스타트 될 때마다 변하기 때문에 고정된 엔드포인트로 호출이 어렵습니다. 이때문에 Pod의 자체 IP를 통해 다른 Pod와 직접 통신하는 것은 권장되지 않고 대신 별도의 고정된 IP를 가진 Service를 만들고 이를 통해 Pod에 접근하는 방식을 주로 사용합니다. 또한 여러 Pod에 같은 App을 운용하는 경우 Pod간의 로드밸런싱도 필요한데 이를 Service가 지원합니다.
이처럼 Service는 IP 주소 할당 방식과 연동 서비스 등에 따라 Cluster IP, NodePort, LoadBalancer, ExternalName으로 나뉘고 앞서 배웠던 오브젝트들처럼 Service도 라벨 셀렉터(label selector)를 사용하여 관리하고자 하는 pod을 정의할 수 있습니다.
ClusterIP
ClusterIP는 Service의 디폴트 설정으로써 클러스터 내부에 새로운 IP를 할당하고 여러개의 Pod을 바라보는 로드밸런서 기능을 제공합니다. 쿠버네티스 클러스터 내에서는 이 서비스에 접근이 가능하지만 클러스터 외부에서는 외부 IP를 할당받지 못했기 때문에 접근이 불가능합니다. 또한 클러스터 내부에서는 Service 이름을 내부 도메인 서버에 등록하여 Pod 간에 통신에서 Service 이름을 사용하여 통신할 수도 있습니다.
ClusterIP 만들어보기
redis pod 배포를 위한 Deployment를 만들고 이 Pod를 Service를 사용해서 클러스터 내부에 노출시켜보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
selector:
matchLabels:
app: counter
tier: db
template:
metadata:
labels:
app: counter
tier: db
spec:
containers:
- name: redis
image: redis
ports:
- containerPort: 6379
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
ports:
- port: 6379
protocol: TCP
selector:
app: counter
tier: db
ClusterIP Service의 설정은 아래와 같습니다.
- spec.ports.port : Service가 생성할 포트
- spec.ports.targetPort : Service가 접근할 Pod의 포트(디폴트는 Service가 생성할 포트와 동일)
- spec.selector : Service가 접근할 Pod의 label 조건
실행결과 redis라는 이름을 가진 deployment와 service가 생성된 것을 확인할 수 있습니다.
redis Service의 상세정보를 보면, Name
에 Service의 이름인 redis가 나타나며 Selector
에 app=counter이고 tier=db인 pod을 바라보는 Service임을 나타내고 있습니다. IP
에는 Service의 고유 ip(10.109.143.192)를 나타내고 Endpoints
는 실제 service의 IP(10.109.143.192)로 들어왔을 때, 어디로 가는지를 나타냅니다. 여기서 endpoint의 값인 172.17.0.3:9379은 redis pod의 ip와 포트입니다.
이제 같은 쿠버네티스 클러스터에서 생성된 Pod은 redis라는 Service의 이름을 도메인으로 해서 redis pod에 접근할 수 있습니다. 이를 테스트 해보기 위해 redis에 접근하기 위한 counter라는 이름의 deployment를 하나 더 생성해보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: apps/v1
kind: Deployment
metadata:
name: counter
spec:
selector:
matchLabels:
app: counter
tier: app
template:
metadata:
labels:
app: counter
tier: app
spec:
containers:
- name: counter
image: ghcr.io/subicura/counter:latest
env:
- name: REDIS_HOST
value: "redis"
- name: REDIS_PORT
value: "6379"
여기서 호스트(REDIS_HOST)로 redis를 사용하는데 이는 도커 컴포즈에서 컨테이너의 이름이 곧 도메인이 된 것처럼 작동합니다. 즉, counter라는 app입장에서는 redis로 접근하면 서비스의 IP인 10.109.143.192로 접근하는 것과 같고 이는 endpoints인 redis pod으로 연결됩니다. 참고로 endpoint는 kubectl get ep
커맨드로도 확인할 수 있습니다.
실제로 counter Pod에 접속한 후 telnet redis 6979
로 redis Pod에 연결하여 커맨드를 입력가능합니다.
추가로 지금은 service가 redis라는 단 하나의 pod만을 바라보고 있어 redis라는 service로 접근을하면 redis pod으로 바로 연결이 가능합니다. 만약 service가 관리하는 pod이 여러개가 되면 service는 여러개의 pod에 번갈아가며 접근하도록 합니다. 즉 내부적으로 로드밸런서의 역할을 수행하기도 한다는 것입니다.
Service 생성과정
Endpoint Controller
는Service
와Pod
을 감시하면서 조건에 맞는 Pod의 IP를 수집Endpoint Controller
가 수집한 IP를 가지고Endpoint
생성Kube-Proxy
는Endpoint
변화를 감시하고 노드의 iptables을 설정CoreDNS
는Service
를 감시하고 서비스 이름과 IP를CoreDNS
에 추가
iptables
는 커널kernel 레벨의 네트워크 도구이고 CoreDNS
는 빠르고 편리하게 사용할 수 있는 클러스터 내부용 도메인 네임 서버 입니다. 각각의 역할은 iptables
설정으로 여러 IP에 트래픽을 전달하고 CoreDNS
를 이용하여 IP 대신 도메인 이름을 사용합니다.
NodePort
쿠버네티스는 기본적으로 클러스터로 작동하고 따라서 여러개의 노드(마스터 + 워커)를 가지고 있습니다. 그래서 외부 트래픽으로 특정 pod에 접근하고자 할때, 모든 노드의 IP와 포트를 통해 접근할 수 있도록 하기위해서 사용되는 것이 NodePort입니다. ClusertIP Service가 클러스터 내부에서만 접근이 가능했다면 클러스터 외부에서 접근할 수 있도록 하기 위해선 NodePort Service를 사용해야 한다고 생각하면 됩니다.
NodePort 만들어보기
counter라는 app을 해당 노드의 31000으로 오픈하도록하여 배포해보겠습니다. 참고로 NodePort의 설정은 아래와 같습니다.
- spec.ports.nodePort : 노드에 오픈할 포트(미지정시 30000-32768의 범위에서 자동 할당)
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: counter-np
spec:
type: NodePort
ports:
- port: 3000
protocol: TCP
nodePort: 31000
selector:
app: counter
tier: app
type이 NodePort인 Service가 생성된 것을 확인할 수 있습니다.
이제 minikube ip로 현재 작업하는 노드의 IP를 구하고 31000포트로 접속해보겠습니다.
정상적으로 curl 신호가 보내지는 것을 확인가능합니다.
minikube는 싱글 노드로 쿠버네티스 클러스터를 구축하고 있기때문에 하나의 노드밖에 없지만, 여러개의 노드로 구축된 쿠버네티스 클러스터에서 NodePort Service를 사용하면 해당 클러스터의 모든 노드에 해당 포트를 오픈합니다. 그리고 이를 통해 지정한 Pod으로 접근할 수 있게 됩니다.
참고로 NodePort는 메커니즘적으로 ClusterIP의 기능을 기본적으로 포함하고 있어 내외부 트래픽을 모두 처리 가능합니다.
LoadBalancer
NodePort는 노드가 다운됐을때 자동으로 다른 노드를 통해 접근이 불가능해진다는 단점이 존재합니다. 가령 3개의 노드가 있을때 3개중 어떤 노드로 접근해도 NodePort로 연결가능하지만, 어떤 노드가 살아있는지는 알수 없습니다.
자동으로 살아 있는 노드에 접근하기 위해서는 모든 노드를 바라보고 있는 로드밸런서가 필요합니다. 브라우저는 NodePort에 직접 요청을 보내는 것이 아니라 로드 밸런서에 요청하고 로드 밸런서 스스로 살아있는 노드에 접근하여 NodePort가 가지는 단점을 상쇄할수 있습니다.
LoadBalancer 만들어보기
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: counter-lb
spec:
type: LoadBalancer
ports:
- port: 30000
targetPort: 3000
protocol: TCP
selector:
app: counter
tier: app
결과를 확인해보면 counter-lb라는 service가 생성된 것을 확인할 수 있습니다.
그런데 External-IP가 Pending로 되어있습니다. 사실 로드밸런서는 클라우드 환경이 아니라면 그 사용에 제약이 있습니다. 특정 서버를 가리키는 무언가(로드밸런서)가 필요한데 이런 것이 가상머신이나 로컬 서버에는 존재하지 않습니다.
이처럼 로드밸런서를 사용할 수 없는 상황에서 가상환경을 만들어 주는 것이 MetalLB라는 것이 있습니다. minikube에서는 현재 사용중인 노드를 로드밸런서로 지정합니다. 이는 minikube addons enable metallb
커맨드로 활성화 가능합니다.
그리고나서 minikube ip
커맨드로 현재 노드의 ip를 확인하고 configMap으로 지정해야합니다. 이를 위해 minikube addons configure metallb
커맨드를 사용합니다.
이제 다시 확인하면 counter-lb라는 service의 external-IP에 현재 노드의 IP가 설정된 것을 확인할 수 있습니다.
추가로 minikube를 사용하지 않고 ConfigMap을 작성하는 방식으로 해결도 가능합니다. 아래와 같은 내용이 작성된 yaml파일을 만든 후 apply 시키면 앞선 minikube와 같은 결과를 얻을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.49.2/32 # minikube ip
마지막으로 NodePort가 ClusterIP의 기능을 기본으로 포함하는 것처럼 LoadBalancer는 NodePort의 기능을 기본으로 포함하고 있습니다.
마무리
쿠버네티스의 첫번째 난관인 Service에 대해 알아봤습니다. 네트워크에 대한 개념이 필요한 파트라 개인적으로는 더 어렵게 느껴졌던 것 같습니다. 게다가 성능과 보완 이슈도 고려해야하기 때문에 깊이 알수록 한없이 더 어려워지는 오브젝트가 아닌가 하는 생각이 듭니다.