第11章:大结局 - 部署一个完整的微服务应用
恭喜你,走到了我们旅程的最后一站!
在过去的十章里,我们从 Docker 的基础一路走来,跨越了镜像、容器、网络、数据卷的重重关卡,最终登上了 Kubernetes 这艘云原生的巨轮,学会了使用 Deployment、Service、ConfigMap 和 PVC 等核心武器。
现在,是时候将所有这些“武功”融会贯通,完成一次终极实战了。
在本章,我们将把一个经典的微服务应用——一个包含前端(React)、后端(Node.js API)和数据库(PostgreSQL)的投票应用——完整地部署到我们的 Minikube 集群上。
这不仅是对你所学知识的一次全面检验,更是一个可以让你自信地写在简历上的、完整的云原生项目经验。
11.1 项目架构概览
我们的投票应用(Example Voting App)由以下几个部分组成:
- Voting App (前端): 一个 Python/Flask 或 .NET/C# 的 Web 应用,提供投票界面(猫 vs. 狗),它会将投票结果发送到 Redis。
- Redis (内存数据库): 接收并临时存储投票数据。
- Worker (工作进程): 一个 .NET/Java 应用,它会连接到 Redis,获取投票数据,然后将其持久化到 PostgreSQL 数据库中。
- DB (数据库): 一个 PostgreSQL 数据库,用于永久存储投票结果。
- Result App (结果展示): 一个 Node.js Web 应用,它会从 PostgreSQL 数据库中读取投票结果,并以图表形式实时展示出来。
这是一个非常典型的微服务架构,每个服务各司其职,通过网络进行通信,数据在其中流动。

我们的任务,就是为这个应用中的每一个服务,编写对应的 Kubernetes YAML 配置文件(Deployment 和 Service),并将它们部署到集群中。
11.2 准备工作:获取项目代码和镜像
为了方便大家,官方已经为这个示例应用的所有服务都构建好了 Docker 镜像,并发布在 Docker Hub 上。我们不需要自己去写 Dockerfile 和构建镜像。
你可以在这里找到项目的所有信息:github.com/dockersampl…
我们将要用到的镜像是:
dockersamples/examplevotingapp_votedockersamples/examplevotingapp_workerdockersamples/examplevotingapp_resultredis:alpinepostgres:15-alpine
11.3 编写 Kubernetes YAML 文件
我们将把所有服务的 K8s 资源定义,写在一个大的 YAML 文件里,用 --- 来分隔每一个资源。创建一个名为 voting-app.yaml 的文件。
1. 数据库 (PostgreSQL)
首先部署我们的有状态服务——数据库。我们需要一个 StatefulSet、一个 PVC 模板来持久化数据,以及一个 Service 来让其他服务可以访问它。
# ----------------- DB Service ----------------- #
apiVersion: v1
kind: Service
metadata:
name: db
labels:
app: db
spec:
ports:
- port: 5432
name: postgres
selector:
app: db
---
# ----------------- DB Deployment (StatefulSet) ----------------- #
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: "db"
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_PASSWORD
value: "postgres"
volumeMounts:
- name: db-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: db-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
2. 缓存 (Redis)
Redis 也是一个有状态服务,但在这个例子里,我们把它当做临时缓存,所以简单地用 Deployment 和一个 ClusterIP 类型的 Service 即可。
# ----------------- Redis Service ----------------- #
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: redis
spec:
ports:
- port: 6379
name: redis
selector:
app: redis
---
# ----------------- Redis Deployment ----------------- #
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
3. 工作进程 (Worker)
Worker 是一个无状态服务,它不需要 Service,因为它不接收任何外部请求,只是默默地连接 Redis 和 DB 工作。
# ----------------- Worker Deployment ----------------- #
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
spec:
replicas: 1
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
spec:
containers:
- name: worker
image: dockersamples/examplevotingapp_worker
4. 投票应用 (Vote App)
这是我们的前端,需要一个 Deployment 和一个 NodePort 类型的 Service,来让我们可以从外部浏览器访问。
# ----------------- Vote Service ----------------- #
apiVersion: v1
kind: Service
metadata:
name: vote
labels:
app: vote
spec:
type: NodePort
ports:
- port: 80
name: http
selector:
app: vote
---
# ----------------- Vote Deployment ----------------- #
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote
spec:
replicas: 2 # 我们可以启动2个副本
selector:
matchLabels:
app: vote
template:
metadata:
labels:
app: vote
spec:
containers:
- name: vote
image: dockersamples/examplevotingapp_vote
ports:
- containerPort: 80
5. 结果应用 (Result App)
这也是一个前端,同样需要 Deployment 和 NodePort Service。
# ----------------- Result Service ----------------- #
apiVersion: v1
kind: Service
metadata:
name: result
labels:
app: result
spec:
type: NodePort
ports:
- port: 80
name: http
selector:
app: result
---
# ----------------- Result Deployment ----------------- #
apiVersion: apps/v1
kind: Deployment
metadata:
name: result
spec:
replicas: 1
selector:
matchLabels:
app: result
template:
metadata:
labels:
app: result
spec:
containers:
- name: result
image: dockersamples/examplevotingapp_result
ports:
- containerPort: 80
11.4 部署并验证
现在,我们有了一个包含了所有资源定义的 voting-app.yaml 文件。
注意:
- 以上每个配置部分之间需要用
---分隔开。 - 如果遇到镜像拉取问题,也可以手动导入镜像到 Minikube
# 在宿主机上拉取镜像 docker pull dockersamples/examplevotingapp_result docker pull dockersamples/examplevotingapp_vote docker pull dockersamples/examplevotingapp_worker # 将镜像导入到 Minikube minikube image load dockersamples/examplevotingapp_result minikube image load dockersamples/examplevotingapp_vote minikube image load dockersamples/examplevotingapp_worker
完成部署:
注:
部署前可以先验证一下配置是否正确,避免部署后出现问题。
如何验证:kubectl apply -f voting-app.yaml --dry-run=client
kubectl apply -f voting-app.yaml
这个命令会一次性创建所有9个资源(4个 Deployment、1个 StatefulSet 和 4个 Service)。
mac@192 resource % kubectl apply -f voting-app.yaml
service/db created
statefulset.apps/db created
service/redis created
deployment.apps/redis created
deployment.apps/worker created
service/vote created
deployment.apps/vote created
service/result created
deployment.apps/result created
验证状态:
# 查看所有 Pods,确保它们都处于 Running 状态
kubectl get pods -w
# 查看所有 Services
kubectl get svc
在 Service 列表中,找到 vote 和 result 这两个 NodePort 类型的服务,我们后面验证要访问这两个的页面。
mac@192 resource % kubectl delete -f voting-app.yaml
service "db" deleted
service "redis" deleted
service "vote" deleted
service "result" deleted
deployment.apps "result" deleted
mac@192 resource % kubectl apply -f voting-app.yaml
service/db created
service/redis created
service/vote created
service/result created
deployment.apps/result created
mac@192 resource % kubectl get pods
NAME READY STATUS RESTARTS AGE
result-59cbf4565b-tlbsl 1/1 Running 0 12m
mac@192 resource % kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
db ClusterIP 10.99.90.74 <none> 5432/TCP 12m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14m
redis ClusterIP 10.111.244.10 <none> 6379/TCP 12m
result NodePort 10.98.46.86 <none> 80:31939/TCP 12m
vote NodePort 10.107.56.229 <none> 80:32735/TCP 12m
mac@192 resource %
访问应用:
还记得我们前面章节提到的访问 NodePort 服务的方法吗?这里也是一样的。
由于我们使用的是 Minikube,访问 NodePort 服务有几种方式:
方法 1:使用 minikube service 命令(推荐) 分别启动 vote 和 result 服务,会自动打开浏览器并处理端口转发。
# 自动打开浏览器并处理端口转发
minikube service vote
minikube service result
方法 2:使用端口转发(最可靠)
# 将 vote 服务端口转发到本地 8080 端口
kubectl port-forward service/vote 8080:80
# 在浏览器访问 http://localhost:8080
# 将 result 服务端口转发到本地 8081 端口
kubectl port-forward service/result 8081:80
# 在浏览器访问 http://localhost:8081
方法 3:直接访问(某些环境可行)
使用 minikube ip 获取集群IP,然后访问:
- 投票页面:
http://<minikube-ip>:<vote-service-nodeport>- 你会看到一个让你在“猫”和“狗”之间投票的界面。随便投几票。
- 结果页面:
http://<minikube-ip>:<result-service-nodeport>- 你会看到一个实时更新的图表,展示了猫和狗的票数。
📋 适合方法 3 的情况:
- Linux 系统 + 裸机 Minikube(不是 Docker Desktop)
- Windows 系统 + Hyper-V 驱动
- macOS + VirtualBox/Parallels 驱动(不是 Docker Desktop)
❌ 不适合的情况:
- macOS + Docker Desktop
- Windows + Docker Desktop
如果访问失败:
如果执行 minikube service vote 出现 SVC_UNREACHABLE 错误,说明对应的 Pod 还没有正常运行。请:
- 等待几分钟让 Pod 完全启动
- 检查 Pod 状态:
kubectl get pods - 查看 Pod 日志:
kubectl logs -l app=vote和kubectl logs -l app=result - 确保所有依赖服务(db, redis)都已正常运行
成功了! 你已经将一个完整的多服务微服务应用,成功地部署到了 Kubernetes 集群上!
清理现有部署(如果要删除的话):
kubectl delete -f voting-app.yaml
11.5 旅程的终点,也是新的起点
恭喜你,完成了这本小册子的所有学习和实践!
让我们回顾一下这趟不凡的旅程:
- 我们从 Docker 的基本概念出发,学会了用 Dockerfile 打包应用。
- 我们掌握了容器的生命周期管理,并学会了如何使用数据卷和网络。
- 我们用 Docker Compose 实现了本地开发环境的“一键启停”。
- 我们理解了 Kubernetes 为解决规模化和高可用而生的价值。
- 我们掌握了 K8s 的核心对象:Pod, Deployment, Service, ConfigMap, PVC。
- 最终,我们亲手将一个完整的微服务应用部署到了 K8s。
你现在所掌握的,已经不仅仅是一些工具的用法,而是一整套云原生应用的开发、部署和管理思想。这套思想和技能,将是你未来技术生涯中一笔宝贵的财富。
当然,Kubernetes 的世界远比我们在这本小册子中探索的要广阔得多。还有 Ingress, Helm, Prometheus, Istio 等等更多强大的工具和概念在等着你。
但这本小册子已经为你打下了最坚实的基础,为你推开了通往云原生世界的大门。
前方的路还很长,愿你保持好奇,不断探索。
Happy Hacking!