基于 Kubernetes 的云原生博客系统设计与实现:从博客功能实现到云原生部署实践
前言
最近在已有 Kubernetes 集群的基础上,我动手部署并扩展了一个云原生博客系统。这个项目一开始并不是完全从零设计的,我参考了 0xReLogic/kubernetes-for-students 中 Chapter 10 的 Personal Blog Platform 示例,但最终落地时已经不再是一个简单的教学演示,而是一个更贴近真实集群环境的多组件博客系统。
我将最终整理好的 YAML 文件公开在这个仓库中:
如果你想直接获取本文涉及的 YAML 配置内容,可以直接到上面的仓库中查看。
这篇文章主要想做三件事:
- 介绍这个博客系统的整体架构和实现思路。
- 梳理我在 Kubernetes 集群中实际部署它的完整过程。
- 总结在部署过程中遇到的问题、原因和解决方式,方便后续复现。
一、项目背景
这个项目的目标并不只是“把一个网页跑起来”,而是尽量把 Kubernetes 课程中学到的几个关键对象串起来,形成一个完整的小型云原生应用。
我希望这个博客系统至少具备下面几类能力:
- 前后端分离
- 数据持久化
- 集群内部服务发现
- 对外访问入口
- 基础的健康检查
- 基础的可观测性能力
在功能层面,我也不满足于最简单的文章新增和查询,而是继续扩展了:
- 分页
- 搜索
- 分类
- 删除
- 健康检查
- 就绪检查
- Prometheus 指标暴露
因此,这个项目最终更适合被理解为一个“基于 Kubernetes 的博客系统部署实践”,而不是一个仅用于演示的最小示例。
二、项目来源与改造方向
这个项目参考了 0xReLogic/kubernetes-for-students 的博客 capstone 示例。
原项目的特点主要是:
- 面向学生学习 Kubernetes
- 使用
minikube - 使用博客项目来串联 Deployment、Service、ConfigMap、Secret、PVC、Ingress 等对象
- 更偏向教学示例,而不是完整功能系统
我在它的基础上主要做了这些改造:
- 将部署环境从
minikube调整到真实 Kubernetes 集群 - 适配我当前使用的
kubectl v1.18.20 - 手动创建 MySQL 的 PV/PVC
- 补充删除、搜索、分类、分页等功能
- 增加
/health、/ready、/metrics - 重做前端页面,使其更适合展示“云原生博客系统”的完整性
- 增加状态检查逻辑和基础监控展示
所以,这个项目和原始示例之间的关系更准确地说是“借鉴与扩展”,而不是简单照搬。
三、系统架构设计
整个系统采用三层结构:
- 前端:Nginx
- 后端:Node.js + Express
- 数据库:MySQL 8.0.41
在 Kubernetes 中,对应关系如下:
blog-frontend:前端 Deployment,负责页面展示blog-api:后端 Deployment,提供 REST APIblog-mysql:数据库 Deployment,持久化保存文章数据Service:负责前后端、后端和数据库之间的服务发现ConfigMap:负责注入后端配置Secret:负责注入数据库用户名密码PersistentVolume / PersistentVolumeClaim:负责 MySQL 数据持久化Ingress:作为可选七层入口NodePort:作为实际可访问入口
可以用一句话概括访问链路:
浏览器 -> Nginx 前端 -> Blog API -> MySQL
除此之外,我还为后端增加了:
/health:健康检查接口/ready:就绪检查接口/metrics:指标暴露接口
这使得它不只是一个“能访问”的博客系统,也开始具备基础的云原生运行特征。
四、最终实现的核心功能
结合最终的 YAML 和内嵌代码实现,目前系统已经具备以下功能:
- 发布文章
- 查询文章列表
- 分页查看文章
- 按关键词搜索文章
- 按分类筛选文章
- 删除指定文章
- 切换文章摘要和全文
- 查看 API 健康状态
- 查看就绪状态
- 查看 Metrics 暴露状态
这也是我认为这个项目比原始示例更适合公开出来的原因之一:它已经不再只是“把 Kubernetes 对象连起来”,而是形成了一个相对完整的小型应用。
五、部署前的准备工作
在正式部署之前,需要先确认自己的环境满足这些条件:
1. 已经有可用的 Kubernetes 集群
本文默认你已经搭建好一个可以正常使用的 Kubernetes 集群,而不是从零开始安装集群。
如果你的集群还没有搭好,我后续也准备单独整理一篇关于 Kubernetes 集群搭建的文章。
2. 确认 Kubernetes 版本
我的环境使用的是:
kubectl v1.18.20
这个版本比较旧,因此有些 YAML 写法会和新版本 Kubernetes 不一样,最明显的地方就是 Ingress。
可以先检查 API 兼容性:
kubectl api-versions | grep networking
如果能看到和 networking.k8s.io 相关的输出,说明集群中存在对应网络 API;如果能看到 networking.k8s.io/v1beta1,通常说明当前环境可以直接使用本文里的 Ingress 写法。
3. 检查存储能力
因为 MySQL 需要持久化数据,所以最好先查看集群里是否有可用的 StorageClass:
kubectl get storageclass
如果能看到已经存在的存储类,说明集群具备一定的存储能力;如果没有合适的动态存储类,也可以像我这样手动创建 PV/PVC。
4. 检查 Ingress Controller
如果你希望后续通过域名访问,可以先检查是否安装过 Ingress Controller:
kubectl get pods -n ingress-nginx
如果能看到 Ingress Controller 相关 Pod,并且状态为 Running,说明控制器已经可以使用;如果没有安装,也可以先完成系统其他部分的部署,后续再补充 Ingress。
六、MySQL 持久化存储设计
数据库是整个博客系统里最需要保证数据不丢失的部分,因此我首先解决的是存储问题。
1. 在目标节点上准备数据目录
因为我采用的是 hostPath + PV/PVC 的方式,所以需要先选定一个节点来保存 MySQL 数据。
例如:
kubectl get nodes
确认节点后,在目标节点上创建目录:
ssh k8s-worker1
sudo mkdir -p /data/blog-mysql
sudo chmod 777 /data/blog-mysql
exit
这里的 777 主要是为了简化实验环境下的权限问题。正式生产环境不建议这么做,应该使用更细粒度的权限控制。
2. 创建 PV 和 PVC
我的 mysql-pv.yaml 中不仅创建了 PVC,还额外创建了一个手动 PV,并通过 nodeAffinity 指定它绑定到固定节点。
这样做的好处是:
- MySQL 数据不会随着 Pod 重建而丢失
- 数据存储位置明确
- 更接近真实集群中的持久化思路
3. 创建命名空间并应用配置
kubectl create namespace blog-platform
kubectl apply -f mysql-pv.yaml
后续可以通过下面命令检查 PVC 是否正常绑定:
kubectl get pvc -n blog-platform
kubectl get pv
如果 PVC 和 PV 状态都是 Bound,说明持久化存储已经准备好了;如果状态仍然是 Pending,一般需要继续检查节点目录、标签匹配关系和 namespace 是否正确。
七、数据库层部署
数据库层涉及两个文件:
mysql-secret.yamlmysql-deployment.yaml
1. Secret
我使用 Secret 注入了以下信息:
- root-password
- database
- user
- password
这样数据库敏感配置就不会直接硬编码在后端应用中。
2. MySQL Deployment
MySQL 使用 mysql:8.0.41 镜像,挂载到 /var/lib/mysql,并配置了:
livenessProbereadinessProbe
其中就绪探针会在数据库可以接受查询之后才将 Pod 标记为可用,这对于后续后端连接数据库很重要。
部署命令如下:
kubectl apply -f mysql-secret.yaml -n blog-platform
kubectl apply -f mysql-deployment.yaml -n blog-platform
可以通过以下命令检查运行情况:
kubectl get pods -n blog-platform
kubectl describe svc blog-mysql -n blog-platform
正确结果应该是:
- MySQL Pod 状态为
Running blog-mysqlService 已正常创建kubectl describe svc blog-mysql -n blog-platform输出中的Selector能匹配到 MySQL Pod 的标签
这里还需要特别注意 Service 的 selector 必须和 MySQL Pod 的 label 一致,否则后端即使能解析服务名,也无法正确访问数据库。
八、分类功能对应的数据库修改
由于添加分类功能会涉及数据库表结构修改,这部分更适合放在数据库层部署完成之后说明。如果数据库中的 posts 表还没有 category 列,那么分类功能将无法正常工作。
1. 找到 MySQL Pod 名称
先执行:
kubectl get pods -n blog-platform -l tier=database
正确结果应该是可以看到一个 MySQL Pod,状态通常为 Running,例如:
blog-mysql-xxxxxx-xxxxx 1/1 Running
2. 进入 Pod 并打开 MySQL 客户端
拿到 Pod 名称后执行:
kubectl exec -it <mysql-pod-name> -n blog-platform -- mysql -uroot -p
这里输入的密码就是 mysql-secret.yaml 中定义的 root-password,默认是:
RootPass123
3. 检查当前表结构
进入 MySQL 后执行:
USE blog_db;
SHOW TABLES;
DESCRIBE posts;
如果当前表结构中只有:
idtitlecontentauthorcreated_at
说明还没有添加分类字段。
4. 添加 category 列
推荐使用 ALTER TABLE,这样可以保留已有数据:
ALTER TABLE posts ADD COLUMN category VARCHAR(100) NOT NULL DEFAULT 'general';
执行完成后再次检查:
DESCRIBE posts;
正确结果应该是可以看到新增的 category 列。
5. 退出 MySQL
EXIT;
6. 数据备份(可选但推荐)
如果想更稳妥一些,可以在修改前先导出当前数据:
kubectl exec -it <mysql-pod-name> -n blog-platform -- mysqldump -uroot -pRootPass123 blog_db > backup.sql
执行成功后,会在当前本地目录中生成一个 backup.sql 文件。
7. 补充说明
当前后端代码里其实已经加入了分类字段兼容逻辑,启动时会尝试执行:
ALTER TABLE posts ADD COLUMN category VARCHAR(100) NOT NULL DEFAULT 'general';
如果字段已经存在,就忽略重复添加错误。
不过从部署和排错角度来看,手动掌握这一步仍然非常重要,因为它直接关系到分类功能能否正常运行,也更有助于理解后端功能和数据库结构之间的关系。
九、后端 API 设计与部署
后端是整个系统最核心的业务层,对应文件包括:
api-configmap.yamlapi-deployment.yaml
1. ConfigMap
后端通过 ConfigMap 注入这些配置:
DB_HOSTDB_PORTAPI_PORT
2. API 实现方式
后端使用 node:16-alpine 镜像,在容器启动时动态创建并运行一个 Express 应用。
后端逻辑主要包括:
- 初始化 MySQL 连接池
- 自动创建
posts表 - 自动兼容
category字段 - 提供
/posts、/posts/search、/posts/:id等接口 - 提供
/health、/ready、/metrics
3. 为什么我没有直接把后端代码单独做成镜像
这是因为当前项目更偏向“使用 YAML 快速复现和教学展示”,所以我把后端脚本直接写进了 Deployment 中。
这种做法的优点是:
- 复现简单
- 不需要额外构建自定义镜像
- 便于课程和实验环境快速落地
缺点也很明显:
- 工程化程度有限
- 不适合生产环境
- 后端代码和部署配置耦合较深
如果后续继续完善,这部分最值得进一步重构成独立应用源码仓库。
4. 后端部署
kubectl apply -f api-configmap.yaml -n blog-platform
kubectl apply -f api-deployment.yaml -n blog-platform
部署完成后,可以检查:
kubectl get pods -n blog-platform
kubectl logs -n blog-platform -l tier=backend
正确结果应该是:
blog-api的两个副本都能正常启动- Pod 状态为
Running - 日志中可以看到数据库初始化成功、服务监听成功等信息
我这里将后端副本数设置为 2,这样能够体现 Deployment 多副本管理的能力。
十、前端页面设计与部署
前端对应文件是:
frontend-deployment.yaml
它的特点是:前端页面和 Nginx 配置同样是动态写入容器的,而不是从外部镜像仓库中拉一个完整前端项目。
1. 前端做了哪些事情
前端不仅仅是一个文章展示页,而是一个具有“系统展示”性质的单页应用,包含:
- 首页概览
- 系统状态区
- 文章管理区
- 搜索与分类筛选
- 删除弹窗
- Metrics 展示说明
页面里还通过 /api/health、/api/ready、/api/metrics 实时检查后端状态,用于体现这个项目的云原生特征。
2. 前端和后端如何通信
在 Nginx 配置中,我将:
location /api/ {
proxy_pass http://blog-api:3000/;
}
这样浏览器请求 /api/... 时,就会转发给后端服务 blog-api。
这也是为什么前端页面中可以直接用:
const API_URL = '/api';
3. 前端部署
kubectl apply -f frontend-deployment.yaml -n blog-platform
前端副本数同样设置为 2,对应的 Service 类型为:
type: NodePort
nodePort: 30080
因此部署完成后,可以通过:
http://<NodeIP>:30080
访问博客系统。
正确结果应该是:
- 前端 Pod 状态为
Running blog-frontendService 已创建成功- 浏览器访问
http://<NodeIP>:30080时可以正常打开博客首页
十一、Ingress 配置
如果你的集群里已经安装了 Ingress Controller,还可以继续部署:
kubectl apply -f blog-ingress.yaml -n blog-platform
我的 Ingress 配置使用的是:
networking.k8s.io/v1beta1- host:
blog.local
如果 Ingress Controller 已经正常运行,并且本地已经做好 hosts 映射或 DNS 解析,就可以通过下面的地址访问:
http://blog.local
这里必须特别说明一下:这不是新版本 Kubernetes 推荐的写法,而是因为我的环境是 kubectl v1.18.20,所以需要使用旧版本 API。
这也是这个项目在公开时必须写清楚的兼容性信息之一。
十二、推荐的部署顺序
如果从头开始复现,我建议按下面顺序执行:
kubectl create namespace blog-platform
kubectl apply -f mysql-pv.yaml
kubectl apply -f mysql-secret.yaml -n blog-platform
kubectl apply -f mysql-deployment.yaml -n blog-platform
kubectl apply -f api-configmap.yaml -n blog-platform
kubectl apply -f api-deployment.yaml -n blog-platform
kubectl apply -f frontend-deployment.yaml -n blog-platform
kubectl apply -f blog-ingress.yaml -n blog-platform
部署完成后,可以用下面命令继续检查:
kubectl get ingress -n blog-platform
正确结果应该是可以看到 blog-ingress 被成功创建,并且 host 为 blog.local。
十三、页面截图展示
1. 系统主页截图

2. 文章管理页面截图

3. 系统状态页面截图

十四、部署完成后的验证方式
系统部署完成后,我通常会从下面几个方面验证:
1. 基础资源状态
kubectl get pods -n blog-platform
kubectl get svc -n blog-platform
kubectl get pvc -n blog-platform
kubectl get pv
2. 前端访问
在浏览器中访问:
http://<NodeIP>:30080
如果已经配置了 Ingress,也可以访问:
http://blog.local
3. 功能验证
重点测试:
- 能否发布文章
- 能否查看文章
- 分页是否正常
- 搜索是否正常
- 分类是否正常
- 删除是否正常
4. 状态接口验证
还可以手动验证:
/api/health
/api/ready
/api/metrics
如果这些接口可用,说明后端运行状态和可观测性入口都已经基本正常。
十五、遇到的问题与解决方式
这一部分是整个项目里我认为最有价值的地方,因为真实部署和只看教程最大的差别,就在这里。
1. PVC 无法立即正常绑定
现象
在还没有整理好 namespace 或 PV/PVC 依赖关系时,存储对象无法按预期工作。
原因
- PVC 所在 namespace 还没有创建
- 手动 PV 和 PVC 的创建顺序没有理清
- label 与 selector 对不上
解决方式
- 先创建
blog-platformnamespace - 再创建 PV/PVC
- 检查
matchLabels是否一致
实际操作中,先把 namespace 建好再统一 apply,问题会少很多。
2. Ingress Controller 不可用
现象
集群中没有安装 Ingress Controller,或者安装后 Pod 没有正常 Running。
原因
- 集群初始状态没有 Ingress Controller
- 控制器镜像拉取失败
解决方式
先检查:
kubectl get pods -n ingress-nginx
如果镜像拉不下来,可以先手动拉取对应镜像,再让 Kubernetes 复用本地镜像。
另外,在实验环境里,NodePort 和 Ingress 都可以保留:前者适合直接访问验证,后者适合基于域名进行访问。
3. Service selector 与 Pod label 不匹配
现象
Service 已经创建成功,但前端访问后端或后端访问数据库依旧异常。
原因
Kubernetes Service 能否转发流量,核心依赖 selector 是否选中了正确的 Pod。
例如 MySQL 的 Service:
- selector:
app: blog、tier: database
如果 Pod 没有这组标签,Service 就相当于“没有后端”。
解决方式
检查:
kubectl get pods -n blog-platform --show-labels
kubectl describe svc blog-mysql -n blog-platform
逐项对比 selector 和 labels。
4. 旧版本 Kubernetes API 兼容性问题
现象
某些网上的 YAML 示例在本地集群中无法直接使用。
原因
我的集群使用的是 kubectl v1.18.20,而很多新教程默认使用的是更新版本的 API。
解决方式
在公开项目和博客中明确标注版本,并使用适配旧版本的写法,例如:
Ingress使用networking.k8s.io/v1beta1
这类兼容性说明必须提前告诉读者,否则别人很容易觉得“照着做却跑不起来”。
5. 后端镜像版本和脚本实现需要适配
现象
原始教学示例使用的是 node:18-alpine,但在我实际整理和适配过程中,最终改成了 node:16-alpine。
原因
实验环境、依赖安装方式和整体稳定性需要综合考虑。
解决方式
根据自己实际集群与运行情况选择最终可稳定工作的镜像版本,而不是机械照抄示例。
6. 前端能打开,但功能不完整
现象
系统页面打开之后,并不意味着整个系统已经完全可用。
原因
前端只是入口页面,真正的功能依赖后端 API 和数据库都要正常。
解决方式
除了看页面是否能打开,还要继续验证:
/api/health/api/ready/api/metrics- 文章 CRUD 是否正常
这也是我为什么在前端页面里加入状态看板的原因之一。
十六、总结
这个博客系统项目让我对 Kubernetes 的理解不再停留在“知道有哪些对象”,而是开始真正理解:
- Deployment 如何管理副本
- Service 如何完成服务发现
- ConfigMap 和 Secret 如何解耦配置
- PV/PVC 如何为数据库提供持久化能力
- NodePort 和 Ingress 如何承担访问入口
- 健康检查、就绪检查和 metrics 如何体现云原生应用的运行特征
如果只是看教程,很多内容看上去都很顺;但当你真正开始在集群里部署时,PV、selector、Ingress、版本兼容、探针和访问路径这些问题会一个个冒出来。也正因为这样,这个项目才真正让我把 Kubernetes 的基础对象串成了一套完整实践。
后续如果继续完善,我还想继续做两件事:
- 单独整理一篇 Kubernetes 集群搭建文章
- 将当前这种“YAML 内嵌代码”的方式继续重构成更工程化的前后端项目结构
如果你也在做类似的 Kubernetes 部署练习,希望这篇文章和上面的仓库能帮你少踩一些坑。
更多推荐



所有评论(0)