Step 06 — 서비스와 DNS

학습 목표

  • Service 가 왜 필요한지(파드 IP 는 계속 바뀐다) 이해한다
  • ClusterIP·NodePort·LoadBalancer·Headless 네 타입의 차이를 실습으로 구분한다
  • 서비스 DNS 이름(<svc>.<ns>.svc.cluster.local)으로 통신한다
  • Endpoints/EndpointSlice 가 "서비스 → 파드" 연결의 실체임을 안다
  • 셀렉터 오타 한 글자로 트래픽이 0 이 되는 함정을 직접 재현하고 진단·수리한다

선행 스텝: Step 05 — 디플로이먼트 예상 소요: 55분


6-1. 왜 Service 인가 — 파드 IP 는 못 믿는다

Step 05 에서 파드는 언제든 죽고 새로 생깁니다. 그때마다 파드 IP 가 바뀝니다. 롤링 업데이트를 하면 IP 3개가 통째로 갈립니다. 그러면 이 파드들에 접속하려는 쪽은 매번 새 IP 를 어떻게 알까요?

Service 는 "변하지 않는 안정적인 접속 지점"을 제공합니다. 서비스는 레이블 셀렉터로 파드 묶음을 고르고, 그 앞에 고정 가상 IP(ClusterIP)와 DNS 이름을 세웁니다. 뒤의 파드가 아무리 바뀌어도 앞의 이름/IP 는 그대로입니다. 들어온 요청은 살아있는 파드들로 자동 분산됩니다.

이 스텝의 백엔드는 응답에 자기 파드 이름을 넣어주는 hello-kubernetes 3개입니다.

# manifests/backend.yaml (발췌)
apiVersion: apps/v1
kind: Deployment
metadata: { name: hello, namespace: step06 }
spec:
  replicas: 3
  selector: { matchLabels: { app: hello } }
  template:
    metadata:
      labels: { app: hello }     # <-- 서비스 셀렉터가 찾을 레이블
    spec:
      containers:
        - name: hello
          image: paulbouwer/hello-kubernetes:1.10.1
          ports: [{ containerPort: 8080 }]
kubectl apply -f manifests/backend.yaml
kubectl rollout status deployment/hello -n step06

6-2. 네 가지 서비스 타입

한 백엔드(app=hello)를 네 가지로 노출합니다.

# manifests/services.yaml (발췌)
apiVersion: v1
kind: Service
metadata: { name: hello-clusterip, namespace: step06 }
spec:
  type: ClusterIP            # 기본 타입, 클러스터 내부 전용
  selector: { app: hello }
  ports:
    - { name: http, port: 80, targetPort: 8080 }   # 서비스 80 → 컨테이너 8080

port 는 서비스가 여는 포트, targetPort 는 실제 컨테이너 포트입니다. 둘은 달라도 됩니다.

kubectl apply -f manifests/services.yaml
kubectl get svc -n step06 -o wide

실행 결과

NAME              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        SELECTOR
hello-clusterip   ClusterIP      10.96.119.109   <none>        80/TCP         app=hello
hello-headless    ClusterIP      None            <none>        80/TCP         app=hello
hello-lb          LoadBalancer   10.96.13.175    <pending>     80:31609/TCP   app=hello
hello-nodeport    NodePort       10.96.89.111    <none>        80:30443/TCP   app=hello
타입접근 범위특징
ClusterIP클러스터 내부만기본값. 고정 가상 IP + DNS
NodePort외부(노드 IP:포트)모든 노드의 30000~32767 포트 하나를 연다
LoadBalancer외부(전용 IP)클라우드 LB 를 프로비저닝. kind 엔 없어 pending
Headless내부(파드 IP 직접)clusterIP: None. 로드밸런싱 없이 DNS 로 파드 IP 나열

6-3. ClusterIP + 서비스 DNS

클러스터 안에서는 서비스 이름이 곧 DNS 이름입니다. 정식 이름은 <svc>.<ns>.svc.cluster.local 이고, 같은 네임스페이스 안에서는 <svc> 짧은 이름만으로도 됩니다.

테스트용 클라이언트 파드를 띄워 확인합니다.

kubectl run client --image=busybox:1.36 -n step06 --restart=Never --command -- sleep 3600
kubectl exec -n step06 client -- nslookup hello-clusterip.step06.svc.cluster.local
Name:	hello-clusterip.step06.svc.cluster.local
Address: 10.96.119.109        <- 서비스의 고정 가상 IP 하나

DNS 는 서비스의 가상 IP 하나를 돌려줍니다. 이 IP 로 보낸 트래픽을 노드의 커널(kube-proxy 규칙)이 살아있는 파드로 분산합니다. HTTP 로 접근해 봅시다.

kubectl exec -n step06 client -- wget -qO- http://hello-clusterip/ | grep -A1 pod:
pod=hello-7f48fdfbbd-9ts99      <- 요청이 백엔드 파드 중 하나로 도달

💡 실무 팁: 다른 네임스페이스의 서비스를 부를 땐 반드시 <svc>.<ns> 를 붙이세요(hello-clusterip.step06). 짧은 이름은 "같은 네임스페이스"에서만 통합니다. 이 실수가 "로컬에선 되는데 배포하면 연결이 안 되는" 흔한 원인입니다.


6-4. NodePort — 외부에서 접근

NodePort 는 모든 노드에 고정 포트를 열어 클러스터 밖에서 들어올 수 있게 합니다. 이 학습용 kind 클러스터는 노드 포트 30080(및 30443)을 호스트로 매핑해 두어, 브라우저에서 http://localhost:30080 으로 바로 닿습니다.

# manifests/services.yaml (NodePort 발췌)
spec:
  type: NodePort
  selector: { app: hello }
  ports:
    - { name: http, port: 80, targetPort: 8080, nodePort: 30080 }
# 호스트(내 노트북)에서 직접
curl -s http://localhost:30080/ | grep -A1 pod:
# 여러 번 호출하면 요청이 3개 파드로 분산된다
for i in $(seq 1 8); do curl -s http://localhost:30080/ \
  | grep -A1 '<th>pod:</th>' | grep td; done | sort | uniq -c

실행 결과 (요청 8번이 3개 파드로 분산됨)

   2 <td>hello-7f48fdfbbd-8xv5j</td>
   2 <td>hello-7f48fdfbbd-9ts99</td>
   4 <td>hello-7f48fdfbbd-lc7cz</td>

같은 URL 을 반복 호출했는데 응답하는 파드가 매번 다릅니다. 이것이 서비스의 로드밸런싱입니다.

⚠️ 함정 (NodePort 는 클러스터 전역 자원): nodePort 번호는 클러스터 전체에서 유일해야 합니다. 이미 누가 30080 을 쓰고 있으면 apply 가 이렇게 거부됩니다.

The Service "hello-nodeport" is invalid: spec.ports[0].nodePort:
  Invalid value: 30080: provided port is already allocated

이 교재를 검증할 당시 다른 실습(step25)이 30080 을 점유하고 있어, 검증은 똑같이 호스트로 매핑된 30443 으로 했습니다(위 결과의 파드 분산은 30443 으로 확인). 혼자 실습하면 30080 이 그대로 열립니다. nodePort 를 생략하면 Kubernetes 가 범위 안에서 비어있는 포트를 알아서 골라 충돌을 피합니다.


6-5. LoadBalancer — kind 에선 왜 pending 인가

LoadBalancer 타입은 클라우드(AWS·GCP 등)에 외부 IP 를 가진 로드밸런서를 만들어 달라고 요청합니다. 그 요청을 처리하는 건 클라우드 프로바이더의 컨트롤러입니다.

kind 는 노트북 위 로컬 클러스터라 그런 프로바이더가 없습니다. 그래서 EXTERNAL-IP 가 영원히 pending 입니다.

kubectl get svc hello-lb -n step06
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
hello-lb   LoadBalancer   10.96.13.175   <pending>     80:31609/TCP   3m33s

주목할 점: pending 이어도 ClusterIP 와 NodePort(31609)는 이미 동작합니다. LoadBalancer 는 사실 NodePort 위에 외부 LB 를 얹는 구조라, 외부 IP 만 못 받았을 뿐 내부 접근은 됩니다.

💡 실무 팁: 로컬에서 LoadBalancer 를 진짜로 테스트하려면 metallb 같은 것을 설치하거나, 학습 목적이면 그냥 NodePort/kubectl port-forward 를 쓰세요. 실제 클라우드(EKS/GKE)에선 이 타입이 자동으로 외부 IP 를 받습니다.


6-6. Headless 서비스 — 파드 IP 를 직접

clusterIP: None 으로 만들면 가상 IP 가 없는 Headless 서비스가 됩니다. DNS 조회 시 서비스 IP 하나가 아니라 뒤에 있는 파드 IP 전부를 돌려줍니다.

kubectl exec -n step06 client -- nslookup hello-headless.step06.svc.cluster.local
Name:	hello-headless.step06.svc.cluster.local
Address: 10.244.2.77       <- 파드 IP 1
Name:	hello-headless.step06.svc.cluster.local
Address: 10.244.2.78       <- 파드 IP 2
Name:	hello-headless.step06.svc.cluster.local
Address: 10.244.1.78       <- 파드 IP 3

앞의 ClusterIP 서비스가 가상 IP 하나를 준 것과 대조됩니다. 클라이언트가 개별 파드를 직접 골라야 할 때(예: 각 파드에 고유 ID 가 있는 StatefulSet, 카프카·DB 클러스터) 씁니다. Step 11(StatefulSet)에서 다시 만납니다.


6-7. Endpoints / EndpointSlice — 연결의 실체

"서비스 → 어떤 파드들"의 실제 매핑은 Endpoints(구식)와 EndpointSlice(신식)에 들어있습니다. 서비스가 셀렉터로 고른 파드 중 준비된(ready) 것들의 IP:포트 목록입니다.

kubectl get endpoints -n step06
NAME              ENDPOINTS
hello-clusterip   10.244.1.78:8080,10.244.2.77:8080,10.244.2.78:8080
hello-headless    10.244.1.78:8080,10.244.2.77:8080,10.244.2.78:8080

파드가 죽거나 준비 안 되면 이 목록에서 자동으로 빠집니다. 서비스가 트래픽을 못 보내면 십중팔구 이 Endpoints 가 비어있습니다. 다음 절에서 그걸 재현합니다.

💡 실무 팁: 최신 클러스터는 EndpointSlice(discovery.k8s.io/v1)를 씁니다. kubectl get endpoints 는 "deprecated" 경고를 냅니다. 진단할 땐 kubectl get endpointslice -n step06 를 쓰세요. 둘 다 같은 정보를 담습니다.


6-8. ⚠️ 함정: 셀렉터 오타 한 글자 → 트래픽 0

가장 흔하고 악명 높은 함정입니다. 셀렉터를 app: helo(오타)로 준 서비스를 apply 합니다.

# manifests/broken-service.yaml (발췌)
spec:
  selector:
    app: helo          # <-- 오타! 실제 파드는 app=hello
kubectl apply -f manifests/broken-service.yaml
service/hello-broken created      <- YAML 은 아무 문제 없이 생성됨

apply 는 성공합니다. 에러 한 줄 없습니다. 하지만 Endpoints 를 보면 텅 비어있습니다.

kubectl get endpoints hello-broken -n step06
NAME           ENDPOINTS
hello-broken   <none>            <- 매칭되는 파드가 0개

접근하면 연결이 안 됩니다(엔드포인트가 없어 거절).

kubectl exec -n step06 client -- wget -qO- --timeout=2 http://hello-broken/
wget: can't connect to remote host (10.96.114.248): Connection refused

진단 순서: 서비스는 있는데 트래픽이 0 이면 → ① kubectl get endpoints <svc> 로 비었는지 확인 → ② 비었으면 서비스 셀렉터와 파드 레이블을 나란히 비교.

kubectl get svc hello-broken hello-clusterip -n step06 \
  -o custom-columns='NAME:.metadata.name,SELECTOR:.spec.selector'
NAME              SELECTOR
hello-broken      map[app:helo]     <- 오타
hello-clusterip   map[app:hello]    <- 정상

한 글자 차이가 보입니다. 고칩니다.

kubectl patch svc hello-broken -n step06 -p '{"spec":{"selector":{"app":"hello"}}}'
kubectl get endpoints hello-broken -n step06
service/hello-broken patched
NAME           ENDPOINTS
hello-broken   10.244.1.78:8080,10.244.2.77:8080,10.244.2.78:8080   <- 즉시 채워짐

셀렉터를 고치자마자 Endpoints 가 채워지고 트래픽이 흐릅니다.

⚠️ 함정 정리: 서비스 셀렉터와 파드 레이블은 글자 하나까지 정확히 일치해야 합니다. app: web vs app: Web(대소문자), tier: frontend vs tier: front-end(하이픈) 전부 매칭 실패입니다. 그리고 이건 apply 시점에 아무도 안 막아줍니다. 배포 후 kubectl get endpoints 로 확인하는 습관이 유일한 방어선입니다.


정리

타입clusterIP외부 접근쓰는 곳
ClusterIP가상 IP 1개불가(내부만)내부 서비스 간 통신(기본)
NodePort가상 IP + 노드포트노드IP:30000~32767간단한 외부 노출, 로컬 실습
LoadBalancer가상 IP + 노드포트 + 외부 IP클라우드 LB프로덕션 외부 노출
HeadlessNone내부(파드 IP 직접)StatefulSet, 클러스터형 앱

핵심 진단 도구: kubectl get endpoints <svc> — 비어있으면 셀렉터를 의심하라.

연습 과제 → challenge.md

다음 단계

Step 07 — 설정과 시크릿


실습 파일

이 스텝은 파일 3개 + 스크립트 1개로 진행합니다. 먼저 manifests/backend.yamlstep06 네임스페이스와 백엔드 파드 3개를 띄우고(6-1), 이어서 manifests/services.yaml 로 같은 백엔드를 ClusterIP·NodePort·LoadBalancer·Headless 네 타입으로 동시에 노출합니다(6-2~6-6). 마지막에 manifests/broken-service.yaml 로 셀렉터 오타 함정을 재현하고 진단·수리합니다(6-8). commands.sh 는 이 흐름을 강의 순서 그대로 담은 명령 모음입니다.

manifests/backend.yaml

서비스가 트래픽을 보낼 대상 파드를 만드는 매니페스트입니다. 6-1 절 맨 처음에 kubectl apply -f manifests/backend.yaml 로 적용합니다.

  • 한 파일에 문서 두 개(--- 로 구분)가 들어 있어 Namespace: step06 을 먼저 만들고 그 안에 Deployment: hello 를 넣습니다. 이 파일을 가장 먼저 적용해야 하는 이유가 이것입니다 — 네임스페이스가 없으면 뒤의 서비스 매니페스트가 전부 실패합니다.
  • replicas: 3 이 로드밸런싱을 눈으로 보게 해 주는 장치입니다. 파드가 3개라 6-4 절에서 curl 을 8번 날렸을 때 응답이 세 파드로 갈립니다.
  • template.metadata.labelsapp: hello이 스텝 전체의 핵심 키입니다. services.yaml 의 네 서비스가 전부 이 레이블을 셀렉터로 찾고, broken-service.yaml 은 이 값을 helo 로 잘못 적어 함정을 만듭니다.
  • 이미지는 paulbouwer/hello-kubernetes:1.10.1 이고 containerPort: 8080 을 엽니다. 이 8080 이 서비스의 targetPort 와 짝을 이룹니다.
  • envKUBERNETES_POD_NAMEfieldRefmetadata.name 을 주입해 파드가 응답 HTML 에 자기 이름을 찍게 합니다. 6-4 절의 grep -A1 '<th>pod:</th>' 가 이 값을 뽑아내는 것입니다.
# manifests/backend.yaml
# 서비스가 트래픽을 보낼 백엔드. hello-kubernetes 는 응답에 자신의 파드 이름을 넣어줘
# 로드밸런싱(요청이 여러 파드로 분산되는 것)을 눈으로 확인하기 좋습니다.
apiVersion: v1
kind: Namespace
metadata:
  name: step06
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
  namespace: step06
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello        # <-- 서비스 셀렉터가 이 레이블을 찾습니다
    spec:
      containers:
        - name: hello
          image: paulbouwer/hello-kubernetes:1.10.1
          ports:
            - containerPort: 8080
          env:
            - name: KUBERNETES_POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name

manifests/services.yaml

백엔드가 준비된 뒤 적용하는, 한 백엔드를 네 가지 타입으로 노출하는 매니페스트입니다. 6-2 절의 kubectl apply -f manifests/services.yaml 에 해당하며, 6-3~6-6 절이 여기서 만든 서비스를 하나씩 관찰합니다.

  • hello-clusterip (type: ClusterIP): port: 80targetPort: 8080 매핑이 핵심입니다. 서비스는 80 으로 받아 컨테이너의 8080 으로 넘깁니다. 6-3 절에서 wget http://hello-clusterip/ 가 포트 없이 되는 이유가 port: 80 이기 때문입니다.
  • hello-nodeport (type: NodePort): nodePort: 30080명시했습니다. kind 클러스터가 30080 을 호스트로 매핑해 둔 덕에 노트북 브라우저에서 http://localhost:30080 이 바로 열립니다. 다만 nodePort 는 클러스터 전역 자원이라 이미 점유돼 있으면 apply 가 provided port is already allocated 로 거부됩니다(6-4 절 함정 박스). 충돌이 나면 이 줄을 지워 Kubernetes 가 알아서 고르게 하면 됩니다.
  • hello-lb (type: LoadBalancer): nodePort 를 안 적었으므로 Kubernetes 가 임의 포트(예시 결과에선 31609)를 배정합니다. kind 에는 LB 프로바이더가 없어 EXTERNAL-IP 가 계속 pending 이지만, 내부적으로 ClusterIP + NodePort 를 함께 만들기 때문에 클러스터 안에서는 정상 동작합니다(6-5 절).
  • hello-headless (clusterIP: None): type 을 안 적어 기본 ClusterIP 지만 clusterIP: None 한 줄이 이 서비스를 Headless 로 바꿉니다. 가상 IP 가 없어 DNS 조회가 파드 IP 3개를 그대로 나열합니다(6-6 절).
  • 네 서비스 모두 셀렉터가 app: hello완전히 같습니다. 하나의 파드 묶음을 노출 방식만 달리해 비교하는 구성이라, 6-7 절에서 kubectl get endpoints 를 찍으면 서로 다른 서비스가 똑같은 엔드포인트 목록을 갖는 것이 보입니다.
# manifests/services.yaml
# 같은 백엔드(app=hello)를 4가지 서비스 타입으로 노출합니다.
# ---------------------------------------------------------------
# 1) ClusterIP — 클러스터 내부 전용 가상 IP (기본 타입)
apiVersion: v1
kind: Service
metadata:
  name: hello-clusterip
  namespace: step06
spec:
  type: ClusterIP
  selector:
    app: hello          # 이 레이블을 가진 파드로 트래픽 분산
  ports:
    - name: http
      port: 80          # 서비스 포트
      targetPort: 8080  # 파드(컨테이너) 포트
---
# 2) NodePort — 모든 노드의 고정 포트로 외부 노출
#    kind 클러스터가 30080 을 호스트로 매핑 → http://localhost:30080
apiVersion: v1
kind: Service
metadata:
  name: hello-nodeport
  namespace: step06
spec:
  type: NodePort
  selector:
    app: hello
  ports:
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 30080   # 30000~32767 범위. 클러스터에서 이 값 노출됨
---
# 3) LoadBalancer — 클라우드 LB 요청 (kind 에는 LB 프로바이더가 없어 pending)
apiVersion: v1
kind: Service
metadata:
  name: hello-lb
  namespace: step06
spec:
  type: LoadBalancer
  selector:
    app: hello
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
# 4) Headless — clusterIP: None. 가상 IP 없이 DNS 가 파드 IP 를 직접 반환
apiVersion: v1
kind: Service
metadata:
  name: hello-headless
  namespace: step06
spec:
  clusterIP: None       # <-- 이게 headless 로 만듦
  selector:
    app: hello
  ports:
    - name: http
      port: 80
      targetPort: 8080

manifests/broken-service.yaml

의도적으로 망가뜨린 실습 파일입니다. 6-8 절에서 적용해 "가장 흔한 서비스 장애"를 재현합니다.

  • 잘못된 곳은 딱 한 줄, selector.apphello 가 아니라 helo(l 하나 빠진 오타) 입니다. 나머지(type: ClusterIP, port: 80, targetPort: 8080)는 hello-clusterip 과 똑같습니다.
  • 증상이 고약한 이유: kubectl applyservice/hello-broken created 를 찍고 성공합니다. 스키마상 아무 문제가 없기 때문에 API 서버도, 검증 도구도 막아주지 않습니다.
  • 대신 kubectl get endpoints hello-broken -n step06<none> 을 반환합니다. app=helo 레이블을 가진 파드가 0개라 엔드포인트가 채워지지 않는 것입니다.
  • 실제 접속하면 wget: can't connect to remote host (...): Connection refused — 서비스 IP 는 존재하는데 보낼 곳이 없어 커널이 즉시 거절합니다.
  • 수리는 셀렉터만 고치면 됩니다: kubectl patch svc hello-broken -n step06 -p '{"spec":{"selector":{"app":"hello"}}}'. patch 직후 엔드포인트가 즉시 채워집니다. 여기서 얻어갈 습관은 하나입니다 — 서비스가 먹통이면 제일 먼저 kubectl get endpoints <svc> 를 보라.
# manifests/broken-service.yaml
# 함정 재현: 셀렉터 오타(app: helo) → 매칭되는 파드가 없어 Endpoints 가 비고
# 트래픽이 0 이 됩니다. YAML 은 정상 apply 되지만 서비스는 "먹통"입니다.
apiVersion: v1
kind: Service
metadata:
  name: hello-broken
  namespace: step06
spec:
  type: ClusterIP
  selector:
    app: helo           # <-- 오타! 실제 파드는 app=hello
  ports:
    - name: http
      port: 80
      targetPort: 8080

commands.sh

강의 본문 6-1 부터 6-8 까지의 명령을 등장 순서 그대로 모아 둔 스크립트입니다. 통째로 실행하기보다 한 줄씩 복사해 붙여 넣으며 출력을 관찰하는 용도입니다.

  • 맨 앞의 kubectl config current-contextkind-learn 클러스터가 맞는지 먼저 확인합니다. 다른 컨텍스트(운영 클러스터!)에 붙어 있으면 여기서 멈춰야 합니다.
  • 6-3 절 블록의 kubectl wait --for=condition=Ready pod/client -n step06 --timeout=60s 가 중요합니다. kubectl run 직후 곧바로 exec 하면 파드가 아직 안 떠서 실패하므로, Ready 를 기다린 뒤 nslookup/wget 을 실행합니다.
  • 6-4 절의 for i in $(seq 1 8); do curl ... ; done | sort | uniq -c 는 8번 요청을 파드 이름별로 집계해 로드밸런싱을 수치로 보여주는 한 줄입니다. 이 부분만 kubectl exec 가 아니라 호스트에서 직접 curl localhost:30080 을 때린다는 점에 유의하세요.
  • kubectl get endpointskubectl get endpointslice둘 다 찍습니다(6-7 절 내용). 다만 스크립트에서는 별도 블록이 아니라 맨 앞 6-1~2 블록의 kubectl apply -f manifests/services.yaml 바로 다음에 붙어 있습니다 — 서비스를 만들자마자 엔드포인트가 채워지는지 확인하는 순서이기 때문입니다. 전자는 deprecated 경고를 내지만 같은 정보를 보여주므로 비교해 보라는 의도입니다.
  • ⚠️ 주의: 맨 마지막 줄 kubectl delete namespace step06 은 이 스텝에서 만든 리소스를 전부 삭제합니다. challenge.md 의 연습 과제를 아직 안 풀었다면 이 줄은 실행하지 마세요.
#!/usr/bin/env bash
# Step 06 — 서비스와 DNS
# README 순서 그대로. 한 줄씩 실행하며 관찰하세요.
export PATH="/opt/homebrew/bin:$PATH"

kubectl config current-context   # kind-learn 이어야 함

# --- 6-1~2. 백엔드 + 서비스들 ---
kubectl apply -f manifests/backend.yaml
kubectl rollout status deployment/hello -n step06
kubectl apply -f manifests/services.yaml
kubectl get svc -n step06 -o wide
kubectl get endpoints -n step06
kubectl get endpointslice -n step06

# --- 6-3. ClusterIP + DNS (클라이언트 파드로) ---
kubectl run client --image=busybox:1.36 -n step06 --restart=Never --command -- sleep 3600
kubectl wait --for=condition=Ready pod/client -n step06 --timeout=60s
kubectl exec -n step06 client -- nslookup hello-clusterip.step06.svc.cluster.local
kubectl exec -n step06 client -- wget -qO- http://hello-clusterip/ | grep -A1 pod:

# --- 6-4. NodePort — 호스트에서 접근 (혼자 실습 시 30080) ---
curl -s http://localhost:30080/ | grep -A1 pod:
for i in $(seq 1 8); do curl -s http://localhost:30080/ | grep -A1 '<th>pod:</th>' | grep td; done | sort | uniq -c

# --- 6-5. LoadBalancer 는 kind 에서 pending ---
kubectl get svc hello-lb -n step06
kubectl describe svc hello-lb -n step06 | grep -iE "Type|LoadBalancer"

# --- 6-6. Headless (파드 IP 직접 반환) ---
kubectl exec -n step06 client -- nslookup hello-headless.step06.svc.cluster.local

# --- 6-8. 함정: 셀렉터 오타 → 트래픽 0 ---
kubectl apply -f manifests/broken-service.yaml          # 정상 apply 됨
kubectl get endpoints hello-broken -n step06            # <none> (비어있음!)
kubectl get svc hello-broken hello-clusterip -n step06 -o custom-columns='NAME:.metadata.name,SELECTOR:.spec.selector'
kubectl exec -n step06 client -- wget -qO- --timeout=2 http://hello-broken/   # Connection refused
# 수리
kubectl patch svc hello-broken -n step06 -p '{"spec":{"selector":{"app":"hello"}}}'
kubectl get endpoints hello-broken -n step06            # 즉시 채워짐

# --- 정리 ---
kubectl delete namespace step06