课程目录(第 17 章 / 共 33 章)
综合实战:部署一个完整的三层应用
从零把 web、api、redis 三层应用部署到 demo 命名空间,用 ConfigMap 与 Secret 注入配置,通过 Ingress 用域名访问,每一步都给完整 YAML 和验证方法。
学完这一章,你将能够
- ✓独立完成一次从命名空间到 Ingress 的完整部署
- ✓把 ConfigMap、Secret、PVC、探针与资源限制串成一套可用的清单
- ✓用 endpointslices 与 curl 逐层验证流量是否打通
- ✓出问题时能按固定顺序判断是哪一层断了
目标与整体架构
前面每一章都在解决一个单独的问题,这一章不引入任何新概念,只做一件事:把学过的东西按真实项目的方式拼起来,跑通一次完整部署。
场景是一个最小但完整的三层应用:nginx 做前端,hashicorp/http-echo 做后端 API,redis 做数据库并带持久化卷。三者都在 demo 命名空间,通过 Ingress 用域名 kube101.local 暴露:根路径给前端,/api 给后端。配置走 ConfigMap,密码走 Secret。
浏览器 http://kube101.local
│
┌─────────────▼──────────────┐
│ Ingress(ingress-nginx) │
│ / → web:80 /api → api:5678│
└───────┬────────────┬────────┘
│ │
┌─────────────▼──┐ ┌────▼────────────┐
│ web: nginx │ │ api: http-echo │
│ 2 副本 + Svc │ │ 2 副本 + Svc │
└────────────────┘ └────┬────────────┘
│ REDIS_HOST
┌───────▼────────┐
│ redis + PVC │
└────────────────┘前置条件:一个能用的集群(第 3 章 搭一个本地集群的 kind 单节点集群就够),并装好 Ingress 控制器。
kubectl get nodes
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.3/deploy/static/provider/kind/deploy.yaml
kubectl -n ingress-nginx wait --for=condition=Ready pod -l app.kubernetes.io/component=controller --timeout=180s如果上面的 tag 已被官方更新,用 ingress-nginx 安装文档 里的最新命令。
让 kube101.local 真的能访问
kind 集群要在创建时把节点 80/443 映射到宿主机(extraPortMappings),否则域名解析通了也连不上。同时在 /etc/hosts 里加一行:
echo '127.0.0.1 kube101.local' | sudo tee -a /etc/hosts没有端口映射就用 port-forward 兜底,效果一样:
kubectl port-forward -n ingress-nginx svc/ingress-nginx-controller 8080:80
curl -s -H 'Host: kube101.local' http://localhost:8080/api目录结构
清单按资源类型拆文件,编号前缀保证 kubectl apply -f manifests/ 按顺序执行:
manifests/
├── 01-namespace.yaml Namespace
├── 02-config.yaml ConfigMap app-config + Secret db-secret
├── 03-redis.yaml PVC + Deployment + Service
├── 04-api.yaml Deployment + Service
├── 05-web.yaml Deployment + Service
└── 06-ingress.yaml Ingress下面每一步都是「保存文件 → apply → 验证」。不要跳过验证,出问题时它就是你的对照基线。
第 1 步:创建命名空间
apiVersion: v1
kind: Namespace
metadata:
name: demokubectl apply -f manifests/01-namespace.yaml验证:kubectl get ns demo 的 STATUS 是 Active。
第 2 步:配置与密码(ConfigMap / Secret)
配置和密码分开管理:前者是明文、可以进 git,后者是敏感信息、不该进版本库。
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo
data:
APP_ENV: production
LOG_LEVEL: info
REDIS_HOST: redis
API_GREETING: hello from api
---
apiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: demo
type: Opaque
stringData:
REDIS_PASSWORD: kube101-rediskubectl apply -f manifests/02-config.yaml验证:
kubectl get configmap app-config -n demo -o jsonpath='{.data.APP_ENV}{"\n"}'
kubectl get secret db-secret -n demo -o jsonpath='{.data.REDIS_PASSWORD}' | base64 -d; echo期望分别输出 production 和 kube101-redis。kubectl get secret 默认只显示字节数,要 base64 -d 解码;stringData 是写入时的便捷字段,读回来统一变成 data。
第 3 步:Redis(PVC + Deployment + Service)
数据库先起,它依赖存储,后面的 api 也要用它的地址。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data
namespace: demo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: demo
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.2
args:
- redis-server
- --requirepass
- $(REDIS_PASSWORD)
ports:
- containerPort: 6379
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: REDIS_PASSWORD
volumeMounts:
- name: data
mountPath: /data
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: data
persistentVolumeClaim:
claimName: redis-data
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: demo
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379kubectl apply -f manifests/03-redis.yaml
kubectl rollout status deployment/redis -n demo验证:
kubectl get pvc redis-data -n demo # STATUS 期望 Bound
kubectl get pods -n demo -l app=redis # READY 期望 1/1
kubectl exec -n demo deploy/redis -- sh -c 'redis-cli -a "$REDIS_PASSWORD" set kube101 ok'
kubectl exec -n demo deploy/redis -- sh -c 'redis-cli -a "$REDIS_PASSWORD" get kube101'最后一行期望输出 ok。strategy: Recreate 是刻意的:ReadWriteOnce 的 PVC 同一时间只能挂到一个节点,滚动更新时新旧 Pod 会抢卷,先停旧的再起新的更稳妥。删掉 Pod 让 Deployment 重建,再 get kube101 一次,数据应该还在:
kubectl delete pod -n demo -l app=redis
kubectl rollout status deployment/redis -n demo
kubectl exec -n demo deploy/redis -- sh -c 'redis-cli -a "$REDIS_PASSWORD" get kube101'第 4 步:api(Deployment + Service)
后端用 hashicorp/http-echo,它会把启动参数里的文本原样返回,正好用来验证「请求确实打到了后端」。
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: hashicorp/http-echo:1.0
args:
- -listen=:5678
- -text=$(API_GREETING)
ports:
- containerPort: 5678
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
- name: API_GREETING
valueFrom:
configMapKeyRef:
name: app-config
key: API_GREETING
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
readinessProbe:
httpGet:
path: /healthz
port: 5678
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: demo
spec:
selector:
app: api
ports:
- port: 5678
targetPort: 5678kubectl apply -f manifests/04-api.yaml
kubectl rollout status deployment/api -n demo验证:
kubectl get pods -n demo -l app=api # READY 期望 2/2
kubectl get endpointslices -n demo -l kubernetes.io/service-name=api
kubectl run tmp -n demo --rm -it --restart=Never --image=busybox:1.36 \
-- wget -qO- -T 3 http://api.demo.svc.cluster.local期望 EndpointSlice 里有两个地址,最后一条输出 hello from api。$(API_GREETING) 是 Kubernetes 的环境变量展开语法,只展开同一容器内先定义的变量,所以 env 的声明顺序很关键。
http-echo 不真的连数据库
真实项目里 api 会用 REDIS_HOST 去连 redis,这里只演示「配置从 ConfigMap 流进容器」的路径。要验证 redis 通不通,用第 3 步的 redis-cli。
第 5 步:web(Deployment + Service)
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: demo
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80kubectl apply -f manifests/05-web.yaml
kubectl rollout status deployment/web -n demo验证:
kubectl get pods -n demo -l app=web # READY 期望 2/2
kubectl port-forward svc/web 8080:80 -n demo &
sleep 2
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080
kill %1期望输出 200,说明 web 已经能独立对外服务——此时还没经过 Ingress,正好为下一步做对照。
第 6 步:Ingress
把两个 Service 挂到同一个域名下,靠路径区分:/api 给后端,/ 给前端。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kube101
namespace: demo
spec:
ingressClassName: nginx
rules:
- host: kube101.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 5678kubectl apply -f manifests/06-ingress.yaml验证:
kubectl get ingress -n demo # ADDRESS 列期望有 IP
curl -s -o /dev/null -w '%{http_code}\n' http://kube101.local/
curl -s http://kube101.local/api期望依次是 200 和 hello from api。路径按长度匹配,/api 比 / 长所以优先命中;别把这两条规则拆到不同 Ingress 里,行为取决于具体控制器实现。
第 7 步:探针与资源限制
前面每个 Deployment 都写了 readinessProbe 和 resources,这里解释为什么。
| 字段 | 作用 | 配错的后果 |
|---|---|---|
| readinessProbe | 判断 Pod 能否接流量 | 失败时不进 Endpoints,Ingress 返回 503 |
| livenessProbe | 判断容器是否还活着 | 失败时 kubelet 重启容器,配错会反复重启 |
| startupProbe | 慢启动应用的宽限期 | 不配它而用 liveness 卡启动,会陷入重启循环 |
| requests | 调度依据与保底资源 | 写太大 Pod 直接 Pending |
| limits | 资源上限 | 超过内存 limit 会被 OOMKilled |
启动特别慢的应用,加一段 startupProbe 更安全,含义是「最多给 60 秒启动,这期间 liveness 与 readiness 都不生效」:
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 30
periodSeconds: 2验证:
kubectl describe pod -n demo -l app=api | grep -E 'Liveness|Readiness|Limits|Requests'
kubectl top pods -n demo # 需要 metrics-server期望看到探针地址和 cpu/memory 的 requests 与 limits;没有 metrics-server 时 kubectl top 会报错,忽略即可。
第 8 步:端到端验证
从外到内走一遍,任何一层断了都能立刻定位。先把「一次请求到底走了哪几跳」画清楚,验证时你才知道每条命令对应哪一跳。
一次 curl 请求走过的完整路径
curl http://kube101.local/api
│
│ ① 域名解析:/etc/hosts 里 kube101.local → 127.0.0.1
▼
宿主机 80 端口(kind 的 extraPortMappings 把它映射进节点)
│
│ ② ingress-nginx controller 收到请求,按 Host 与 path 选后端
│ /api 比 / 更长,所以 /api 优先命中
▼
Service api(ClusterIP:5678,Endpoints 指向 2 个 Pod)
│
│ ③ kube-proxy 或 eBPF 做 DNAT,把 ClusterIP 换成某个 Pod 的 IP
▼
Pod api(hashicorp/http-echo 监听 :5678)
│
│ ④ 容器把 ConfigMap 里的 API_GREETING 当作响应体返回
▼
hello from api对应关系是:① 靠 /etc/hosts 与端口映射,② 靠 kubectl get ingress 与控制器日志,③ 靠 kubectl get endpointslices,④ 靠 kubectl logs。哪一跳出问题,就查哪一跳的证据。
端到端验证
kubectl get all -n demo
kubectl get pvc -n demo
kubectl get ingress -n demo
kubectl get endpointslices -n demo
curl -s -o /dev/null -w 'web: %{http_code}\n' http://kube101.local/
curl -s http://kube101.local/api期望:web 与 api 都是 2/2、redis 是 1/1;redis-data 为 Bound;Ingress 有地址;两个 Service 的 Endpoints 都有地址;最后两行输出 web: 200 和 hello from api。
恭喜,这一章没有新知识
你刚刚用到的 Namespace、ConfigMap、Secret、PVC、Deployment、Service、Ingress、探针、资源限制,都在前面章节单独学过。这一章证明的是:你已经能把它们组合成一个能对外服务的系统。
把 redis 换成生产可用的有状态方案
第 3 步的 redis 是 Deployment + 一块 PVC + Recreate 策略,练习够用,但生产上有三个短板:只有 1 个副本,Pod 或节点挂了就中断;没有自动故障切换,重建期间应用只能等;没有备份机制,redis-data 被误删就真没了。所以「上生产」不是把 replicas 改成 3——三个 Redis 副本互相不认识,谁都不是主。
| 方案 | 适用场景 | 代价 |
|---|---|---|
| 单实例 Deployment + PVC(本章) | 缓存、会话,丢了能重建 | 无高可用,重启期间不可用 |
| StatefulSet + 主从复制(自建) | 想完全掌控复制与卷 | 选主、追平、切换都要自己写脚本 |
| Redis Operator(KubeBlocks 等) | 要自动切换与统一运维 | 多一套 CRD 与控制器要维护 |
| 云托管 Redis | 团队没有专职 DBA | 成本与数据合规要评估 |
换法很直接:删掉 03-redis.yaml 里的 Deployment + PVC + Service,改成 KubeBlocks 的 Cluster 资源(清单见数据库 Operator)。api 侧只改一个值——REDIS_HOST 从 redis 换成 Operator 给出的读写端点,密码继续用 Secret 注入:
# 1. 确认可用版本,再建一套一主一从 + 哨兵
kbcli clusterversion list | grep redis
kbcli cluster create demo-redis --cluster-definition redis --cluster-version redis-7.2.4 \
--pvc type=redis,name=data,mode=ReadWriteOnce,size=10Gi \
--set cpu=1,memory=1Gi,replicas=2 --namespace demo
# 2. 从 Endpoints 的 ReadWrite 行取真实域名
kbcli cluster describe demo-redis --namespace demo | sed -n '/Endpoints/,/Topology/p'
kubectl -n demo patch configmap app-config --type merge \
-p '{"data":{"REDIS_HOST":"demo-redis-redis.demo.svc.cluster.local"}}'
# 3. ConfigMap 改完必须滚动重启,env 不会自动更新
kubectl -n demo rollout restart deployment/api三个必须一起改的地方:应用连读写端点,不连具体 Pod(切换后旧主变从库,连上去只能读);客户端要能处理主从切换(或统一走哨兵/代理端点),否则重连还会命中旧地址;配好 BackupRepo 并演练恢复,否则高可用只覆盖「节点挂了」,没覆盖「数据删错了」。练习环境想省事就保留本章的单实例版本,但要在文档里写清它是「练习配置」。
参考:reference/k8s-in-action/db/kubeblocks/kbcli-0.9.3/day-3-redis.md、reference/k8s-in-action/db/redis/README.md
常见坑与排错
按排障手册的顺序,先看 Pod 状态、再看 Events、最后看日志。这张表是本章专属的快速对照:
| 现象 | 原因 | 怎么确认 |
|---|---|---|
| 连不上 kube101.local | hosts 或端口映射没配 | curl -v http://kube101.local |
| Ingress 返回 404 | host 或 path 不匹配 | kubectl get ingress -n demo -o yaml |
| Ingress 返回 503 | Service 没有就绪的 Endpoints | kubectl get endpointslices -n demo |
/api 内容不对 | ConfigMap 改了没重启 Pod | kubectl rollout restart deployment/api -n demo |
| Pod 一直 Pending | PVC 没绑定或资源不足 | kubectl describe pod -n demo -l app=redis |
| Pod 一直 ContainerCreating | 卷挂载慢或镜像在拉取 | kubectl describe pod -n demo -l app=web |
| Pod CrashLoopBackOff | 启动命令或配置错误 | kubectl logs -n demo -l app=api --previous |
一个高频陷阱:改了 ConfigMap 后 Pod 不会自动重启,改完记得滚动重启:
kubectl rollout restart deployment/web -n demo
kubectl rollout restart deployment/api -n demo清理环境
练完就删,避免占着端口和存储:
kubectl delete namespace demo
kubectl get pv # 若还有 Released 的残留卷,手动清理删除命名空间会连带删掉其中的 Pod、Service、Ingress、ConfigMap、Secret 和 PVC;PV 是否回收取决于 StorageClass 的 reclaimPolicy。Ingress 控制器不再需要也可以删掉:
kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.3/deploy/static/provider/kind/deploy.yaml自测题
自测:为什么 redis 要用 `strategy: Recreate` 而不是默认的滚动更新?(点击展开答案)
因为 redis-data 是 ReadWriteOnce 的 PVC:同一时间只能被一个节点上的一个 Pod 挂载。默认的滚动更新会先起新 Pod、再停旧 Pod,两个 Pod 会同时抢这块卷,新 Pod 就卡在 ContainerCreating 等挂载,整个发布卡死。Recreate 的顺序是「先删旧 Pod,再建新 Pod」,中间会有短暂不可用,但不会争抢卷。这就是有状态服务和无状态服务的根本差别:无状态可以并行替换,有状态必须保证同一份数据只有一个写者。真要减少中断,正确方向是换用支持多挂载的存储或用 StatefulSet 配合副本级复制,而不是改回滚动更新。
自测:为什么改了 ConfigMap,Pod 里的值还是旧的?(点击展开答案)
因为通过 env 注入的配置是在容器启动那一刻读一次的,之后 ConfigMap 再怎么改,运行中的容器也不会重新读取。kubectl exec 进去 env | grep APP_ENV 看到的仍是旧值。想让它生效必须重建 Pod,最省事的做法是 kubectl rollout restart deployment/web -n demo。用卷挂载方式(把 ConfigMap 作为文件挂进容器)的情况会好一些,文件内容最终会被更新,但应用通常需要自己监听文件变化,或者配合 reload 信号——所以「配置变更怎么生效」一定要在部署时就设计好。
自测:为什么 Ingress 返回的是 503,而不是 404?(点击展开答案)
404 的意思是「网关收到了请求,但没有任何规则匹配这个 host 或 path」,属于路由层没匹配上;503 的意思是「路由匹配成功了,但后端没有可用的实例」,属于后端层不可用。所以看到 503,说明 Ingress 规则是对的、Service 也存在,问题在于这个 Service 的 Endpoints 是空的——通常是 Pod 没就绪(readinessProbe 失败)、标签选择器写错,或者 Pod 还在启动。验证方法就是 kubectl get endpointslices -n demo:没有地址就是 503 的根因,有地址还 503 才需要去看 Pod 内部。
小结
- 用 Namespace 给一套应用划出独立边界,所有命令都带
-n demo。 - 用 ConfigMap 注入普通配置、用 Secret 注入密码,理解
configMapKeyRef与secretKeyRef的区别。 - 用 PVC 让 redis 数据在 Pod 重建后依然存在,并知道
ReadWriteOnce为什么要配Recreate。 - 用 Deployment 管无状态副本,用 Service 给一组 Pod 一个稳定入口,用 Ingress 按域名和路径分发流量。
- 用 readinessProbe 控流量、livenessProbe 控重启、requests/limits 控资源。
- 用
endpointslices、curl、logs --previous逐层验证和定位问题。 - 用 Helm 的思路(Chart + Values)理解同一套清单如何支撑多环境部署。
- 这次演练真正练到的能力是逐层定位:域名、Ingress、Service、Endpoints、Pod 各有独立的验证命令,出问题时能立刻判断断在哪一层,而不是盲目重启。
- 这套清单可以直接改成你自己项目的骨架:换掉镜像、域名与资源规格,其余结构与验证方法原样可用。
练习
- 把 web 副本数改成 3,给 api 加
podAntiAffinity让副本尽量分散,再验证 Endpoints 数量。 - 用第 16 章 Helm 入门里的 chart 把这套清单重写一遍,用
values-prod.yaml把域名改成kube101.example.com、副本数改成 3,再helm install一次。 - 给 redis 加一个
backupCronJob(batch/v1),每天把/data打包到新 PVC 里,先只写 YAML 并用kubectl apply --dry-run=server -f验证语法。
到这里,kube101 的主线就走完了:你从「为什么需要 Kubernetes」一路走到能独立部署并排障一套完整应用。接下来该往哪走?翻到进阶路线,那里按集群、网络、存储、可观测性和平台化五个方向列出了下一步的学习地图。