M4. Service·Ingress·클러스터 내부 DNS
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”NKS에서 web-app이 api-service의 Pod IP를 직접 호출하면 Pod 교체 때 연결 대상이
사라집니다. Service의 안정된 이름, 실제 backend 목록인 EndpointSlice, 외부 HTTP 규칙인
Ingress를 분리해서 볼 수 있어야 DNS·대상 Pod·외부 진입 장애를 같은 문제로 뭉뚱그리지 않고
리뷰할 수 있습니다.
개념 정의의 정본은 Kubernetes Basics와
DNS basics입니다. 원본 경로는 각각
content/topics/L5/kubernetes-basics.mdx, content/topics/L2/dns-basics.mdx이며, 이
모듈은 개념을 재정의하지 않고 M3의 일회용 Pod 집합에 안정된 호출 경로를 붙여 관찰합니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”flowchart LR Labels["Service selector + Pod Ready"] -->|"control plane이 계산"| Slice["EndpointSlice: Ready backend IP"] Slice -->|"watch하여 규칙 반영"| Agent["Service proxy / CNI dataplane 구현"] Client["호출 Pod"] -->|"1. DNS: api-service"| DNS["CoreDNS"] DNS -->|"2. Service ClusterIP"| VIP["Service virtual IP :80"] VIP -->|"3. 실제 packet"| Agent Agent -->|"4. targetPort :8080"| PodA["Pod A 또는 Pod B"]
| 관찰 대상 | 답하는 질문 | 이 랩의 확인 방법 |
|---|---|---|
| Service DNS | 안정된 이름을 주소로 해석할 수 있는가? | Pod 안 nslookup |
Service.spec.clusterIP | 이름이 가리키는 cluster 내부 virtual IP는 무엇인가? | Service JSONPath |
| Service selector | 어떤 label의 Pod를 backend 후보로 고르는가? | selector 변경 전·후 비교 |
| EndpointSlice | 지금 실제 트래픽 대상으로 계산된 IP는 무엇인가? | Pod IP와 ready endpoint 집합 비교 |
port / targetPort | client port와 application listen port는 무엇인가? | 80 → 8080, 실제 HTTP 요청 |
| Ingress + Ingress Controller | 어떤 HTTP rule을 누가 실제 proxy에 반영하는가? | Ingress spec, Traefik 경유 요청 |
Service selector는 backend 주소를 직접 보관하지 않습니다. control plane이 selector와 Pod label·Ready 상태를 비교해 EndpointSlice를 갱신하고, Service proxy 또는 CNI dataplane 구현이 그 control state를 읽어 packet 전달 규칙을 반영합니다. EndpointSlice API object 자체는 packet이 통과하는 hop이 아닙니다. 따라서 Service DNS가 정상이어도 EndpointSlice가 비어 있으면 application에는 도달하지 않습니다.
flowchart LR Request["HTTP Host: workbook.invalid"] --> Proxy["Traefik Ingress Controller"] Rule["Ingress: host/path → Service:80"] -->|"controller가 관찰"| Proxy Proxy --> Service["api-service Service"] Service --> Backend["Ready backend Pod :8080"]
Ingress object만 생성한다고 proxy나 cloud load balancer가 생기지는 않습니다. 이 로컬
cluster에는 K3s packaged Traefik Controller가 있어 ingressClassName: traefik을 실행할 수
있습니다. NKS에서는 승인된 Controller, class, annotation과 외부 load balancer 연결을 별도로
확인해야 하며 k3d 결과를 NKS 노출 증거로 사용하지 않습니다.
| 이름 형태 | 해석 기준 | 판단 |
|---|---|---|
api-service | 호출 Pod와 같은 namespace의 Service | 짧지만 namespace 의존 |
api-service.<namespace> | 명시한 namespace의 Service | namespace 경계를 드러냄 |
api-service.<namespace>.svc.cluster.local | 기본 cluster domain까지 적은 FQDN | search suffix 오해를 줄임 |
api-service.<missing namespace>.svc.cluster.local | 존재하지 않는 namespace의 Service | guard 후 NXDOMAIN |
시점 의존 설명은 2026-07-15에 다음 공식 1차 자료로 확인했습니다.
- Service: selector, ClusterIP,
port와targetPort, EndpointSlice 연결 - EndpointSlices: Service backend 주소와 ready condition
- DNS for Services and Pods: namespace별 Service 이름과 FQDN
- Ingress: HTTP(S) rule과 Controller 필요 조건, frozen API와 Gateway 권장 방향
- K3s Networking Services: 이 로컬 환경의 packaged CoreDNS·Traefik·ServiceLB 경계
Ingress API는 stable이지만 기능 확장은 frozen 상태이고 Kubernetes project는 새 설계에 Gateway API를 권장합니다. 이 워크북은 3서비스 이전에서 리뷰할 Ingress object와 NKS Controller 경계를 우선 다루며 Gateway API 구현은 심화 범위로 남깁니다.
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
| Service | 담당자가 바뀌어도 유지되는 팀 대표번호 | 실제 application 건강을 보증하지는 않습니다. |
| EndpointSlice | 대표번호가 지금 연결할 수 있는 담당자 명단 | 보통 사용자가 직접 유지하는 주소록이 아닙니다. |
| cluster DNS | 팀 이름을 대표번호로 찾는 사내 전화번호부 | HTTP 연결과 응답 성공까지 확인하지 않습니다. |
| Ingress / Controller | 안내 규칙표와 그 규칙을 실제 집행하는 안내 데스크 | TCP 전체나 provider load balancer 구현까지 같지는 않습니다. |
4. 핸즈온 랩
섹션 제목: “4. 핸즈온 랩”검증 환경
섹션 제목: “검증 환경”| 항목 | 검증값 |
|---|---|
| 기준일 | 2026-07-15 |
| host | macOS arm64 |
| Docker client / engine | 29.6.1 / 29.6.1 |
| k3d | 5.9.0 |
| K3s image / API server | rancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1 |
| kubectl | 1.36.1 |
| Helm | 4.2.3 — 직접 사용하지 않고 K3s packaged Traefik만 관찰 |
| container image | busybox:1.37.0 + Setup의 고정 multi-platform digest |
| evidence 환경 | local-cluster; 고정 context + k8s-wb-m4-<UTC run ID> |
모든 명령 묶음은 executed-local입니다. 실제 NKS LoadBalancer·공인 주소·TLS 인증서·DNS와
Ingress Controller 설치는 승인된 non-production 환경이 없으므로 official-doc-only 또는
excluded이며 예상 출력을 제시하지 않습니다.
사전 조건
섹션 제목: “사전 조건”- 저장소 root에서 실행하며 M0~M3를 완료한 상태여야 합니다.
- Docker Desktop과 M2의
cs-study-workbookcluster가 실행 중이어야 합니다. - 모든 workload API 명령은
--context k3d-cs-study-workbook과 고유 namespace를 명시합니다. - K3s packaged CoreDNS와
traefikIngressClass를 Setup에서 확인합니다. - BusyBox HTTP fixture는 Service 경로만 관찰하며 실제 production API health·TLS·business response를 나타내지 않습니다.
- 예상 소요 시간은 105분입니다.
Setup
섹션 제목: “Setup”Backend, Service, client 만들기
섹션 제목: “Backend, Service, client 만들기”preflight와 context guard 뒤 replica 2개의 api-service, ClusterIP Service와 client Pod를
같은 namespace에 만듭니다. container는 8080, client는 Service의 80을 사용합니다.
set -euo pipefailrm -rf /tmp/k8s-wb-m4mkdir -p /tmp/k8s-wb-m4node automation/k8s-workbook/preflight.mjs --require-clustertest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="k8s-wb-m4-$(date -u +%Y%m%d%H%M%S)"printf '%s' "$NS" > /tmp/k8s-wb-m4/namespaceif kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then echo "namespace collision: $NS" >&2 exit 1fikubectl --context k3d-cs-study-workbook create namespace "$NS"kubectl --context k3d-cs-study-workbook -n kube-system wait \ --for=condition=Available deployment/coredns deployment/traefik --timeout=90s >/dev/nulltest "$(kubectl --context k3d-cs-study-workbook get ingressclass traefik \ -o jsonpath='{.spec.controller}')" = "traefik.io/ingress-controller"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: apps/v1kind: Deploymentmetadata: name: api-servicespec: replicas: 2 selector: matchLabels: app: api-service template: metadata: labels: app: api-service spec: terminationGracePeriodSeconds: 3 containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c"] args: - | mkdir -p /www echo "backend=$(hostname)" > /www/index.html exec httpd -f -p 8080 -h /www ports: - name: http containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: api-servicespec: selector: app: api-service ports: - name: http port: 80 targetPort: http---apiVersion: v1kind: Podmetadata: name: network-clientspec: terminationGracePeriodSeconds: 3 containers: - name: client image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"]YAMLkubectl --context k3d-cs-study-workbook -n "$NS" \ rollout status deployment/api-service --timeout=90s >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" \ wait --for=condition=Ready pod/network-client --timeout=90s >/dev/nullecho 'dependencies=coredns,traefik ready=true'echo 'fixture=deployment/service/client ready=true'예상 출력:
PASS platform: darwin/arm64PASS architecture: arm64PASS docker-cli: v29.6.1PASS docker-engine: v29.6.1PASS k3d: v5.9.0PASS kubectl-skew: v1.36.1PASS helm: v4.2.3PASS context: k3d-cs-study-workbookPASS kubernetes-server: v1.36.2+k3s1namespace/k8s-wb-m4-<UTC 14자리 run ID> createddeployment.apps/api-service createdservice/api-service createdpod/network-client createddependencies=coredns,traefik ready=truefixture=deployment/service/client ready=truehost architecture, Docker patch version과 namespace run ID는 변동 필드입니다.
Lab 1 — Service, Pod, EndpointSlice 집합 맞추기
섹션 제목: “Lab 1 — Service, Pod, EndpointSlice 집합 맞추기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" get service api-service \ -o jsonpath='service={.metadata.name} type={.spec.type} clusterIP={.spec.clusterIP} port={.spec.ports[0].port} targetPort={.spec.ports[0].targetPort}{"\n"}'POD_IPS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l app=api-service \ -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' | sort)"ENDPOINT_IPS=""for _ in $(seq 1 30); do ENDPOINT_IPS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[*]}{.conditions.ready}{" "}{.addresses[0]}{"\n"}{end}' \ | awk '$1 == "true" {print $2}' | sort)" test "$POD_IPS" = "$ENDPOINT_IPS" && break sleep 1donetest "$(printf '%s\n' "$POD_IPS" | sed '/^$/d' | wc -l | tr -d ' ')" = "2"test "$POD_IPS" = "$ENDPOINT_IPS"printf 'pod_ips=%s\n' "$(printf '%s\n' "$POD_IPS" | paste -sd, -)"printf 'ready_endpoint_ips=%s match=true\n' "$(printf '%s\n' "$ENDPOINT_IPS" | paste -sd, -)"예상 출력:
service=api-service type=ClusterIP clusterIP=<Service IPv4> port=80 targetPort=httppod_ips=<Pod IPv4 A>,<Pod IPv4 B>ready_endpoint_ips=<Pod IPv4 A>,<Pod IPv4 B> match=trueIP와 순서는 변동값입니다. clusterIP는 Pod IP가 아니며 두 집합의 match=true가
selector→EndpointSlice 연결의 증거입니다.
Lab 2 — 짧은 이름과 FQDN으로 실제 HTTP 요청하기
섹션 제목: “Lab 2 — 짧은 이름과 FQDN으로 실제 HTTP 요청하기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"SERVICE_IP="$(kubectl --context k3d-cs-study-workbook -n "$NS" get service api-service \ -o jsonpath='{.spec.clusterIP}')"SHORT_LOOKUP="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ sh -c 'nslookup api-service 2>&1 || true')"SHORT_IP="$(printf '%s\n' "$SHORT_LOOKUP" \ | awk '/^Name:/ {answer=1; next} answer && /^Address:/ {print $2; exit}')"FQDN_IP="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ nslookup "api-service.$NS.svc.cluster.local" | awk '/^Address: / {print $2}' | tail -n 1)"test "$SHORT_IP" = "$SERVICE_IP"test "$FQDN_IP" = "$SERVICE_IP"RESPONSE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ wget -qO- http://api-service/)"printf 'short_dns=%s fqdn_dns=%s service_cluster_ip=%s match=true\n' \ "$SHORT_IP" "$FQDN_IP" "$SERVICE_IP"printf '%s\n' "$RESPONSE"예상 출력:
short_dns=<Service IPv4> fqdn_dns=<같은 Service IPv4> service_cluster_ip=<같은 Service IPv4> match=truebackend=api-service-<ReplicaSet hash>-<Pod suffix>응답한 Pod suffix는 요청마다 달라질 수 있고 한 번씩 번갈아 응답한다는 보장은 없습니다.
BusyBox nslookup은 짧은 이름의 search 후보 중 일부 NXDOMAIN도 함께 출력하고 성공 답이
있어도 non-zero로 끝날 수 있어, 명령은 Name: 뒤의 실제 answer만 정규화합니다. 짧은 이름의
최종 사용 가능성은 바로 다음 wget 성공으로 별도 확인합니다.
Lab 3 — 잘못된 namespace를 NXDOMAIN으로 분리하기
섹션 제목: “Lab 3 — 잘못된 namespace를 NXDOMAIN으로 분리하기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"MISSING_NS="missing-$NS"if kubectl --context k3d-cs-study-workbook get namespace "$MISSING_NS" >/dev/null 2>&1; then echo "missing namespace guard failed: $MISSING_NS exists" >&2 exit 1fiset +ekubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ nslookup "api-service.$MISSING_NS.svc.cluster.local" \ > /tmp/k8s-wb-m4/wrong-namespace.log 2>&1STATUS=$?set -etest "$STATUS" = "1"grep -q 'NXDOMAIN' /tmp/k8s-wb-m4/wrong-namespace.logprintf 'lookup=missing-namespace-service-fqdn result=NXDOMAIN exit=%s\n' "$STATUS"exit 1예상 출력과 exit code:
lookup=missing-namespace-service-fqdn result=NXDOMAIN exit=1전체 예상 exit code는 1입니다. NXDOMAIN은 HTTP나 TCP 이전의 이름 해석 실패입니다.
복구 — 실제 namespace를 넣은 FQDN 사용하기
섹션 제목: “복구 — 실제 namespace를 넣은 FQDN 사용하기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"FQDN="api-service.$NS.svc.cluster.local"RESPONSE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ wget -qO- "http://$FQDN/")"printf 'recovery=fqdn-correct http=200 %s\n' "$RESPONSE"예상 출력:
recovery=fqdn-correct http=200 backend=api-service-<ReplicaSet hash>-<Pod suffix>Lab 4 — selector가 틀리면 DNS와 HTTP 결과가 갈리는지 보기
섹션 제목: “Lab 4 — selector가 틀리면 DNS와 HTTP 결과가 갈리는지 보기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" patch service api-service \ --type=merge -p '{"spec":{"selector":{"app":"selector-does-not-exist"}}}' >/dev/nullENDPOINT_IPS="not-empty"for _ in $(seq 1 30); do ENDPOINT_IPS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}')" test -z "$ENDPOINT_IPS" && break sleep 1donetest -z "$ENDPOINT_IPS"SERVICE_IP="$(kubectl --context k3d-cs-study-workbook -n "$NS" get service api-service \ -o jsonpath='{.spec.clusterIP}')"DNS_IP="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ nslookup "api-service.$NS.svc.cluster.local" \ | awk '/^Address: / {print $2}' | tail -n 1)"test "$DNS_IP" = "$SERVICE_IP"set +ekubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ wget -T 3 -qO- http://api-service/ >/tmp/k8s-wb-m4/no-endpoint.log 2>&1HTTP_STATUS=$?set -etest "$HTTP_STATUS" = "1"printf 'dns=%s dns_ok=true ready_endpoints=0 http_exit=%s\n' "$DNS_IP" "$HTTP_STATUS"exit 1예상 출력과 exit code:
dns=<Service IPv4> dns_ok=true ready_endpoints=0 http_exit=1전체 예상 exit code는 1입니다. DNS 성공은 Service 이름이 존재한다는 증거이지 backend가
있다는 증거가 아닙니다.
복구 — selector를 Pod label과 다시 맞추기
섹션 제목: “복구 — selector를 Pod label과 다시 맞추기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" patch service api-service \ --type=merge -p '{"spec":{"selector":{"app":"api-service"}}}' >/dev/nullREADY_COUNT="0"for _ in $(seq 1 30); do READY_COUNT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[*]}{.conditions.ready}{"\n"}{end}' \ | awk '$1 == "true" {count++} END {print count+0}')" test "$READY_COUNT" = "2" && break sleep 1donetest "$READY_COUNT" = "2"RESPONSE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ wget -qO- http://api-service/)"printf 'selector=restored ready_endpoints=%s http=200 %s\n' "$READY_COUNT" "$RESPONSE"예상 출력:
selector=restored ready_endpoints=2 http=200 backend=api-service-<hash>-<suffix>Lab 5 — Ingress host rule을 Controller로 실행하기
섹션 제목: “Lab 5 — Ingress host rule을 Controller로 실행하기”workbook.invalid은 외부 DNS에 등록하지 않는 fixture host입니다. 같은 cluster의 client가
Traefik Service에 Host header를 보내 rule→Service→Pod 경로를 검증합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: sample-suitespec: ingressClassName: traefik rules: - host: workbook.invalid http: paths: - path: / pathType: Prefix backend: service: name: api-service port: number: 80YAMLRESPONSE=""STATUS="1"for _ in $(seq 1 30); do set +e RESPONSE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ wget -T 3 -qO- --header='Host: workbook.invalid' \ http://traefik.kube-system.svc.cluster.local/ 2>/dev/null)" STATUS=$? set -e test "$STATUS" = "0" && test -n "$RESPONSE" && break sleep 1donetest "$STATUS" = "0"test -n "$RESPONSE"CLASS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get ingress sample-suite \ -o jsonpath='{.spec.ingressClassName}')"BACKEND="$(kubectl --context k3d-cs-study-workbook -n "$NS" get ingress sample-suite \ -o jsonpath='{.spec.rules[0].http.paths[0].backend.service.name}:{.spec.rules[0].http.paths[0].backend.service.port.number}')"printf 'ingress_class=%s host=workbook.invalid backend=%s http=200 %s\n' \ "$CLASS" "$BACKEND" "$RESPONSE"예상 출력:
ingress.networking.k8s.io/sample-suite createdingress_class=traefik host=workbook.invalid backend=api-service:80 http=200 backend=api-service-<hash>-<suffix>이는 cluster 내부에서 Traefik이 host rule을 실행했다는 증거입니다. 공인 DNS, Internet 접근, TLS termination이나 NKS load balancer 생성은 검증하지 않습니다.
Lab 6 — Pod가 교체돼도 Service identity 유지하기
섹션 제목: “Lab 6 — Pod가 교체돼도 Service identity 유지하기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"SERVICE_IP_BEFORE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get service api-service \ -o jsonpath='{.spec.clusterIP}')"kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l app=api-service \ -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | sort \ > /tmp/k8s-wb-m4/pods-beforeOLD_POD="$(head -n 1 /tmp/k8s-wb-m4/pods-before)"kubectl --context k3d-cs-study-workbook -n "$NS" delete pod "$OLD_POD" --wait=false >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" \ wait --for=delete "pod/$OLD_POD" --timeout=90s >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" \ rollout status deployment/api-service --timeout=90s >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" get pod -l app=api-service \ -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | sort \ > /tmp/k8s-wb-m4/pods-afterNEW_POD="$(comm -13 /tmp/k8s-wb-m4/pods-before /tmp/k8s-wb-m4/pods-after)"test -n "$NEW_POD"test "$(wc -l < /tmp/k8s-wb-m4/pods-after | tr -d ' ')" = "2"SERVICE_IP_AFTER="$(kubectl --context k3d-cs-study-workbook -n "$NS" get service api-service \ -o jsonpath='{.spec.clusterIP}')"test "$SERVICE_IP_BEFORE" = "$SERVICE_IP_AFTER"RESPONSE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec network-client -- \ wget -qO- http://api-service/)"printf 'removed=%s replacement=%s service_cluster_ip=%s stable=true\n' \ "$OLD_POD" "$NEW_POD" "$SERVICE_IP_AFTER"printf 'dns=api-service http=200 %s\n' "$RESPONSE"예상 출력:
removed=api-service-<hash>-<옛 suffix> replacement=api-service-<hash>-<새 suffix> service_cluster_ip=<같은 Service IPv4> stable=truedns=api-service http=200 backend=api-service-<hash>-<현재 suffix>Cleanup
섹션 제목: “Cleanup”Namespace와 fixture 제거하기
섹션 제목: “Namespace와 fixture 제거하기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m4/namespace)"kubectl --context k3d-cs-study-workbook delete namespace "$NS" --wait=false >/dev/nullkubectl --context k3d-cs-study-workbook wait --for=delete "namespace/$NS" --timeout=120s >/dev/nullif kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then echo "cleanup failed: namespace remains" >&2 exit 1firm -rf /tmp/k8s-wb-m4test ! -e /tmp/k8s-wb-m4READY_NODES="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers \ | awk '$2 == "Ready" {count++} END {print count+0}')"test "$READY_NODES" = "2"printf 'cleanup namespace=0 fixture=0 nodes_ready=%s cluster_retained=true\n' "$READY_NODES"예상 출력:
cleanup namespace=0 fixture=0 nodes_ready=2 cluster_retained=true5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”| 실행 뒤 | 실제 변화 | 운영 판단 |
|---|---|---|
| Service 생성 | ClusterIP 1개, ready EndpointSlice 주소 2개 | 안정 접점과 현재 backend 목록은 다른 객체입니다. |
| 짧은 이름/FQDN 조회 | 둘 다 같은 Service ClusterIP | 짧은 이름은 같은 namespace search suffix를 씁니다. |
| 잘못된 namespace FQDN | NXDOMAIN, exit 1 | TCP·HTTP보다 먼저 이름과 namespace를 고칩니다. |
| selector 불일치 | DNS 성공, endpoint 0, HTTP 실패 | DNS 성공만으로 application 경로를 승인하지 않습니다. |
| Ingress 요청 | host rule→api-service:80→HTTP 200 | object와 Controller가 함께 있어야 실행됩니다. |
| backend Pod 삭제 | Pod 이름·IP 변경, Service IP·이름 유지 | client가 Pod identity에 의존하면 안 됩니다. |
| cleanup | M4 object와 fixture 0, 공유 cluster 유지 | 다음 모듈은 새 namespace에서 시작합니다. |
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”Q1. web-app에서 api-service 이름은 풀리지만 요청이 실패합니다. 어느 순서로 확인해야 할까요?
섹션 제목: “Q1. web-app에서 api-service 이름은 풀리지만 요청이 실패합니다. 어느 순서로 확인해야 할까요?”DNS 주소가 Service ClusterIP인지 확인한 뒤 selector와 Pod label, EndpointSlice ready 주소 수,
port → targetPort, backend process listen port 순서로 좁힙니다. DNS 성공만 반복 확인하거나
CoreDNS부터 재시작하면 selector 문제를 놓칩니다.
Q2. AI가 NKS용 Ingress YAML을 만들었지만 Ingress Controller가 확인되지 않습니다. merge해도 될까요?
섹션 제목: “Q2. AI가 NKS용 Ingress YAML을 만들었지만 Ingress Controller가 확인되지 않습니다. merge해도 될까요?”안 됩니다. Ingress object 자체는 요청을 proxy하지 않습니다. 사용할 Controller와 IngressClass, provider별 annotation, Service backend, 외부 DNS·TLS owner를 먼저 결정하고 non-production에서 검증해야 합니다. 이 랩의 Traefik 성공을 NKS 증거로 대체할 수 없습니다.
Q3. ai-service를 다른 namespace로 옮긴 뒤 http://ai-service만 실패했습니다. image를 롤백해야 할까요?
섹션 제목: “Q3. ai-service를 다른 namespace로 옮긴 뒤 http://ai-service만 실패했습니다. image를 롤백해야 할까요?”먼저 롤백하지 않습니다. 짧은 이름은 호출 Pod의 namespace에서 동명 Service를 찾으므로 새 namespace를 포함한 이름이나 FQDN으로 조회하고 Service 존재 여부를 확인합니다. 그 뒤에도 실패할 때 EndpointSlice와 HTTP 경로를 봅니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 1. containerPort, targetPort, port를 같은 값으로 가정하기
섹션 제목: “함정 1. containerPort, targetPort, port를 같은 값으로 가정하기”containerPort는 Pod spec의 의도·메타데이터이고 process를 대신 listen시키지 않습니다.
client는 Service port로 연결하고 Service는 targetPort로 backend를 고릅니다. named port가
실제 process listen port와 맞는지 확인합니다.
함정 2. Service DNS 성공을 backend 정상으로 승인하기
섹션 제목: “함정 2. Service DNS 성공을 backend 정상으로 승인하기”Service가 존재하면 selector가 틀려 EndpointSlice가 비어도 DNS는 ClusterIP를 돌려줄 수
있습니다. nslookup, 실제 HTTP 요청, EndpointSlice ready 주소를 서로 다른 증거로 봅니다.
함정 3. Ingress와 Ingress Controller를 한 객체로 생각하기
섹션 제목: “함정 3. Ingress와 Ingress Controller를 한 객체로 생각하기”Ingress는 host/path→Service 규칙이고 Controller는 이를 실제 proxy나 provider resource에 반영합니다. k3d Traefik class와 NKS의 Controller·annotation 계약이 같다고 가정하지 않습니다.
관리형 NKS의 control plane·CNI 내부 구현, Terraform, production load balancer·DNS·TLS 변경은 제외합니다. NodePort·LoadBalancer·ExternalName·headless Service와 Gateway API는 심화 범위입니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”M5에서는 이 안정된 Service 호출 경로를 유지한 채 ConfigMap·Secret 값을 image 밖에서 container env로 주입하고 공개 설정과 민감값의 운영 경계를 나눕니다.