쿠버네티스 Service
핵심만 콕 쿠버네티스 - 쿠버네티스 네트워킹
쿠버네티스에서 Service 리소스는 네트워크를 담당하고 있다. Service 리소스는 Pod과 마찬가지로 YAML 정의서로 정의할 수 있다. Pod 역시 IP를 가지고 있지만, Service 리소스는 Pod의 IP와는 다른 독자적인 IP를 부여받아 서비스의 엔드포인트를 제공하며 라벨링 시스템을 통해 Pod으로 트래픽을 전달할 수 있다. Service 리소스는 Pod의 앞단에 위치하여 마치 로드 밸런서처럼 동작한다.
1. Service 소개
쿠버네티스에서는 Pod에도 IP가 부여된다. 다음과 같이 nginx를 실행하는 Pod을 만들고 curl 명령을 보내면 정상적으로 요청을 받는 것을 확인할 수 있다.
kubectl run mynginx --image nginx
kubectl get pod -o wide
kubectl exec mynginx -- curl -s [IP]
이렇게 Pod에 할당된 IP에 접근할 수 있음에도 불구하고 Service라는 리소스를 만들어서 사용하는 이유는 Pod이라는 리소스를 불안정한 리소스로 여기기 때문이다.
Pod은 언제든지, 무슨 이유로든 종료될 수 있는 리소스로 생각하는데, 이러한 특징으로 인해 Pod은 불안정한 엔드포인드를 제공한다. Pod 리소스에 부여되는 IP를 이용하여 서비스를 호출하는 경우, 사용자는 끊임없이 서비스의 엔드포인트 이상 여부와 바뀐 IP를 추적해야 한다.
따라서 Pod의 생명주기와는 상관없이 안정적인 서비스 끝점을 제공하는 Service라는 리소스가 등장하게 됐다. Service는 Pod의 앞단에 위치하며 Service로 들어오는 트래픽을 Pod으로 전달하는 리버스 프록시의 역할을 수행한다. Service 리소스가 클라이언트의 요청에 따라 알맞은 Pod에 접근하기 때문에 사용자는 Service의 IP만으로 서비스에 접근할 수 있고, 여러 Pod 중 하나가 죽더라도 Service가 다른 Pod으로 트래픽을 전달해주기 때문에 안정성 및 가용성을 높일 수 있다.
Service 탐색
Service 리소스는 안정적인 IP를 제공해주고, 서비스 탐색 기능을 수행하는 도메인 이름 기반의 서비스 엔드포인트를 제공한다. 사용자는 쿠버네티스 클러스터 내에서 Service 리소스의 이름을 기반으로 DNS 참조가 가능하다. 이를 통해 사용자는 손쉽게 다른 서비스를 탐색하고 참조할 수 있다.
예를 들어, myservice라는 이름의 Service 리소스를 생성하면 사용자는 myservice라는 도메인 주소로 해당 Service에 요청할 수 있다.
Service 첫 만남
# myservice.yaml
apiVersion: v1
kind: Service
metadata:
labels:
hello: world
name: myservice
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 80
selector:
run: mynginx
Service는 Pod과 마찬가지로 YAML 정의서로 만들 수 있다.
- apiVersion: Pod과 마찬가지로 v1 API 버전을 나타냄
- kind: Service 리소스 선언
- metadata: 리소스의 메타 정소
- labels: Service에도 라벨을 부여할 수 있다.
- name: 이름을 지정한다. 해당 이름이 도메인 주소로 활용된다.
- spec: Service의 스펙을 정의한다.
- ports: Services의 포트들을 정의한다.
- port: Service로 들어오는 포트를 정의한다.
- protocol: 사용하는 프로토콜을 지정한다. TCP, UDP, HTTP 등이 있다.
- targetPort: 트래픽을 전달할 컨테이너의 포트를 지정한다.
- selector: 트래픽을 전달할 컨테이너의 라벨을 선택한다. 예제에서는
run=mynginx
라벨을 가진 Pod에 Service 트래픽을 전달한다.
- ports: Services의 포트들을 정의한다.
kubectl run mynginx --image nginx
를 사용하여 Pod을 생성하고kubectl get pod mynginx -o yaml
명령으로 YAML 정의서를 보면run=mynginx
라벨을 가진 것을 확인할 수 있다.
Service의 YAML 정의서를 보면 트래픽을 전달할 Pod을 라벨 셀렉터로 지정하고 있는 것을 확인할 수 있다. 단순히 Pod의 이름을 지정하거나 Pod의 IP를 이용할 수도 있었을 텐데, 라벨링 시스템을 사용하는 이유는 무엇일까?
쿠버네티스에서는 각 리소스의 관계를 느슨한 연결 관계로 표현하고자 한다. 리소스간 느슨한 관계로 연결하게 되면 여러 가지 이점이 있다. 느슨한 연결 관계란 결국 특정 리소스를 직접 참조하는 것이 아니라, 간접 참조한다는 것을 의미한다. Service에서 Pod의 이름이나 IP를 직접 참조하게 되면 Pod의 생명주기에 따라 사용자가 매번 새로운 Pod 정보를 Service에 등록 및 삭제해야 한다.
하지만 라벨링 시스템을 통해 느슨한 관계를 유지할 경우 Service에서 바라보는 특정 라벨을 가지고 있는 어떠한 Pod에도 트래픽을 전달할 수 있다. Pod 입장에서도 Service를 직접 참조할 필요 없이 Service에서 바라보는 특정 라벨을 달기만 하면 바로 Service에 등록되어 트래픽을 받을 수 있다. 쿠버네티스가 Service와 Pod을 매핑시켜주기 때문이다.
# Service 실행
kubectl apply -f myservice.yaml
# Service 리소스 조회
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 17d
myservice ClusterIP 10.110.176.133 <none> 8080/TCP 33s
# Pod IP 확인
kubectl get pod -o wide
kubernetes라는 서비스는 쿠버네티스 API 서버로 트래픽을 전송하는 서비스 끝점이다. 쿠버네티스를 설치하면 기본적으로 생성된다. myservice
의 IP와 mynginx
Pod의 IP가 서로 다른 것을 확인할 수 있다. 각각의 주소를 이용하여 트래픽을 전달해보자.
# 새로운 Pod 생성
kubectl run client --image nginx
# Pod IP 확인
kubectl get pod -o wide
# 요청 보내기
kubectl exec client -- curl {client-ip} # Pod IP로 접근
kubectl exec client -- curl {myservice-ip:8080} # Service IP, Port로 접근
kubectl exec client -- curl {myservice:8080} # Service Domain, Port로 접근
myservice의 IP 주소를 확인하기 위해 DNS lookup을 수행한다.
kubectl exec client -- sh -c "apt update && apt install -y dnsutils"
kubectl exec client -- nslookup myservice
Name: myservice.default.svc.cluster.local
Address: 10.110.176.133
Service 도메인 주소 법칙
Service 리소스의 전체 도메인 이름 법칙은 다음과 같다.
<서비스이름>.<네임스페이스>.svc.cluster.local
.svc.cluster.local
은 postfix로 다른 값으로 변경이 가능하지만 일반적으로 기본값을 사용한다. Service의 전체 도메인 이름이나 <서비스이름>.<네임스페이스>
를 사용해서 DNS Lookup을 진행해도 동일한 결과를 얻을 수 있다.
# 모두 동일한 DNS Lookup 값을 가짐
kubectl exec client -- nslookup myservce
kubectl exec client -- nslookup myservice.default.svc.cluster.local
kubectl exec client -- nslookup myservice.default
클러스터 DNS 서버
Service 이름을 도메인 주소로 사용할 수 있는 이유는 쿠버네티스에서 제공하는 DNS 서버 덕분이다. 리눅스에서 DNS 서버 설정은 /etc/resolv.conf
에서 하는데, 이를 확인해보자.
kubectl exec client -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
결과로 nameserver 10.96.0.10
이라는 IP를 확인할 수 있는데, 쿠버네티스의 모든 Pod들은 바로 이 IP로 DNS를 조회하게 된다.
이 IP의 주인은 kube-dns
라는 Service인데, 쿠버네티스 핵심 컴포넌트가 존재하는 kube-system 네임스페이스의 Service 리소스 중 하나이다.
kubectl get svc -n kube-system
kubectl get svc kube-dns -n kube-system --show-labels
kube-dns
는 k8s-app=kube-dns
라벨을 가지고 있는데, 라벨을 통해 Pod을 매핑한다는 점을 가지고 해당 라벨이 어떤 Pod에 있는지 확인할 수 있다.
kubectl get pod -n kube-system -l k8s-app=kube-dns
확인을 해보면 coredns-xxx
라는 Pod이 조회되는데, coredns는 쿠버네티스에서 제공하는 클러스터 DNS 서버다. 모든 Pod들은 클러스터 내부, 외부 DNS 질의를 coredns를 통해 수행한다. 이러한 이유로 쿠버네티스 클러스터 안에서 자체적인 DNS를 가질 수 있게 되는 것이다.
example.com 10.43.0.10
Pod -------------> DNS Resolver ------------> Service(kube-dns) ----------> Pod(Coredns)
<------------ <------------ <----------
192.01.11.33 192.01.11.33 192.01.11.33
2. Service 종류
Service 리소스에는 총 4가지 타입이 있다.
ClusterIP
Service 리소스의 가장 기본이 되는 타입이다. 타입 지정을 하지 않으면 기본적으로 ClusterIP로 설정된다. ClusterIP 타입의 서비스 엔드포인트는 쿠버네티스 클러스터 내부에서만 접근이 가능하다. 클러스터 내에 존재하는 Pod에서만 ClusterIP 타입의 Service로 접근이 가능할 뿐 클러스터 외부에서는 접근할 수 없다.
# ClusterIP Service 템플릿 생성
kubectl run cluster-ip --image nginx --expose --port 80 \
--dry-run=client -o yaml > cluster-ip.yaml
vim cluster-ip.yaml
#cluster-ip.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
name: cluster-ip
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: cluster-ip
status:
loadBalancer: {}
---
---
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: cluster-ip
name: cluster-ip
spec:
containers:
- image: nginx
name: cluster-ip
ports:
- containerPort: 80
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
cluster-ip.yaml 파일에는 ClusterIP 타입 Service 리소스와 그에 대응하는 Pod이 생긴 것을 확인할 수 있다. 앞서 만든 myservice Service도 ClusterIP 타입의 Service이기 때문에 localhost에서 바로 엔드포인트로 요청을 보낸 것이 아니라 Pod을 만들어서 요청을 보냈다.
이제 만든 템플릿을 실행할 차례이다.
kubectl apply -f cluster-ip.yaml
# service/cluster-ip created
# pod/cluster-ip created
kubectl get svc cluster-ip -oyaml | grep type
# type: ClusterIP
kubectl exec client -- curl -s cluster-ip
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# ...
NodePort
ClusterIP 타입으로 외부 트래픽을 클러스터 내로 전달하지 못한다. NodePort 타입은 도커 컨테이너 포트 매핑과 비슷하게 로컬 호스트의 특정 포트를 Service의 특정 포트와 연결시켜 외부 트래픽을 Service까지 전달한다.
# node-port.yaml
apiVersion: v1
kind: Service
metadata:
name: node-port
spec:
type: NodePort # type 추가
ports:
- port: 8080
protocol: TCP
targetPort: 80
nodePort: 30080 # 호스트(노드)의 포트 지정
selector:
run: node-port
---
apiVersion: v1
kind: Pod
metadata:
labels:
run: node-port
name: node-port
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
kubectl apply -f node-port.yaml
# service/node-port created
# pod/node-port created
kubectl get svc
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 26d
# myservice ClusterIP 10.43.152.73 <none> 8080/TCP 2d
# cluster-ip ClusterIP 10.43.9.166 <none> 8080/TCP 42h
# node-port NodePort 10.43.94.27 <none> 8080:30080/TCP 42h
실행해보면 ClusterIP와는 다르게 타입이 명시적으로 NodePort로 설정되어 있으며, YAML 정의서에서 정의한 8080 포트와 NodePort의 30080 포트가 매핑되어 있는 것을 확인할 수 있다.
NodePort 타입은 단지 Pod이 위치한 노드 뿐만 아니라, 모든 노드에서 동일하게 엔드포인트를 제공한다. node-port Pod이 마스터 노드에 있다고 가정해도, 마스터, 워커 모두 동일한 NodePort로 서비스에 접근할 수 있다.
# 트래픽을 전달 받을 Pod가 마스터 노드에 위치합니다.
kubectl get pod node-port -owide
# NAME READY STATUS RESTARTS AGE IP NODE
# node-port 1/1 Running 0 14m 10.42.0.28 master
MASTER_IP=$(kubectl get node master -ojsonpath="{.status.addresses[0].address}")
curl $MASTER_IP:30080
# <!DOCTYPE html>
# <html>
# <head>
# ...
WORKER_IP=$(kubectl get node worker -ojsonpath="{.status.addresses[0].address}")
curl $WORKER_IP:30080
# <!DOCTYPE html>
# <html>
# <head>
# ...
쿠버네티스는 클러스터 시스템이기 때문에 특정 노드에서만 서비스가 동작하지 않고 모든 노드에 동일하게 적용된다.
kube-proxy 컴포넌트는 리눅스 커널의 netfilter를 이용하여 리눅스 커널 레벨에서 특정 트래픽을 중간에서 가로채 다른 곳으로 라우팅해주는 역할을 수행한다. 이를 통해 Pod가 위치한 노드 뿐만 아니라 모든 노드에서 동일한 NodePort로 원하는 서비스에 접근할 수 있다.
이처럼 NodePort에서 설정한 NodePort를 통해 외부 트래픽을 쿠버네티스 클러스터로 전달할 수 있다.
LoadBalancer
NodePort를 사용하면 단순히 한 개의 노드 뿐만 아니라 모든 노드에서 동일한 포트로 접근할 수 있게 된다. 쿠버네티스에서는 더 나아가 노드 앞단에 로드밸런서를 두고 해당 로드밸런서가 각 노드로 트래픽을 분산할 수 있게 로드밸런서 타입을 제공한다.
로드밸런서 타입을 이용하면 퍼블릭 클라우드 플랫폼에서 제공하는 로드밸런서를 Service 리소스에 연결할 수 있다. NodePort가 있는데도 로드밸런서 타입이 필요한 이유는 다음과 같다.
- 보안적인 측면으로 노드포트 대역(30000-32767)을 직접 외부에 공개할 필요 없이 서버를 내부 네트워크에 두고 로드밸런서만 외부 네트워크에 위치하여 well-known 포트로 엔드포인트를 제공할 수 있다.
- 로드밸런서가 클러스터 앞단에 존재하면 사용자가 각각의 서버 IP를 직접 알 필요 없이 로드밸런서의 IP 또는 도메인 주소만 가지고 요청을 보낼 수 있어 편리하다.
따라서 ClusterIP 타입의 리소스가 Pod 레벨에서의 안정적인 서비스 엔드포인트를 제공하는 것이라면 로드밸런서 타입은 노드 레벨에서의 안정적인 서비스 엔드포인트를 제공해주는 것이라고 볼 수 있다.
# load-bal.yaml
apiVersion: v1
kind: Service
metadata:
name: load-bal
spec:
type: LoadBalancer # 타입 LoadBalancer
ports:
- port: 8080
protocol: TCP
targetPort: 80
nodePort: 30088 # 30088로 변경
selector:
run: load-bal
---
apiVersion: v1
kind: Pod
metadata:
labels:
run: load-bal
name: load-bal
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
kubectl apply -f load-bal.yaml
# service/load-bal created
# pod/load-bal created
kubectl get svc load-bal
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# load-bal LoadBalancer 10.111.150.61 localhost 8080:30088/TCP
watch kubectl logs load-bal
curl localhost:8080
일반적으로 로드밸런서 타입의 서비스는 클라우드 플랫폼에서 제공하는 것을 사용하지만, 쿠버네티스 클러스터에서 제공해주는 소프트웨어 기반의 로드밸런서를 생성해서 테스트를 진행한다. spec의 type이 LoadBalancer
로 되어 있고, 로드밸런서가 사용하는 포트를 30088로 지정한 것을 확인할 수 있다.
로드밸런서 타입의 Service를 생성하면 EXTERNAL-IP에 로드밸런서의 IP가 설정되는데, 클라우드 플랫폼에서 제공해주는 로드밸런서를 사용하는 경우 IP와 DNS를 확인할 수 있다.
ExternalName
ExternalName은 외부 DNS 주소에 클러스터 내부에서 사용할 새로운 별칭을 만들 때 사용한다.
# external.yaml
apiVersion: v1
kind: Service
metadata:
name: google-svc # 별칭
spec:
type: ExternalName
externalName: google.com # 외부 DNS
kubectl apply -f external.yaml
# service/google-svc created
kubectl run call-google --image curlimages/curl \
-- curl -s -H "Host: google.com" google-svc
# pod/call-google created
kubectl logs call-google
# <HTML><HEAD><meta http-equiv="content-type" content="text/..">
# <TITLE>301 Moved</TITLE></HEAD><BODY>
# <H1>301 Moved</H1>
# ...
YAML 정의서에서 google-svc
라는 별칭을 통해 외부의 google.com
으로 연결할 수 있는 서비스 엔드포인트를 만들고 있다.
ExternalName 타입은 쿠버네티스 클러스터에 편입되지 않는 외부 서비스에 쿠버네티스 네트워킹 기능을 연결하고 싶은 경우 사용할 수 있다.
3. 네트워크 모델
쿠버네티스 네트워크 모델의 특징은 다음과 같다.
- 각 노드 간 NAT 없이 통신이 가능해야 한다.
- 각 Pod간 NAT 없이 통신이 가능해야 한다.
- 노드와 Pod간 NAT 없이 통신이 가능해야 한다.
- 각 Pod는 고유의 IP를 부여받는다.
- 각 Pod의 IP는 네트워크 제공자를 통해 할당받는다.
- Pod IP는 클러스터 내부 어디서든 접근이 가능해야 한다.
요약 : NAT를 통한 네트워킹을 싫어한다.
NAT 없는 통신을 가지는 쿠버네티스 모델은 다음과 같은 장점을 가진다.
- 모든 리소스가 다른 모든 리소스를 고유의 IP로 접근할 수 있다.
- NAT 통신으로 인한 부작용에 대해 신경 쓸 필요가 없다.
- 새로운 프로토콜을 재정의할 필요 없이 기존의 TCP, UDP, IP 프로토콜을 그대로 사용할 수 있다.
- Pod끼리의 네트워킹이 어느 노드에서든지 동일하게 동작한다.
참고자료
- 네트워크 구조: https://kubernetes.io/docs/concepts/cluster-administration/networking/#the-kubernetes-network-model
- 쿠버네티스 네트워킹 이해하기: https://coffeewhale.com/k8s/network/2019/04/19/k8s-network-01
kubectl delete svc --all
kubectl delete pod --all
'쿠버네티스' 카테고리의 다른 글
Pod 살펴보기 (0) | 2022.06.30 |
---|---|
쿠버네티스 기본 명령어 (0) | 2022.06.26 |
쿠버네티스 설치 (0) | 2022.06.24 |
쿠버네티스 소개 (0) | 2022.06.07 |
도커 기초 (0) | 2022.05.31 |