콘텐츠로 이동

M12. 관리형 NKS 경계와 예시 3서비스 시스템 캡스톤

web-app, api-service, ai-service를 NKS로 옮기는 마지막 단계에서는 “Pod가 뜬다”보다 세 서비스의 호출 경로가 끝까지 이어지고, 무엇을 NKS가 맡으며 무엇을 팀이 승인해야 하는지를 증거로 구분해야 합니다. 이 캡스톤에서는 앞 모듈의 개별 기술을 하나의 release candidate로 조립하고, AI가 만든 변경을 운영자가 어떤 순서로 검토할지 연습합니다.

개념 정의의 정본은 Kubernetes Basics, Docker Basics, Secrets Management입니다. 원본은 각각 content/topics/L5/kubernetes-basics.mdx, content/topics/L5/docker-basics.mdx, content/topics/L3/secrets-management.mdx입니다. M12는 Kubernetes나 관리형 서비스를 다시 정의하지 않고 M3의 owner, M4의 Service·Ingress, M5의 설정 주입, M6의 배치·resources, M9의 probe·rollout, M10의 stateless 판단, M11의 증거 기반 디버깅을 배포 승인 단위로 통합합니다.

예시 3서비스 시스템 로컬 release candidate의 호출 경로
flowchart LR
Client["외부 HTTP 요청"] --> Ingress["Ingress Controller
host rule"]
Ingress --> FrontSvc["Service
web-app"]
FrontSvc --> FrontPod["Deployment
web-app ×2"]
FrontPod --> BackSvc["Service DNS
api-service"]
BackSvc --> BackPod["Deployment
api-service ×2"]
BackPod --> AiSvc["Service DNS
ai-service"]
AiSvc --> AiPod["Deployment
ai-service ×1
AI pool label"]

이 그림은 애플리케이션의 논리 호출 경로입니다. k3d의 Traefik과 Node label은 NKS ALB와 실제 node pool의 실측 대체물이 아닙니다.

NKS가 제공·관리하는 면애플리케이션 팀이 결정·검증하는 면이 모듈의 증거
Kubernetes APIsystem-managed control area, upgrade·patch 기능manifest, rollout 승인, namespace 범위, 종료 조건official-doc-only
Worker·node poolNode provisioning과 node pool 기능pool 역할, 사양·수량·autoscaling 범위, Pod requests와 배치 제약label 배치만 로컬 실측
ApplicationKubernetes controller의 desired-state 수렴image, command, probes, resources, replicas, dependency 계약executed-local
내부 통신cluster network와 Service 기능이 동작할 기반Service selector·port, DNS 이름, timeout, dependency 실패 처리executed-local
외부 진입NKS와 연동되는 Load Balancer·Controller 기능Ingress rule, Controller class·annotation, DNS·TLS·rollback ownerTraefik만 로컬 실측
접근·감사NKS IAM access entry 정책과 audit log 연동 기능최소 권한 scope, 승인 주체, credential 수명, 로그 보존·알림official-doc-only

“관리형”은 workload 운영까지 위임했다는 뜻이 아닙니다. provider가 control plane과 cloud integration을 제공해도 잘못된 selector, 부족한 request, 실패하는 probe, 과도한 권한과 잘못된 rollout은 사용자 manifest에서 생깁니다.

AI 제안에서 운영 승인까지
flowchart LR
Proposal["AI 제안
manifest diff"] --> Local["로컬 gate
schema·호출 체인·failure recovery"]
Local --> Review["사람의 판단
범위·최근 변경·rollback·비용"]
Review --> NonProd["NKS non-production gate
IAM·node pool·ALB·observability"]
NonProd --> Change["승인된 변경 창
apply·관찰·중단 조건"]
Change --> Handoff["인수인계
owner·dashboard·runbook·rollback"]
승인 질문허용할 증거부족한 표현
세 서비스가 연결됐는가?내부 DNS HTTP 2단계와 Ingress HTTP 응답“Pod가 전부 Running”
ai-service 배치가 의도와 맞는가?Pod의 실제 Node와 검증된 pool label“nodeSelector가 YAML에 있음”
장애 복구가 desired state로 돌아왔는가?EndpointSlice 0 재현 후 원본 manifest apply와 endpoint·HTTP 회복“Service를 다시 만들었음”
NKS 공개 경로를 승인할 수 있는가?Controller·IngressClass·annotation·subnet·DNS·TLS owner 검증“k3d Ingress가 성공했음”
운영 변경을 넘길 수 있는가?owner, 관찰 지표, rollback 기준, 미검증 provider gate 명시“배포 완료”

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

  • NKS 개요: system-managed control area, worker Node provisioning, on-demand upgrade·patch와 node pool 단위 Cluster Autoscaler
  • NKS 클러스터 접근·관리: control plane upgrade 중 API 조회·수정·삭제가 일시적으로 제한될 수 있는 운영 경계
  • NKS Cluster Autoscaler: node pool 단위 활성화, min·max와 autoscaler 상태 확인, 수동 node 수 변경의 경계
  • NKS IAM 액세스 항목: cluster·namespace scope와 NKSClusterAdminPolicy·NKSAdminPolicy·NKSEditPolicy·NKSViewPolicy
  • NKS Load Balancer 연동: Kubernetes로 생성한 Load Balancer는 Kubernetes resource로 관리하며 console·API 직접 변경을 피해야 하는 경계
  • NKS ALB Ingress Controller: Ingress를 ALB와 target group으로 수렴시키는 Controller와 Kubernetes resource 기반 변경 원칙
  • Kubernetes Service: 바뀌는 Pod 집합에서 client를 분리하는 Service와 selector·EndpointSlice
  • Kubernetes Ingress: HTTP rule과 이를 실제로 실행하는 Ingress Controller의 분리

NKS IAM 생성·변경, node pool·Cluster Autoscaler 조작, ALB·public IP·subnet·DNS·TLS 변경, audit log와 Cloud Log Analytics 설정은 승인된 non-production 환경이 없어 official-doc-only입니다. production NKS 배포와 실제 production credential·endpoint 사용은 excluded입니다. control plane·etcd 자가 운영, CNI 내부 구현, Terraform 작성도 범위 밖입니다.

개념비유비유의 경계
관리형 책임 경계건물 관리사가 공용 설비를 관리하고 입주사가 매장을 운영provider가 application의 정상 동작까지 보증하지 않습니다.
3서비스 호출 체인접수 창구→업무 담당→전문 분석실의 전달 경로실제 요청은 timeout·retry·인증 같은 추가 계약이 있습니다.
release gate이륙 전 계기·항로·회항 조건을 함께 확인하는 점검체크리스트만으로 production 결과가 자동 보장되진 않습니다.
desired manifest로 복구승인된 도면과 현장을 비교해 어긋난 배관만 되돌리는 작업GitOps나 admission policy 자체를 구현하는 비유는 아닙니다.
항목검증값
기준일2026-07-16
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 — 이 랩에서는 사용하지 않음
workload imagebusybox:1.37.0 + 고정 multi-platform digest
evidence 환경local-cluster; k8s-wb-m12-<UTC run ID>

모든 shell lab은 executed-local입니다. NKS provider 항목은 official-doc-only, production 변경은 excluded이며 예상 출력을 쓰지 않습니다.

  • 저장소 root에서 실행하며 M0~M11을 완료한 상태여야 합니다.
  • Docker Desktop과 M2의 cs-study-workbook cluster가 실행 중이어야 합니다.
  • 모든 API 명령은 k3d-cs-study-workbook context와 고유 namespace를 명시합니다.
  • 각 fence는 별도 Bash process에서 블록 단위로 실행합니다. Lab 4의 exit 1은 selector drift를 관찰하고 원본 manifest로 복구까지 검증했다는 의도된 결과입니다.
  • 예상 소요 시간은 120분입니다.
Gate먼저 답할 질문통과 증거
1manifest가 API와 owner 경계를 만족하는가?server dry-run object 9개
2replicas·probe·배치·endpoint가 수렴했는가?Deployment 3개, Ready Pod 5개, endpoint 2/2/1
3실제 호출이 세 서비스를 관통하는가?Service DNS 2단계와 Traefik Ingress 응답
4drift를 증거로 좁히고 desired state로 고쳤는가?DNS 성공·endpoint 0·HTTP 실패→manifest apply→HTTP 복구
5NKS에서 아직 검증하지 않은 것은 무엇인가?provider gate를 official-doc-only로 남긴 인수인계 기록

고유 namespace와 로컬 AI pool label 만들기

섹션 제목: “고유 namespace와 로컬 AI pool label 만들기”

실패하면 이 실행이 만든 namespace와 Node label만 rollback합니다. rollback도 실패하면 /tmp 소유권 표식을 보존하고 exit 70으로 중단합니다.

Terminal window
set -euo pipefail
rm -rf /tmp/k8s-wb-m12
mkdir -p /tmp/k8s-wb-m12
chmod 700 /tmp/k8s-wb-m12
node automation/k8s-workbook/preflight.mjs --require-cluster
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NODE_COUNT="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers | wc -l | tr -d ' ')"
test "$NODE_COUNT" = "2"
AI_NODE="$(kubectl --context k3d-cs-study-workbook get nodes -o name \
| sed 's#node/##' | awk '!/server/ {print}')"
test -n "$AI_NODE"
test "$(printf '%s\n' "$AI_NODE" | wc -l | tr -d ' ')" = "1"
EXISTING_LABEL="$(kubectl --context k3d-cs-study-workbook get node "$AI_NODE" \
-o jsonpath='{.metadata.labels.workbook\.cs-study/capstone-pool}')"
test -z "$EXISTING_LABEL"
NS="k8s-wb-m12-$(date -u +%Y%m%d%H%M%S)"
printf '%s' "$NS" >/tmp/k8s-wb-m12/namespace
printf '%s' "$AI_NODE" >/tmp/k8s-wb-m12/ai-node
if kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then
echo "namespace collision: $NS" >&2
rm -rf /tmp/k8s-wb-m12
exit 1
fi
rollback() {
STATUS=$?
trap - EXIT
set +e
FAILED=0
kubectl --context k3d-cs-study-workbook delete namespace "$NS" \
--ignore-not-found --wait=true >/dev/null 2>&1 || FAILED=1
CURRENT_LABEL="$(kubectl --context k3d-cs-study-workbook get node "$AI_NODE" \
-o jsonpath='{.metadata.labels.workbook\.cs-study/capstone-pool}' 2>/dev/null)"
test $? = 0 || FAILED=1
if test -n "$CURRENT_LABEL"; then
kubectl --context k3d-cs-study-workbook label node "$AI_NODE" \
workbook.cs-study/capstone-pool- >/dev/null 2>&1 || FAILED=1
fi
REMAINING_NS="$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \
--ignore-not-found -o name 2>/dev/null)"
test $? = 0 || FAILED=1
test -z "$REMAINING_NS" || FAILED=1
REMAINING_LABELS="$(kubectl --context k3d-cs-study-workbook get nodes \
-l workbook.cs-study/capstone-pool -o name 2>/dev/null | awk 'END {print NR+0}')"
test $? = 0 || FAILED=1
test "$REMAINING_LABELS" = 0 || FAILED=1
if test "$FAILED" != 0; then
echo 'rollback incomplete: preserve /tmp/k8s-wb-m12 and inspect owned state' >&2
exit 70
fi
rm -rf /tmp/k8s-wb-m12
exit "$STATUS"
}
trap rollback EXIT
kubectl --context k3d-cs-study-workbook create namespace "$NS" >/dev/null
kubectl --context k3d-cs-study-workbook label node "$AI_NODE" \
workbook.cs-study/capstone-pool=ai >/dev/null
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"
trap - EXIT
printf 'setup namespace=%s nodes=2 ai_pool_labels=1 dependencies=ready\n' "$NS"

예상 출력(exit 0):

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
setup namespace=k8s-wb-m12-<UTC 14자리 run ID> nodes=2 ai_pool_labels=1 dependencies=ready

host architecture, patch version, namespace와 Node 이름은 변동 필드입니다. 이 label은 NKS node pool label의 로컬 fixture이지 provider가 만든 label이 아닙니다.

Lab 1 — release manifest를 server dry-run으로 검토하기

섹션 제목: “Lab 1 — release manifest를 server dry-run으로 검토하기”

실제 값이 아닌 실습용 fixture ConfigMap·Secret과 3개 Deployment·Service·Ingress를 하나의 release candidate로 만듭니다. --dry-run=server는 저장하지 않고 현재 API server의 schema와 admission 경로를 통과시키지만, application 호출 성공까지 보장하지는 않습니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m12/namespace)"
cat > /tmp/k8s-wb-m12/suite.yaml <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata:
name: suite-config
data:
suite-env: capstone-local
suite-revision: v1
---
apiVersion: v1
kind: Secret
metadata:
name: suite-fixture-secret
type: Opaque
stringData:
api-mode: fixture-only
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-service
labels: {suite: sample}
spec:
replicas: 1
strategy:
rollingUpdate: {maxUnavailable: 0, maxSurge: 1}
selector:
matchLabels: {app: ai-service}
template:
metadata:
labels: {app: ai-service, suite: sample}
spec:
nodeSelector:
workbook.cs-study/capstone-pool: ai
terminationGracePeriodSeconds: 3
containers:
- name: app
image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
mkdir -p /www
printf 'ai-ready env=%s mode=%s revision=%s\n' \
"$SUITE_ENV" "$API_MODE" "$SUITE_REVISION" >/www/index.html
exec httpd -f -p 8080 -h /www
env:
- name: SUITE_ENV
valueFrom: {configMapKeyRef: {name: suite-config, key: suite-env}}
- name: SUITE_REVISION
valueFrom: {configMapKeyRef: {name: suite-config, key: suite-revision}}
- name: API_MODE
valueFrom: {secretKeyRef: {name: suite-fixture-secret, key: api-mode}}
ports: [{name: http, containerPort: 8080}]
startupProbe: {httpGet: {path: /, port: http}, periodSeconds: 1, failureThreshold: 30}
readinessProbe: {httpGet: {path: /, port: http}, periodSeconds: 1, failureThreshold: 3}
livenessProbe: {httpGet: {path: /, port: http}, periodSeconds: 5, failureThreshold: 3}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 50m, memory: 64Mi}
---
apiVersion: v1
kind: Service
metadata:
name: ai-service
spec:
selector: {app: ai-service}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
labels: {suite: sample}
spec:
replicas: 2
strategy:
rollingUpdate: {maxUnavailable: 0, maxSurge: 1}
selector:
matchLabels: {app: api-service}
template:
metadata:
labels: {app: api-service, suite: sample}
spec:
terminationGracePeriodSeconds: 3
containers:
- name: app
image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
cat >/tmp/backend-server.sh <<'SH'
#!/bin/sh
while IFS= read -r LINE; do
test "$LINE" = "$(printf '\r')" && break
done
if DEPENDENCY="$(wget -T 2 -qO- http://ai-service 2>/dev/null)"; then
STATUS='200 OK'
BODY="backend-ready dependency=$DEPENDENCY revision=$SUITE_REVISION"
else
STATUS='503 Service Unavailable'
BODY='backend-unavailable dependency=ai-service'
fi
LENGTH="$(printf '%s' "$BODY" | wc -c | tr -d ' ')"
printf 'HTTP/1.1 %s\r\nContent-Length: %s\r\nConnection: close\r\n\r\n%s\n' \
"$STATUS" "$LENGTH" "$BODY"
SH
chmod 700 /tmp/backend-server.sh
exec nc -lk -p 8080 -e /tmp/backend-server.sh
env:
- name: SUITE_REVISION
valueFrom: {configMapKeyRef: {name: suite-config, key: suite-revision}}
ports: [{name: http, containerPort: 8080}]
startupProbe: {httpGet: {path: /, port: http}, periodSeconds: 1, failureThreshold: 30}
readinessProbe: {httpGet: {path: /, port: http}, periodSeconds: 1, failureThreshold: 3}
livenessProbe: {httpGet: {path: /, port: http}, periodSeconds: 5, failureThreshold: 3}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 50m, memory: 64Mi}
---
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector: {app: api-service}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
labels: {suite: sample}
spec:
replicas: 2
strategy:
rollingUpdate: {maxUnavailable: 0, maxSurge: 1}
selector:
matchLabels: {app: web-app}
template:
metadata:
labels: {app: web-app, suite: sample}
spec:
terminationGracePeriodSeconds: 3
containers:
- name: app
image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
cat >/tmp/frontend-server.sh <<'SH'
#!/bin/sh
while IFS= read -r LINE; do
test "$LINE" = "$(printf '\r')" && break
done
if DEPENDENCY="$(wget -T 2 -qO- http://api-service 2>/dev/null)"; then
STATUS='200 OK'
BODY="frontend-ready dependency=$DEPENDENCY"
else
STATUS='503 Service Unavailable'
BODY='frontend-unavailable dependency=api-service'
fi
LENGTH="$(printf '%s' "$BODY" | wc -c | tr -d ' ')"
printf 'HTTP/1.1 %s\r\nContent-Length: %s\r\nConnection: close\r\n\r\n%s\n' \
"$STATUS" "$LENGTH" "$BODY"
SH
chmod 700 /tmp/frontend-server.sh
exec nc -lk -p 8080 -e /tmp/frontend-server.sh
ports: [{name: http, containerPort: 8080}]
startupProbe: {httpGet: {path: /, port: http}, periodSeconds: 1, failureThreshold: 30}
readinessProbe: {httpGet: {path: /, port: http}, periodSeconds: 1, failureThreshold: 3}
livenessProbe: {httpGet: {path: /, port: http}, periodSeconds: 5, failureThreshold: 3}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 50m, memory: 64Mi}
---
apiVersion: v1
kind: Service
metadata:
name: web-app
spec:
selector: {app: web-app}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sample-suite
spec:
ingressClassName: traefik
rules:
- host: capstone.invalid
http:
paths:
- path: /
pathType: Prefix
backend:
service: {name: web-app, port: {number: 80}}
YAML
OBJECTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" apply \
--dry-run=server -f /tmp/k8s-wb-m12/suite.yaml -o name | sort)"
test "$(printf '%s\n' "$OBJECTS" | wc -l | tr -d ' ')" = "9"
PERSISTED="0"
for RESOURCE in \
configmap/suite-config secret/suite-fixture-secret \
deployment/ai-service deployment/api-service deployment/web-app \
service/ai-service service/api-service service/web-app ingress/sample-suite; do
FOUND="$(kubectl --context k3d-cs-study-workbook -n "$NS" get "$RESOURCE" \
--ignore-not-found -o name)"
test -z "$FOUND" || PERSISTED=$((PERSISTED + 1))
done
test "$PERSISTED" = "0"
printf '%s\n' "$OBJECTS"
printf 'review objects=9 persisted=%s fixture_secret=true\n' "$PERSISTED"

예상 출력(exit 0):

configmap/suite-config
deployment.apps/web-app
deployment.apps/ai-service
deployment.apps/api-service
ingress.networking.k8s.io/sample-suite
secret/suite-fixture-secret
service/web-app
service/ai-service
service/api-service
review objects=9 persisted=0 fixture_secret=true

fixture-only는 공개 재현값이며 실제 Secret이 아닙니다. 실제 credential을 manifest, shell history, AI 대화나 evidence에 넣는 방법은 이 워크북에서 다루지 않습니다.

Lab 2 — 3서비스 topology를 배포하고 수렴 확인하기

섹션 제목: “Lab 2 — 3서비스 topology를 배포하고 수렴 확인하기”
Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m12/namespace)"
AI_NODE="$(cat /tmp/k8s-wb-m12/ai-node)"
kubectl --context k3d-cs-study-workbook -n "$NS" apply \
-f /tmp/k8s-wb-m12/suite.yaml >/dev/null
for DEPLOYMENT in ai-service api-service web-app; do
kubectl --context k3d-cs-study-workbook -n "$NS" rollout status \
"deployment/$DEPLOYMENT" --timeout=120s >/dev/null
done
PODS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l suite=sample \
-o jsonpath='{.items[*].metadata.name}' | wc -w | tr -d ' ')"
READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-o jsonpath='{range .items[*]}{.status.readyReplicas}{"\n"}{end}' \
| awk '{sum += $1} END {print sum+0}')"
RESTARTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l suite=sample \
-o jsonpath='{range .items[*].status.containerStatuses[*]}{.restartCount}{"\n"}{end}' \
| awk '{sum += $1} END {print sum+0}')"
AI_SERVICE_NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l app=ai-service \
-o jsonpath='{.items[0].spec.nodeName}')"
ready_endpoints() {
kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \
-l "kubernetes.io/service-name=$1" \
-o jsonpath='{range .items[*]}{range .endpoints[*]}{.conditions.ready}{"\n"}{end}{end}' \
| awk '$1 == "true" {count++} END {print count+0}'
}
FRONT_ENDPOINTS="$(ready_endpoints web-app)"
BACK_ENDPOINTS="$(ready_endpoints api-service)"
AI_ENDPOINTS="$(ready_endpoints ai-service)"
test "$PODS" = "5"
test "$READY" = "5"
test "$RESTARTS" = "0"
test "$AI_SERVICE_NODE" = "$AI_NODE"
test "$FRONT_ENDPOINTS/$BACK_ENDPOINTS/$AI_ENDPOINTS" = "2/2/1"
printf 'topology deployments=3 pods=%s ready=%s restarts=%s endpoints=%s/%s/%s ai_pool=ai\n' \
"$PODS" "$READY" "$RESTARTS" "$FRONT_ENDPOINTS" "$BACK_ENDPOINTS" "$AI_ENDPOINTS"

예상 출력(exit 0):

topology deployments=3 pods=5 ready=5 restarts=0 endpoints=2/2/1 ai_pool=ai

Pod 이름·IP와 선택된 Node 이름은 변동 필드입니다. ai_pool=ai는 실제 Pod의 .spec.nodeName과 Node label을 비교한 결과입니다.

Lab 3 — 내부 DNS 2단계와 Ingress HTTP 경로 검증하기

섹션 제목: “Lab 3 — 내부 DNS 2단계와 Ingress HTTP 경로 검증하기”

진단 client는 같은 namespace에서 Service 이름으로 호출합니다. web-appapi-service의 실습용 HTTP handler는 요청을 받을 때마다 다음 Service를 다시 호출하므로, 응답 본문 하나로 두 Service hop을 검증할 수 있습니다. Traefik 요청은 local Controller의 host rule 실행만 검증하며 public DNS·TLS·NKS ALB를 검증하지 않습니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m12/namespace)"
kubectl --context k3d-cs-study-workbook -n "$NS" run review-client \
--image=busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 \
--restart=Never --command -- sh -c 'sleep 3600' >/dev/null
kubectl --context k3d-cs-study-workbook -n "$NS" wait \
--for=condition=Ready pod/review-client --timeout=60s >/dev/null
BACK_STATUS="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -S -O /dev/null http://api-service 2>&1 \
| awk '/^[[:space:]]*HTTP\/1.1/ {code=$2} END {print code}')"
FRONT_STATUS="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -S -O /dev/null http://web-app 2>&1 \
| awk '/^[[:space:]]*HTTP\/1.1/ {code=$2} END {print code}')"
INGRESS_STATUS="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -S -O /dev/null --header='Host: capstone.invalid' \
http://traefik.kube-system.svc.cluster.local/ 2>&1 \
| awk '/^[[:space:]]*HTTP\/1.1/ {code=$2} END {print code}')"
test "$BACK_STATUS/$FRONT_STATUS/$INGRESS_STATUS" = "200/200/200"
BACKEND="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -qO- http://api-service)"
FRONTEND="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -qO- http://web-app)"
INGRESS="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -qO- --header='Host: capstone.invalid' \
http://traefik.kube-system.svc.cluster.local/)"
case "$BACKEND" in
'backend-ready dependency=ai-ready env=capstone-local mode=fixture-only revision=v1 revision=v1') ;;
*) printf 'unexpected backend response: %s\n' "$BACKEND" >&2; exit 2 ;;
esac
test "$FRONTEND" = "frontend-ready dependency=$BACKEND"
test "$INGRESS" = "$FRONTEND"
printf 'call_chain=web-app>api-service>ai-service dns_hops=2 ingress_http=200 revision=v1\n'

예상 출력(exit 0):

call_chain=web-app>api-service>ai-service dns_hops=2 ingress_http=200 revision=v1

Lab 4 — Service selector drift를 증거로 좁히고 복구하기

섹션 제목: “Lab 4 — Service selector drift를 증거로 좁히고 복구하기”

DNS는 Service object를 찾지만 잘못된 selector 때문에 ready endpoint가 0이 되고 HTTP는 실패합니다. 원본 release manifest를 다시 적용해 live object를 desired state로 복구합니다. 이 묶음은 의도적으로 exit 1을 반환합니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m12/namespace)"
kubectl --context k3d-cs-study-workbook -n "$NS" patch service api-service \
--type=merge -p '{"spec":{"selector":{"app":"api-service-drift"}}}' >/dev/null
ENDPOINTS="-1"
for _ in $(seq 1 30); do
ENDPOINTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \
-l kubernetes.io/service-name=api-service \
-o jsonpath='{range .items[*]}{range .endpoints[*]}{.conditions.ready}{"\n"}{end}{end}' \
| awk '$1 == "true" {count++} END {print count+0}')"
test "$ENDPOINTS" = "0" && break
sleep 1
done
test "$ENDPOINTS" = "0"
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 review-client -- \
nslookup "api-service.$NS.svc.cluster.local" \
| awk '/^Address: / {print $2}' | tail -n 1)"
test "$DNS_IP" = "$SERVICE_IP"
set +e
FAILED_TRACE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -t 1 -S -O- --header='Host: capstone.invalid' \
http://traefik.kube-system.svc.cluster.local/ 2>&1)"
HTTP_EXIT=$?
set -e
test "$HTTP_EXIT" = "1"
FAILED_STATUS="$(printf '%s\n' "$FAILED_TRACE" \
| awk '/^[[:space:]]*HTTP\/1.1/ {code=$2} END {print code}')"
test "$FAILED_STATUS" = "503"
kubectl --context k3d-cs-study-workbook -n "$NS" apply \
-f /tmp/k8s-wb-m12/suite.yaml >/dev/null
RECOVERED="0"
for _ in $(seq 1 60); do
RECOVERED="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \
-l kubernetes.io/service-name=api-service \
-o jsonpath='{range .items[*]}{range .endpoints[*]}{.conditions.ready}{"\n"}{end}{end}' \
| awk '$1 == "true" {count++} END {print count+0}')"
test "$RECOVERED" = "2" && break
sleep 1
done
test "$RECOVERED" = "2"
RECOVERY_STATUS="0"
for _ in $(seq 1 30); do
set +e
RECOVERY_TRACE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -S -O /dev/null --header='Host: capstone.invalid' \
http://traefik.kube-system.svc.cluster.local/ 2>&1)"
set -e
RECOVERY_STATUS="$(printf '%s\n' "$RECOVERY_TRACE" \
| awk '/^[[:space:]]*HTTP\/1.1/ {code=$2} END {print code}')"
test "$RECOVERY_STATUS" = "200" && break
sleep 1
done
test "$RECOVERY_STATUS" = "200"
RESPONSE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -qO- --header='Host: capstone.invalid' \
http://traefik.kube-system.svc.cluster.local/)"
case "$RESPONSE" in frontend-ready*) ;; *) exit 2 ;; esac
printf 'selector_drift dns=resolved endpoints=0 ingress_status=503 http_exit=%s recovery_endpoints=%s recovery_status=200\n' \
"$HTTP_EXIT" "$RECOVERED"
exit 1

예상 출력(exit 1):

selector_drift dns=resolved endpoints=0 ingress_status=503 http_exit=1 recovery_endpoints=2 recovery_status=200

Lab 5 — 로컬 인수인계 종료 조건 판정하기

섹션 제목: “Lab 5 — 로컬 인수인계 종료 조건 판정하기”

NKS provider gate는 통과했다고 꾸미지 않고 official-doc-only로 남깁니다. 로컬 gate는 desired replica, Ready, restart, Service, Ingress backend와 실제 호출 경로를 다시 확인합니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m12/namespace)"
DEPLOYMENTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l suite=sample -o name | wc -l | tr -d ' ')"
DESIRED="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l suite=sample -o jsonpath='{range .items[*]}{.spec.replicas}{"\n"}{end}' \
| awk '{sum += $1} END {print sum+0}')"
READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l suite=sample -o jsonpath='{range .items[*]}{.status.readyReplicas}{"\n"}{end}' \
| awk '{sum += $1} END {print sum+0}')"
RESTARTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l suite=sample \
-o jsonpath='{range .items[*].status.containerStatuses[*]}{.restartCount}{"\n"}{end}' \
| awk '{sum += $1} END {print sum+0}')"
SERVICES="$(kubectl --context k3d-cs-study-workbook -n "$NS" get service \
-o name | wc -l | tr -d ' ')"
INGRESS_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}')"
FINAL=""
for _ in $(seq 1 30); do
set +e
FINAL="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec review-client -- \
wget -T 3 -qO- --header='Host: capstone.invalid' \
http://traefik.kube-system.svc.cluster.local/ 2>/dev/null)"
set -e
case "$FINAL" in frontend-ready*) break ;; esac
sleep 1
done
test "$DEPLOYMENTS/$DESIRED/$READY/$RESTARTS/$SERVICES" = "3/5/5/0/3"
test "$INGRESS_BACKEND" = "web-app:80"
case "$FINAL" in frontend-ready*) ;; *) exit 2 ;; esac
printf 'handoff local_gate=pass deployments=3 replicas=5/5 restarts=0 services=3 ingress=web-app:80 nks_gate=official-doc-only\n'

예상 출력(exit 0):

handoff local_gate=pass deployments=3 replicas=5/5 restarts=0 services=3 ingress=web-app:80 nks_gate=official-doc-only

Namespace, 진단 client와 Node label 제거하기

섹션 제목: “Namespace, 진단 client와 Node label 제거하기”
Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m12/namespace)"
AI_NODE="$(cat /tmp/k8s-wb-m12/ai-node)"
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
kubectl --context k3d-cs-study-workbook label node "$AI_NODE" \
workbook.cs-study/capstone-pool- >/dev/null
test -z "$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \
--ignore-not-found -o name)"
test "$(kubectl --context k3d-cs-study-workbook get nodes \
-l workbook.cs-study/capstone-pool -o name | awk 'END {print NR+0}')" = "0"
rm -rf /tmp/k8s-wb-m12
test ! -e /tmp/k8s-wb-m12
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 workload=0 node_labels=0 fixture=0 nodes_ready=%s cluster_retained=true\n' \
"$READY_NODES"

예상 출력(exit 0):

cleanup namespace=0 workload=0 node_labels=0 fixture=0 nodes_ready=2 cluster_retained=true
실행 뒤실제 관찰운영 판단
server dry-runobject 9개, persisted 0schema 통과와 runtime 성공은 다른 gate입니다.
3서비스 rolloutDeployment 3개, Ready 5개, endpoint 2/2/1replica와 실제 traffic target을 함께 봅니다.
ai-service 배치검증한 label의 Node와 실제 .spec.nodeName 일치YAML 의도만으로 node pool 배치를 승인하지 않습니다.
내부 DNS·Ingress 요청요청 시 두 Service hop과 Traefik host rule이 같은 chain 반환Running보다 사용자 경로에 가까운 증거입니다.
selector driftDNS 성공, endpoint 0, Ingress HTTP 503·exit 1CoreDNS나 application image보다 selector를 먼저 고칩니다.
원본 manifest 재적용endpoint 2, Ingress HTTP 200console 수동 보정 대신 desired state source로 돌아갑니다.
인수인계 gatelocal PASS, NKS official-doc-only미검증 provider 결과를 완료로 표현하지 않습니다.
cleanupnamespace·workload·label·fixture 0, Node 2 Readycluster-scoped 변경까지 소유권 기준으로 제거합니다.

NKS non-production에서 별도로 확인할 인수인계 항목

섹션 제목: “NKS non-production에서 별도로 확인할 인수인계 항목”

아래는 실행 명령이 아니라 승인 체크리스트입니다. 실제 context, endpoint나 값은 공개 evidence에 복사하지 않습니다.

영역승인 전에 확인할 것실패 시 중단 기준
AccessIAM access entry 정책, cluster/namespace scope, break-glass owner실제 업무에 cluster-admin이 기본값
Node poolpool label·taint, 사양·min/max, autoscaler 상태, 비용 ownerPending 원인 확인 없이 node 증설
Registry·Secretimage digest, pull 권한, secret manager owner·rotation실제 credential이 manifest·AI 대화에 노출
Ingress·ALBController·IngressClass, 지원 annotation, LB subnet, DNS·TLS ownerk3d annotation을 복사하거나 console로 직접 drift 생성
Rolloutreplicas, PDB·probe, 관찰 창, rollback revision, 중단 임계값apply exit 0만으로 전환 완료 선언
Observabilityapplication log·metric·trace, Kubernetes Event, audit log 보존·알림사용자 경로·Node·control plane 신호를 구분 못함
Data세 서비스의 stateless 범위, 외부 DB·Object Storage·PVC owner와 복구 시험Pod filesystem을 업무 데이터 원본으로 사용

Q1. 세 Deployment가 모두 Available이지만 web-app 요청이 실패합니다. 배포를 승인하시겠습니까?

섹션 제목: “Q1. 세 Deployment가 모두 Available이지만 web-app 요청이 실패합니다. 배포를 승인하시겠습니까?”

승인하지 않습니다. web-app → api-service → ai-service 내부 Service 호출과 실제 Ingress 경로를 각각 확인하고, EndpointSlice·probe·timeout과 최근 변경을 연결해야 합니다. Deployment Available은 각 Pod의 readiness를 말할 뿐 전체 dependency chain을 보증하지 않습니다.

Q2. AI가 k3d에서 성공한 ingressClassName: traefik을 그대로 NKS production에 적용하자고 합니다. 무엇을 요구하시겠습니까?

섹션 제목: “Q2. AI가 k3d에서 성공한 ingressClassName: traefik을 그대로 NKS production에 적용하자고 합니다. 무엇을 요구하시겠습니까?”

NKS에서 사용할 Controller와 IngressClass, 지원 annotation 버전, Load Balancer subnet·network type, DNS·TLS와 비용 owner를 먼저 요구합니다. 승인된 non-production에서 Kubernetes resource와 실제 ALB·target group 수렴을 확인하기 전에는 k3d 성공을 provider 증거로 인정하지 않습니다.

Q3. ai-servicePending이자 Node 사용률이 높아 AI가 node pool max를 바로 늘리자고 합니다. 승인하시겠습니까?

섹션 제목: “Q3. ai-service가 Pending이자 Node 사용률이 높아 AI가 node pool max를 바로 늘리자고 합니다. 승인하시겠습니까?”

바로 승인하지 않습니다. scheduler Event에서 request, selector·affinity, taint·toleration, volume topology 중 어느 제약이 원인인지 먼저 확인합니다. 실제 capacity 부족이면 현재 pool의 min·max, Cluster Autoscaler 상태와 비용·축소 안전성을 검토하고, 변경 후 Pending 해소와 application 경로를 종료 조건으로 둡니다.

함정 1 — 관리형 NKS가 workload 정상성까지 책임진다고 생각하기

섹션 제목: “함정 1 — 관리형 NKS가 workload 정상성까지 책임진다고 생각하기”

NKS가 control plane과 cloud integration을 제공해도 image, probe, resources, selector, rollout과 application dependency는 사용자 결정입니다. provider status와 workload SLO를 별도 증거로 봅니다.

함정 2 — 로컬 성공을 provider 검증으로 승격하기

섹션 제목: “함정 2 — 로컬 성공을 provider 검증으로 승격하기”

k3d Traefik, local-path storage, 로컬 Node label의 성공은 Kubernetes object와 application 계약의 증거입니다. NKS IAM, ALB, CSI, node pool·autoscaler 동작의 실측 증거가 아닙니다.

함정 3 — console hotfix와 manifest를 동시에 정본으로 두기

섹션 제목: “함정 3 — console hotfix와 manifest를 동시에 정본으로 두기”

Kubernetes가 관리하는 Load Balancer나 Service를 console에서 직접 고치면 Controller가 다시 수렴시키거나 drift가 남을 수 있습니다. 긴급 조치에도 변경 주체와 rollback을 기록하고 승인된 desired state source로 되돌아가야 합니다.

M12로 모듈 학습은 끝나며, 다음 단계는 M0~M12 전체를 깨끗한 환경에서 순서대로 재실행하고 용어·난이도·교차참조·사이트·downstream 회귀를 확인하는 최종 통합 Gate입니다.