M12. 관리형 NKS 경계와 예시 3서비스 시스템 캡스톤
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”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의
증거 기반 디버깅을 배포 승인 단위로 통합합니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”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 API | system-managed control area, upgrade·patch 기능 | manifest, rollout 승인, namespace 범위, 종료 조건 | official-doc-only |
| Worker·node pool | Node provisioning과 node pool 기능 | pool 역할, 사양·수량·autoscaling 범위, Pod requests와 배치 제약 | label 배치만 로컬 실측 |
| Application | Kubernetes 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 owner | Traefik만 로컬 실측 |
| 접근·감사 | NKS IAM access entry 정책과 audit log 연동 기능 | 최소 권한 scope, 승인 주체, credential 수명, 로그 보존·알림 | official-doc-only |
“관리형”은 workload 운영까지 위임했다는 뜻이 아닙니다. provider가 control plane과 cloud integration을 제공해도 잘못된 selector, 부족한 request, 실패하는 probe, 과도한 권한과 잘못된 rollout은 사용자 manifest에서 생깁니다.
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 작성도 범위 밖입니다.
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
| 관리형 책임 경계 | 건물 관리사가 공용 설비를 관리하고 입주사가 매장을 운영 | provider가 application의 정상 동작까지 보증하지 않습니다. |
| 3서비스 호출 체인 | 접수 창구→업무 담당→전문 분석실의 전달 경로 | 실제 요청은 timeout·retry·인증 같은 추가 계약이 있습니다. |
| release gate | 이륙 전 계기·항로·회항 조건을 함께 확인하는 점검 | 체크리스트만으로 production 결과가 자동 보장되진 않습니다. |
| desired manifest로 복구 | 승인된 도면과 현장을 비교해 어긋난 배관만 되돌리는 작업 | GitOps나 admission policy 자체를 구현하는 비유는 아닙니다. |
4. 핸즈온 랩
섹션 제목: “4. 핸즈온 랩”검증 환경
섹션 제목: “검증 환경”| 항목 | 검증값 |
|---|---|
| 기준일 | 2026-07-16 |
| 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 — 이 랩에서는 사용하지 않음 |
| workload image | busybox: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-workbookcluster가 실행 중이어야 합니다. - 모든 API 명령은
k3d-cs-study-workbookcontext와 고유 namespace를 명시합니다. - 각 fence는 별도 Bash process에서 블록 단위로 실행합니다. Lab 4의 exit 1은 selector drift를 관찰하고 원본 manifest로 복구까지 검증했다는 의도된 결과입니다.
- 예상 소요 시간은 120분입니다.
| Gate | 먼저 답할 질문 | 통과 증거 |
|---|---|---|
| 1 | manifest가 API와 owner 경계를 만족하는가? | server dry-run object 9개 |
| 2 | replicas·probe·배치·endpoint가 수렴했는가? | Deployment 3개, Ready Pod 5개, endpoint 2/2/1 |
| 3 | 실제 호출이 세 서비스를 관통하는가? | Service DNS 2단계와 Traefik Ingress 응답 |
| 4 | drift를 증거로 좁히고 desired state로 고쳤는가? | DNS 성공·endpoint 0·HTTP 실패→manifest apply→HTTP 복구 |
| 5 | NKS에서 아직 검증하지 않은 것은 무엇인가? | provider gate를 official-doc-only로 남긴 인수인계 기록 |
Setup
섹션 제목: “Setup”고유 namespace와 로컬 AI pool label 만들기
섹션 제목: “고유 namespace와 로컬 AI pool label 만들기”실패하면 이 실행이 만든 namespace와 Node label만 rollback합니다. rollback도 실패하면 /tmp
소유권 표식을 보존하고 exit 70으로 중단합니다.
set -euo pipefailrm -rf /tmp/k8s-wb-m12mkdir -p /tmp/k8s-wb-m12chmod 700 /tmp/k8s-wb-m12node automation/k8s-workbook/preflight.mjs --require-clustertest "$(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/namespaceprintf '%s' "$AI_NODE" >/tmp/k8s-wb-m12/ai-nodeif 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 1firollback() { 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 EXITkubectl --context k3d-cs-study-workbook create namespace "$NS" >/dev/nullkubectl --context k3d-cs-study-workbook label node "$AI_NODE" \ workbook.cs-study/capstone-pool=ai >/dev/nullkubectl --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"trap - EXITprintf 'setup namespace=%s nodes=2 ai_pool_labels=1 dependencies=ready\n' "$NS"예상 출력(exit 0):
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+k3s1setup namespace=k8s-wb-m12-<UTC 14자리 run ID> nodes=2 ai_pool_labels=1 dependencies=readyhost 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 호출 성공까지 보장하지는 않습니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m12/namespace)"cat > /tmp/k8s-wb-m12/suite.yaml <<'YAML'apiVersion: v1kind: ConfigMapmetadata: name: suite-configdata: suite-env: capstone-local suite-revision: v1---apiVersion: v1kind: Secretmetadata: name: suite-fixture-secrettype: OpaquestringData: api-mode: fixture-only---apiVersion: apps/v1kind: Deploymentmetadata: 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: v1kind: Servicemetadata: name: ai-servicespec: selector: {app: ai-service} ports: [{name: http, port: 80, targetPort: http}]---apiVersion: apps/v1kind: Deploymentmetadata: 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: v1kind: Servicemetadata: name: api-servicespec: selector: {app: api-service} ports: [{name: http, port: 80, targetPort: http}]---apiVersion: apps/v1kind: Deploymentmetadata: 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: v1kind: Servicemetadata: name: web-appspec: selector: {app: web-app} ports: [{name: http, port: 80, targetPort: http}]---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: sample-suitespec: ingressClassName: traefik rules: - host: capstone.invalid http: paths: - path: / pathType: Prefix backend: service: {name: web-app, port: {number: 80}}YAMLOBJECTS="$(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))donetest "$PERSISTED" = "0"printf '%s\n' "$OBJECTS"printf 'review objects=9 persisted=%s fixture_secret=true\n' "$PERSISTED"예상 출력(exit 0):
configmap/suite-configdeployment.apps/web-appdeployment.apps/ai-servicedeployment.apps/api-serviceingress.networking.k8s.io/sample-suitesecret/suite-fixture-secretservice/web-appservice/ai-serviceservice/api-servicereview objects=9 persisted=0 fixture_secret=truefixture-only는 공개 재현값이며 실제 Secret이 아닙니다. 실제 credential을 manifest, shell history,
AI 대화나 evidence에 넣는 방법은 이 워크북에서 다루지 않습니다.
Lab 2 — 3서비스 topology를 배포하고 수렴 확인하기
섹션 제목: “Lab 2 — 3서비스 topology를 배포하고 수렴 확인하기”set -euo pipefailtest "$(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/nullfor DEPLOYMENT in ai-service api-service web-app; do kubectl --context k3d-cs-study-workbook -n "$NS" rollout status \ "deployment/$DEPLOYMENT" --timeout=120s >/dev/nulldonePODS="$(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=aiPod 이름·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-app과
api-service의 실습용 HTTP handler는 요청을 받을 때마다 다음 Service를 다시 호출하므로,
응답 본문 하나로 두 Service hop을 검증할 수 있습니다. Traefik 요청은 local Controller의 host
rule 실행만 검증하며 public DNS·TLS·NKS ALB를 검증하지 않습니다.
set -euo pipefailtest "$(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/nullkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/review-client --timeout=60s >/dev/nullBACK_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 ;;esactest "$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=v1Lab 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을 반환합니다.
set -euo pipefailtest "$(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/nullENDPOINTS="-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 1donetest "$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 +eFAILED_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 -etest "$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/nullRECOVERED="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 1donetest "$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 1donetest "$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 ;; esacprintf '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=200Lab 5 — 로컬 인수인계 종료 조건 판정하기
섹션 제목: “Lab 5 — 로컬 인수인계 종료 조건 판정하기”NKS provider gate는 통과했다고 꾸미지 않고 official-doc-only로 남깁니다. 로컬 gate는 desired
replica, Ready, restart, Service, Ingress backend와 실제 호출 경로를 다시 확인합니다.
set -euo pipefailtest "$(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 1donetest "$DEPLOYMENTS/$DESIRED/$READY/$RESTARTS/$SERVICES" = "3/5/5/0/3"test "$INGRESS_BACKEND" = "web-app:80"case "$FINAL" in frontend-ready*) ;; *) exit 2 ;; esacprintf '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-onlyCleanup
섹션 제목: “Cleanup”Namespace, 진단 client와 Node label 제거하기
섹션 제목: “Namespace, 진단 client와 Node label 제거하기”set -euo pipefailtest "$(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/nullkubectl --context k3d-cs-study-workbook wait \ --for=delete "namespace/$NS" --timeout=120s >/dev/nullkubectl --context k3d-cs-study-workbook label node "$AI_NODE" \ workbook.cs-study/capstone-pool- >/dev/nulltest -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-m12test ! -e /tmp/k8s-wb-m12READY_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=true5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”| 실행 뒤 | 실제 관찰 | 운영 판단 |
|---|---|---|
| server dry-run | object 9개, persisted 0 | schema 통과와 runtime 성공은 다른 gate입니다. |
| 3서비스 rollout | Deployment 3개, Ready 5개, endpoint 2/2/1 | replica와 실제 traffic target을 함께 봅니다. |
ai-service 배치 | 검증한 label의 Node와 실제 .spec.nodeName 일치 | YAML 의도만으로 node pool 배치를 승인하지 않습니다. |
| 내부 DNS·Ingress 요청 | 요청 시 두 Service hop과 Traefik host rule이 같은 chain 반환 | Running보다 사용자 경로에 가까운 증거입니다. |
| selector drift | DNS 성공, endpoint 0, Ingress HTTP 503·exit 1 | CoreDNS나 application image보다 selector를 먼저 고칩니다. |
| 원본 manifest 재적용 | endpoint 2, Ingress HTTP 200 | console 수동 보정 대신 desired state source로 돌아갑니다. |
| 인수인계 gate | local PASS, NKS official-doc-only | 미검증 provider 결과를 완료로 표현하지 않습니다. |
| cleanup | namespace·workload·label·fixture 0, Node 2 Ready | cluster-scoped 변경까지 소유권 기준으로 제거합니다. |
NKS non-production에서 별도로 확인할 인수인계 항목
섹션 제목: “NKS non-production에서 별도로 확인할 인수인계 항목”아래는 실행 명령이 아니라 승인 체크리스트입니다. 실제 context, endpoint나 값은 공개 evidence에 복사하지 않습니다.
| 영역 | 승인 전에 확인할 것 | 실패 시 중단 기준 |
|---|---|---|
| Access | IAM access entry 정책, cluster/namespace scope, break-glass owner | 실제 업무에 cluster-admin이 기본값 |
| Node pool | pool label·taint, 사양·min/max, autoscaler 상태, 비용 owner | Pending 원인 확인 없이 node 증설 |
| Registry·Secret | image digest, pull 권한, secret manager owner·rotation | 실제 credential이 manifest·AI 대화에 노출 |
| Ingress·ALB | Controller·IngressClass, 지원 annotation, LB subnet, DNS·TLS owner | k3d annotation을 복사하거나 console로 직접 drift 생성 |
| Rollout | replicas, PDB·probe, 관찰 창, rollback revision, 중단 임계값 | apply exit 0만으로 전환 완료 선언 |
| Observability | application log·metric·trace, Kubernetes Event, audit log 보존·알림 | 사용자 경로·Node·control plane 신호를 구분 못함 |
| Data | 세 서비스의 stateless 범위, 외부 DB·Object Storage·PVC owner와 복구 시험 | Pod filesystem을 업무 데이터 원본으로 사용 |
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”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-service가 Pending이자 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 경로를 종료 조건으로 둡니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 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로 되돌아가야 합니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”M12로 모듈 학습은 끝나며, 다음 단계는 M0~M12 전체를 깨끗한 환경에서 순서대로 재실행하고 용어·난이도·교차참조·사이트·downstream 회귀를 확인하는 최종 통합 Gate입니다.