# Service
⚡️ 목표
Service(서비스)를 이용하여 Pod을 노출하고 클러스터 외부에서 접근할 수 있는 방법을 알아봅니다.
Pod은 자체 IP를 가지고 다른 Pod과 통신할 수 있지만, 쉽게 사라지고 생성되는 특징 때문에 직접 통신하는 방법은 권장하지 않습니다. 쿠버네티스는 Pod과 직접 통신하는 방법 대신, 별도의 고정된 IP를 가진 서비스를 만들고 그 서비스를 통해 Pod에 접근하는 방식을 사용합니다.

이러한 개념은 도커에 익숙할수록 처음 접하면 참 어렵습니다. 개념도 생소한데 노출 범위에 따라 CluterIP, NodePort, LoadBalancer 타입으로 나누어져 더욱 헷갈립니다. 하나씩 차근차근 알아봅시다.
서비스 종류
서비스 종류 중에 ExternalName도 있지만 여기서는 다루지 않습니다.
# Service(ClusterIP) 만들기
ClusterIP는 클러스터 내부에 새로운 IP를 할당하고 여러 개의 Pod을 바라보는 로드밸런서 기능을 제공합니다. 그리고 서비스 이름을 내부 도메인 서버에 등록하여 Pod 간에 서비스 이름으로 통신할 수 있습니다.
그럼, 다중 컨테이너를 설명할 때 만들었던, counter 앱 중에 redis를 서비스로 노출해보겠습니다.
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
구분자
하나의 YAML파일에 여러 개의 리소스를 정의할 땐 "---"를 구분자로 사용합니다.
redis를 먼저 만들어 봅니다.
kubectl apply -f counter-redis-svc.yml
# Pod, ReplicaSet, Deployment, Service 상태 확인
kubectl get all
실행 결과
NAME READY STATUS RESTARTS AGE
pod/redis-57d787df44-mf5w5 1/1 Running 0 10s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d19h
service/redis ClusterIP 10.103.50.102 <none> 6379/TCP 10s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/redis 1/1 1 1 10s
NAME DESIRED CURRENT READY AGE
replicaset.apps/redis-57d787df44 1 1 1 10s
redis Deployment와 Service가 생성된 것을 볼 수 있습니다.

같은 클러스터에서 생성된 Pod이라면 redis라는 도메인으로 redis Pod에 접근 할 수 있습니다. (redis.default.svc.cluster.local로도 접근가능 합니다. 서로 다른 namespace와 cluster를 구분할 수 있습니다.)
CluterIP 서비스의 설정을 살펴봅니다.
| 정의 | 설명 |
|---|---|
spec.ports.port | 서비스가 생성할 Port |
spec.ports.targetPort | 서비스가 접근할 Pod의 Port (기본: port랑 동일) |
spec.selector | 서비스가 접근할 Pod의 label 조건 |
redis Service의 selector는 redis Deployment에 정의한 label을 사용했기 때문에 해당 Pod을 가리킵니다. 그리고 해당 Pod의 6379 포트로 연결하였습니다.
이제 redis에 접근할 counter 앱을 Deployment로 만듭니다.
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"
counter app Pod에서 redis Pod으로 접근이 되는지 테스트 해보겠습니다.
kubectl apply -f counter-app.yml
# counter app에 접근
kubectl get po
kubectl exec -it counter-<xxxxx> -- sh
# curl localhost:3000
# curl localhost:3000
# telnet redis 6379
dbsize
KEYS *
GET count
quit
Service를 통해 Pod과 성공적으로 연결되었습니다.
# Service 생성 흐름
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 대신 도메인 이름을 사용합니다.
iptables
iptables는 규칙이 많아지면 성능이 느려지는 이슈가 있어, ipvs를 사용하는 옵션도 있습니다.
CoreDNS
CoreDNS는 클러스터에서 호환성을 위해 kube-dns라는 이름으로 생성됩니다.
갑자기 Endpoint가 나왔습니다. Endpoint는 서비스의 접속 정보를 가지고 있습니다. Endpoint의 상태를 확인해 보겠습니다.
kubectl get endpoints
kubectl get ep #줄여서
# redis Endpoint 확인
kubectl describe ep/redis
실행 결과
Name: redis
Namespace: default
Labels: <none>
Annotations: endpoints.kubernetes.io/last-change-trigger-time: ...
Subsets:
Addresses: 172.17.0.2
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 6379 TCP
Events: <none>
Endpoint Addresses 정보에 Redis Pod의 IP가 보입니다. (Replicas가 여러개였다면 여러 IP가 보입니다.)
# Service(NodePort) 만들기
CluterIP는 클러스터 내부에서만 접근할 수 있습니다. 클러스터 외부(노드)에서 접근할 수 있도록 NodePort 서비스를 만들어봅니다.
apiVersion: v1
kind: Service
metadata:
name: counter-np
spec:
type: NodePort
ports:
- port: 3000
protocol: TCP
nodePort: 31000
selector:
app: counter
tier: app
| 정의 | 설명 |
|---|---|
spec.ports.nodePort | 노드에 오픈할 Port (미지정시 30000-32768 중에 자동 할당) |
counter app을 해당 노드의 31000으로 오픈합니다.
kubectl apply -f counter-nodeport.yml
# 서비스 상태 확인
kubectl get svc
실행 결과
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
counter-np NodePort 10.101.168.165 <none> 3000:31000/TCP 13s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d19h
redis ClusterIP 10.103.50.102 <none> 6379/TCP 30m
minikube ip로 테스트 클러스터의 노드 IP를 구하고 31000으로 접근해봅니다.
curl 192.168.64.4:31000 # 또는 브라우저에서 접근
Docker
Docker driver를 사용중이라면 minikube service counter-np 명령어를 이용하여 접속하세요.

NodePort는 클러스터의 모든 노드에 포트를 오픈합니다. 지금은 테스트라서 하나의 노드밖에 없지만 여러 개의 노드가 있다면 아무 노드로 접근해도 지정한 Pod으로 쏘옥 접근할 수 있습니다.

NodePort와 ClusterIP
NodePort는 CluterIP의 기능을 기본으로 포함합니다.
# Service(LoadBalancer) 만들기
NodePort의 단점은 노드가 사라졌을 때 자동으로 다른 노드를 통해 접근이 불가능하다는 점입니다. 예를 들어, 3개의 노드가 있다면 3개 중에 아무 노드로 접근해도 NodePort로 연결할 수 있지만 어떤 노드가 살아 있는지는 알 수가 없습니다.

자동으로 살아 있는 노드에 접근하기 위해 모든 노드를 바라보는 Load Balancer가 필요합니다. 브라우저는 NodePort에 직접 요청을 보내는 것이 아니라 Load Balancer에 요청하고 Load Balancer가 알아서 살아 있는 노드에 접근하면 NodePort의 단점을 없앨 수 있습니다.
Load Balancer를 생성해 봅니다.
apiVersion: v1
kind: Service
metadata:
name: counter-lb
spec:
type: LoadBalancer
ports:
- port: 30000
targetPort: 3000
protocol: TCP
selector:
app: counter
tier: app
kubectl apply -f counter-lb.yml
실행 결과
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
counter-lb LoadBalancer 10.109.133.129 <pending> 30000:31535/TCP 9s
counter-np NodePort 10.101.168.165 <none> 3000:31000/TCP 34m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d20h
redis ClusterIP 10.103.50.102 <none> 6379/TCP 65m
counter-lb가 생성되었지만, EXTERNAL-IP가 <pending>인 것을 확인할 수 있습니다. 사실 Load Balancer는 AWS, Google Cloud, Azure 같은 클라우드 환경이 아니면 사용이 제한적입니다. 특정 서버(노드)를 가리키는 무언가(Load Balancer)가 필요한데 이런 무언가가 가상머신이나 로컬 서버에는 존재하지 않습니다.
# minikube에 가상 LoadBalancer 만들기
Load Balancer를 사용할 수 없는 환경에서 가상 환경을 만들어 주는 것이 MetalLB라는 것입니다. minikube에서는 현재 떠 있는 노드를 Load Balancer로 설정합니다. minikube의 addons 명령어로 활성화합니다.
minikube addons enable metallb
그리고 minikube ip명령어로 확인한 IP를 ConfigMap으로 지정해야 합니다.
minikube를 이용하여 손쉽게 metallb 설정을 할 수 있습니다.
minikube addons configure metallb
-- Enter Load Balancer Start IP: # minikube ip 결과값 입력
-- Enter Load Balancer End IP: # minikube ip 결과값 입력
▪ Using image metallb/speaker:v0.9.6
▪ Using image metallb/controller:v0.9.6
✅ metallb was successfully configured
minikube를 사용하지 않고 직접 ConfigMap을 작성할 수도 있습니다.
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.64.4/32 # minikube ip
kubectl apply -f metallb-cm.yml
# 다시 서비스 확인
kubectl get svc
실행 결과
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
counter-lb LoadBalancer 10.109.133.129 192.168.64.4 30000:31535/TCP 1m
counter-np NodePort 10.101.168.165 <none> 3000:31000/TCP 35m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d20h
redis ClusterIP 10.103.50.102 <none> 6379/TCP 66m
이제 192.168.64.4:30000으로 접근해봅니다.
Docker
Docker driver를 사용중이라면 minikube service counter-lb 명령어를 이용하여 접속하세요.
LoadBalancer와 NodePort
LoadBalancer는 NodePort의 기능을 기본으로 포함합니다.
# 마무리
서비스는 로우레벨low level 수준의 네트워크를 이해하고 성능, 보안 이슈를 신경 써야 합니다. 파고들면 한없이 복잡하고 어려운 부분으로 일단 그렇구나.. 하고 넘어갑니다. 😭
실전에선 NodePort와 LoadBalancer를 제한적으로 사용합니다. 보통 웹 애플리케이션을 배포하면 80 또는 443 포트를 사용하고 하나의 포트에서 여러 개의 서비스를 도메인이나 경로에 따라 다르게 연결하기 때문입니다. 이 부분은 뒤에 Ingress에서 자세히 알아봅니다.
# 참고
Service v1 core (opens new window) 네트워크 통신 시각화 (opens new window)
# 문제
문제1. echo 서비스를 NodePort로 32000 포트로 오픈합니다.
| 키 | 값 |
|---|---|
Deployment 이름 | echo |
Deployment Label | app: echo |
Deployment 복제수 | 3 |
Container 이름 | echo |
Container 이미지 | ghcr.io/subicura/echo:v1 |
NodePort 이름 | echo |
NodePort Port | 3000 |
NodePort NodePort | 32000 |
정답
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo
spec:
replicas: 3
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
spec:
containers:
- name: echo
image: ghcr.io/subicura/echo:v1
---
apiVersion: v1
kind: Service
metadata:
name: echo
spec:
type: NodePort
ports:
- port: 3000
protocol: TCP
nodePort: 32000
selector:
app: echo