阶段二 · 进阶与实践

Lab 参考答案

先做再看

本篇是第 09 篇实践 Lab 的参考答案.
强烈建议先独立尝试每个 Lab,遇到卡点翻对应理论篇(01-08),无法解决再查阅.

Lab 1 - Pod 基础

1.1 基础 Pod

apiVersion: v1
kind: Pod
metadata:
name: nginx-basic
namespace: lab
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
$ kubectl apply -f nginx-basic.yaml
$ kubectl get pod nginx-basic # 等待 Running
$ kubectl exec nginx-basic -- curl -s localhost
# 应返回 Nginx 默认欢迎页 HTML

1.2 多容器 Pod + emptyDir 共享

apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
namespace: lab
spec:
volumes:
- name: shared-log
emptyDir: {}

containers:
# 主容器:nginx 提供 /usr/share/nginx/html/log.txt 下载
- name: nginx
image: nginx:1.25-alpine
volumeMounts:
- name: shared-log
mountPath: /usr/share/nginx/html

# Sidecar:每秒写入时间戳
- name: writer
image: busybox:1.36
command: ["/bin/sh", "-c"]
args:
- while true; do date >> /logs/log.txt; sleep 1; done
volumeMounts:
- name: shared-log
mountPath: /logs
$ kubectl apply -f sidecar-demo.yaml
$ sleep 5
$ kubectl exec sidecar-demo -c nginx -- cat /usr/share/nginx/html/log.txt
# 应看到每秒递增的时间戳--两个容器通过 emptyDir 共享文件

1.3 探针配置

apiVersion: v1
kind: Pod
metadata:
name: probe-demo
namespace: lab
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
readinessProbe:
httpGet:
path: /healthz # 故意用不存在的路径
port: 80
initialDelaySeconds: 3
periodSeconds: 5
$ kubectl apply -f probe-demo.yaml
$ kubectl get pod probe-demo
# READY: 0/1(readiness 失败),STATUS: Running(liveness 正常)
# → Pod 在跑但不接收流量

# 修正:把 readinessProbe 的 path 改为 /,重新 apply
# → READY: 1/1

关键理解:liveness 失败 → K8s 重启容器;readiness 失败 → Pod 从 Endpoints 移除(不接收流量),但容器不重启.

1.4 Init Container

apiVersion: v1
kind: Pod
metadata:
name: init-demo
namespace: lab
spec:
volumes:
- name: workdir
emptyDir: {}

initContainers:
- name: init-download
image: busybox:1.36
command: ["sh", "-c", "echo 'Hello from Init Container' > /work/message.txt"]
volumeMounts:
- name: workdir
mountPath: /work

containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "cat /data/message.txt && sleep 3600"]
volumeMounts:
- name: workdir
mountPath: /data
$ kubectl apply -f init-demo.yaml
$ kubectl logs init-demo # 主容器日志应输出 "Hello from Init Container"
$ kubectl get pod init-demo
# Init:0/1 → PodInitializing → Running(可用 -w 观察过渡状态)

1.5 资源限制与 OOMKilled

apiVersion: v1
kind: Pod
metadata:
name: oom-demo
namespace: lab
spec:
containers:
- name: stress
image: polinux/stress
command: ["stress", "--vm", "1", "--vm-bytes", "200M"] # 申请 200MB,超出 limits
resources:
requests:
memory: 64Mi
limits:
memory: 128Mi
$ kubectl apply -f oom-demo.yaml
$ kubectl get pod oom-demo -w
# 会看到 OOMKilled → CrashLoopBackOff
# 原因:进程试图使用 200MB,但 cgroup 限制为 128MB → 内核 OOM Killer 杀掉进程

Lab 2 - 工作负载控制器

2.1 Deployment 基础

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
namespace: lab
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.24
ports:
- containerPort: 80
$ kubectl apply -f nginx-deploy.yaml
$ kubectl get pods -l app=nginx # 3 个 Running

# 手动删一个 Pod,观察自动重建
$ kubectl delete pod <pod-name>
$ kubectl get pods -l app=nginx # 仍然 3 个(新 Pod 名字不同)

2.2 滚动更新 + 观察

# 终端 1:实时观察
$ kubectl get pods -l app=nginx -w

# 终端 2:触发更新
$ kubectl set image deployment/nginx-deploy nginx=nginx:1.25

# 终端 1 会看到:
# nginx-deploy-xxx-old Terminating
# nginx-deploy-yyy-new ContainerCreating → Running
# 逐个替换,始终保持可用副本 ≥ 2

2.3 调整更新策略

# 修改策略:先删后建(资源受限模式)
$ kubectl patch deployment nginx-deploy -p '
spec:
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1'

# 再次更新,观察行为差异
$ kubectl set image deployment/nginx-deploy nginx=nginx:1.26
# 这次会看到:先有一个 Pod Terminating,然后新 Pod 才 Creating
# 任何时刻最多 2 个可用 Pod(而不是之前的"先建再删")

2.4 回滚

$ kubectl rollout history deployment/nginx-deploy
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 <none>

$ kubectl rollout undo deployment/nginx-deploy --to-revision=1
$ kubectl get deployment nginx-deploy -o jsonpath='{.spec.template.spec.containers[0].image}'
# → nginx:1.24

2.5 StatefulSet 有序性

apiVersion: v1
kind: Service
metadata:
name: sts-svc
namespace: lab
spec:
clusterIP: None
selector:
app: sts-demo
ports:
- port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-demo
namespace: lab
spec:
serviceName: sts-svc
replicas: 3
selector:
matchLabels:
app: sts-demo
template:
metadata:
labels:
app: sts-demo
spec:
containers:
- name: busybox
image: busybox:1.36
command: ["sleep", "3600"]
$ kubectl apply -f sts-demo.yaml
$ kubectl get pods -w -l app=sts-demo
# 观察顺序:sts-demo-0 Running → sts-demo-1 Creating → Running → sts-demo-2 Creating → Running

$ kubectl scale statefulset sts-demo --replicas=1
# 观察:sts-demo-2 Terminating → sts-demo-1 Terminating → 只剩 sts-demo-0

Lab 3 - Service 与服务发现

3.1 ClusterIP + 负载均衡验证

apiVersion: v1
kind: Service
metadata:
name: nginx-svc
namespace: lab
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
$ kubectl apply -f nginx-svc.yaml

# 启动临时 Pod 做测试
$ kubectl run tmp --image=curlimages/curl -n lab -it --rm -- sh

# 在临时 Pod 内:
/ $ for i in $(seq 1 10); do curl -s nginx-svc | grep "Server name"; done
# 应看到不同的 Pod hostname(证明负载均衡在工作)

# 如果 nginx 默认页没有 hostname,可以用以下命令看返回的 Pod:
/ $ for i in $(seq 1 6); do curl -s http://nginx-svc -o /dev/null -w "%{remote_ip}\n"; done

3.2 DNS 发现

# 在临时 Pod 内:
/ $ nslookup nginx-svc
Server: 10.43.0.10
Address 1: 10.43.0.10 kube-dns.kube-system.svc.cluster.local
Name: nginx-svc
Address 1: 10.43.xxx.xxx ← ClusterIP

/ $ nslookup nginx-svc.lab.svc.cluster.local
# 同样解析到 ClusterIP

3.3 Endpoints 联动

# 当前 Endpoints 应有 3 个 Pod IP
$ kubectl get endpoints nginx-svc -n lab
NAME ENDPOINTS
nginx-svc 10.42.0.5:80,10.42.1.3:80,10.42.2.4:80

# 给 Deployment 加一个必定失败的 readinessProbe
$ kubectl patch deployment nginx-deploy -n lab -p '
spec:
template:
spec:
containers:
- name: nginx
readinessProbe:
httpGet:
path: /nonexistent
port: 80
periodSeconds: 2'

# 等 Pod 重建后检查
$ kubectl get endpoints nginx-svc -n lab
NAME ENDPOINTS
nginx-svc <none> ← 空!所有 Pod 都 readiness 失败

# 恢复:删除 readinessProbe
$ kubectl patch deployment nginx-deploy -n lab --type=json \
-p '[{"op": "remove", "path": "/spec/template/spec/containers/0/readinessProbe"}]'

3.4 Headless Service

# 使用 Lab 2 的 StatefulSet + 已有的 sts-svc(clusterIP: None)
$ kubectl run tmp2 --image=busybox:1.36 -n lab -it --rm -- sh

/ $ nslookup sts-svc.lab.svc.cluster.local
# 返回多个 Pod IP(不是 ClusterIP)

/ $ nslookup sts-demo-0.sts-svc.lab.svc.cluster.local
# 返回 sts-demo-0 的单个 IP

Lab 4 - Ingress 七层路由

4.1 域名路由

# 两个 Deployment + Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-a
namespace: lab
spec:
replicas: 1
selector:
matchLabels:
app: app-a
template:
metadata:
labels:
app: app-a
spec:
containers:
- name: echo
image: hashicorp/http-echo
args: ["-text=Hello from App A"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: app-a-svc
namespace: lab
spec:
selector:
app: app-a
ports:
- port: 80
targetPort: 5678
---
# app-b 同理,text 改为 "Hello from App B"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-routing
namespace: lab
spec:
rules:
- host: a.lab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-a-svc
port:
number: 80
- host: b.lab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-b-svc
port:
number: 80
# 修改 /etc/hosts
$ echo "127.0.0.1 a.lab.local b.lab.local" | sudo tee -a /etc/hosts

$ curl http://a.lab.local
Hello from App A

$ curl http://b.lab.local
Hello from App B

4.2 TLS 自签名证书

# 生成自签名证书
$ openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout tls.key \
-out tls.crt \
-subj "/CN=a.lab.local"

# 创建 Secret
$ kubectl create secret tls lab-tls -n lab \
--cert=tls.crt --key=tls.key
# Ingress 加 tls 段
spec:
tls:
- hosts:
- a.lab.local
secretName: lab-tls
rules:
- host: a.lab.local
...
$ curl -k https://a.lab.local
Hello from App A

Lab 5 - 配置管理

5.1 ConfigMap 环境变量

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: lab
data:
APP_ENV: "dev"
LOG_LEVEL: "debug"
---
apiVersion: v1
kind: Pod
metadata:
name: config-env-demo
namespace: lab
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
envFrom:
- configMapRef:
name: app-config
$ kubectl exec config-env-demo -n lab -- env | grep -E "APP_ENV|LOG_LEVEL"
APP_ENV=dev
LOG_LEVEL=debug

5.2 热更新验证

# 修改 ConfigMap
$ kubectl patch configmap app-config -n lab -p '{"data":{"LOG_LEVEL":"info"}}'

# 等 2 分钟后:
$ kubectl exec config-env-demo -n lab -- env | grep LOG_LEVEL
LOG_LEVEL=debug ← 没变!(环境变量不热更新)

# 如果是 Volume 挂载的文件则会更新(需要另建一个 Pod 验证)

结论:环境变量注入永不热更新;Volume 挂载(不用 subPath)约 30-60s 后自动更新文件内容.

Lab 6 - 持久化存储

6.1 emptyDir 生命周期

apiVersion: v1
kind: Pod
metadata:
name: emptydir-demo
namespace: lab
spec:
volumes:
- name: data
emptyDir: {}
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "echo 'persistent?' > /data/test.txt && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
$ kubectl apply -f emptydir-demo.yaml
$ kubectl exec emptydir-demo -n lab -- cat /data/test.txt
persistent?

$ kubectl delete pod emptydir-demo -n lab
$ kubectl apply -f emptydir-demo.yaml
$ kubectl exec emptydir-demo -n lab -- cat /data/test.txt
persistent? ← 注意:这是新写入的,不是旧数据!因为 init command 又执行了一遍
# 如果 command 是 sleep(不写文件),则 /data/test.txt 不存在 → 数据确实丢失了

6.2 PVC 持久化

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lab-pvc
namespace: lab
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pvc-demo
namespace: lab
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: lab-pvc
containers:
- name: writer
image: busybox:1.36
command: ["sleep", "3600"]
volumeMounts:
- name: data
mountPath: /data
$ kubectl apply -f pvc-demo.yaml
$ kubectl exec pvc-demo -n lab -- sh -c "echo 'I survive pod deletion' > /data/proof.txt"
$ kubectl delete pod pvc-demo -n lab

# 重新创建(PVC 还在)
$ kubectl apply -f pvc-demo.yaml # 只重建 Pod 部分
$ kubectl exec pvc-demo -n lab -- cat /data/proof.txt
I survive pod deletion ← 数据保留!PVC 独立于 Pod 生命周期

Lab 7 - 综合实战(核心思路)

Lab 7 较为复杂,这里给出架构设计和关键 YAML 片段.完整实现需要你根据前面 Lab 的经验自行组装.

架构图

                    ┌─────────────┐
│ Ingress │
│ app.lab.local│
└──────┬──────┘

┌────────────┼────────────┐
│ /api │ / │
↓ ↓ │
┌──────────┐ ┌──────────┐ │
│ api-svc │ │front-svc │ │
│ (CIP) │ │ (CIP) │ │
└────┬─────┘ └──────────┘ │
│ │
↓ │
┌────────────────┐ │
│ api Deployment │ │
│ (2 replicas) │ │
│ + HPA │ │
└───┬────────┬───┘ │
│ │ │
↓ ↓ │
┌───────────┐ ┌───────────┐ │
│ pg-svc │ │ redis-svc │ │
│(Headless) │ │(Headless) │ │
└─────┬─────┘ └─────┬─────┘ │
│ │ │
↓ ↓ │
┌───────────┐ ┌───────────┐ │
│PostgreSQL │ │ Redis │ │
│StatefulSet│ │StatefulSet│ │
│ + PVC │ │ + PVC │ │
└───────────┘ └───────────┘

关键 YAML 片段

# PostgreSQL Secret
apiVersion: v1
kind: Secret
metadata:
name: pg-credentials
namespace: lab
stringData:
POSTGRES_USER: admin
POSTGRES_PASSWORD: "lab-secret-123"
POSTGRES_DB: labdb

---
# PostgreSQL StatefulSet(关键部分)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: lab
spec:
serviceName: pg-svc
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: pg-credentials
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: pgdata
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 2Gi

---
# API Deployment(关键部分)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: lab
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: hashicorp/http-echo
args: ["-text=API OK - connected to pg-svc and redis-svc"]
ports:
- containerPort: 5678
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: api-config
key: DB_HOST
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: pg-credentials
key: POSTGRES_PASSWORD
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi

---
# HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: lab
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60

验证步骤

# 1. 确认所有组件就绪
$ kubectl get all -n lab
# 应看到:postgres-0 Running, redis-0 Running, api-xxx Running x2, front-xxx Running x2

# 2. 修改 /etc/hosts
$ echo "127.0.0.1 app.lab.local" | sudo tee -a /etc/hosts

# 3. 测试完整链路
$ curl http://app.lab.local/api
API OK - connected to pg-svc and redis-svc

$ curl http://app.lab.local/
# 前端响应

# 4. 模拟故障
$ kubectl delete pod postgres-0 -n lab
$ kubectl get pod -w -n lab # 观察 StatefulSet 自动重建
# 重建后 PVC 自动重新挂载 → 数据不丢

$ kubectl delete pod -l app=api -n lab
$ kubectl get pod -l app=api -n lab # Deployment 立即补齐到 2 个

小结

快速回顾

  • Lab 1:Pod 的 YAML 结构,多容器共享 Volume,探针行为差异,OOMKilled 触发条件
  • Lab 2:Deployment 自愈能力,滚动更新可视化观察,回滚操作,StatefulSet 严格有序
  • Lab 3:Service DNS 解析验证,Endpoints 随 readiness 联动,Headless 逐 Pod DNS
  • Lab 4:Ingress 域名/路径路由配置,TLS 证书创建和挂载
  • Lab 5:环境变量不热更新,Volume 文件自动更新,subPath 的限制
  • Lab 6:emptyDir 数据随 Pod 消失,PVC 数据跨 Pod 生命周期保留
  • Lab 7:完整微服务编排--组件间通过 Service 名通信,ConfigMap/Secret 传递配置,StatefulSet+PVC 保障数据持久化

恭喜完成

如果你独立完成了全部 7 个 Lab,说明你已经具备了在生产环境中部署和管理 K8s 应用的基础能力.下一步建议:

  1. 尝试用 Helm 打包 Lab 7 的完整应用
  2. 在真实的云平台(如 TKE,EKS)上重复 Lab 7
  3. 深入学习你感兴趣的进阶主题(如 Operator 开发,Service Mesh)