Kubernetes 集群交互实验手册 - 在线容器编排实战

Kubernetes 集群交互实验手册 - 在线容器编排实战
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Kubernetes 集群交互实验手册 - 在线容器编排实战</title>
  <meta name="keywords" content="Kubernetes, K8s, Play with Kubernetes, PWK, 集群交互实验, 容器编排, kubectl, DevOps, 边学边练, 云原生">
  <meta name="description" content="基于 Play with Kubernetes 的 Kubernetes 集群交互实验手册,提供免安装的在线云原生靶场,涵盖集群初始化、节点管理、Pod 与 Deployment 部署、Service 网络暴露与持久化存储全流程实操。">
  <meta name="author" content="边学边练">
  <meta name="robots" content="index, follow">
  <link rel="canonical" href="https://www.skillup.host/7/k8s/k8s-interactive-demo.html">
  <meta property="og:type" content="article">
  <meta property="og:title" content="Kubernetes 集群交互实验手册 - 在线容器编排实战">
  <meta property="og:description" content="基于 Play with Kubernetes 的 Kubernetes 集群交互实验手册,提供免安装的在线云原生靶场,涵盖集群初始化、节点管理、Pod 与 Deployment 部署、Service 网络暴露与持久化存储全流程实操。">
  <meta property="og:url" content="https://www.skillup.host/7/k8s/k8s-interactive-demo.html">
  <meta property="og:image" content="https://file.lae.skillup.host/lae2/public/b9b5eb82/caad0e7a/0f7ee933/dc814ee3.png">
  <style>
    :root, [data-theme="light"] {
      --bg-base: #f6f8fa;
      --bg-card: #ffffff;
      --bg-subtle: #f6f8fa;
      --border-color: #d0d7de;
      --border-muted: #e2e8f0;
      --text-primary: #1f2328;
      --text-secondary: #656d76;
      --text-muted: #8c959f;
      --k8s-blue: #326ce5;
      --primary: #0969da;
      --primary-hover: #0550ae;
      --primary-bg: #ddf4ff;
      --success: #1a7f37;
      --success-bg: #dafbe1;
      --danger: #cf222e;
      --danger-hover: #a40e26;
      --danger-bg: #ffebe9;
      --code-bg: #161b22;
      --code-text: #e6edf3;
      --code-prompt: #7d8590;
      --output-bg: #f6f8fa;
      --output-text: #24292f;
      --output-border: #d0d7de;
      --btn-bg: #ffffff;
      --btn-border: #d0d7de;
      --btn-text: #24292f;
      --btn-hover: #f3f4f6;
      --card-radius: 6px;
      --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
      --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    }

    [data-theme="dark"] {
      --bg-base: #0d1117;
      --bg-card: #161b22;
      --bg-subtle: #21262d;
      --border-color: #30363d;
      --border-muted: #21262d;
      --text-primary: #e6edf3;
      --text-secondary: #7d8590;
      --text-muted: #6e7681;
      --k8s-blue: #388bfd;
      --primary: #2f81f7;
      --primary-hover: #58a6ff;
      --primary-bg: #033d8b;
      --success: #3fb950;
      --success-bg: #033a16;
      --danger: #f85149;
      --danger-hover: #ff7b72;
      --danger-bg: #490202;
      --code-bg: #0d1117;
      --code-text: #e6edf3;
      --code-prompt: #7d8590;
      --output-bg: #161b22;
      --output-text: #e6edf3;
      --output-border: #30363d;
      --btn-bg: #21262d;
      --btn-border: #363b42;
      --btn-text: #c9d1d9;
      --btn-hover: #30363d;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      background-color: var(--bg-base);
      color: var(--text-primary);
      font-family: var(--font-sans);
      font-size: 13px;
      line-height: 1.5;
      padding: 16px;
      -webkit-font-smoothing: antialiased;
    }

    .lab-container {
      max-width: 960px;
      margin: 0 auto;
    }

    /* 顶部导航与元信息 */
    .lab-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-bottom: 14px;
      margin-bottom: 16px;
      border-bottom: 1px solid var(--border-color);
      flex-wrap: wrap;
      gap: 10px;
    }

    .header-left {
      display: flex;
      align-items: center;
      gap: 12px;
    }

    .k8s-logo {
      width: 28px;
      height: 28px;
      flex-shrink: 0;
    }

    .lab-title-group h1 {
      font-size: 16px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .meta-badges {
      display: flex;
      align-items: center;
      gap: 6px;
      margin-top: 4px;
      flex-wrap: wrap;
    }

    .badge {
      font-size: 11px;
      font-family: var(--font-mono);
      padding: 1px 6px;
      border-radius: 4px;
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      color: var(--text-secondary);
    }

    .badge-primary {
      color: var(--primary);
      border-color: var(--border-color);
    }

    .header-right {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .theme-toggle {
      background: var(--btn-bg);
      border: 1px solid var(--btn-border);
      color: var(--btn-text);
      padding: 4px 8px;
      border-radius: var(--card-radius);
      font-size: 12px;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      transition: background 0.15s ease;
    }

    .theme-toggle:hover {
      background: var(--btn-hover);
    }

    /* 架构概览区 */
    .arch-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: var(--card-radius);
      padding: 14px 16px;
      margin-bottom: 16px;
    }

    .arch-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 10px;
    }

    .arch-title {
      font-size: 13px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .arch-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      gap: 12px;
    }

    .node-col {
      background: var(--bg-subtle);
      border: 1px solid var(--border-muted);
      border-radius: 4px;
      padding: 10px 12px;
    }

    .node-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 8px;
      padding-bottom: 6px;
      border-bottom: 1px solid var(--border-color);
    }

    .node-name {
      font-family: var(--font-mono);
      font-weight: 600;
      font-size: 12px;
      color: var(--text-primary);
    }

    .node-tag {
      font-size: 10px;
      font-family: var(--font-mono);
      padding: 1px 5px;
      border-radius: 3px;
      background: var(--success-bg);
      color: var(--success);
      border: 1px solid currentColor;
    }

    .comp-list {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .comp-item {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--text-secondary);
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .comp-port {
      color: var(--text-muted);
      font-size: 10px;
    }

    .arch-note {
      font-size: 11px;
      color: var(--text-secondary);
      margin-top: 10px;
      padding-top: 8px;
      border-top: 1px dashed var(--border-color);
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 6px;
    }

    /* 任务卡片体系 */
    .task-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: var(--card-radius);
      margin-bottom: 16px;
      overflow: hidden;
    }

    .task-header {
      padding: 10px 14px;
      background: var(--bg-subtle);
      border-bottom: 1px solid var(--border-color);
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      flex-wrap: wrap;
    }

    .task-title {
      font-size: 13px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .task-id {
      font-family: var(--font-mono);
      font-size: 11px;
      padding: 1px 6px;
      border-radius: 3px;
      background: var(--primary);
      color: #ffffff;
      font-weight: normal;
    }

    .task-body {
      padding: 14px;
    }

    .task-desc {
      font-size: 12px;
      color: var(--text-secondary);
      margin-bottom: 12px;
      line-height: 1.6;
    }

    .task-desc code {
      font-family: var(--font-mono);
      font-size: 11px;
      padding: 1px 4px;
      border-radius: 3px;
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
    }

    /* 代码块与操作区 */
    .cmd-block {
      border: 1px solid var(--border-color);
      border-radius: 4px;
      margin-bottom: 10px;
      overflow: hidden;
      background: var(--code-bg);
    }

    .cmd-bar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: rgba(255, 255, 255, 0.04);
      padding: 6px 10px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
      gap: 8px;
      flex-wrap: wrap;
    }

    .cmd-label {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--code-prompt);
    }

    .cmd-actions {
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .btn-action {
      background: #21262d;
      border: 1px solid #363b42;
      color: #c9d1d9;
      padding: 2px 8px;
      font-size: 11px;
      font-family: var(--font-mono);
      border-radius: 3px;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      transition: all 0.15s ease;
    }

    .btn-action:hover {
      background: #30363d;
      border-color: #8b949e;
      color: #ffffff;
    }

    .btn-action-primary {
      background: #1f6feb;
      border-color: #388bfd;
      color: #ffffff;
    }

    .btn-action-primary:hover {
      background: #388bfd;
    }

    .btn-action-danger {
      background: #21262d;
      border-color: var(--danger);
      color: var(--danger);
    }

    .btn-action-danger:hover {
      background: var(--danger);
      color: #ffffff;
    }

    .cmd-content {
      padding: 8px 12px;
      font-family: var(--font-mono);
      font-size: 12px;
      color: var(--code-text);
      overflow-x: auto;
      white-space: pre-wrap;
      word-break: break-all;
      cursor: pointer;
    }

    .cmd-prompt {
      color: var(--code-prompt);
      user-select: none;
      margin-right: 6px;
    }

    /* 预期输出块 */
    .output-box {
      background: var(--output-bg);
      border: 1px solid var(--output-border);
      border-radius: 4px;
      padding: 8px 12px;
      margin-top: 8px;
      margin-bottom: 10px;
    }

    .output-title {
      font-family: var(--font-mono);
      font-size: 10px;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      margin-bottom: 4px;
      user-select: none;
    }

    .output-content {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--output-text);
      line-height: 1.4;
      white-space: pre;
      overflow-x: auto;
    }

    /* 多行预格式文本 */
    .multiline-pre {
      padding: 10px 12px;
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--code-text);
      max-height: 240px;
      overflow-y: auto;
      white-space: pre;
      line-height: 1.45;
    }

    /* 底部轻量提示 */
    .toast {
      position: fixed;
      bottom: 16px;
      right: 16px;
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
      padding: 6px 12px;
      border-radius: 4px;
      font-size: 12px;
      font-family: var(--font-mono);
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      display: none;
      align-items: center;
      gap: 6px;
      z-index: 1000;
    }

    @media (max-width: 600px) {
      body {
        padding: 10px;
      }
      .arch-grid {
        grid-template-columns: 1fr;
      }
      .header-right {
        width: 100%;
        justify-content: flex-end;
      }
    }
  </style>
</head>
<body>
  <div class="lab-container">
    <!-- 头部规范 -->
    <header class="lab-header">
      <div class="header-left">
        <svg class="k8s-logo" viewBox="0 0 32 32" fill="none">
          <circle cx="16" cy="16" r="15" fill="#326ce5"/>
          <path d="M16 6l8 4.5v9L16 24l-8-4.5v-9L16 6z" stroke="#fff" stroke-width="2" fill="none"/>
          <circle cx="16" cy="15" r="3" fill="#fff"/>
        </svg>
        <div class="lab-title-group">
          <h1>Kubernetes 集群交互实验手册</h1>
          <div class="meta-badges">
            <span class="badge">环境: Play with Kubernetes</span>
            <span class="badge">版本: v1.30</span>
            <span class="badge">任务: Task 0 - 16</span>
            <span class="badge badge-primary">通信: 终端直连</span>
          </div>
        </div>
      </div>
      <div class="header-right">
        <button id="theme-toggle-btn" class="theme-toggle" onclick="toggleTheme()" title="切换明暗主题">
          <span id="theme-text">主题</span>
        </button>
      </div>
    </header>

    <!-- 拓扑与架构状态 -->
    <section class="arch-card">
      <div class="arch-header">
        <span class="arch-title">集群架构与节点角色概览 (Cluster Architecture)</span>
        <span class="badge">3 Nodes</span>
      </div>
      <div class="arch-grid">
        <div class="node-col">
          <div class="node-head">
            <span class="node-name">master-01</span>
            <span class="node-tag">Control Plane</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>kube-apiserver</span><span class="comp-port">:6443</span></div>
            <div class="comp-item"><span>etcd</span><span class="comp-port">:2379</span></div>
            <div class="comp-item"><span>kube-controller-mgr</span></div>
            <div class="comp-item"><span>kube-scheduler</span></div>
          </div>
        </div>

        <div class="node-col">
          <div class="node-head">
            <span class="node-name">worker-01</span>
            <span class="node-tag">Worker Node</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>kubelet</span></div>
            <div class="comp-item"><span>kube-proxy</span></div>
            <div class="comp-item"><span>flannel CNI</span></div>
            <div class="comp-item"><span>containerd CRI</span></div>
          </div>
        </div>

        <div class="node-col">
          <div class="node-head">
            <span class="node-name">worker-02</span>
            <span class="node-tag">Worker Node</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>kubelet</span></div>
            <div class="comp-item"><span>kube-proxy</span></div>
            <div class="comp-item"><span>coredns</span></div>
            <div class="comp-item"><span>flannel CNI</span></div>
          </div>
        </div>
      </div>
      <div class="arch-note">
        <span>执行命令可在右侧或下方终端实时运行。验证集群真实状态请执行 <code>kubectl get nodes -o wide</code> 与 <code>kubectl get pods -A</code>。</span>
      </div>
    </section>

    <!-- Task 0: 靶场环境配置与启动 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 0</span>
          <span>环境准备:启动与接入 PWK 靶场服务</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          若靶场尚未运行,可在宿主终端(0号终端)运行下方 Compose 配置初始化 PWK 实验环境并映射 18080 端口。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">docker-compose.yml (Heredoc 写入并启动)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runPwkHeredoc()">配置并启动</button>
              <button class="btn-action" onclick="copyPwkHeredoc()">复制</button>
              <button class="btn-action" data-target="terminal" data-cmd="docker ps" onclick="triggerAction('检查状态命令已发送')">检查容器</button>
              <button class="btn-action" onclick="openPwkPlayground()">打开练习场 (18080)</button>
            </div>
          </div>
          <pre class="multiline-pre"><code>mkdir -p /root/play-with-k8s && cd /root/play-with-k8s && cat &lt;&lt; 'EOF' &gt; docker-compose.yml
services:
  l2:
    image: local/play-with-k8s:latest
    container_name: l2
    restart: unless-stopped
    entrypoint: ["/app/l2"]
    command:
      - -ssh_key_path
      - /etc/ssh/ssh_host_rsa_key
      - -name
      - l2
      - -save
      - /pwd/networks
    environment:
      DOCKER_API_VERSION: "1.44"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - pwk8s-networks:/pwd

  pwd:
    image: local/play-with-k8s:latest
    container_name: pwd
    restart: unless-stopped
    depends_on:
      - l2
    ports:
      - "18080:3000"
    environment:
      DOCKER_API_VERSION: "1.44"
      DIND_IMAGE: pwk-node:local
    command:
      - -name
      - pwd
      - -port
      - "3000"
      - -save
      - /pwd/sessions
      - -playground-domain
      - localhost:18080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - pwk8s-sessions:/pwd

volumes:
  pwk8s-networks:
    name: pwk8s-networks
  pwk8s-sessions:
    name: pwk8s-sessions
EOF
docker-compose up -d</code></pre>
        </div>
      </div>
    </article>

    <!-- Task 1: 初始化集群控制平面 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 1</span>
          <span>初始化集群控制平面 (Bootstrap Cluster)</span>
        </div>
      </div>
      <div class="task-body">
        <div style="background: rgba(227, 179, 65, 0.15); border-left: 4px solid #d29922; padding: 10px 12px; border-radius: 4px; margin-bottom: 12px; font-size: 12px; color: var(--text-primary);">
          <strong>⚠️ 核心前置步骤:</strong>在 PWK 练习场新建节点(如 <code>node1</code>)后,实例默认尚未运行 Kubernetes 控制平面。<strong>若未初始化直接运行 <code>kubectl</code> 将报错 <code>connection refused (localhost:8080)</code></strong>。请务必先点击下方【一键初始化】(耗时约 40 秒),待终端输出 <code>node1 Ready control-plane</code> 后再继续执行后续任务!
        </div>
        <div class="task-desc">
          点击下方按钮或在练习场终端执行 <code>pwk-init</code>,系统将自动配置 kubeadm、拉取国内高速控制面镜像、初始化网络并生成 <code>~/.kube/config</code>。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">一键初始化命令 (pwk-init)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="pwk-init" onclick="triggerAction('集群初始化命令已发送')">一键初始化</button>
              <button class="btn-action" onclick="copyCommand('pwk-init')">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="pwk-init"><span class="cmd-prompt">$</span>pwk-init</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">[pwk-init] kubeadm init v1.27.2 on 172.19.0.3 (log: /root/pwk-init.log)
[pwk-init] installing flannel (host-gw, 10.5.0.0/16)
[pwk-init] xt_statistic available: multi-endpoint Services are fine
[pwk-init] waiting for node to become Ready
node1   Ready    control-plane   35s   v1.27.2   172.19.0.3
[pwk-init] done</div>
        </div>
      </div>
    </article>

    <!-- Task 2: 检查集群状态与节点 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 2</span>
          <span>验证集群控制平面与节点就绪状态 (Health Check)</span>
        </div>
      </div>
      <div class="task-body">
        <div style="background: rgba(56, 139, 253, 0.1); border-left: 4px solid var(--primary); padding: 8px 12px; border-radius: 4px; margin-bottom: 12px; font-size: 11px; color: var(--text-primary);">
          💡 <strong>前置要求:</strong>执行以下命令前请确保已完成 Task 1 的 <code>pwk-init</code> 初始化;若未初始化直接查询将报错 <code>connection refused (localhost:8080)</code>。
        </div>
        <div class="task-desc">
          使用 <code>kubectl</code> 查询当前集群所有节点的就绪状态、角色分配及内网 IP 地址。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl get nodes -o wide" onclick="triggerAction('查询节点命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand('kubectl get nodes -o wide')">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get nodes -o wide"><span class="cmd-prompt">$</span>kubectl get nodes -o wide</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME        STATUS   ROLES           AGE   VERSION   INTERNAL-IP
master-01   Ready    control-plane   12m   v1.30.0   192.168.0.18
worker-01   Ready    &lt;none&gt;          10m   v1.30.0   192.168.0.17
worker-02   Ready    &lt;none&gt;          9m    v1.30.0   192.168.0.16</div>
        </div>

        <div class="cmd-block" style="margin-top: 10px;">
          <div class="cmd-bar">
            <span class="cmd-label">集群信息核验</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl cluster-info" onclick="triggerAction('查询集群信息命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand('kubectl cluster-info')">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl cluster-info"><span class="cmd-prompt">$</span>kubectl cluster-info</div>
        </div>
      </div>
    </article>

    <!-- Task 3: 部署 Pod -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 3</span>
          <span>命令式创建并调度单实例 Pod (Create Pod)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          使用命令式命令在默认命名空间拉起一个轻量 <code>nginx:alpine</code> Pod,容器监听端口为 80。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl run nginx-app --image=nginx:alpine --port=80" onclick="triggerAction('部署 Pod 命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand('kubectl run nginx-app --image=nginx:alpine --port=80')">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl run nginx-app --image=nginx:alpine --port=80"><span class="cmd-prompt">$</span>kubectl run nginx-app --image=nginx:alpine --port=80</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">pod/nginx-app created</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          验证 Pod 调度分布与容器 IP:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">验证命令</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl get pod nginx-app -o wide" onclick="triggerAction('查询 Pod 命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand('kubectl get pod nginx-app -o wide')">复制</button>
              <button class="btn-action" data-cmd="kubectl describe pod nginx-app" onclick="triggerAction('查看详情命令已发送')">查看详情</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get pod nginx-app -o wide"><span class="cmd-prompt">$</span>kubectl get pod nginx-app -o wide</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME        READY   STATUS    RESTARTS   AGE   IP           NODE        NOMINATED NODE   READINESS GATES
nginx-app   1/1     Running   0          14s   10.244.1.2   worker-01   &lt;none&gt;           &lt;none&gt;</div>
        </div>
      </div>
    </article>

    <!-- Task 4: 暴露服务 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 4</span>
          <span>创建 NodePort 外部服务暴露 (Expose Service)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          Pod IP 为集群内部虚拟网段,外部无法直接寻址。通过将其暴露为 <code>NodePort</code> 类型的 Service,在集群所有节点随机分配 30000-32767 宿主端口,实现跨节点流量负载转发。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl expose pod nginx-app --type=NodePort --port=80" onclick="triggerAction('暴露服务命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand('kubectl expose pod nginx-app --type=NodePort --port=80')">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl expose pod nginx-app --type=NodePort --port=80"><span class="cmd-prompt">$</span>kubectl expose pod nginx-app --type=NodePort --port=80</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">service/nginx-app exposed</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          查询 Service 分配的 ClusterIP 与对外暴露的 NodePort 端口:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">验证命令</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl get svc nginx-app" onclick="triggerAction('查询服务命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand('kubectl get svc nginx-app')">复制</button>
              <button class="btn-action" data-cmd="kubectl get pods,svc -l run=nginx-app" onclick="triggerAction('联合查询命令已发送')">联合查看</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get svc nginx-app"><span class="cmd-prompt">$</span>kubectl get svc nginx-app</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
nginx-app   NodePort   10.102.164.88   &lt;none&gt;        80:31245/TCP   8s</div>
        </div>
      </div>
    </article>

    <!-- Task 5: 服务访问验证 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 5</span>
          <span>访问 NodePort 端口验证服务可用性 (Verify Nginx Service)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          通过动态提取 Service 分配的 <code>NodePort</code> 端口发起 HTTP 请求,验证 Nginx Pod 容器与宿主机端口转发链路已正常打通,确认返回 <code>HTTP/1.1 200 OK</code>。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command (HTTP 状态码验证)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="curl -sI http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}')" onclick="triggerAction('访问验证命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;curl -sI http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}')&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="curl -sI http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}')"><span class="cmd-prompt">$</span>curl -sI http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}')</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">HTTP/1.1 200 OK
Server: nginx/1.25.x
Date: Tue, 27 Oct 2026 ...
Content-Type: text/html
Content-Length: 896
Connection: keep-alive</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          亦可直接抓取 Nginx 欢迎首页内容,确认页面正常响应:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">首页内容验证</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="curl -s http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}') | head -n 12" onclick="triggerAction('获取首页命令已发送')">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;curl -s http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}') | head -n 12&quot;)">复制</button>
              <button class="btn-action" data-cmd="curl -sI http://$(kubectl get svc nginx-app -o jsonpath='{.spec.clusterIP}'):80" onclick="triggerAction('ClusterIP验证已发送')">ClusterIP验证</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="curl -s http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}') | head -n 12"><span class="cmd-prompt">$</span>curl -s http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}') | head -n 12</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
...
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;</div>
        </div>
      </div>
    </article>

    <!-- Task 6: 资源清理 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 6</span>
          <span>实验环境清理与资源释放 (Teardown & Cleanup)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          实验完成后,删除测试创建的 Pod 与 Service 资源,释放端口与集群内存。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">清理指令</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="kubectl delete pod nginx-app; kubectl delete svc nginx-app" onclick="triggerAction('清理资源命令已发送')">执行清理</button>
              <button class="btn-action" onclick="copyCommand('kubectl delete pod nginx-app && kubectl delete svc nginx-app')">复制</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl delete pod nginx-app; kubectl delete svc nginx-app"><span class="cmd-prompt">$</span>kubectl delete pod nginx-app &amp;&amp; kubectl delete svc nginx-app</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">pod "nginx-app" deleted
service "nginx-app" deleted</div>
        </div>
      </div>
    </article>
    <!-- Task 7: 声明式部署 Deployment + Service -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 7</span>
          <span>声明式部署 Deployment + NodePort Service(多副本)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          生产推荐<strong>声明式</strong>下发:先写清单再 <code>kubectl apply</code>。清单里已带 <code>replicas</code>、资源 requests/limits、readiness/liveness 探针。<br>
          ⚠️ 若靶场拉不动指定版本 tag,把 <code>image</code> 改成 <code>nginx:alpine</code>,Task 10 用 <code>nginx:1.25-alpine</code> 作为新版本即可。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">web-deploy.yaml(Heredoc 写入并应用)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-deploy')">写入并部署</button>
              <button class="btn-action" onclick="copyBlock('blk-deploy')">复制</button>
              <button class="btn-action" data-cmd="kubectl rollout status deployment/web --timeout=180s">发布状态</button>
              <button class="btn-action" data-cmd="kubectl get deploy,rs,pods -o wide">查看三层结构</button>
            </div>
          </div>
          <pre class="multiline-pre"><code id="blk-deploy">cat &lt;&lt; 'EOF' &gt; web-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10
EOF
kubectl apply -f web-deploy.yaml</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">deployment.apps/web created
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES
deployment.apps/web    2/2     2            2           10s   nginx        nginx:alpine
NAME                              DESIRED   CURRENT   READY   AGE
replicaset.apps/web-6f8c9d7b5c    2         2         2       10s
NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE
pod/web-6f8c9d7b5c-2k4pz    1/1     Running   0          10s   10.5.1.12    worker-01
pod/web-6f8c9d7b5c-qw7rt    1/1     Running   0          10s   10.5.2.9     worker-02</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          创建 Service 暴露(固定 NodePort 30080;端口冲突时删掉 <code>nodePort</code> 行让集群自动分配):
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">web-svc.yaml(Heredoc 写入并应用)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-svc')">写入并暴露</button>
              <button class="btn-action" onclick="copyBlock('blk-svc')">复制</button>
              <button class="btn-action" data-cmd="kubectl get svc,endpoints web">查看服务与后端</button>
              <button class="btn-action" data-cmd="curl -sI http://localhost:30080/">访问验证</button>
            </div>
          </div>
          <pre class="multiline-pre"><code id="blk-svc">cat &lt;&lt; 'EOF' &gt; web-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080
EOF
kubectl apply -f web-svc.yaml</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">service/web created
NAME          ENDPOINTS                       AGE
endpoints/web 10.5.1.12:80,10.5.2.9:80        3s</div>
        </div>
      </div>
    </article>

    <!-- Task 8: 手动扩缩容 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 8</span>
          <span>手动扩缩容与流量分摊验证 (Scale Up/Down)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          扩容副本数,观察 Service 的 <code>Endpoints</code> 随之前端伸新副本;新副本通过 readinessProbe 后立即承担流量,<strong>无需改动 Service</strong>。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">扩容到 5 副本</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl scale deployment/web --replicas=5">扩容到5</button>
              <button class="btn-action" data-cmd="kubectl rollout status deployment/web --timeout=180s">等待就绪</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">查看分布</button>
              <button class="btn-action" data-cmd="kubectl patch deployment/web -p '{"spec":{"replicas":3}}'">patch改副本</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl scale deployment/web --replicas=5"><span class="cmd-prompt">$</span>kubectl scale deployment/web --replicas=5</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">deployment.apps/web scaled
NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE
pod/web-6f8c9d7b5c-2k4pz    1/1     Running   0          90s   10.5.1.12    worker-01
pod/web-6f8c9d7b5c-qw7rt    1/1     Running   0          90s   10.5.2.9     worker-02
pod/web-6f8c9d7b5c-b8n2x    1/1     Running   0          12s   10.5.1.13    worker-01
pod/web-6f8c9d7b5c-h7v9m    1/1     Running   0          12s   10.5.2.10    worker-02
pod/web-6f8c9d7b5c-t5jd6    1/1     Running   0          12s   10.5.1.14    worker-01</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          给每个副本首页写上自己的主机名,再连打 20 次请求统计分布,验证负载均衡:
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command(写入主机名)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="for p in $(kubectl get pods -l app=web -o name); do kubectl exec $p -- sh -c 'hostname &gt; /usr/share/nginx/html/index.html'; done">写入主机名</button>
              <button class="btn-action" onclick="copyBlock('cmd-hostname')">复制</button>
            </div>
          </div>
          <div class="cmd-content" id="cmd-hostname"><span class="cmd-prompt">$</span>for p in $(kubectl get pods -l app=web -o name); do kubectl exec $p -- sh -c 'hostname &gt; /usr/share/nginx/html/index.html'; done</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">统计流量分布</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="for i in $(seq 1 20); do curl -s http://localhost:30080/ ; done | sort | uniq -c">统计分布</button>
              <button class="btn-action" onclick="copyBlock('cmd-dist')">复制</button>
              <button class="btn-action" data-cmd="kubectl get endpoints web">Endpoints</button>
            </div>
          </div>
          <div class="cmd-content" id="cmd-dist"><span class="cmd-prompt">$</span>for i in $(seq 1 20); do curl -s http://localhost:30080/ ; done | sort | uniq -c</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">   4 web-6f8c9d7b5c-2k4pz
   5 web-6f8c9d7b5c-b8n2x
   3 web-6f8c9d7b5c-h7v9m
   4 web-6f8c9d7b5c-qw7rt
   4 web-6f8c9d7b5c-t5jd6</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">缩容回 2 副本</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl scale deployment/web --replicas=2">缩容到2</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">查看副本</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl scale deployment/web --replicas=2"><span class="cmd-prompt">$</span>kubectl scale deployment/web --replicas=2</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">deployment.apps/web scaled
NAME                        READY   STATUS        RESTARTS   AGE   NODE
pod/web-6f8c9d7b5c-qw7rt    1/1     Terminating   0          3m    worker-02
pod/web-6f8c9d7b5c-t5jd6    1/1     Terminating   0          2m    worker-01</div>
        </div>
      </div>
    </article>

    <!-- Task 9: 自愈与调度封锁 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 9</span>
          <span>控制器自愈与节点封锁 (Self-Healing / Cordon)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          裸 Pod(Task 3 的 <code>nginx-app</code>)删掉就没了;由 ReplicaSet 托管的 Pod 被删后会被立刻重建,这就是<strong>副本数自愈</strong>。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">删除 Pod 观察自愈</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl get pods -l app=web -o wide">当前副本</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o jsonpath='{.items[0].metadata.name}{&quot;\n&quot;}' | xargs -r kubectl delete pod">删除第一个</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">再看副本</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get pods -l app=web -o wide"><span class="cmd-prompt">$</span>kubectl get pods -l app=web -o wide</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">pod "web-6f8c9d7b5c-2k4pz" deleted
NAME                        READY   STATUS    RESTARTS   AGE
pod/web-6f8c9d7b5c-qw7rt    1/1     Running   0          4m
pod/web-6f8c9d7b5c-x9m3c    1/1     Running   0          3s   # 新名字,被自动补齐</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          封锁节点:<code>cordon</code> 后新副本会卡 <code>Pending</code>,<code>uncordon</code> 后恢复调度。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">调度封锁演练(务必 uncordon 恢复)</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl cordon node1">封锁节点</button>
              <button class="btn-action" data-cmd="kubectl scale deployment/web --replicas=4">扩容触发 Pending</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">查看 Pending</button>
              <button class="btn-action btn-action-danger" data-cmd="kubectl uncordon node1">解除封锁</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl cordon node1"><span class="cmd-prompt">$</span>kubectl cordon node1</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">node/node1 cordoned
NAME                        READY   STATUS    RESTARTS   AGE   NODE
pod/web-6f8c9d7b5c-b1m0s    0/1     Pending   0          8s    &lt;none&gt;   # SchedulingDisabled</div>
        </div>
      </div>
    </article>

    <!-- Task 10: 滚动更新与回滚 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 10</span>
          <span>滚动更新、金丝雀暂停与版本回滚 (Rollout)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          更新镜像触发滚动发布;<code>maxUnavailable: 0</code> 保证全程副本数不下降。旧 ReplicaSet 保留着上一版 Pod 模板,是回滚的「存档」。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">发布新版本</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl set image deployment/web nginx=nginx:1.25-alpine">更新镜像</button>
              <button class="btn-action" data-cmd="kubectl rollout status deployment/web --timeout=180s">发布状态</button>
              <button class="btn-action" data-cmd="kubectl get rs -o wide">查看新旧 RS</button>
              <button class="btn-action" data-cmd="kubectl rollout history deployment/web">发布历史</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl set image deployment/web nginx=nginx:1.25-alpine"><span class="cmd-prompt">$</span>kubectl set image deployment/web nginx=nginx:1.25-alpine</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">deployment.apps/web image updated
NAME                DESIRED   CURRENT   READY   AGE   IMAGES
web-6f8c9d7b5c      0         0         0       6m    nginx:1.24-alpine   # 旧版本存档
web-7d4b9f2k8v      2         2         2       15s   nginx:1.25-alpine   # 当前版本</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          金丝雀:先 <code>pause</code> 只放少量新副本,验证 OK 再 <code>resume</code>;也可调整更新策略控制发布节奏。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">暂停发布 / 恢复 / 调整策略</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl rollout pause deployment/web">暂停发布</button>
              <button class="btn-action" data-cmd="kubectl set image deployment/web nginx=nginx:1.26-alpine">再更新一次</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">观察冻结</button>
              <button class="btn-action btn-action-primary" data-cmd="kubectl rollout resume deployment/web">恢复发布</button>
              <button class="btn-action" data-cmd="kubectl patch deployment/web -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":"50%","maxUnavailable":"0"}}}}'">改更新策略</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl rollout pause deployment/web"><span class="cmd-prompt">$</span>kubectl rollout pause deployment/web</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          回滚:<code>undo</code> 回上一版,<code>--to-revision=N</code> 跳指定版本(本质是换回旧 RS 的 Pod 模板,再走一次滚动更新)。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">版本回滚</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="kubectl rollout undo deployment/web">回滚上一版</button>
              <button class="btn-action btn-action-danger" data-cmd="kubectl rollout undo deployment/web --to-revision=1">回到 revision 1</button>
              <button class="btn-action" data-cmd="kubectl rollout history deployment/web">再看历史</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl rollout undo deployment/web"><span class="cmd-prompt">$</span>kubectl rollout undo deployment/web</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">deployment.apps/web rolled back
REVISION  CHANGE-CAUSE
1         &lt;none&gt;
2         &lt;none&gt;
3         &lt;none&gt;</div>
        </div>

        <div class="task-desc" style="margin-top: 12px; margin-bottom: 6px;">
          失败演练:把镜像改成不存在的版本,新 Pod 一直 <code>ImagePullBackOff</code>,但<strong>旧副本一个都不删</strong>,流量完全不受影响。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">失败发布与安全回滚</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl set image deployment/web nginx=nginx:does-not-exist">错误版本</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">看 ImagePullBackOff</button>
              <button class="btn-action btn-action-danger" data-cmd="kubectl rollout undo deployment/web">回滚救回</button>
              <button class="btn-action" data-cmd="kubectl rollout status deployment/web --timeout=180s">确认恢复</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl set image deployment/web nginx=nginx:does-not-exist"><span class="cmd-prompt">$</span>kubectl set image deployment/web nginx=nginx:does-not-exist</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME                        READY   STATUS             RESTARTS   AGE
pod/web-7d4b9f2k8v-k2x8n    1/1     Running            0          2m    # 旧副本继续服务
pod/web-6d9c7p1q4z-w8v3f    0/1     ImagePullBackOff   0          20s   # 新副本卡住</div>
        </div>
      </div>
    </article>

    <!-- Task 11: HPA 自动扩缩容 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 11</span>
          <span>自动扩缩容 HPA 与 Metrics Server (Autoscaling)</span>
        </div>
      </div>
      <div class="task-body">
        <div style="background: rgba(227, 179, 65, 0.15); border-left: 4px solid #d29922; padding: 10px 12px; border-radius: 4px; margin-bottom: 12px; font-size: 12px; color: var(--text-primary);">
          <strong>⚠️ 前置依赖:</strong>HPA 依赖 Metrics Server。三个高频坑:① 必须用 <code>releases/download/</code> 地址(<code>raw.githubusercontent</code> 的 components.yaml 是 404);② kubeadm 的 kubelet 是自签证书,必须补 <code>--kubelet-insecure-tls</code>,否则 <code>kubectl top</code> 报 <code>Metrics not available</code>;③ Pod 没写 <code>requests.cpu</code> 时 HPA 的 TARGETS 恒为 <code>&lt;unknown&gt;</code>,<strong>永远不扩</strong>。Task 7 的清单已写 <code>requests.cpu: 50m</code>。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">安装 Metrics Server</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl apply -n kube-system -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.4/components.yaml">安装</button>
              <button class="btn-action" data-cmd="kubectl patch -n kube-system deployment/metrics-server --type=json -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'">补 insecure-tls</button>
              <button class="btn-action" data-cmd="kubectl rollout status -n kube-system deployment/metrics-server --timeout=180s">等待就绪</button>
              <button class="btn-action" data-cmd="kubectl top nodes">验证指标</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl top nodes"><span class="cmd-prompt">$</span>kubectl top nodes</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME        CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
node1       187m         9%     1204Mi          32%</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">创建 HPA 并压测</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl autoscale deployment/web --min=2 --max=8 --cpu-percent=20">建 HPA</button>
              <button class="btn-action" data-cmd="kubectl run load-gen --image=busybox:1.36 --restart=Never -- sh -c 'while true; do wget -q -O- http://web &gt; /dev/null 2&gt;&amp;1; done'">起压测</button>
              <button class="btn-action" data-cmd="kubectl get hpa web">观察指标</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">副本数变化</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get hpa web"><span class="cmd-prompt">$</span>kubectl get hpa web</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME   REFERENCE         TARGETS    MINPODS   MAXPODS   REPLICAS   AGE
web    Deployment/web    62%/20%    2         8         6          40s</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">停止压测,观察缩容(默认 5 分钟稳定窗口)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="kubectl delete pod load-gen --force">停压测</button>
              <button class="btn-action" data-cmd="kubectl get hpa web">再看 HPA</button>
              <button class="btn-action" data-cmd="kubectl describe hpa web">看扩缩事件</button>
              <button class="btn-action" data-cmd="kubectl delete hpa web">删除 HPA</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl describe hpa web"><span class="cmd-prompt">$</span>kubectl describe hpa web</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">Events:
  Normal  SuccessfulRescale  New size: 4; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  New size: 2; reason: All metrics below target</div>
        </div>
      </div>
    </article>

    <!-- Task 12: 探针 Readiness / Liveness -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 12</span>
          <span>健康检查探针 Readiness / Liveness (Probes)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          两者职责完全不同:<code>readinessProbe</code> 失败 → 从 Endpoints 摘流量,<strong>不重启</strong>;<code>livenessProbe</code> 失败 → 杀容器重启。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Readiness 失败:流量被摘除(RESTARTS 仍为 0)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl patch deployment/web --type=json -p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/healthz"}]'">改成坏路径</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">看 0/1 Ready</button>
              <button class="btn-action" data-cmd="kubectl get endpoints web">Endpoints 变空</button>
              <button class="btn-action" data-cmd="for p in $(kubectl get pods -l app=web -o name); do kubectl exec $p -- sh -c 'echo ok &gt; /usr/share/nginx/html/healthz'; done">补健康文件</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get endpoints web"><span class="cmd-prompt">$</span>kubectl get endpoints web</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME          ENDPOINTS      AGE
endpoints/web &lt;none&gt;         90s   # 后端全空,Service 不再转发</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Liveness 失败:容器重启直到 CrashLoopBackOff</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl patch deployment/web --type=json -p='[{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/httpGet/path","value":"/dead-always"}]'">改成坏路径</button>
              <button class="btn-action" data-cmd="kubectl get pods -l app=web -o wide">看 RESTARTS</button>
              <button class="btn-action" data-cmd="kubectl describe pod -l app=web">看失败原因</button>
              <button class="btn-action btn-action-primary" data-cmd="kubectl apply -f web-deploy.yaml; kubectl rollout status deployment/web --timeout=180s">恢复健康状态</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl get pods -l app=web -o wide"><span class="cmd-prompt">$</span>kubectl get pods -l app=web -o wide</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">NAME                        READY   STATUS             RESTARTS      AGE
pod/web-5c8d9f1k2m-z9x4v    0/1     CrashLoopBackOff   5 (40s ago)   3m</div>
        </div>
      </div>
    </article>

    <!-- Task 13: ConfigMap / Secret -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 13</span>
          <span>配置注入 ConfigMap / Secret (Configuration)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          改配置不需要重建镜像。<strong>文件挂载</strong>方式的 ConfigMap 更新会被 kubelet 周期性同步(约 1 分钟,Pod 不重启);<strong>环境变量</strong>方式更新 Secret 后必须重启才生效。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">创建配置与密钥</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl create configmap web-index --from-literal=index.html='&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello from ConfigMap v1&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;'">建 ConfigMap</button>
              <button class="btn-action" data-cmd="kubectl create secret generic db-secret --from-literal=DB_USER=admin --from-literal=DB_PASSWORD='S3cret!Pass'">建 Secret</button>
              <button class="btn-action" data-cmd="kubectl get configmap web-index -o yaml">看 ConfigMap</button>
              <button class="btn-action" data-cmd="kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d; echo">解密 Secret</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl create secret generic db-secret --from-literal=DB_USER=admin"><span class="cmd-prompt">$</span>kubectl create secret generic db-secret --from-literal=DB_USER=admin</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">以文件挂载到容器</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl set volume deployment/web --add --name=index-html --configmap-name=web-index --mount-path=/usr/share/nginx/html">挂载</button>
              <button class="btn-action" data-cmd="kubectl rollout status deployment/web --timeout=180s">等待生效</button>
              <button class="btn-action" data-cmd="kubectl exec deploy/web -- cat /usr/share/nginx/html/index.html">容器内查看</button>
              <button class="btn-action" data-cmd="curl -s http://localhost:30080/">外部访问</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl exec deploy/web -- cat /usr/share/nginx/html/index.html"><span class="cmd-prompt">$</span>kubectl exec deploy/web -- cat /usr/share/nginx/html/index.html</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello from ConfigMap v1&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">更新 ConfigMap(文件挂载自动同步)与环境变量注入</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="kubectl create configmap web-index --from-literal=index.html='&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello from ConfigMap v2&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;' --dry-run=client -o yaml | kubectl apply -f -">更新 v2</button>
              <button class="btn-action" data-cmd="sleep 45; kubectl exec deploy/web -- cat /usr/share/nginx/html/index.html">等待同步</button>
              <button class="btn-action" data-cmd="kubectl patch deployment/web --type=json -p='[{"op":"add","path":"/spec/template/spec/containers/0/env","value":[{"name":"DB_USER","valueFrom":{"secretKeyRef":{"name":"db-secret","key":"DB_USER"}}}]}]'">注入 env</button>
              <button class="btn-action" data-cmd="kubectl exec deploy/web -- env | grep DB_USER">看环境变量</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl exec deploy/web -- env | grep DB_USER"><span class="cmd-prompt">$</span>kubectl exec deploy/web -- env | grep DB_USER</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello from ConfigMap v2&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;   # 已同步,Pod 未重启
DB_USER=admin</div>
        </div>
      </div>
    </article>

    <!-- Task 14: PV / PVC 持久化 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 14</span>
          <span>持久化存储 PV / PVC (Persistent Volume)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          kubeadm 默认<strong>没有 StorageClass</strong>,标准 PVC 会一直 <code>Pending</code>。这里手工造一个 <code>hostPath</code> 类型的 PV 做练习(生产请用 CSI / local-path-provisioner / NFS)。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">pv-lab.yaml(PV + PVC,Heredoc 写入并应用)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-pv')">写入并创建</button>
              <button class="btn-action" onclick="copyBlock('blk-pv')">复制</button>
              <button class="btn-action" data-cmd="kubectl get pv,pvc">查看绑定</button>
              <button class="btn-action" data-cmd="kubectl get storageclass">确认无 SC</button>
            </div>
          </div>
          <pre class="multiline-pre"><code id="blk-pv">cat &lt;&lt; 'EOF' &gt; pv-lab.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    path: /tmp/lab-web-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: web-data
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 256Mi
EOF
kubectl apply -f pv-lab.yaml</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">persistentvolume/pv-local created
persistentvolumeclaim/web-data created
NAME                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM
persistentvolume/pv-local   1Gi        RWO            Retain           Bound    default/web-data</div>
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">挂到 Pod 并验证「删 Pod 数据仍在」</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-pvpod')">创建挂载 Pod</button>
              <button class="btn-action" onclick="copyBlock('blk-pvpod')">复制</button>
              <button class="btn-action" data-cmd="kubectl wait --for=condition=Ready pod/pv-demo --timeout=120s">等待 Ready</button>
              <button class="btn-action" data-cmd="kubectl exec pv-demo -- sh -c 'echo &quot;&lt;h1&gt;Persistent: hello PVC&lt;/h1&gt;&quot; &gt; /usr/share/nginx/html/index.html'">写入文件</button>
              <button class="btn-action" data-cmd="kubectl delete pod pv-demo --force; kubectl apply -f pv-pod.yaml; kubectl wait --for=condition=Ready pod/pv-demo --timeout=120s; kubectl exec pv-demo -- cat /usr/share/nginx/html/index.html">删除重建验证</button>
            </div>
          </div>
          <pre class="multiline-pre"><code id="blk-pvpod">cat &lt;&lt; 'EOF' &gt; pv-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pv-demo
spec:
  containers:
    - name: nginx
      image: nginx:alpine
      ports:
        - containerPort: 80
      volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: web-data
EOF
kubectl apply -f pv-pod.yaml</code></pre>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">pod/pv-demo created
pod "pv-demo" deleted
pod/pv-demo created
&lt;h1&gt;Persistent: hello PVC&lt;/h1&gt;   # 数据仍在,容器可丢弃、数据可持久</div>
        </div>
      </div>
    </article>

    <!-- Task 15: 排障三板斧 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 15</span>
          <span>排障三板斧与异常状态速查 (Troubleshooting)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          90% 的故障原因写在 <code>Events</code> 里。标准路径:<code>get -o wide</code> → <code>describe</code> → <code>logs</code> → <code>exec</code> → <code>events</code>。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">排障常用命令</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="kubectl describe pod -l app=web">describe</button>
              <button class="btn-action" data-cmd="kubectl logs -l app=web --tail=50">logs</button>
              <button class="btn-action" data-cmd="kubectl logs -l app=web --previous">崩溃前日志</button>
              <button class="btn-action" data-cmd="kubectl get events --sort-by=.metadata.creationTimestamp | tail -n 20">events</button>
              <button class="btn-action" data-cmd="kubectl top pods">资源占用</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl describe pod -l app=web"><span class="cmd-prompt">$</span>kubectl describe pod -l app=web</div>
        </div>

        <div class="output-box">
          <div class="output-title">状态速查 (Pod Status Cheat Sheet)</div>
          <div class="output-content" style="white-space: normal; font-family: var(--font-sans); font-size: 12px;">
            <table style="width: 100%; border-collapse: collapse;">
              <tbody>
                <tr style="border-bottom: 1px solid var(--border-color);">
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">Pending</td>
                  <td style="padding: 4px 6px;">资源不足 / nodeSelector 不匹配 / 污点未容忍 / PVC 未绑定</td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border-color);">
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">ImagePullBackOff</td>
                  <td style="padding: 4px 6px;">tag 写错 / 镜像不存在 / 私有仓库缺 imagePullSecrets / 无外网</td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border-color);">
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">CrashLoopBackOff</td>
                  <td style="padding: 4px 6px;">启动即退出、配置缺失、liveness 探测失败 → 看 <code>logs --previous</code></td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border-color);">
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">OOMKilled (137)</td>
                  <td style="padding: 4px 6px;">内存超过 limits.memory → describe 的 Last State</td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border-color);">
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">CreateContainerConfigError</td>
                  <td style="padding: 4px 6px;">引用了不存在的 ConfigMap / Secret key</td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border-color);">
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">Evicted</td>
                  <td style="padding: 4px 6px;">节点磁盘 / 内存压力被 kubelet 驱逐</td>
                </tr>
                <tr>
                  <td style="padding: 4px 6px; font-family: var(--font-mono); color: var(--danger);">Terminating 卡住</td>
                  <td style="padding: 4px 6px;">finalizer 未处理 / 节点失联,可用 <code>--force</code> 强行移除</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </article>

    <!-- Task 16: 全部清理 -->
    <article class="task-card">
      <div class="task-header">
        <div class="task-title">
          <span class="task-id">Task 16</span>
          <span>进阶实验环境清理 (Teardown)</span>
        </div>
      </div>
      <div class="task-body">
        <div class="task-desc">
          清理 <code>web</code> 相关工作负载、压测 Pod 与挂载 Pod。<strong>注意:</strong>Task 9 若把节点 <code>cordon</code> 了,务必先 <code>uncordon</code>,否则后续所有 Pod 都会 Pending。
        </div>

        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">清理指令</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="kubectl delete hpa web --ignore-not-found; kubectl delete pod load-gen pv-demo --ignore-not-found --force; kubectl delete all -l app=web --ignore-not-found; kubectl delete pvc web-data pv pv-local configmap web-index secret db-secret --ignore-not-found">执行清理</button>
              <button class="btn-action" data-cmd="kubectl uncordon node1">恢复节点调度</button>
            </div>
          </div>
          <div class="cmd-content" data-cmd="kubectl delete all -l app=web --ignore-not-found"><span class="cmd-prompt">$</span>kubectl delete all -l app=web --ignore-not-found</div>
        </div>

        <div class="output-box">
          <div class="output-title">预期输出 (Expected Output)</div>
          <div class="output-content">horizontalpodautoscaler.autoscaling "web" deleted
pod "load-gen" deleted
deployment.apps "web" deleted
service "web" deleted
persistentvolumeclaim "web-data" deleted
persistentvolume "pv-local" deleted</div>
        </div>

        <div class="arch-note" style="margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--border-color);">
          <span>完整图文版练习手册(含 Module 0 环境自检、QoS/OOM、调度约束、cheat sheet):<code>k8s/k8s-practice-lab.md</code></span>
        </div>
      </div>
    </article>
  </div>

  <!-- 轻量 Toast 提示 -->
  <div id="toast" class="toast">命令已发送至终端</div>

  <script>
    // 统一下发:target="terminal" 走宿主原生终端(node1 的 0 号终端),缺省走练习场(PWK)节点终端
    function dispatchCommand(cmd, target) {
      const toTerminal = target === "terminal";
      if (window.sendTerminalCommand) {
        window.sendTerminalCommand(cmd, toTerminal ? "terminal" : null);
        return;
      }
      if (window.parent && window.parent !== window) {
        const payload = { type: "LAE_TERMINAL_COMMAND", command: cmd };
        if (toTerminal) {
          payload.target = "terminal";
        } else {
          // 练习场(PWK)协议:按实例选路并自动回车执行,切勿再带 target 否则会落到宿主终端
          payload.instance = "node1";
          payload.autoExecute = true;
        }
        window.parent.postMessage(payload, "*");
      }
    }

    // 监听 data-cmd 委托并发送到练习场/终端
    document.addEventListener("click", function(e) {
      // 若宿主平台 SDK 已经注入并处理了 data-cmd 委托,避免课件自身重复触发 postMessage 造成双弹窗
      if (window.__LAE_SDK_ATTACHED__ || window.__TERMINAL_BRIDGE_LOADED__) {
        return;
      }
      const trigger = e.target.closest("[data-cmd]");
      if (trigger) {
        const cmd = trigger.getAttribute("data-cmd");
        const target = trigger.getAttribute("data-target") || null;
        if (cmd) {
          // 带 btn-copy 类的按钮只负责复制,不下发终端
          if (trigger.classList.contains("btn-copy")) {
            copyCommand(cmd);
            return;
          }
          dispatchCommand(cmd, target);
        }
      }
    });

    // 下发多行命令块(读 pre/code 的纯文本)
    function blockText(id) {
      const el = document.getElementById(id);
      return el ? el.textContent : "";
    }

    // 课件里的 YAML/清单块都是 kubectl 操作,必须下发到练习场(PWK)节点,而不是宿主终端
    function runBlock(id) {
      const cmd = blockText(id);
      if (cmd) {
        dispatchCommand(cmd, null);
        showToast("命令已发送至练习场");
      }
    }

    function copyBlock(id) {
      const cmd = blockText(id);
      if (cmd) {
        copyCommand(cmd);
      }
    }

    function openPwkPlayground() {
      if (window.parent && window.parent !== window) {
        window.parent.postMessage({
          type: "open-in-playground",
          url: "http://localhost:18080/",
          port: 18080,
          name: "PWK 集群"
        }, "*");
      }
      showToast("练习场已打开");
    }

    function copyCommand(text) {
      if (navigator.clipboard) {
        navigator.clipboard.writeText(text).then(() => {
          showToast("已复制");
        }).catch(() => {
          fallbackCopy(text);
        });
      } else {
        fallbackCopy(text);
      }
    }

    function fallbackCopy(text) {
      const input = document.createElement("textarea");
      input.value = text;
      document.body.appendChild(input);
      input.select();
      document.execCommand("copy");
      document.body.removeChild(input);
      showToast("已复制");
    }

    function triggerAction(msg) {
      showToast(msg);
    }

    const PWK_HEREDOC_CMD = `mkdir -p /root/play-with-k8s && cd /root/play-with-k8s && cat << 'EOF' > docker-compose.yml
services:
  l2:
    image: local/play-with-k8s:latest
    container_name: l2
    restart: unless-stopped
    entrypoint: ["/app/l2"]
    command:
      - -ssh_key_path
      - /etc/ssh/ssh_host_rsa_key
      - -name
      - l2
      - -save
      - /pwd/networks
    environment:
      DOCKER_API_VERSION: "1.44"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - pwk8s-networks:/pwd

  pwd:
    image: local/play-with-k8s:latest
    container_name: pwd
    restart: unless-stopped
    depends_on:
      - l2
    ports:
      - "18080:3000"
    environment:
      DOCKER_API_VERSION: "1.44"
      DIND_IMAGE: pwk-node:local
    command:
      - -name
      - pwd
      - -port
      - "3000"
      - -save
      - /pwd/sessions
      - -playground-domain
      - localhost:18080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - pwk8s-sessions:/pwd

volumes:
  pwk8s-networks:
    name: pwk8s-networks
  pwk8s-sessions:
    name: pwk8s-sessions
EOF
docker-compose up -d`;

    function copyPwkHeredoc() {
      copyCommand(PWK_HEREDOC_CMD);
    }

    // docker-compose 起靶场属于宿主操作,必须下发到 node1 原生终端,不能进练习场
    function runPwkHeredoc() {
      dispatchCommand(PWK_HEREDOC_CMD, "terminal");
      showToast("配置命令已发送至宿主终端");
    }

    let toastTimer = null;
    function showToast(msg) {
      const toast = document.getElementById("toast");
      if (toast) {
        toast.textContent = msg;
        toast.style.display = "inline-flex";
        if (toastTimer) clearTimeout(toastTimer);
        toastTimer = setTimeout(() => {
          toast.style.display = "none";
        }, 1500);
      }
    }

    // 主题切换管理
    function initTheme() {
      const urlParams = new URLSearchParams(window.location.search);
      const urlTheme = urlParams.get("theme");
      const savedTheme = localStorage.getItem("k8s_theme");
      let initialTheme = urlTheme || savedTheme || "light";
      setTheme(initialTheme, false);

      window.addEventListener("message", (event) => {
        if (event.data && (event.data.type === "THEME_CHANGE" || event.data.type === "SET_THEME")) {
          const t = event.data.theme;
          if (t === "dark" || t === "light") {
            setTheme(t, true);
          }
        }
      });
    }

    function setTheme(theme, save = true) {
      document.documentElement.setAttribute("data-theme", theme);
      const text = document.getElementById("theme-text");
      if (text) {
        text.textContent = theme === "dark" ? "暗色模式" : "亮色模式";
      }
      if (save) {
        localStorage.setItem("k8s_theme", theme);
      }
    }

    function toggleTheme() {
      const curr = document.documentElement.getAttribute("data-theme") || "light";
      const next = curr === "dark" ? "light" : "dark";
      setTheme(next, true);
    }

    initTheme();
  </script>
</body>
</html>

💻 配套实训环境与动手练习

本文涉及的相关命令操作、开发环境与容器编排已内置在我们的在线脱机环境中,无需安装依赖,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有
  • 上一篇:
  • 下一篇: