<!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 << '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</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 <none> 10m v1.30.0 192.168.0.17
worker-02 Ready <none> 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 <none> <none></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 <none> 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("curl -sI http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}')")">复制</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("curl -s http://localhost:$(kubectl get svc nginx-app -o jsonpath='{.spec.ports[0].nodePort}') | head -n 12")">复制</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"><!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
<h1>Welcome to nginx!</h1></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 && 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 << 'EOF' > 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 << 'EOF' > 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 > /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 > /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}{"\n"}' | 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 <none> # 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 <none>
2 <none>
3 <none></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><unknown></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 > /dev/null 2>&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 > /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 <none> 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='<html><body><h1>Hello from ConfigMap v1</h1></body></html>'">建 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"><html><body><h1>Hello from ConfigMap v1</h1></body></html></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='<html><body><h1>Hello from ConfigMap v2</h1></body></html>' --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"><html><body><h1>Hello from ConfigMap v2</h1></body></html> # 已同步,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 << 'EOF' > 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 "<h1>Persistent: hello PVC</h1>" > /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 << 'EOF' > 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
<h1>Persistent: hello PVC</h1> # 数据仍在,容器可丢弃、数据可持久</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>
💻 配套实训环境与动手练习
本文涉及的相关命令操作、开发环境与容器编排已内置在我们的在线脱机环境中,无需安装依赖,随时在浏览器中实践体验:
📚 热门技术专题与实战文库精选 深度技术实战沉淀 · 覆盖7,600+篇开发者指南
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有