CKA 3주차 - Service 4종, EndpointSlice, CoreDNS와 Service 장애 진단
시리즈: 1주차 - 클러스터 구조와 Pod · Deployment · 2주차 - ConfigMap · Secret, 스케줄링 · 3주차 (이번 글)
1주차에서 Pod를 지우면 Deployment가 새 Pod를 만들어준다는 것을 확인했다. 그런데 새 Pod는 IP가 달라진다. 다른 앱이 Pod IP로 직접 접속하고 있었다면 Pod가 바뀔 때마다 연결이 끊긴다.
Service는 이 문제를 해결한다. Pod가 몇 번 바뀌어도 그대로인 IP와 DNS 이름을 하나 만들고, 들어온 요청을 그 시점에 살아 있는 Pod들로 나눠 보낸다. 출제 비중이 20%인 Services & Networking 도메인의 첫 부분이다.
1. Service가 만들어주는 것
Service를 하나 만들면 세 가지가 생긴다.
- 고정된 내부 IP: ClusterIP
- DNS 이름:
<service>.<namespace>.svc.cluster.local - 요청을 보낼 Pod 목록: selector(라벨 조건)에 맞는 Pod의 IP를 EndpointSlice에 모은다
nginx Pod 3개짜리 Deployment를 만들고 Service로 노출했다.
k create deployment web --image=nginx --replicas=3
k expose deployment web --name=web-service --port=80 --target-port=80
k get service web-service
k describe service web-servicek expose는 Deployment의 Pod 라벨(app=web)을 읽어서 같은 라벨을 selector로 가진 Service를 만든다.
Pod를 교체해도 Service IP는 그대로다
Pod 하나를 지운 뒤 다시 확인했다.
k delete pod <web-pod-이름>
k get service web-service # CLUSTER-IP 그대로
k describe service web-service # Endpoints 목록만 새 Pod IP로 바뀜Service의 ClusterIP는 바뀌지 않았고, Endpoints 항목만 새 Pod의 IP로 자동 갱신됐다. 클라이언트는 Pod가 바뀐 사실을 몰라도 된다.
2. 포트 3개 구분하기
Service YAML에는 포트가 세 종류 나온다. 처음에 가장 헷갈렸던 부분이다.
nodePort: 클러스터 밖에서 각 Node로 들어오는 포트 (30000~32767)port: Service가 요청을 받는 포트targetPort: Service가 요청을 넘겨줄 Pod(컨테이너)의 포트
요청이 들어오는 경로로 보면 이렇다.
- 다른 Pod
- Service:80 (port)
- Pod:80 (targetPort)
- NodeIP:32254 (nodePort)
- Service:80 (port)
- Pod:80 (targetPort)
nginx는 컨테이너 안에서 80번 포트를 쓰니까 targetPort는 80이다. port는 다른 Pod가 Service에 접속할 때 쓰는 번호라서 꼭 80일 필요는 없다.
3. Service의 type 4가지
type은 다른 리소스를 만드는 옵션이 아니다. Service의 입구를 어디까지 열지 정하는 spec.type 값이다. 앞의 type이 가진 기능에 하나씩 더해지는 구조다.
- ClusterIP (기본값): 클러스터 내부에서만 접속할 수 있는 고정 IP
- NodePort: ClusterIP에 더해, 모든 Node의 특정 포트를 바깥에 연다
- LoadBalancer: NodePort에 더해, 클라우드의 외부 로드밸런서를 연결한다
- ExternalName: Pod와 연결하지 않고, 외부 DNS 이름의 별칭(CNAME)만 제공한다
요청이 거쳐 가는 경로를 그려보면 "앞의 type을 포함한다"는 말이 보인다.
- 클러스터 내부
- ClusterIP
- Pod (app=web)
- NodeIP:3xxxx
- NodePort
- ClusterIP
- Pod (app=web)
- 외부 LB IP
- LoadBalancer
- NodePort
- ClusterIP
- Pod (app=web)
- my-svc
- example.com (CNAME)
NodePort
k expose deployment web --name=web-nodeport --port=80 --target-port=80 --type=NodePort
k get service web-nodeport
# PORT(S): 80:32254/TCP80:32254는 "Service 포트 80, nodePort 32254"라는 뜻이다. nodePort를 지정하지 않으면 30000~32767 사이에서 자동으로 배정된다.
맥의 Docker Desktop 환경에서는 Node IP로 직접 접속할 수 없었다. minikube 노드가 Docker 내부 네트워크에 있어서 macOS에서 그 IP로 가는 경로가 없기 때문이다. 그래서 minikube가 터널을 열어준다.
minikube service web-nodeport --url
# http://127.0.0.1:64328- Mac 127.0.0.1:64328
- minikube 터널
- NodePort 32254
- Service:80
- Pod:80
32254는 Service에 저장된 실제 nodePort다.64328은 내 맥에서만 쓰는 임시 터널 포트다. 터널을Ctrl+C로 끄면 사라지지만, Service의32254는 그대로 남는다.- 이 터널은 minikube 로컬 환경의 제약을 우회하는 기능일 뿐, NodePort의 원래 동작은 아니다.
LoadBalancer
k expose deployment web --name=web-loadbalancer --port=80 --type=LoadBalancer
minikube tunnel # 별도 터미널에서 실행해둔다
k get service web-loadbalancer # EXTERNAL-IP: 127.0.0.1
curl http://127.0.0.1:80클라우드(EKS)에서는 이 type을 쓰면 AWS가 실제 로드밸런서를 만들어 붙여준다. 로컬에는 그런 것이 없어서 minikube tunnel이 그 역할을 흉내 낸다. 터널 없이 보면 EXTERNAL-IP가 계속 <pending>으로 남는다.
ExternalName
k create service externalname external-example --external-name=example.com
k exec -n client dns-test -- nslookup external-example.default.svc.cluster.local조회해보면 CoreDNS가 example.com을 CNAME으로 돌려준다. 이 Service는 selector도 없고 Pod와 연결되지도 않는다. 클러스터 안에서 외부 주소를 내부 이름으로 부르고 싶을 때 쓴다.
4. EndpointSlice와 kube-proxy
EndpointSlice: 실제로 요청을 보낼 목적지 목록
Service 자체는 트래픽을 처리하지 않는다. Service가 가진 것은 "어떤 라벨의 Pod로 보낼지"라는 조건(selector)뿐이다. 그 조건에 맞는 Pod IP와 포트 목록을 쿠버네티스가 따로 관리하는데, 그게 EndpointSlice다.
k get endpointslicesNAME ENDPOINTS
web-service-z2gx2 10.244.1.25, 10.244.1.26, 10.244.0.11
web-nodeport-8pktj 10.244.1.25, 10.244.1.26, 10.244.0.11
web-loadbalancer-rgtrb 10.244.1.25, 10.244.1.26, 10.244.0.11세 Service가 모두 app=web을 selector로 쓰기 때문에, 각자의 EndpointSlice에 같은 Pod IP 3개가 들어 있다. ExternalName Service는 Pod와 연결되지 않으므로 EndpointSlice가 없다.
kube-proxy: 노드마다 전달 규칙을 만든다
그럼 ClusterIP로 온 요청을 Pod IP로 실제로 바꿔주는 것은 누구일까. 각 노드에서 실행되는 kube-proxy 다. kube-proxy는 Service와 EndpointSlice의 변화를 지켜보다가, 노드의 네트워크 규칙(iptables 등)에 "이 ClusterIP:port로 온 요청은 이 Pod IP들 중 하나로 보낸다"는 규칙을 써넣는다.
k get pods -n kube-system -o wide | grep kube-proxy로 확인하니 노드 2개에 kube-proxy Pod가 하나씩 떠 있었다. 2주차에 배운 DaemonSet으로 배포된 것이다.
정리하면 Service에서 실제 전달 규칙까지 이렇게 이어진다.
- Service (selector: app=web)
- EndpointSlice (Pod IP 3개)
- 각 노드의 네트워크 규칙
- 쿠버네티스가 selector와 Pod 라벨이 맞는지 계속 확인해서, 맞는 Pod의 IP를 EndpointSlice에 채운다.
- 각 노드의 kube-proxy가 EndpointSlice의 변화를 감지해서, "ClusterIP:80으로 온 요청은 Pod IP:80 중 하나로 보낸다"는 규칙을 노드에 써넣는다.
5. CoreDNS: Service 이름을 IP로 바꿔준다
Pod 안에서는 Service를 IP가 아니라 이름으로 부를 수 있다. 이름을 ClusterIP로 바꿔주는 것이 클러스터 내부 DNS 서버인 CoreDNS 다.
Namespace에 따라 쓸 수 있는 이름이 달라진다.
- 같은 Namespace에서:
web-service - 다른 Namespace에서:
web-service.default - 전체 이름(FQDN):
web-service.default.svc.cluster.local
다른 Namespace에서도 접속되는지 확인하려고 client Namespace에 테스트용 Pod를 하나 만들었다. 이 Pod는 서비스의 일부가 아니라 DNS와 HTTP 요청을 보내보기 위한 임시 클라이언트다.
k create namespace client
k run dns-test -n client --image=busybox:1.36 --restart=Never -- sleep 86400
k exec -n client dns-test -- nslookup web-service.default.svc.cluster.local
k exec -n client dns-test -- wget -qO- http://web-service.default.svc.cluster.localServer: 10.96.0.10 ← CoreDNS의 주소
Name: web-service.default.svc.cluster.local
Address: 10.108.33.233 ← web-service의 ClusterIP이름 조회와 HTTP 응답(nginx 기본 페이지)이 모두 정상이었다. 여기까지 실습한 전체 흐름을 정리하면 이렇다.
- dns-test Pod
- CoreDNS (10.96.0.10)
- ClusterIP (10.108.33.233)
- EndpointSlice
- nginx Pod
- Mac
- minikube 터널
- NodePort (32254)
- web-nodeport Service
- EndpointSlice
- nginx Pod
그리고 이 모든 흐름의 뒤에서는 Deployment가 Pod 3개를 유지하고, 라벨이 맞는 Pod IP가 EndpointSlice에 자동으로 반영된다.
web-service.default.svc.cluster.local은 클러스터 안에서만 통하는 이름이다. 맥 브라우저에 입력해도 열리지 않는다.
6. Service 장애 진단: selector가 Pod와 안 맞을 때
CKA는 트러블슈팅 비중이 30%다. 그래서 Service가 고장 나는 상황을 일부러 만들어봤다. web-service의 selector를 app=broken으로 바꿔서 Pod 라벨(app=web)과 맞지 않게 했다.
k edit service web-service로 spec.selector.app 값을 web에서 broken으로 바꿨다. 이때 나타난 증상은 이렇다.
- DNS 조회: 정상.
web-service는 여전히 ClusterIP로 해석된다 - Pod 상태: 3개 모두
Running - EndpointSlice: 비어 있음
- HTTP 요청:
Connection refused
Service도 있고, Pod도 Running이고, DNS도 정상인데 접속만 안 된다. 겉으로는 다 멀쩡해 보이는 상황이다. 아래 순서로 원인을 찾았다.
# 1. Service가 어떤 라벨을 찾고 있는지, Endpoints가 비어 있는지
k describe service web-service
# Selector: app=broken
# Endpoints: <none>
# 2. Pod에 실제로 붙어 있는 라벨
k get pods --show-labels
# app=web
# 3. 이 Service의 EndpointSlice
k get endpointslices -l kubernetes.io/service-name=web-serviceselector와 Pod 라벨이 다르다는 것을 확인하고 app=web으로 되돌렸다. 바로 EndpointSlice에 Pod IP 3개가 다시 채워졌고, HTTP 응답도 정상으로 돌아왔다.
Service는 있고 Pod도 Running인데 접속이 안 되면, Service의
selector와 Pod의labels가 일치하는지, EndpointSlice가 비어 있지 않은지를 먼저 본다. 그다음targetPort가 컨테이너가 실제로 쓰는 포트와 같은지 확인한다.
실습이 끝난 뒤에는 web Deployment, Service 4개, client Namespace를 모두 지워 정리했다. Namespace를 지우면 그 안의 리소스(dns-test Pod)도 함께 삭제된다.
7. 3주차 명령어 정리
| 목적 | 명령 |
|---|---|
| ClusterIP Service 생성 | k expose deployment web --name=web-service --port=80 --target-port=80 |
| NodePort Service 생성 | k expose deployment web --port=80 --type=NodePort |
| ExternalName Service 생성 | k create service externalname <이름> --external-name=example.com |
| Service · Endpoints 확인 | k get svc · k describe svc <이름> |
| EndpointSlice 확인 | k get endpointslices -l kubernetes.io/service-name=<이름> |
| Pod 라벨 확인 | k get pods --show-labels |
| DNS · HTTP 확인 | k exec <pod> -- nslookup <svc> · k exec <pod> -- wget -qO- http://<svc> |
| minikube 외부 접속 | minikube service <이름> --url · minikube tunnel |
정리
- Service는 바뀌는 Pod IP 앞에 고정된 ClusterIP와 DNS 이름을 만든다. 어떤 Pod로 보낼지는 selector로 정한다.
- type은 입구를 어디까지 여는지의 차이다. ClusterIP → NodePort → LoadBalancer 순서로 앞의 기능을 포함한다. ExternalName만 Pod와 연결되지 않는다.
- 실제 목적지 목록은 EndpointSlice에 있고, 각 노드의 kube-proxy가 그 목록으로 전달 규칙을 만든다.
- CoreDNS가
<service>.<namespace>.svc.cluster.local을 ClusterIP로 바꿔준다. - 접속이 안 되면
describe svc→ Pod 라벨 → EndpointSlice 순서로 본다.
1~3주차를 마치며
3주 동안 공부한 내용은 결국 하나의 흐름으로 이어진다. Deployment가 Pod 개수를 유지하고(1주차), 스케줄러가 requests와 라벨 · taint를 보고 Pod를 노드에 배치하고(2주차), Service가 그 Pod들 앞에 고정된 입구를 만든다(3주차).
공부하면서 가장 도움이 된 습관은 일부러 망가뜨려 보는 것이었다. CPU requests를 100으로 적어 Pending을 만들고, taint로 Pod를 쫓아내고, selector를 틀리게 바꿔 접속을 끊었다. 그때마다 describe의 Events와 EndpointSlice를 보면서 원인을 찾았는데, 이게 그대로 트러블슈팅 문제를 푸는 연습이 됐다.
다음 4주차에는 Ingress와 NetworkPolicy를 공부한다.