콘텐츠로 이동

M4. Service·Ingress·클러스터 내부 DNS

NKS에서 web-appapi-service의 Pod IP를 직접 호출하면 Pod 교체 때 연결 대상이 사라집니다. Service의 안정된 이름, 실제 backend 목록인 EndpointSlice, 외부 HTTP 규칙인 Ingress를 분리해서 볼 수 있어야 DNS·대상 Pod·외부 진입 장애를 같은 문제로 뭉뚱그리지 않고 리뷰할 수 있습니다.

개념 정의의 정본은 Kubernetes BasicsDNS basics입니다. 원본 경로는 각각 content/topics/L5/kubernetes-basics.mdx, content/topics/L2/dns-basics.mdx이며, 이 모듈은 개념을 재정의하지 않고 M3의 일회용 Pod 집합에 안정된 호출 경로를 붙여 관찰합니다.

EndpointSlice control state와 실제 request path
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 / targetPortclient 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에는 도달하지 않습니다.

Ingress 선언과 실제 HTTP 경로
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의 Servicenamespace 경계를 드러냄
api-service.<namespace>.svc.cluster.local기본 cluster domain까지 적은 FQDNsearch suffix 오해를 줄임
api-service.<missing namespace>.svc.cluster.local존재하지 않는 namespace의 Serviceguard 후 NXDOMAIN

시점 의존 설명은 2026-07-15에 다음 공식 1차 자료로 확인했습니다.

Ingress API는 stable이지만 기능 확장은 frozen 상태이고 Kubernetes project는 새 설계에 Gateway API를 권장합니다. 이 워크북은 3서비스 이전에서 리뷰할 Ingress object와 NKS Controller 경계를 우선 다루며 Gateway API 구현은 심화 범위로 남깁니다.

개념비유비유의 경계
Service담당자가 바뀌어도 유지되는 팀 대표번호실제 application 건강을 보증하지는 않습니다.
EndpointSlice대표번호가 지금 연결할 수 있는 담당자 명단보통 사용자가 직접 유지하는 주소록이 아닙니다.
cluster DNS팀 이름을 대표번호로 찾는 사내 전화번호부HTTP 연결과 응답 성공까지 확인하지 않습니다.
Ingress / Controller안내 규칙표와 그 규칙을 실제 집행하는 안내 데스크TCP 전체나 provider load balancer 구현까지 같지는 않습니다.
항목검증값
기준일2026-07-15
hostmacOS arm64
Docker client / engine29.6.1 / 29.6.1
k3d5.9.0
K3s image / API serverrancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1
kubectl1.36.1
Helm4.2.3 — 직접 사용하지 않고 K3s packaged Traefik만 관찰
container imagebusybox: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-workbook cluster가 실행 중이어야 합니다.
  • 모든 workload API 명령은 --context k3d-cs-study-workbook과 고유 namespace를 명시합니다.
  • K3s packaged CoreDNS와 traefik IngressClass를 Setup에서 확인합니다.
  • BusyBox HTTP fixture는 Service 경로만 관찰하며 실제 production API health·TLS·business response를 나타내지 않습니다.
  • 예상 소요 시간은 105분입니다.

preflight와 context guard 뒤 replica 2개의 api-service, ClusterIP Service와 client Pod를 같은 namespace에 만듭니다. container는 8080, client는 Service의 80을 사용합니다.

Terminal window
set -euo pipefail
rm -rf /tmp/k8s-wb-m4
mkdir -p /tmp/k8s-wb-m4
node automation/k8s-workbook/preflight.mjs --require-cluster
test "$(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/namespace
if kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then
echo "namespace collision: $NS" >&2
exit 1
fi
kubectl --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/null
test "$(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/v1
kind: Deployment
metadata:
name: api-service
spec:
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: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api-service
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: v1
kind: Pod
metadata:
name: network-client
spec:
terminationGracePeriodSeconds: 3
containers:
- name: client
image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "sleep 3600"]
YAML
kubectl --context k3d-cs-study-workbook -n "$NS" \
rollout status deployment/api-service --timeout=90s >/dev/null
kubectl --context k3d-cs-study-workbook -n "$NS" \
wait --for=condition=Ready pod/network-client --timeout=90s >/dev/null
echo 'dependencies=coredns,traefik ready=true'
echo 'fixture=deployment/service/client ready=true'

예상 출력:

PASS platform: darwin/arm64
PASS architecture: arm64
PASS docker-cli: v29.6.1
PASS docker-engine: v29.6.1
PASS k3d: v5.9.0
PASS kubectl-skew: v1.36.1
PASS helm: v4.2.3
PASS context: k3d-cs-study-workbook
PASS kubernetes-server: v1.36.2+k3s1
namespace/k8s-wb-m4-<UTC 14자리 run ID> created
deployment.apps/api-service created
service/api-service created
pod/network-client created
dependencies=coredns,traefik ready=true
fixture=deployment/service/client ready=true

host architecture, Docker patch version과 namespace run ID는 변동 필드입니다.

Lab 1 — Service, Pod, EndpointSlice 집합 맞추기

섹션 제목: “Lab 1 — Service, Pod, EndpointSlice 집합 맞추기”
Terminal window
set -euo pipefail
test "$(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 1
done
test "$(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=http
pod_ips=<Pod IPv4 A>,<Pod IPv4 B>
ready_endpoint_ips=<Pod IPv4 A>,<Pod IPv4 B> match=true

IP와 순서는 변동값입니다. clusterIP는 Pod IP가 아니며 두 집합의 match=true가 selector→EndpointSlice 연결의 증거입니다.

Lab 2 — 짧은 이름과 FQDN으로 실제 HTTP 요청하기

섹션 제목: “Lab 2 — 짧은 이름과 FQDN으로 실제 HTTP 요청하기”
Terminal window
set -euo pipefail
test "$(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=true
backend=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으로 분리하기”
Terminal window
set -euo pipefail
test "$(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 1
fi
set +e
kubectl --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>&1
STATUS=$?
set -e
test "$STATUS" = "1"
grep -q 'NXDOMAIN' /tmp/k8s-wb-m4/wrong-namespace.log
printf '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 사용하기”
Terminal window
set -euo pipefail
test "$(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 결과가 갈리는지 보기”
Terminal window
set -euo pipefail
test "$(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/null
ENDPOINT_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 1
done
test -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 +e
kubectl --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>&1
HTTP_STATUS=$?
set -e
test "$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과 다시 맞추기”
Terminal window
set -euo pipefail
test "$(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/null
READY_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 1
done
test "$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 경로를 검증합니다.

Terminal window
set -euo pipefail
test "$(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/v1
kind: Ingress
metadata:
name: sample-suite
spec:
ingressClassName: traefik
rules:
- host: workbook.invalid
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
YAML
RESPONSE=""
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 1
done
test "$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 created
ingress_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 유지하기”
Terminal window
set -euo pipefail
test "$(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-before
OLD_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/null
kubectl --context k3d-cs-study-workbook -n "$NS" \
wait --for=delete "pod/$OLD_POD" --timeout=90s >/dev/null
kubectl --context k3d-cs-study-workbook -n "$NS" \
rollout status deployment/api-service --timeout=90s >/dev/null
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-after
NEW_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=true
dns=api-service http=200 backend=api-service-<hash>-<현재 suffix>
Terminal window
set -euo pipefail
test "$(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/null
kubectl --context k3d-cs-study-workbook wait --for=delete "namespace/$NS" --timeout=120s >/dev/null
if kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then
echo "cleanup failed: namespace remains" >&2
exit 1
fi
rm -rf /tmp/k8s-wb-m4
test ! -e /tmp/k8s-wb-m4
READY_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=true
실행 뒤실제 변화운영 판단
Service 생성ClusterIP 1개, ready EndpointSlice 주소 2개안정 접점과 현재 backend 목록은 다른 객체입니다.
짧은 이름/FQDN 조회둘 다 같은 Service ClusterIP짧은 이름은 같은 namespace search suffix를 씁니다.
잘못된 namespace FQDNNXDOMAIN, exit 1TCP·HTTP보다 먼저 이름과 namespace를 고칩니다.
selector 불일치DNS 성공, endpoint 0, HTTP 실패DNS 성공만으로 application 경로를 승인하지 않습니다.
Ingress 요청host rule→api-service:80→HTTP 200object와 Controller가 함께 있어야 실행됩니다.
backend Pod 삭제Pod 이름·IP 변경, Service IP·이름 유지client가 Pod identity에 의존하면 안 됩니다.
cleanupM4 object와 fixture 0, 공유 cluster 유지다음 모듈은 새 namespace에서 시작합니다.

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 경로를 봅니다.

함정 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는 심화 범위입니다.

M5에서는 이 안정된 Service 호출 경로를 유지한 채 ConfigMap·Secret 값을 image 밖에서 container env로 주입하고 공개 설정과 민감값의 운영 경계를 나눕니다.