// 이 파일이 하는 일: "쿠버네티스와 클라우드 네이티브" 코스 — docker compose로 운영하던
// 사람이 다음 단계로 넘어갈 때 필요한 '선언형 운영'의 사고방식을, 파드·디플로이먼트·서비스·
// 인그레스 같은 핵심 오브젝트부터 헬스체크·오토스케일링·생태계 조망까지 8개 섹션으로 안내하는
// 정적 학습 페이지. 프레이밍은 "compose를 아는 개발자가 보는 쿠버네티스" — 이미 아는
// 컨테이너·게이트웨이 라우팅·ENV 주입 컨벤션을 다리 삼아 새 개념을 얹는다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_COMPOSE_LIMIT = `docker compose ──────────────── 쿠버네티스
(한 대의 서버)                  (여러 대를 하나처럼)

서버 1대 위에서만 동작            여러 노드에 알아서 배치(스케줄링)
컨테이너 죽으면 restart 정도      죽으면 새로 만들어 원래 수를 복원(자가 치유)
배포 = 잠깐 끊고 다시 up          새것 띄우고 확인 후 옛것 종료(무중단)
스케일 = 사람이 서버 늘리고 설정   부하 보고 파드 수를 자동 조절(HPA)

핵심: compose는 "컨테이너를 어떻게 띄울지"를 적고,
쿠버네티스는 "시스템이 어떤 상태여야 하는지"를 적습니다.
띄우는 건 내가, 유지하는 건 시스템이 — 역할이 넘어갑니다.`;

const CODE_DECLARATIVE = `# 명령형(imperative) — "이걸 해라"를 순서대로 지시
docker run -d --name auth-1 tdatalab/auth:1.2
docker run -d --name auth-2 tdatalab/auth:1.2
# auth-2가 죽으면? → 사람이 알아채고 다시 run 해야 함

# 선언형(declarative) — "이 상태가 되어라"를 문서로 선언
# deployment.yaml
spec:
  replicas: 2            # auth 파드는 항상 2개여야 한다
  template:
    spec:
      containers:
        - name: auth
          image: tdatalab/auth:1.2

# 하나가 죽으면? → 컨트롤 루프가 차이(2개여야 하는데 1개)를
# 감지하고 스스로 새 파드를 만들어 2개로 되돌립니다. 사람 개입 없이.`;

const CODE_DEPLOYMENT = `# gateway-deployment.yaml — 디플로이먼트: "이 앱을 이렇게 유지해라"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tbox-gateway
spec:
  replicas: 3                     # 항상 3개 — 하나가 죽어도 서비스는 계속
  selector:
    matchLabels:
      app: tbox-gateway
  strategy:
    type: RollingUpdate           # 새 버전을 한 개씩 갈아 끼우는 무중단 배포
    rollingUpdate:
      maxUnavailable: 0           # 교체 중에도 가용 파드 수는 줄이지 않는다
      maxSurge: 1                 # 잠깐 4개까지 허용하며 하나씩 교체
  template:
    metadata:
      labels:
        app: tbox-gateway
    spec:
      containers:
        - name: gateway
          image: tdatalab/gateway:1.4.0   # 태그를 바꿔 apply하면 롤링 업데이트 시작
          ports:
            - containerPort: 8080

# 롤백도 선언 한 줄: kubectl rollout undo deployment/tbox-gateway`;

const CODE_SERVICE_INGRESS = `# 파드 IP는 재생성될 때마다 바뀝니다. 그래서 '안정적 접점'이 필요해요.
# gateway-service.yaml — Service: 파드들 앞의 고정된 문패 + 내장 로드밸런싱
apiVersion: v1
kind: Service
metadata:
  name: tbox-gateway            # 클러스터 안에서는 이 이름(DNS)으로 호출
spec:
  selector:
    app: tbox-gateway           # 이 라벨을 단 파드들에게 트래픽을 분배
  ports:
    - port: 80
      targetPort: 8080
---
# ingress.yaml — Ingress: 외부 트래픽을 경로별로 라우팅 (게이트웨이 라우팅 선언과 닮은꼴)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tbox-ingress
spec:
  rules:
    - host: tdatalab.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: tbox-gateway   # /api → gateway Service로
                port:
                  number: 80`;

const CODE_CONFIG = `# ConfigMap·Secret — 설정을 이미지 밖으로 분리 (ENV 주입 컨벤션의 k8s 버전)
apiVersion: v1
kind: ConfigMap
metadata:
  name: auth-config
data:
  SPRING_PROFILES_ACTIVE: "prod"
  VALKEY_HOST: "tbox-valkey"        # Service 이름이 곧 호스트명
---
apiVersion: v1
kind: Secret                        # 시크릿은 별도 오브젝트로 — 접근 권한을 다르게
metadata:
  name: auth-secret
stringData:
  JWT_SECRET: "운영값은-여기가-아니라-외부-시크릿-매니저에서"
---
# 파드에서 참조 — 컨테이너에게는 그냥 환경변수로 보입니다
    spec:
      containers:
        - name: auth
          envFrom:
            - configMapRef:
                name: auth-config
            - secretRef:
                name: auth-secret`;

const CODE_PROBES = `# 헬스체크 프로브 — "살았나"와 "받을 준비 됐나"는 다른 질문
    spec:
      containers:
        - name: auth
          livenessProbe:            # 살았나? 실패 → 컨테이너를 재시작
            httpGet:
              path: /actuator/health/liveness
              port: 8081
            periodSeconds: 10
          readinessProbe:           # 트래픽 받을 준비 됐나? 실패 → 트래픽만 차단
            httpGet:
              path: /actuator/health/readiness
              port: 8081
            initialDelaySeconds: 5  # JVM 기동에 시간이 필요 — 너무 이르면 오탐
            periodSeconds: 5

# 무중단 배포의 실제 순서:
# ① 새 파드 생성 → ② readiness 통과 → ③ Service가 트래픽을 새 파드로
# → ④ 구 파드에 SIGTERM → ⑤ 처리 중인 요청 마무리(graceful shutdown) → 종료`;

const CODE_HPA = `# 리소스 요청·제한 — 스케줄링과 안정성의 기준선
    spec:
      containers:
        - name: gateway
          resources:
            requests:               # "최소 이만큼은 필요" → 스케줄러가 배치 기준으로 사용
              cpu: "250m"           # 0.25 코어
              memory: "512Mi"
            limits:                 # "이 이상은 금지" → CPU 초과는 스로틀링,
              cpu: "1"              #                    메모리 초과는 OOMKill(강제 종료)
              memory: "1Gi"
---
# HPA — 수평 오토스케일러: 지표를 보고 replicas를 자동 조절
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: tbox-gateway-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: tbox-gateway
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70   # 평균 CPU 70%를 넘으면 파드를 늘린다`;

const CODE_KIND_LAB = `# 실습 — 로컬 kind 클러스터에서 자가 치유를 눈으로 확인하기
# ① 클러스터 생성 (kind = Kubernetes IN Docker, 도커 안에 k8s 노드를 띄움)
kind create cluster --name tbox-lab

# ② gateway·auth 두 서비스를 선언형으로 배포
kubectl apply -f k8s/gateway-deployment.yaml -f k8s/gateway-service.yaml
kubectl apply -f k8s/auth-deployment.yaml -f k8s/auth-service.yaml
kubectl apply -f k8s/ingress.yaml
kubectl get pods -w        # 파드 3개가 Running으로 수렴하는 과정을 관찰

# ③ 파드를 강제로 죽여 본다 — 자가 치유의 순간
kubectl delete pod -l app=tbox-gateway --wait=false
kubectl get pods -w        # 지운 즉시 새 파드가 생겨나 3개로 복원되는 걸 확인

# ④ 이미지 태그를 바꿔 apply → 무중단 롤링 업데이트 관찰
kubectl set image deployment/tbox-gateway gateway=tdatalab/gateway:1.4.1
kubectl rollout status deployment/tbox-gateway
kubectl rollout undo deployment/tbox-gateway   # 문제가 있으면 한 줄 롤백`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: 'compose 다음' },
  { n: 2, label: '선언형 사고' },
  { n: 3, label: '파드·디플로이먼트' },
  { n: 4, label: '서비스·인그레스' },
  { n: 5, label: '상태의 문제' },
  { n: 6, label: '헬스체크·자가 치유' },
  { n: 7, label: '오토스케일링' },
  { n: 8, label: '생태계 조망' },
];

export default function Lv4KubernetesCloudNativePage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 특급 과정</div>
        <h1>☸️ 쿠버네티스와 클라우드 네이티브<br />— 선언하면 알아서 굴러가는 인프라</h1>
        <p>
          docker compose로 서비스를 올리고 내리는 건 이제 손에 익었죠. 그런데 서버가 여러 대가
          되고, 새벽에 컨테이너가 죽고, 배포 때마다 몇 초씩 끊긴다면? <strong>쿠버네티스</strong>는
          "이 상태를 유지해라"라고 <strong>선언</strong>하면 시스템이 알아서 맞추는 운영 방식을
          제안합니다. 파드·서비스·디플로이먼트부터 오토스케일링까지, 선언형 운영의 사고방식을
          익혀 봅시다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 90분</span>
          <span className="chip">실습: kind 클러스터에 gateway+auth 올리기</span>
          <span className="chip">선수 지식: Docker·compose 운영 경험</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="compose에서 쿠버네티스로" sub="무엇이 부족해서 넘어가나">
        <p>
          우리는 지금 <span className="icode">docker compose up -d --build</span> 한 줄로 서비스
          여섯 개와 인프라를 올리고 있어요. 훌륭한 방식이지만, 세 가지 벽이 있습니다.
          첫째, <strong>단일 호스트</strong> — compose는 서버 한 대 위에서만 돕니다. 트래픽이
          늘어 서버를 늘리려면 배치·네트워크·설정을 전부 사람이 다시 설계해야 해요.
          둘째, <strong>수동 복구</strong> — 컨테이너가 죽으면 <span className="icode">restart</span>{' '}
          정책이 다시 띄워 주긴 하지만, 서버 자체가 죽으면 그걸로 끝입니다. 셋째,{' '}
          <strong>무중단 배포의 어려움</strong> — <span className="icode">up --build</span>는
          옛 컨테이너를 내리고 새것을 올리는 사이에 짧은 단절이 생겨요.
        </p>
        <Code>{CODE_COMPOSE_LIMIT}</Code>
        <p>
          쿠버네티스가 약속하는 건 세 단어로 요약됩니다. <strong>스케줄링</strong> — 여러 노드
          (서버) 중 자리가 맞는 곳에 컨테이너를 알아서 배치하고, <strong>자가 치유</strong> —
          죽으면 스스로 다시 만들어 선언된 수를 복원하고, <strong>선언형 상태 수렴</strong> —
          "어떻게 할지"가 아니라 "어떤 상태여야 하는지"를 적으면 시스템이 그 상태로 계속
          수렴합니다.
        </p>
        <div className="warn">
          <b>도입 비용도 큽니다</b> 쿠버네티스는 배울 것(오브젝트·네트워크·스토리지 모델)과
          운영할 것(클러스터 자체의 업그레이드·모니터링)이 확 늘어나는 기술이에요. 서버 한 대에
          compose로 잘 돌아가고, 몇 초의 배포 단절을 감내할 수 있는 지금 규모라면{' '}
          <strong>compose가 정답일 수 있습니다.</strong> 냉정한 판단 기준은 이것: 노드가 여러 대
          필요한가? 새벽 장애에 자동 복구가 필요한가? 배포가 하루 수차례라 무중단이 필수인가?
          셋 다 "아니오"면 아직입니다. 이 코스는 "지금 갈아타자"가 아니라, <strong>그 시점이
          왔을 때 알아볼 수 있는 눈</strong>을 만드는 과정이에요.
        </div>
      </Section>

      <Section n={2} title="선언형 사고" sub="원하는 상태를 적으면 시스템이 맞춘다">
        <p>
          쿠버네티스에서 가장 중요한 건 명령어 암기가 아니라 <strong>사고방식의 전환</strong>이에요.
          지금까지 우리는 <strong>명령형(imperative)</strong>으로 일했습니다 — "이 컨테이너를
          띄워라, 저걸 재시작해라"처럼 <strong>절차</strong>를 지시했죠. 쿠버네티스는{' '}
          <strong>선언형(declarative)</strong>입니다 — "auth 파드가 항상 2개인 상태가 되어라"라고{' '}
          <strong>결과</strong>를 적어 두면, 절차는 시스템이 알아서 밟아요.
        </p>
        <Code>{CODE_DECLARATIVE}</Code>
        <p>
          이걸 가능하게 하는 엔진이 <strong>컨트롤 루프(reconciliation loop)</strong>예요.
          쿠버네티스 안의 컨트롤러들은 끊임없이 두 가지를 비교합니다 — <strong>원하는 상태</strong>
          (내가 YAML에 선언한 것)와 <strong>현재 상태</strong>(실제 클러스터). 차이가 있으면
          그 차이를 좁히는 행동을 하고, 다시 비교하고, 또 좁히고… 온도를 설정해 두면 알아서
          냉난방을 조절하는 <strong>보일러의 온도 조절기</strong>와 같은 원리죠. 파드가 죽어 2개가
          1개가 되면, "차이 발견 → 1개 생성"이 자동으로 일어납니다.
        </p>
        <div className="tip">
          <b>YAML이 곧 문서이자 코드</b> 선언형의 부수 효과가 강력해요. "운영 환경이 어떻게
          생겼나?"의 답이 누군가의 머릿속이 아니라 <strong>YAML 파일</strong>에 있으니, 그 파일을
          git에 넣으면 인프라의 변경 이력·리뷰·롤백이 코드와 똑같아집니다. 이 발상을 끝까지
          밀어붙인 게 섹션 8에서 만날 <strong>GitOps</strong>예요 — "git에 선언된 상태 = 클러스터의
          실제 상태"를 자동으로 유지하는 방식. 씨앗은 여기, 선언형 사고에 있습니다.
        </div>
      </Section>

      <Section n={3} title="핵심 오브젝트 1 — 파드, 디플로이먼트, 레플리카셋" sub="컨테이너를 감싸는 세 겹의 껍질">
        <p>
          쿠버네티스는 컨테이너를 바로 띄우지 않고 <strong>파드(Pod)</strong>라는 최소 배포
          단위로 감쌉니다. 파드는 보통 컨테이너 1개지만, 네트워크와 저장소를 공유하는 도우미
          컨테이너(로그 수집기 같은)를 옆에 붙일 수도 있어요. 왜 한 겹 감싸냐면 — "함께 배포되고
          함께 죽는 단위"에 이름과 IP를 부여해, 스케줄링·복제·치유의 <strong>공통 단위</strong>로
          삼기 위해서입니다. 컨테이너 낱개를 상대하면 이 모든 관리가 제각각이 되거든요.
        </p>
        <p>
          그런데 파드는 <strong>일회용</strong>이에요. 죽으면 그걸로 끝, 스스로 부활하지 않습니다.
          그래서 그 위에 <strong>레플리카셋(ReplicaSet)</strong>이 있어요 — "이 파드를 N개
          유지해라"를 책임지는 컨트롤러죠. 그리고 그 위에 <strong>디플로이먼트(Deployment)</strong>가
          있습니다 — 레플리카셋을 버전 단위로 관리하면서 <strong>롤링 업데이트와 롤백</strong>을
          선언으로 처리해 줘요. 실무에서 우리가 직접 만지는 건 대부분 디플로이먼트입니다.
        </p>
        <Code>{CODE_DEPLOYMENT}</Code>
        <p>
          <span className="icode">replicas: 3</span>의 의미를 곱씹어 보세요. 파드 하나가 죽어도
          나머지 둘이 트래픽을 받고, 그 사이 레플리카셋이 세 번째를 다시 만듭니다. 즉{' '}
          <strong>"하나쯤 죽어도 서비스는 계속된다"</strong>가 설정 한 줄로 선언되는 거예요.
          배포도 마찬가지 — 이미지 태그만 바꿔 <span className="icode">apply</span>하면
          디플로이먼트가 새 레플리카셋을 만들어 한 개씩 갈아 끼우고, 문제가 생기면{' '}
          <span className="icode">rollout undo</span> 한 줄로 이전 레플리카셋으로 되돌립니다.
        </p>
        <div className="tip">
          <b>일회용이라는 설계 철학</b> "파드는 언제든 죽고 새로 만들어진다"는 전제는 불편이
          아니라 <strong>힘</strong>이에요. 어느 파드가 죽어도 괜찮게 앱을 만들면(무상태 —
          섹션 5), 노드 장애·스케일링·배포가 전부 "파드 지우고 새로 만들기"라는 같은 동작으로
          풀립니다. 소(pet)처럼 이름 붙여 돌보지 말고, 소떼(cattle)처럼 다루라는 유명한 비유가
          여기서 나왔어요.
        </div>
      </Section>

      <Section n={4} title="핵심 오브젝트 2 — 서비스, 인그레스, 컨피그" sub="바뀌는 파드 앞의 고정된 문패">
        <p>
          파드가 일회용이라면 곤란한 게 하나 생겨요 — <strong>파드 IP가 계속 바뀝니다.</strong>{' '}
          gateway가 auth를 호출하려는데 auth 파드가 방금 재생성되어 IP가 달라졌다면? 이 문제를
          푸는 게 <strong>서비스(Service)</strong>예요. 라벨(<span className="icode">app:
          tbox-auth</span>)로 파드 무리를 묶어 <strong>고정된 이름과 가상 IP</strong>를 부여하고,
          들어온 요청을 뒤의 파드들에게 <strong>알아서 분배</strong>합니다. 클러스터 안에서는{' '}
          <span className="icode">http://tbox-auth</span>처럼 서비스 이름이 곧 DNS 호스트명이
          돼요 — 내장 로드밸런서가 공짜로 따라오는 셈이죠.
        </p>
        <Code>{CODE_SERVICE_INGRESS}</Code>
        <p>
          외부에서 들어오는 트래픽은 <strong>인그레스(Ingress)</strong>가 맡아요. "이 도메인의{' '}
          <span className="icode">/api</span>는 이 서비스로" 같은 라우팅 규칙을 선언하는
          오브젝트인데 — 어디서 본 구조 같지 않나요? 우리 게이트웨이의{' '}
          <span className="icode">application.yml</span>에 경로별 라우트를 선언하는 것과 정확히
          닮은꼴입니다. 익숙한 "경로 → 목적지" 선언이 클러스터의 문 앞으로 올라간 것뿐이에요.
        </p>
        <p>
          설정 분리는 <strong>ConfigMap과 Secret</strong>이 담당합니다. 우리는 이미 설정값을
          전부 <span className="icode">{'${ENV:default}'}</span> 형태로 env에서 주입하고
          시크릿은 <span className="icode">.env</span>로 빼는 컨벤션을 쓰고 있죠 — ConfigMap이
          그 env 파일의 쿠버네티스 버전이고, Secret은 민감한 값만 따로 담아 접근 권한을 다르게
          걸 수 있는 상자입니다. 컨테이너 입장에서는 둘 다 그냥 환경변수로 보여서,{' '}
          <strong>앱 코드는 한 줄도 안 바꿔도</strong> 돼요.
        </p>
        <Code>{CODE_CONFIG}</Code>
        <div className="warn">
          <b>Secret은 이름만큼 비밀스럽지 않다</b> 기본 Secret은 base64 <strong>인코딩</strong>일
          뿐 암호화가 아니에요(인코딩은 누구나 되돌릴 수 있죠). 클러스터 접근 권한이 있으면
          읽을 수 있으니, 운영에서는 접근 제어(RBAC)와 외부 시크릿 매니저 연동까지 봐야 합니다.
          "Secret에 넣었으니 안전"이라고 넘기지 않는 게 특급 과정의 감각이에요.
        </div>
      </Section>

      <Section n={5} title="스테이트리스 vs 스테이트풀" sub="DB는 조심해서">
        <p>
          섹션 3의 "파드는 소떼처럼"이 성립하려면 조건이 하나 있어요 — <strong>파드가 상태를
          갖지 않아야</strong> 합니다. 우리 auth·gateway 같은 API 서버가 좋은 예죠. 요청을
          처리할 뿐 자기 디스크에 중요한 걸 남기지 않으니(세션도 토큰 저장소도 밖에 있으니),
          어느 파드가 죽고 어느 파드가 새로 떠도 <strong>아무도 눈치채지 못합니다.</strong>{' '}
          이런 무상태(stateless) 앱은 복제·치유·스케일링이 전부 공짜예요. 쿠버네티스와 궁합이
          가장 좋은 워크로드입니다.
        </p>
        <p>
          문제는 <strong>상태를 가진(stateful)</strong> 워크로드 — 대표적으로 DB예요. Postgres
          파드가 죽고 새로 뜨면 <strong>데이터는?</strong> 이를 위해{' '}
          <strong>PersistentVolume</strong>(파드와 수명이 분리된 디스크)과{' '}
          <strong>StatefulSet</strong>(파드마다 고정된 이름·순서·자기 디스크를 보장하는
          디플로이먼트의 형제)이 있긴 합니다. 하지만 디스크 연결, 복제 순서, 장애 시 승격(failover)
          같은 DB 고유의 어려움은 쿠버네티스가 대신 풀어 주지 않아요 — 오브젝트가 있다는 것과
          운영이 쉽다는 건 다른 얘기입니다.
        </p>
        <Code>{`궁합 좋음 (stateless)              조심해서 (stateful)
──────────────────────────────────────────────────────
gateway · auth · content 등        PostgreSQL · Kafka · Valkey(영속 모드)
API 서버, 워커                      메시지 브로커, 검색 엔진

파드가 죽어도 잃을 게 없다           파드가 죽으면 디스크·복제·승격 문제
Deployment + HPA로 끝               StatefulSet + PV + 깊은 운영 지식 필요`}</Code>
        <div className="tip">
          <b>실무의 흔한 선택</b> 그래서 많은 팀이 이렇게 선을 긋습니다 — <strong>무상태 앱은
          쿠버네티스에, DB·Kafka 같은 상태 워크로드는 관리형 서비스(RDS, MSK 등)에.</strong>{' '}
          백업·패치·복제·장애 조치를 클라우드가 대신 짊어지는 대가로 비용을 내는 거래인데,
          DB 운영 사고의 비용을 생각하면 남는 장사인 경우가 많아요. "전부 클러스터에 넣어야
          진짜 쿠버네티스"라는 건 없습니다 — 워크로드 성격 따라 두는 곳을 고르는 게 설계예요.
        </div>
      </Section>

      <Section n={6} title="헬스체크와 자가 치유" sub="살았나 vs 받을 준비 됐나 — 다른 질문, 다른 프로브">
        <p>
          자가 치유가 동작하려면 쿠버네티스가 <strong>"이 파드가 건강한가"를 판단할 근거</strong>가
          필요해요. 그 근거가 프로브(probe)인데, 두 종류를 구분하는 게 핵심입니다.{' '}
          <strong>liveness</strong>는 "살았나?"를 묻습니다 — 실패하면 컨테이너가 회복 불능
          (데드락 등)이라 보고 <strong>재시작</strong>해요. <strong>readiness</strong>는 "지금
          트래픽을 받을 준비가 됐나?"를 묻습니다 — 실패하면 재시작이 아니라 <strong>Service의
          분배 대상에서만 잠시 제외</strong>합니다. JVM 워밍업 중이거나 의존 서비스 연결 전이라면
          죽은 게 아니라 아직 준비가 안 된 것뿐이니까요.
        </p>
        <Code>{CODE_PROBES}</Code>
        <p>
          readiness를 잘못 짜면 어떻게 될까요? 배포 때마다 502가 나는 고전적 사고가 여기서
          나옵니다. 새 파드가 <strong>아직 요청을 못 받는데 readiness가 성급하게 OK</strong>를
          주면(예: 프로브 경로가 앱 기동과 무관한 정적 응답이거나,{' '}
          <span className="icode">initialDelaySeconds</span>가 너무 짧으면), Service가 준비 안 된
          파드로 트래픽을 흘려보내요. 반대로 readiness에 <strong>의존 서비스 상태까지</strong>{' '}
          묶어 버리면, DB가 잠깐 흔들릴 때 모든 파드가 일제히 "준비 안 됨"이 되어 멀쩡한 앱
          전체가 트래픽에서 빠지는 연쇄 장애가 됩니다. 프로브는 <strong>"이 파드 하나"의 준비
          상태만</strong> 답하게 짜는 게 원칙이에요.
        </p>
        <p>
          이 프로브들이 갖춰지면 <strong>무중단 배포의 실제 순서</strong>가 완성됩니다.
          ① 새 버전 파드 생성 → ② readiness 통과를 기다림 → ③ Service가 트래픽을 새 파드로
          돌림 → ④ 구 파드에 종료 신호(SIGTERM) → ⑤ 처리 중이던 요청을 마무리하고(graceful
          shutdown) 종료. 어느 순간에도 "준비 안 된 파드가 트래픽을 받는" 구간이 없죠.
          섹션 3의 <span className="icode">maxUnavailable: 0</span>과 이 프로브들이 합쳐져야
          비로소 무중단이 됩니다 — 어느 한쪽만으로는 안 돼요.
        </p>
        <div className="tip">
          <b>우리 스택과의 연결</b> Spring Boot Actuator는 이 구분을 이미 알고 있어요 —{' '}
          <span className="icode">/actuator/health/liveness</span>와{' '}
          <span className="icode">/readiness</span> 엔드포인트를 따로 제공합니다. 앱 쪽 준비는
          사실상 되어 있는 셈이라, 프로브 선언만 정확히 얹으면 됩니다.
        </div>
      </Section>

      <Section n={7} title="오토스케일링과 리소스 관리" sub="자동화가 청구서로 돌아오는 지점까지">
        <p>
          스케줄러가 파드를 어느 노드에 둘지 정하려면 <strong>"이 파드가 자원을 얼마나
          쓰는지"</strong>를 알아야 해요. 그 신고서가 <span className="icode">requests</span>와{' '}
          <span className="icode">limits</span>입니다. <span className="icode">requests</span>는
          "최소 보장" — 스케줄러는 이 값 기준으로 자리가 있는 노드를 고릅니다.{' '}
          <span className="icode">limits</span>는 "상한" — 넘으려 하면 제재가 들어가는데,{' '}
          <strong>CPU와 메모리의 제재가 달라요.</strong> CPU 초과는{' '}
          <strong>스로틀링</strong>(속도를 늦출 뿐 죽이진 않음), 메모리 초과는{' '}
          <strong>OOMKill</strong>(컨테이너 강제 종료)입니다. "앱이 이유 없이 재시작돼요"의
          단골 범인이 메모리 limit이 너무 빡빡한 경우예요 — JVM 힙 설정과 limit이 어긋나면
          특히 잘 일어납니다.
        </p>
        <Code>{CODE_HPA}</Code>
        <p>
          파드 수를 자동 조절하는 게 <strong>HPA(Horizontal Pod Autoscaler)</strong>예요.
          "평균 CPU 70%를 넘으면 늘리고, 여유가 생기면 줄여라"처럼 <strong>지표와 목표</strong>를
          선언하면, 컨트롤 루프가 replicas를 계속 조정합니다. 어떤 지표로 늘릴지가 설계
          포인트인데 — CPU는 무난한 기본값이지만, 트래픽 특성에 따라 메모리, 초당 요청 수,
          큐 길이 같은 커스텀 지표가 더 정직할 수 있어요. 시세 스트리밍처럼 연결 수가 부하를
          결정하는 워크로드라면 CPU만 봐서는 늦게 반응합니다.
        </p>
        <div className="warn">
          <b>자동화가 청구서로 돌아오는 지점</b> 파드를 늘리다 노드가 꽉 차면? 노드 자체를
          늘리는 <strong>클러스터 오토스케일러</strong>가 클라우드에 새 서버를 주문합니다.
          여기서부터 "선언 한 줄"이 <strong>실제 돈</strong>이 돼요. maxReplicas 상한 없이 HPA를
          열어 두거나, requests를 실사용보다 크게 잡아 노드를 낭비하면, 월말 청구서가
          그 대가를 알려 줍니다. 오토스케일링 설계는 성능 설계이자 <strong>비용 설계</strong> —
          상한을 정하고, 실사용 지표로 requests를 주기적으로 다듬는 것까지가 한 세트예요.
        </div>
      </Section>

      <Section n={8} title="클라우드 네이티브 생태계 조망" sub="지도 없이 도구부터 사지 않기">
        <p>
          '클라우드 네이티브'는 쿠버네티스 하나가 아니라 <strong>사고방식과 도구의
          생태계</strong>예요. 뿌리에는 <strong>12-factor 앱</strong> 원칙이 있습니다 — 설정은
          환경변수로, 앱은 무상태로, 로그는 표준 출력으로, 프로세스는 쉽게 죽고 쉽게 뜨게.
          눈치챘겠지만 우리가 이미 지키는 컨벤션(<span className="icode">{'${ENV:default}'}</span>{' '}
          주입, 무상태 API 서버, 컨테이너 빌드)이 상당 부분 이거예요. 컨테이너 친화적 설계를
          해 뒀다면 쿠버네티스 도입의 절반은 이미 끝난 셈입니다.
        </p>
        <p>
          그 위의 도구들은 위치만 잡아 둡시다. <strong>헬름(Helm)</strong>은 YAML 뭉치의
          패키징·템플릿 도구 — 서비스가 많아져 매니페스트가 수십 장이 될 때 필요해집니다.{' '}
          <strong>GitOps(ArgoCD 등)</strong>는 섹션 2에서 심은 씨앗의 완성형 — "git의 선언 =
          클러스터의 실제"를 자동 동기화하죠. <strong>서비스 메시(Istio 등)</strong>는 서비스 간
          통신에 암호화·재시도·트래픽 분할을 앱 코드 수정 없이 끼워 넣는 층인데, 복잡도가 커서
          "서비스 수십 개 + 전담 팀" 규모부터 고려하는 게 보통이에요. 셋 다 <strong>"지금
          필요한가?"</strong>를 먼저 물으세요 — 도구는 문제가 생긴 뒤에 사도 늦지 않습니다.
        </p>
        <Code>{`도입 로드맵 스케치 — 한 번에 점프하지 않는다

지금        compose 운영 + 12-factor 컨벤션 유지 (이미 진행 중)
1단계       무상태 서비스 몇 개만 매니페스트 작성, kind로 로컬 검증
2단계       스테이징 클러스터에 gateway·auth부터 — 프로브·리소스 다듬기
3단계       나머지 무상태 서비스 이전, HPA 도입 · DB는 관리형 검토
그 뒤       매니페스트가 늘면 헬름, 배포가 잦으면 GitOps 검토

각 단계마다 "이 단계가 실제 문제를 풀었나?"를 확인하고 다음으로.`}</Code>
        <p>
          traderbox의 현실도 정직하게 보죠 — 일부 서비스에는 <span className="icode">k8s/</span>{' '}
          디렉터리가 있지만 <span className="icode">service-macro</span>·
          <span className="icode">service-content</span>에는 없고, 실제 운영은 compose입니다.
          이건 부끄러운 게 아니라 <strong>점진 도입의 자연스러운 중간 상태</strong>예요.
          위 로드맵처럼 "어느 서비스부터, 어떤 문제를 풀려고" 옮기는지 순서를 그릴 수 있다면,
          이 코스의 목표는 달성입니다.
        </p>
        <div className="tip">
          <b>실습 — 눈으로 확인하는 자가 치유</b> 개념은 손으로 만져야 남아요. 로컬에서{' '}
          <span className="icode">kind</span>로 클러스터를 만들고, gateway·auth를 디플로이먼트·
          서비스·인그레스로 올린 뒤, <strong>파드를 강제로 죽여 보세요.</strong> 지우는 순간
          새 파드가 생겨나 선언된 수로 복원되는 것, 이미지 태그를 바꿔 롤링 업데이트가 한 개씩
          진행되는 것을 <span className="icode">kubectl get pods -w</span>로 지켜보는 경험이
          이 코스의 하이라이트입니다.
        </div>
        <Code>{CODE_KIND_LAB}</Code>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>☸️ 여기까지 왔다면</h3>
          <p className="muted">
            이제 "쿠버네티스 = 어려운 유행어"가 아니라, <strong>선언형 상태 수렴이라는 하나의
            원리</strong>로 스케줄링·자가 치유·무중단 배포·오토스케일링이 전부 설명된다는 걸
            알게 됐어요. 그리고 그만큼 중요한 감각 — 도입 비용과 시점을 냉정하게 판단하고,
            상태 있는 워크로드 앞에서 멈춰 서는 감각도요. 그런데 파드가 수십 개로 늘어나는
            순간, 새 질문이 옵니다. <strong>지금 어느 파드가 느린지, 요청이 어느 서비스에서
            막혔는지 어떻게 알죠?</strong> 다음{' '}
            <Link to="/learn/lv4-observability-metrics-tracing"><strong>관측성 — 보이지 않으면
            고칠 수 없다</strong></Link> 코스에서, 분산된 시스템의 안을 들여다보는 눈을 이어서
            만들어 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
