Skip to content

这份文档是什么

一份可以照抄的通用部署手册:从 9 台白板服务器开始,把「若依微服务(已拆分为独立代码仓库)」跑在自建 K8s 集群上,并用 Jenkins 实现「提交代码 → 自动构建 → 自动部署」的全流程。

  • 不依赖任何特定环境:网段使用 192.168.0.0/24,IP 与主机名都可按你的实际规划替换。
  • 中间件尽量独立部署:数据库、缓存、注册中心、镜像仓库各占一台,避免互相抢资源。
  • 基础中间件只给"要准备成什么样":Gitea / MySQL / Redis 属于「装好即可、与本文主题无关」的部分,只在表格里给出 IP 与必须的配置要求,不展开安装步骤。
  • K8s、Harbor、Nacos 给完整步骤:它们是本方案的核心架构组件,逐步写清。
  • 每一条命令都有解释:命令块下方用「这条命令在做什么」说明它的作用与为什么需要它。

关于"版本"的两个提醒

  1. 本文档中的版本号是写作时实测可用的最新稳定版。K8s 生态迭代很快,半年后建议重新确认一次(尤其是 K8s 主版本)。
  2. 所有下载地址都实测可用,并尽量走国内镜像。第 12 章有一张完整的源地址汇总表,便于你替换。

01 环境规划与目标架构

1.1 服务器规划总表

本文档全程使用下面 9 台服务器。建议先按这个表格把机器开出来、装好 Rocky Linux 9 (或 AlmaLinux 9 / CentOS Stream 9),再往下看。

#IP主机名配置系统盘角色在这台机器上部署的东西
1192.168.0.10k8s-master4C8G100GK8s 控制平面kube-apiserver / etcd / controller-manager / scheduler、containerd、kubectl
2192.168.0.11k8s-node18C16G200GK8s 工作节点kubelet、containerd、业务 Pod(若依微服务)
3192.168.0.12k8s-node28C16G200GK8s 工作节点kubelet、containerd、业务 Pod
4192.168.0.20ci-jenkins8C16G200GCI 构建机Jenkins、JDK 17、Maven 3.8.9、Docker
5192.168.0.21harbor4C8G500G镜像仓库Harbor 2.15.2 + Docker(含内置 PostgreSQL/Redis)
6192.168.0.22mysql4C8G200G数据库MySQL 8.0(只给准备要求,不展开安装
7192.168.0.23redis2C4G50G缓存Redis 7(只给准备要求
8192.168.0.24nacos4C8G100G注册中心 + 配置中心Nacos 2.5.4 + JDK 17(完整部署步骤
9192.168.0.25git2C4G100G代码仓库Gitea 1.27.3(只给准备要求

为什么要这么分

三个原则,也是这套规划的依据:

  1. 构建机与集群分开:Jenkins 跑 Maven 打包很吃 CPU/内存(7 个微服务串行构建时单核经常打满)。如果和 K8s 节点挤在一起,构建时的资源尖峰会直接造成业务 Pod 被驱逐。
  2. 镜像仓库独立:Harbor 要存所有历史镜像,磁盘占用增长快(500G 起步不夸张);同时它又是所有节点拉镜像的必经之路,挂了就全线瘫痪,所以不该和别的服务抢 IO。
  3. 中间件一台一个:MySQL、Redis、Nacos 三者的资源模型完全不同(MySQL 吃内存和磁盘 IO、Redis 吃内存和网络、Nacos 吃 CPU 和长连接)。混部时任何一个出问题都会连带影响其他两个,排查成本极高。

1.2 网络与访问关系

text
开发机 ──git push──▶ Gitea(192.168.0.25:3000)

                       │ webhook / 轮询 prod* 分支

                  Jenkins(192.168.0.20:8080)
                       │ 1) mvn package
                       │ 2) docker build
                       │ 3) docker push

                  Harbor(192.168.0.21:10086)

                       │ 4) kubectl apply(节点从这里拉镜像)

      ┌──────────── K8s 集群 ────────────┐
      │ master(10)  node1(11)  node2(12) │
      │   命名空间 ruoyi 下的 7 个微服务  │
      └──────────────┬───────────────────┘
                     │ 运行时依赖

   MySQL(22:3306)   Redis(23:6379)   Nacos(24:8848)

需要放通的端口(云服务器记得同步配置安全组):

目标端口用途
全部 K8s 节点全部 K8s 节点6443 2379-2380 10250 10257 10259 4789(UDP)控制平面与 Calico VXLAN
K8s 全部节点Harbor10086拉取镜像
JenkinsHarbor10086推送镜像
JenkinsK8s master6443kubectl apply
JenkinsGitea3000拉代码、回写状态
K8s 全部节点Nacos / MySQL / Redis8848,9848,9849 / 3306 / 6379业务运行时依赖
使用者Jenkins / Harbor / Gitea / K8s NodePort8080 / 10086 / 3000 / 30000-32767管理与访问

NodePort 的范围不能随便改

K8s 默认只允许 30000-32767 作为 NodePort。若依各服务用的 3108030090 这些都在这段区间内,属于合规用法。

1.3 版本清单

组件版本说明
操作系统Rocky Linux 9.x与 RHEL 9 系完全兼容;AlmaLinux 9 / CentOS Stream 9 同样适用
Kubernetesv1.36.5写作时 1.36 系列的最新补丁版
容器运行时containerd.io 2.x从 Docker 官方 centos 源安装(Rocky 自带源版本偏旧)
CNI 网络插件Calico v3.32.2VXLAN 模式,无需 BGP 配置,适合自建小集群
内核代理kube-proxy IPVS 模式大规模 Service 下比 iptables 性能更好
JDK17若依 Cloud 要求 JDK 17;Nacos 2.5.x 也要求 JDK 8+(这里统一用 17)
Maven3.8.9与常见 Jenkinsfile 中 tools { maven 'Maven-3.8.9' } 对应
Harborv2.15.2离线安装包方式部署
Nacos2.5.4若依 Cloud 用的注册中心 + 配置中心
Jenkins2.528.3通过 rpm 包直接安装
MySQL8.0需开启 utf8mb4、允许远程连接
Redis7.x需设置 requirepassbind

版本兼容性不是"越新越好"

写这份文档时,K8s 官方最新是 v1.37.1,而 Argo CD 等周边工具的支持策略是「只覆盖最近 3 个 minor 版本」。选 1.36 是刻意留出余量:它既不是刚发布、生态还没跟上的最新版,又落在所有主流工具的支持窗口内。

关于工具链的版本选择方法论,见第 12 章末尾。

1.4 最终要达成的效果

一条命令级别的验收标准:

bash
# 在开发机上改一行代码
vim ruoyi-gateway/src/main/java/.../SomeController.java

# 提交并推送
git commit -am "feat: add health detail" && git push origin prod-k8s

# 之后不再需要人工干预:Jenkins 自动编译、打镜像、推仓库、滚动更新
# 约 2~4 分钟后(取决于依赖缓存),验证:
curl -s http://192.168.0.11:31080/actuator/health

这就是本文档要交付的终态。

02 阶段一:K8s 三节点的系统初始化

本章在 192.168.0.10 / .11 / .12 三台机器上执行相同的操作。为了叙述清楚,下面用「每台节点」表示需要重复执行。

先做一次全量升级

bash
# 更新所有已安装的软件包到最新(首次装完系统后做一次即可)
dnf update -y

# 安装后续必用的基础工具
dnf install -y vim wget curl net-tools telnet lsof bash-completion git tar chrony

# 重启使内核更新生效(换了内核的机器必须重启,否则 containerd 会跑在旧内核上)
reboot

这条命令在做什么

  • dnf update -y:把所有软件包升到最新。这一步不能省,因为后面要用到的内核特性(overlayfs、br_netfilter、IPVS 模块)在旧内核上可能缺失。
  • dnf install ...:一次装齐排障时会用到的工具。很多"教程里命令跑不通"的问题,本质是缺 net-toolslsof
  • rebootdnf update 如果更新了内核,必须重启才生效,否则会出现"内核模块加载失败"这种很难查的问题。

2.1 主机名与 hosts 解析

bash
# ===== 在 192.168.0.10 上执行 =====
hostnamectl set-hostname k8s-master
# ===== 在 192.168.0.11 上执行 =====
hostnamectl set-hostname k8s-node1
# ===== 在 192.168.0.12 上执行 =====
hostnamectl set-hostname k8s-node2

这条命令在做什么

  • hostnamectl set-hostname:设置主机名。K8s 会把主机名注册为节点名,一旦集群建好再改主机名,需要 kubectl delete node 后重新加入,非常折腾——所以必须先改主机名再装 K8s
bash
# 在【全部三台】上执行:写入统一的 hosts 解析
cat >> /etc/hosts <<'EOF'
192.168.0.10  k8s-master
192.168.0.11  k8s-node1
192.168.0.12  k8s-node2
192.168.0.20  ci-jenkins
192.168.0.21  harbor
192.168.0.22  mysql
192.168.0.23  redis
192.168.0.24  nacos
192.168.0.25  git
EOF

# 验证解析是否生效
ping -c 1 k8s-master && ping -c 1 harbor

这条命令在做什么

  • cat >> /etc/hosts <<'EOF' ... EOF:把 9 台机器的域名解析写进本地 hosts 文件。这样后面所有配置里都可以写 harbor 而不是 192.168.0.21
  • 为什么值得这么做:镜像地址、Nacos 地址会出现在很多份 YAML 里,写域名意味着以后换 IP 只需改一处。
  • ⚠️ 如果你们有内部 DNS 服务器,更推荐在 DNS 里做解析,hosts 只是最省事的替代方案。

2.2 关闭 Swap

bash
# 临时关闭(立即生效,但重启后会恢复)
swapoff -a

# 永久关闭(注释掉 fstab 里的 swap 行)
sed -i '/swap/s/^/#/' /etc/fstab

# 验证:输出的 Swap 行应该全是 0
free -h

这条命令在做什么

  • swapoff -a:关闭交换分区。K8s 的调度器按「内存请求」做决策,如果内存可以被换出到磁盘,调度器就无法准确判断真实内存压力,会导致节点超卖后集体雪崩。
  • sed -i '/swap/s/^/#/' /etc/fstab:在 /etc/fstab 中把含 swap 的行前面加 # 注释掉,否则重启后 swap 会重新启用。
  • free -h:确认 Swap 行显示为 0B这一步是硬性要求,kubelet 启动时检测到 swap 开启会直接拒绝启动。

2.3 内核参数与模块

bash
# 1) 加载必需的四个内核模块
cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
ip_vs
ip_vs_rr
EOF

# 2) 立即加载(不用等重启)
modprobe overlay
modprobe br_netfilter
modprobe ip_vs
modprobe ip_vs_rr

# 3) 写入内核参数
cat > /etc/sysctl.d/k8s.conf <<'EOF'
# 允许 iptables 检查桥接流量(Calico/Service 转发必需)
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
# 开启 IPv4 转发(Pod 跨节点通信必需)
net.ipv4.ip_forward                 = 1
# 内存溢出时不要直接 panic,先杀进程
vm.panic_on_oom                     = 0
vm.overcommit_memory                = 1
# 允许更多文件描述符与连接跟踪(节点上跑几十个 Pod 时会用到)
fs.inotify.max_user_instances       = 8192
fs.inotify.max_user_watches         = 524288
net.netfilter.nf_conntrack_max      = 1048576
EOF

# 4) 应用参数
sysctl --system

# 5) 验证:这三项都必须为 1
sysctl net.bridge.bridge-nf-call-iptables net.ipv4.ip_forward
lsmod | grep -E "ip_vs|br_netfilter|overlay"

这条命令在做什么

  • overlay:容器文件系统的驱动模块,containerd 默认用它做镜像层叠加。
  • br_netfilter:让内核的 iptables 能"看见"网桥上的流量。没有它,同一个节点上不同 Pod 之间的网络会不通,而且报错信息非常隐晦。
  • ip_vs*:IPVS 负载均衡模块。虽然文档后面用的是默认的 iptables 模式,但提前加载好,将来切 IPVS 时不用再动。
  • net.ipv4.ip_forward = 1:开启内核转发。K8s 的 Service、Pod 跨节点通信全靠它。
  • nf_conntrack_max:连接跟踪表上限。默认值在多 Pod 场景下会被打满,表现为"网络随机超时",且日志里只有 nf_conntrack: table full, dropping packet

如何确认这些参数真的生效了

sysctl --system 会重新读取 /etc/sysctl.d/ 下的所有配置。执行后不要只看"没报错",要显式 sysctl <key> 验证一遍——写错的参数名不会报错,只会静默忽略。

2.4 时间同步

bash
# 启动并设置 chronyd 开机自启
systemctl enable --now chronyd

# 查看时间同步状态
chronyc sources -v
chronyc tracking

# 手动立即校时(首次可能偏差较大时使用)
chronyc makestep

这条命令在做什么

  • systemctl enable --now chronydenable 设置开机自启,--now 表示同时立即启动。这是最常用的组合写法,等价于 systemctl start + systemctl enable
  • chronyc sources -v:列出时间源,前面有 ^* 的表示当前正在使用的源。
  • chronyc makestep:强制立即跳变校正时间,而不是缓慢调整。

时间不同步会导致证书验证失败

K8s 各组件之间用 TLS 双向认证,如果两台机器的时间差超过证书有效期校验的容忍范围(通常几分钟),会报 x509: certificate has expired or is not yet valid。这个错误看起来像证书问题,实际原因往往是时间不对

2.5 防火墙与 SELinux

自建环境建议直接关掉,云环境请用安全组代替

K8s 会动态创建大量 iptables 规则和随机端口,用 firewalld 手工放行非常容易漏。内网自建集群的通行做法是关闭 firewalld,改用云安全组 / 网络 ACL 做边界防护。

bash
# 停止并禁用 firewalld
systemctl disable --now firewalld

# 确认状态应为 inactive (dead)
systemctl status firewalld --no-pager

# 把 SELinux 设为宽容模式(permissive:只记录不阻止)
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

# 验证:应输出 Permissive
getenforce

这条命令在做什么

  • setenforce 0:把 SELinux 切到 permissive(只警告不拦截),立即生效但重启失效
  • sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config:改配置文件,让重启后依然生效。
  • 为什么用 permissive 而不是 disableddisabled 会导致容器挂载某些文件系统时行为异常;permissive 既不影响 K8s 运行,又保留了审计日志,出问题时还能查。

2.6 安装并配置 containerd

K8s 需要一个实现了 CRI(Container Runtime Interface)的容器运行时。containerd 是目前最主流的选择(Docker 内部也在用它)。

bash
# 1) 添加 Docker 官方源(用阿里云镜像,国内速度稳定)
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 2) 生成 dnf 缓存,让新仓库的元数据可用
dnf makecache

# 3) 安装 containerd.io(会自动选最新稳定版,即 2.x)
dnf install -y containerd.io

# 4) 验证安装
containerd --version
runc --version

这条命令在做什么

  • dnf config-manager --add-repo <url>:把一个远程仓库配置文件下载到 /etc/yum.repos.d/。阿里云镜像的路径结构与 Docker 官方完全一致,所以直接换域名即可。
  • dnf makecache:拉取仓库元数据并缓存。加完新仓库一定要执行,否则下一步会报 Cannot find a valid baseurl
  • dnf install -y containerd.io:安装 containerd。选 containerd.io 而不是 containerd,是因为 Docker 官方打包的版本更新更快、且同时提供 runc
  • 为什么不装 Docker:K8s 1.24 起已移除 dockershim,不能再直接用 Docker 作为运行时。集群节点只需 containerd;只有 Jenkins 那台构建机需要 Docker(用来打包镜像)。
bash
# 5) 生成默认配置(containerd 默认没有配置文件,必须先生成)
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml

# 6) 关键改动一:cgroup 驱动改为 systemd(与 kubelet 保持一致)
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# 7) 关键改动二:把 pause 镜像换成国内源
sed -i 's#sandbox_image = "registry.k8s.io/pause:.*"#sandbox_image = "m.daocloud.io/registry.k8s.io/pause:3.10"#' /etc/containerd/config.toml

# 8) 校验这两处改动确实生效了
grep -n "SystemdCgroup" /etc/containerd/config.toml | head -2
grep -n "sandbox_image" /etc/containerd/config.toml

这条命令在做什么

  • containerd config default > /etc/containerd/config.toml:把内置的默认配置导出成文件。必须先导出再改——直接手写容易漏字段,而 containerd 的配置项在不同版本间有增删。
  • SystemdCgroup = true:让 containerd 使用 systemd 管理 cgroup。这是最容易出事的一处:kubelet 默认用 systemd cgroup,如果 containerd 用 cgroupfs,两者不一致会导致容器启动后立刻被 OOM Kill,且 describe pod 里只看到重启计数,看不到原因。
  • sandbox_image:Pod 里那个"什么都不做、只为占住网络命名空间"的 pause 容器。默认地址是 registry.k8s.io,国内拉不到,不换这个,任何 Pod 都起不来(状态会卡在 ContainerCreating)。

pause 镜像的版本怎么确定

K8s 每个版本要求的 pause 版本写在源码里(1.36 对应 pause:3.10)。如果你用了别的 K8s 版本,可以在 kubeadm 的配置文件里查看,或用下面的命令反查现有集群:

bash
kubeadm config images list | grep pause
bash
# 9) 配置镜像加速(针对 Docker Hub 的镜像)
mkdir -p /etc/containerd/certs.d/docker.io

cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
server = "https://docker.io"

[host."https://m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF

# 10) 启动 containerd 并设置开机自启
systemctl daemon-reload
systemctl enable --now containerd

# 11) 确认状态为 active (running)
systemctl status containerd --no-pager

这条命令在做什么

  • /etc/containerd/certs.d/<registry>/hosts.toml:containerd 1.5+ 引入的按仓库粒度的镜像源配置。这段配置的含义是「拉取 docker.io 的镜像时,实际去 m.daocloud.io 取」。比全局 registry.mirrors 更精细,且不会影响其他 registry。
  • capabilities = ["pull", "resolve"]:声明这个镜像站支持拉取与解析。漏掉这一行,containerd 会忽略该镜像源
  • systemctl daemon-reload:修改了 unit 文件或依赖后才需要;这里加上是无害的保险动作。

配置镜像加速的常见误区

网上很多教程教你在 /etc/containerd/config.toml 里写 [plugins."io.containerd.grpc.v1.cri".registry.mirrors]。这在旧版本可行,但 containerd 2.x 推荐用 certs.d 目录方式,两者混用会出现"配置了却不生效"的现象。

判断方法:改完配置重启 containerd,然后 crictl pull hello-world 并观察报错里的实际拉取地址。

2.7 安装 kubeadm / kubelet / kubectl

bash
# 1) 添加 K8s 阿里云镜像源(v1.36 版本线)
cat > /etc/yum.repos.d/kubernetes.repo <<'EOF'
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/repodata/repomd.xml.key
EOF

# 2) 刷新缓存
dnf makecache

# 3) 查看仓库里有哪些可用版本
dnf list available kubeadm --showduplicates | tail -5

# 4) 安装三件套
dnf install -y kubelet kubeadm kubectl

# 5) 锁定版本,防止被 dnf update 意外升级
dnf versionlock add kubelet kubeadm kubectl 2>/dev/null || \
  echo -e "kubelet\nkubeadm\nkubectl" >> /etc/dnf/versionlock.list

# 6) 设置 kubelet 开机自启(注意:这里只 enable,不要 start)
systemctl enable kubelet

# 7) 验证版本
kubeadm version
kubectl version --client

这条命令在做什么

  • baseurl=https://mirrors.aliyun.com/kubernetes-new/...:阿里云维护的 K8s 官方源镜像。路径里的 v1.36 决定了装哪个大版本,想换版本就改这一处。
  • gpgcheck=1 + gpgkey=...:校验包的签名,防止被篡改。阿里云的这个源提供 repomd.xml.key,实测可访问。
  • dnf versionlock(或写入 versionlock.list):锁定版本非常重要。K8s 对 kubeletkube-apiserver 的版本差有严格限制(kubelet 不能比 apiserver 新),一次不经意的 dnf update 就可能升出问题。
  • systemctl enable kubelet只 enable 不 start):kubelet 此时还没有配置文件(/var/lib/kubelet/config.yaml),直接启动会反复重启报错。kubeadm init 会自动生成配置并启动它。这是完全正常的现象,不要以为是装错了

首次执行 kubeadm 前,顺手把镜像先拉下来

bash
# 预拉取 K8s 控制平面所需的全部镜像(用国内源,避免 init 时卡住)
kubeadm config images pull --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers

这条命令在做什么

  • kubeadm 会在 init 时拉取 kube-apiserveretcdcoredns 等约 8 个镜像。默认从 registry.k8s.io 拉取,国内基本会超时,导致 init 卡在 [kubelet-check] 或直接失败。
  • 提前手动拉好,可以让 init 过程快很多,且失败时更容易定位。
  • 实测阿里云的 google_containers 仓库已经有 v1.36.5 的镜像,可以直接使用。

03 阶段二:初始化 K8s 集群

3.1 只在 master 上执行:kubeadm init

先准备配置文件。用配置文件而不是一长串命令行参数,便于以后重放和查阅。

bash
# 在 192.168.0.10 上执行
mkdir -p /root/k8s-install && cd /root/k8s-install

cat > kubeadm-config.yaml <<'EOF'
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.0.10
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///var/run/containerd/containerd.sock
  imagePullPolicy: IfNotPresent
  kubeletExtraArgs:
    # 关键:与 containerd 的 SystemdCgroup 保持一致
    cgroup-driver: systemd
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.5
# 关键:把控制平面镜像源换成国内地址
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
clusterName: ruoyi-cluster
controlPlaneEndpoint: "192.168.0.10:6443"
networking:
  # Service 网段与 Pod 网段都不能和你的物理网段 192.168.0.0/24 重叠
  serviceSubnet: 10.96.0.0/12
  podSubnet: 10.244.0.0/16
  dnsDomain: cluster.local
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
# 大规模 Service 场景下 IPVS 性能优于 iptables;小集群用 iptables 也可以
mode: ipvs
ipvs:
  scheduler: rr
EOF

这个配置在做什么(逐项说明)

字段作用与注意点
advertiseAddress192.168.0.10API Server 对外通告的地址。必须是其他节点能访问到的 IP,不能写 127.0.0.1
criSocketunix:///var/run/containerd/containerd.sock告诉 kubelet 用 containerd。K8s 1.24+ 没有默认值,不写会直接 init 失败
cgroup-driversystemd必须与 containerd 的 SystemdCgroup = true 一致
imageRepository阿里云地址控制平面镜像源。国内环境这一项必填
serviceSubnet10.96.0.0/12Service 的 ClusterIP 从这里分配
podSubnet10.244.0.0/16Pod IP 从这里分配。必须与 CNI 插件配置一致(下一节的 Calico 就是按这个值改的)
mode: ipvsIPVS负载均衡模式。选 IPVS 是因为它在 Service 数量上千后仍能保持 O(1) 查找性能

网段不能和物理网络重叠

podSubnetserviceSubnet 一定不能落在你的内网网段(这里是 192.168.0.0/24)里。一旦重叠,会出现"某些 IP 无法访问"这种极难排查的问题——因为内核会认为目标在自己的物理网段,直接发 ARP 而不是走隧道。

bash
# 执行初始化
kubeadm init --config kubeadm-config.yaml --upload-certs 2>&1 | tee kubeadm-init.log

这条命令在做什么

  • --config:使用上面写的配置文件。
  • --upload-certs:把控制平面证书加密后上传到集群的 Secret 中。单 master 集群其实用不到(只有要加第二个 master 时才需要),但加上无害,将来扩容方便。
  • | tee kubeadm-init.log一定要保存输出。日志末尾的 kubeadm join 命令是节点加入集群的唯一凭据,丢了就得重新生成 token。

初始化成功的输出末尾会是这样:

text
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.0.10:6443 --token abcdef.0123456789abcdef \
        --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxx
bash
# 配置 kubectl 的访问凭据
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

# 验证集群已就绪(此时节点应该是 NotReady,因为还没装 CNI)
kubectl get nodes
kubectl cluster-info

这条命令在做什么

  • admin.conf集群管理员凭据,包含集群 CA 和客户端证书。拷到 ~/.kube/config 后,kubectl 才能免参数连接 API Server。
  • kubectl get nodes 显示 NotReady预期行为——节点缺少 CNI 网络插件,Pod 网络尚未就绪。装上 Calico 后会自动变 Ready

忘记保存 join 命令怎么办

bash
# 重新生成一个永不过期的 join token
kubeadm token create --print-join-command

这条命令会直接输出完整可用的 kubeadm join 语句,任何时候都能重新生成,不必惊慌

3.2 安装 Calico 网络插件(CNI)

bash
# 回到 192.168.0.10 执行
cd /root/k8s-install

# 下载 Calico 官方清单(走国内加速,实测 206 可下载)
curl -fL -o calico.yaml \
  https://gh-proxy.com/https://raw.githubusercontent.com/projectcalico/calico/v3.32.2/manifests/calico.yaml

# 确认文件完整(约 250KB 以上)
ls -lh calico.yaml

这条命令在做什么

  • curl -fL-f 表示 HTTP 错误码时返回非零退出码(避免把错误页保存成文件),-L 表示跟随重定向。
  • gh-proxy.com/https://raw.githubusercontent.com/...这是最实用的一个技巧——在 GitHub 原始地址前加 https://gh-proxy.com/ 前缀,即可通过国内节点加速访问。对 release 下载、raw 文件都有效。
bash
# 关键:把 Calico 的默认 Pod 网段改成和 kubeadm 一致
sed -i 's#- name: CALICO_IPV4POOL_CIDR#- name: CALICO_IPV4POOL_CIDR#' calico.yaml

# 先查一下清单里默认的网段是什么
grep -A 1 "CALICO_IPV4POOL_CIDR" calico.yaml | head -4

这条命令在做什么

  • Calico 清单里默认的 CALICO_IPV4POOL_CIDR 是注释状态的(# - name: CALICO_IPV4POOL_CIDR / # value: "192.168.0.0/16")。
  • 默认值是 192.168.0.0/16,而你的物理网段是 192.168.0.0/24,正好落在里面! 这是必须处理的一个坑。
bash
# 方案:显式取消注释并设为 podSubnet 的值
sed -i 's|^\( *\)# *- name: CALICO_IPV4POOL_CIDR|\1- name: CALICO_IPV4POOL_CIDR|' calico.yaml
sed -i 's|^\( *\)# *value: "192.168.0.0/16"|\1value: "10.244.0.0/16"|' calico.yaml

# 确认修改结果:应该能看到取消注释后的两行
grep -B1 -A1 "CALICO_IPV4POOL_CIDR" calico.yaml | head -6

这是本方案里最容易忽略、后果最严重的一处配置

如果你的内网网段恰好是 192.168.0.0/24(本文档的规划),而 Calico 用默认的 192.168.0.0/16 分配 Pod IP,就会出现:

  • Pod 拿到了 192.168.0.x 的地址,与集群节点的物理 IP 撞车
  • 内核认为这些地址在同一网段,直接发 ARP 而不走隧道
  • 表现为:部分 Pod 之间网络时通时断,跨节点访问随机失败

务必确认 Pod 网段与物理网段不重叠。同理,10.244.0.0/16 也不能和你的其他内网重叠。

bash
# 应用清单
kubectl apply -f calico.yaml

# 观察 Calico 各组件启动(大约需要 1~2 分钟)
kubectl get pods -n kube-system -l k8s-app=calico-node -w
# 看到全部 Running 后按 Ctrl+C

# 确认节点变成 Ready(关键验收点)
kubectl get nodes

这条命令在做什么

  • kubectl apply -f calico.yaml:清单里包含 DaemonSet(每个节点一个 calico-node)、Deployment(calico-kube-controllers)、CRD 与 RBAC 等约 20 个资源。
  • kubectl get pods -w-w 即 watch,实时刷新直到状态稳定。这是观察部署过程最直观的方式
  • kubectl get nodes:CNI 就绪后,节点状态会从 NotReady 变为 Ready如果一直是 NotReady,99% 是 podSubnet 配错或 pause 镜像拉不到。

3.3 工作节点加入集群

bash
# 在 master 上取一次 join 命令(即使之前记下了,也建议重新生成以免 token 过期)
kubeadm token create --print-join-command

输出形如:

text
kubeadm join 192.168.0.10:6443 --token qwerty.abcdefghijklmnop \
        --discovery-token-ca-cert-hash sha256:1a2b3c...
bash
# 在 192.168.0.11 与 192.168.0.12 上分别执行上面输出的命令
kubeadm join 192.168.0.10:6443 --token <你的token> \
        --discovery-token-ca-cert-hash sha256:<你的hash>

这条命令在做什么

  • --token:24 小时有效的引导令牌,用于向 API Server 证明"我有权加入"。
  • --discovery-token-ca-cert-hash:集群 CA 证书的哈希。节点用它校验拿到的 CA 证书是否真的是本集群的,防止加入一个伪造的集群
  • 加入过程会:下载 CA → 生成 kubelet 客户端证书 → 向 API Server 发起 CSR → 由 controller-manager 自动批准(因为 token 权限足够)→ kubelet 启动。
bash
# 回到 master 验证
kubectl get nodes -o wide

预期输出(三个节点都是 Ready):

text
NAME         STATUS   ROLES           AGE   VERSION   INTERNAL-IP    OS-IMAGE
k8s-master   Ready    control-plane   12m   v1.36.5   192.168.0.10   Rocky Linux 9.x
k8s-node1    Ready    <none>          3m    v1.36.5   192.168.0.11   Rocky Linux 9.x
k8s-node2    Ready    <none>          2m    v1.36.5   192.168.0.12   Rocky Linux 9.x

如果你希望业务 Pod 也能跑在 master 上

kubeadm 默认给控制平面节点打上 node-role.kubernetes.io/control-plane:NoSchedule 污点,业务 Pod 不会被调度上去。

bash
# 移除污点(小集群节省资源时可用;生产环境不建议)
kubectl taint nodes k8s-master node-role.kubernetes.io/control-plane:NoSchedule-

# 确认:输出应为 "node/k8s-master untainted"

这条命令在做什么taint ... -(末尾的减号)表示删除这个污点。这是 K8s 里"反向操作加号变减号"的通用约定。

3.4 集群健康检查清单

这一段建议逐条执行。每一条都对应一类常见故障。

bash
# 1) 节点状态:三个都应为 Ready
kubectl get nodes

# 2) 系统组件:所有 Pod 应为 Running
kubectl get pods -n kube-system

# 3) 集群信息与 DNS
kubectl cluster-info

# 4) 看控制平面组件健康(各组件自带 /healthz 端点)
kubectl get --raw /healthz
kubectl get --raw /livez?verbose | head -20

# 5) 验证 DNS 是否工作(起一个临时 Pod 测试)
kubectl run dns-test --image=m.daocloud.io/docker.io/library/busybox:1.37 --rm -it --restart=Never \
  -- nslookup kubernetes.default
# 应输出 kubernetes.default.svc.cluster.local 的地址

# 6) 验证跨节点 Pod 通信(关键!)
kubectl run net-test --image=m.daocloud.io/docker.io/library/nginx:alpine --port=80
kubectl expose pod net-test --port=80 --target-port=80
kubectl run curl-test --image=m.daocloud.io/docker.io/library/busybox:1.37 --rm -it --restart=Never \
  -- wget -qO- net-test
# 应输出 nginx 的欢迎页 HTML

# 清理测试资源
kubectl delete pod net-test --force --grace-period=0
kubectl delete svc net-test

这些命令在做什么

  • kubectl get --raw /healthz:直接读取 API Server 的健康端点。返回 ok 即为正常。
  • kubectl run ... --rm -it --restart=Never:一个临时排障 Pod 的标准写法 —— --rm 退出即删除、-it 交互式、--restart=Never 表示只跑一次(这样才会有退出状态而不是反复重启)。
  • nslookup kubernetes.default:验证 CoreDNS 是否正常。如果这一步失败,Pod 之间就无法通过 Service 名互相访问,若依微服务一定跑不通(它们全靠服务名互相调用)。
  • wget -qO- net-test:验证 Service 的负载均衡与跨节点网络。

3.5 常用排错对照表

现象最可能的原因排查命令
节点一直 NotReadyCNI 未装 / podSubnet 不匹配 / pause 镜像拉不到kubectl describe node k8s-node1Conditionsjournalctl -u kubelet -n 100
Pod 卡在 ContainerCreatingpause 镜像拉取失败、CNI 未就绪kubectl describe pod <name> -n <ns>Events
Pod 卡在 ImagePullBackOff镜像地址错误 / 无 imagePullSecrets / Harbor 证书问题kubectl describe pod 看 Events 里的具体错误
init 卡在 [wait-control-plane]镜像拉取慢或失败crictl images 看镜像是否齐;journalctl -u kubelet
x509: certificate expired节点时间不同步chronyc trackingdate
容器反复重启但无日志containerd / kubelet 的 cgroup 驱动不一致对比 /etc/containerd/config.tomlSystemdCgroup 与 kubelet 的 cgroup-driver
Pod 网络时通时断Pod 网段与物理网段重叠kubectl get pods -o wide 看 Pod IP 是否落在内网段
nf_conntrack: table full连接跟踪表满调大 net.netfilter.nf_conntrack_max

04 阶段三:Harbor 镜像仓库

Harbor 有两个角色:Jenkins 往里推镜像K8s 节点从里拉镜像。它是整条流水线上最关键的一环,所以给完整的部署步骤。

4.1 在 192.168.0.21 上准备环境

bash
# 1) 装 Docker(Harbor 的安装脚本与 docker-compose 依赖它)
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# 2) 启动 Docker
systemctl enable --now docker

# 3) 配置 Docker 的国内镜像加速(构建机与仓库机都建议配)
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": ["https://docker.m.daocloud.io"],
  "insecure-registries": ["harbor:10086", "192.168.0.21:10086"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" },
  "data-root": "/var/lib/docker"
}
EOF

# 4) 重启使配置生效
systemctl restart docker
docker info | grep -A3 "Registry Mirrors"

这条命令在做什么

  • registry-mirrors:Docker Hub 的镜像加速地址。注意这不影响 Harbor 自身的运行,只影响从 Docker Hub 拉镜像的速度
  • insecure-registries:把 Harbor 加入"非 HTTPS 可信列表"。 为什么需要:Harbor 默认用自签证书或 HTTP 方式提供服务,而 Docker 客户端默认只允许 HTTPS,不加这一项会报 http: server gave HTTP response to HTTPS client。 ⚠️ 生产环境建议给 Harbor 配正式证书,而不是长期用 insecure-registries
  • log-opts:限制单个容器日志文件大小。不加这个,长期运行的 Harbor 会把磁盘写满——这是新手最容易踩的坑之一。
  • data-root:Docker 的镜像存储目录。Harbor 那台机器磁盘较大,保持默认(/var/lib/docker)即可。

4.2 下载并安装 Harbor

bash
# 1) 下载 Harbor 离线安装包(约 600MB,含所有镜像,适合离线环境)
cd /opt
curl -fLO https://gh-proxy.com/https://github.com/goharbor/harbor/releases/download/v2.15.2/harbor-offline-installer-v2.15.2.tgz

# 2) 确认下载完整(大小应在 600MB 左右)
ls -lh harbor-offline-installer-v2.15.2.tgz

# 3) 解包
tar -xzf harbor-offline-installer-v2.15.2.tgz -C /opt
cd /opt/harbor

# 4) 看一眼解出来的东西
ls -l

这条命令在做什么

  • 离线包(offline-installer)vs 在线包(online-installer)
    • 离线包:把 Harbor 需要的 9 个 Docker 镜像全部打进 tar 包,安装时不联网。文件大(600MB)但最可靠。
    • 在线包:只有几十 MB,安装时从 Docker Hub 拉镜像。国内环境经常拉不动
    • 这里选离线包,一次性下载、后续无网络依赖。
  • curl -fLO-O 表示以 URL 的文件名保存到本地(即 harbor-offline-installer-v2.15.2.tgz)。
bash
# 5) 生成配置文件
cp harbor.yml.tmpl harbor.yml

接下来编辑 harbor.yml关键改动只有三处

yaml
# ===== 编辑 /opt/harbor/harbor.yml =====
# 修改点 1:把 hostname 改成所有节点能访问到的地址
hostname: 192.168.0.21

# 修改点 2:HTTP 端口改成 10086
#   (避免和别的服务抢 80;本文档全部使用 10086)
http:
  port: 10086

# 修改点 3:注释掉 HTTPS 段(默认没有证书,不注释会启动失败)
# https:
#   port: 443
#   certificate: /your/certificate/path
#   private_key: /your/private/key/path

# 修改点 4:管理员初始密码(默认 Harbor12345,建议改掉)
harbor_admin_password: Harbor12345

# 数据目录(默认 /data,空间不够时可改)
data_volume: /data

# 日志与数据库(默认配置即可,单机安装自带 PostgreSQL 和 Redis)

关于 HTTPS

本文档为了降低上手门槛使用 HTTP。如果你的环境需要 HTTPS,需要:

  1. 准备证书文件(可用内部 CA 签发)
  2. harbor.yml 里配置 https
  3. 把 CA 证书分发到所有 K8s 节点的 /etc/docker/certs.d/192.168.0.21:10086/ca.crt
  4. 重启所有节点的 containerd/docker

更详细的做法见 Harbor 官方文档的「Configure HTTPS Access to Harbor」章节。

bash
# 6) 执行安装(会自动生成配置、启动全部容器)
./install.sh

# 7) 等待所有容器就绪(Harbor 有 9 个组件)
docker compose ps
# 全部应为 running / healthy,约需 1 分钟

# 8) 验证访问
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://192.168.0.21:10086
# 期望输出 HTTP 200

这条命令在做什么

  • ./install.sh:Harbor 的安装脚本,做三件事 —— ① 用模板生成各组件的配置文件;② 加载离线包里的镜像;③ 用 docker compose 启动全部服务。
  • docker compose ps:查看 Harbor 各组件状态。必须全部 healthy,否则登录会失败。

4.3 创建项目与机器人账号

bash
# 1) 用 admin 登录(会提示输入用户名密码)
docker login 192.168.0.21:10086
# Username: admin
# Password: 你在 harbor.yml 里设置的 harbor_admin_password

这条命令在做什么

  • 登录成功后,凭据会保存在 ~/.docker/config.json。后续的 push/pull 不再需要重复登录。
  • 若报 x509server gave HTTP response to HTTPS client,说明 /etc/docker/daemon.jsoninsecure-registries 没配好或没重启 docker。

接下来在 Harbor 的 Web UI(http://192.168.0.21:10086)里做两件事:

操作路径参数为什么
创建项目项目 → 新建项目名称 ruoyi-cloud,访问级别「私有」所有若依镜像都推到这个项目下,隔离清晰
创建机器人账号项目 ruoyi-cloud → 机器人账户 → 新建名称 jenkins-robot,权限勾选「拉取」+「推送」Jenkins 用固定账号推镜像,避免用 admin;账号泄露时影响范围可控
bash
# 2) 用机器人账号验证推送权限
docker logout 192.168.0.21:10086

cat <<'EOF' | docker login 192.168.0.21:10086 -u 'robot$ruoyi-cloud+jenkins-robot' --password-stdin
<机器人账号的 Token>
EOF

这条命令在做什么

  • --password-stdin推荐的登录方式。相比 -p 密码,它不会把密码留在 shell 历史和进程列表里。
  • 机器人账号的用户名格式是 robot$<项目名>+<机器人名>,Token 在创建时只显示一次,务必立即保存。
bash
# 3) 端到端验证:拉一个小镜像、打标签、推上去、再删掉本地
docker pull m.daocloud.io/docker.io/library/nginx:alpine
docker tag m.daocloud.io/docker.io/library/nginx:alpine 192.168.0.21:10086/ruoyi-cloud/test:1
docker push 192.168.0.21:10086/ruoyi-cloud/test:1
docker rmi 192.168.0.21:10086/ruoyi-cloud/test:1

# 4) 在 Harbor UI 的 ruoyi-cloud 项目里应该能看到 test 仓库

这条命令在做什么

  • 这是一个最小闭环验证。在装 Jenkins 之前确认"推"和"拉"都通,可以避免后面在流水线里排查仓库权限问题。

4.4 让 K8s 节点能拉私有镜像

Harbor 的 ruoyi-cloud 项目是私有的,所以 K8s 拉镜像时需要凭据。

bash
# 在 master 上创建命名空间与拉取凭据
kubectl create namespace ruoyi

kubectl create secret docker-registry harbor-secret \
  --namespace ruoyi \
  --docker-server=192.168.0.21:10086 \
  --docker-username='robot$ruoyi-cloud+jenkins-robot' \
  --docker-password='<机器人账号的 Token>' \
  --docker-email=dev@example.com

# 验证
kubectl get secret harbor-secret -n ruoyi

这条命令在做什么

  • kubectl create secret docker-registry:创建一个 kubernetes.io/dockerconfigjson 类型的 Secret。
  • 它的作用:Pod 里声明 imagePullSecrets: [harbor-secret] 后,kubelet 拉镜像时会带上这个凭据。
  • ⚠️ --docker-email 是必填的格式要求(内容随便填),漏掉会报参数错误。

两个命名空间都要创建吗

如果你的网络规划里有 dev/prod 两个环境,每个命名空间都要各自创建一份 Secret(Secret 不能跨命名空间使用)。用一条命令批量做:

bash
for ns in ruoyi ruoyi-dev; do
  kubectl create namespace $ns --dry-run=client -o yaml | kubectl apply -f -
  kubectl -n $ns create secret docker-registry harbor-secret \
    --docker-server=192.168.0.21:10086 \
    --docker-username='robot$ruoyi-cloud+jenkins-robot' \
    --docker-password='<Token>' \
    --docker-email=dev@example.com \
    --dry-run=client -o yaml | kubectl apply -f -
done

--dry-run=client -o yaml | kubectl apply -f - 的写法可以让命令幂等(重复执行不报错),是运维脚本的常用模式。

05 阶段四:Nacos 注册中心与配置中心

5.1 先搞清楚 Nacos 在若依里扮演什么角色

若依 Cloud(微服务版)的架构里,Nacos 同时承担两件事:

角色作用若依里的具体体现
注册中心服务启动时把自己注册进去,调用方从注册表里找到实例地址7 个微服务启动时都会向 Nacos 注册;ruoyi-gateway 通过服务名 ruoyi-system 转发请求,而不是写死 IP
配置中心集中存放配置,服务启动时从 Nacos 拉取各服务的 application.yml 里写着 spring.cloud.nacos.config.server-addr,真正的数据库密码、Redis 地址等都在 Nacos 里

为什么 Nacos 必装,不能"先跳过"

很多人会想「我先把服务跑起来,配置写死在本地行不行」。不行,有三个硬性原因:

  1. 若依的微服务启动时会强制连接 Nacos(bootstrap.yml 里配了 nacos.config),连不上会直接启动失败、反复重启。
  2. 服务间调用(网关 → 系统服务)依赖服务发现,没有 Nacos 就找不到目标实例。
  3. 若依官方提供的 ry-config.sql 里包含所有服务的默认配置,导入后才能正常启动

所以 Nacos 是本方案里必须先于业务服务就绪的基础设施。

5.2 在 192.168.0.24 上准备环境

bash
# 1) 装 JDK 17
dnf install -y java-17-openjdk java-17-openjdk-devel

# 2) 验证
java -version
# 应输出 openjdk version "17.0.x"

这条命令在做什么

  • Nacos 是用 Java 写的,需要 JRE/JDK 运行。这里装的是 OpenJDK 17(RHEL 系自带仓库就有,不需要额外源)。
  • -devel是为了拿到 jpsjcmd 这类诊断工具,排查内存问题时很有用。
bash
# 3) 下载 Nacos 2.5.4
cd /opt
curl -fLO https://gh-proxy.com/https://github.com/alibaba/nacos/releases/download/2.5.4/nacos-server-2.5.4.tar.gz

# 4) 确认大小(约 130MB)
ls -lh nacos-server-2.5.4.tar.gz

# 5) 解包
tar -xzf nacos-server-2.5.4.tar.gz -C /opt

# 6) 确认目录结构
ls -l /opt/nacos
# bin/  conf/  target/

也可以用华为云镜像(无需 gh-proxy)

bash
curl -fLO https://mirrors.huaweicloud.com/nacos/nacos-2.5.4.tar.gz

实测该地址返回 200,速度也不错。建议至少有两条下载路径,避免单点失效。

5.3 初始化 Nacos 的数据库

Nacos 有两种运行模式:

模式数据存哪适用场景
standalone 单机 + 内嵌 Derby本地文件纯测试,重启后配置可能丢
standalone 单机 + MySQLMySQL本文档采用,配置持久化、可备份
cluster 集群 + MySQLMySQL生产高可用
bash
# 1) 在 192.168.0.22 (MySQL) 上创建数据库
#    用 root 登录 MySQL
mysql -uroot -p

CREATE DATABASE nacos_config DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'nacos'@'%' IDENTIFIED BY 'Nacos@2026';
GRANT ALL PRIVILEGES ON nacos_config.* TO 'nacos'@'%';
FLUSH PRIVILEGES;
EXIT;

这条命令在做什么

  • CREATE DATABASE ... utf8mb4:Nacos 的配置内容可能包含中文和 emoji,必须用 utf8mb4utf8 在 MySQL 里是残缺的三字节实现,存 emoji 会报错)。
  • CREATE USER 'nacos'@'%':创建专用账号,% 表示允许从任意主机连接。 ⚠️ 生产环境建议限定来源,如 'nacos'@'192.168.0.24'
  • 不要用 root 账号连 Nacos:一旦 Nacos 的配置泄露,就等于泄露了数据库的完全控制权。
bash
# 2) 导入 Nacos 的表结构(Nacos 自带 SQL 文件)
#    先把 SQL 文件传到 MySQL 机器上,或直接在 MySQL 机器上操作
cd /opt/nacos/conf      # 若 SQL 文件在 nacos 机器上
ls mysql-schema.sql

# 在 MySQL 机器上执行导入(把文件 scp 过去或直接粘贴)
mysql -unacos -pNacos@2026 nacos_config < mysql-schema.sql

# 3) 验证表已创建
mysql -unacos -pNacos@2026 nacos_config -e "SHOW TABLES;"
# 应看到 config_info / config_info_gray / users / roles 等表

这条命令在做什么

  • mysql-schema.sql 是 Nacos 2.5.x 自带的建表脚本,位于 conf/ 目录。
  • 导入后 Nacos 才能把配置写进 MySQL。如果跳过这步,Nacos 启动时会报 Table 'nacos_config.config_info' doesn't exist

5.4 配置并启动 Nacos

bash
# 1) 修改 Nacos 主配置
cd /opt/nacos/conf
cp application.properties application.properties.bak   # 先备份,永远是个好习惯

vim application.properties

需要修改/确认的字段:

properties
# ===== 启动模式(单机) =====
# 也可以不在这里改,用启动参数 -m standalone 指定
server.servlet.contextPath=/nacos
server.port=8848

# ===== 数据源:改成 MySQL =====
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://192.168.0.22:3306/nacos_config?characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
db.user.0=nacos
db.password.0=Nacos@2026

# ===== 认证(Nacos 2.2+ 起默认关闭鉴权,若依要求开启) =====
nacos.core.auth.enabled=true
nacos.core.auth.system.type=nacos
nacos.core.auth.server.identity.key=ruoyiAuthKey
nacos.core.auth.server.identity.value=ruoyiAuthValue
# 以下密钥必须是 Base64 编码、且解码后长度 ≥ 32 字节(Nacos 2.2.0 起的硬性要求)
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg5QUJDREVG

# ===== 集群端口(单机模式下仍会被占用,需一并放通) =====
nacos.inetutils.ip-address=192.168.0.24

这些配置在做什么

配置项作用常见坑
db.url.0指向 MySQLallowPublicKeyRetrieval=true 必须加,否则 MySQL 8 的 caching_sha2_password 插件会验证失败
nacos.core.auth.enabled=true开启鉴权若依的配置文件里带了 Nacos 的用户名密码,服务端不开启鉴权也没问题,但开着更安全
nacos.core.auth.plugin.nacos.token.secret.keyJWT 签名密钥必须是 Base64 编码的字符串,且解码后 ≥ 32 字节。直接写明文会启动报错
nacos.inetutils.ip-address显式指定 Nacos 对外暴露的 IP多网卡机器必填(K8s 节点常有多张网卡),否则 Nacos 会注册错误的地址,客户端连不上

这个 key 是 Nacos 2.2+ 最常见的启动失败原因

Nacos 2.2.0 起对 token.secret.key 有强制要求:Base64 编码 + 解码后 ≥ 32 字节

报错长这样:

text
caused: current secret key is not valid, please check the length of the secret key

生成一个合规的 key:

bash
# 生成 48 字节随机数据并做 Base64 编码
openssl rand -base64 48
# 输出示例:3q2+7wXyZ...(把它填到 token.secret.key)
bash
# 2) 创建 systemd 服务(让 Nacos 能以服务方式管理)
cat > /etc/systemd/system/nacos.service <<'EOF'
[Unit]
Description=Nacos Server
After=network.target

[Service]
Type=forking
User=root
Environment="JAVA_HOME=/usr/lib/jvm/java-17-openjdk"
ExecStart=/opt/nacos/bin/startup.sh -m standalone
ExecStop=/opt/nacos/bin/shutdown.sh
Restart=on-failure
RestartSec=10
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now nacos

# 3) 等待约 30 秒后查看状态
systemctl status nacos --no-pager

# 4) 看日志确认无异常
tail -50 /opt/nacos/logs/start.out

这个 unit 文件在做什么

指令作用
Type=forkingstartup.sh 会在后台启动 Java 进程后立即返回,属于传统的 fork 型服务
Environment="JAVA_HOME=..."必须显式指定。systemd 启动的服务不会继承你 shell 里的环境变量,Nacos 的脚本靠 JAVA_HOME 找 Java
Restart=on-failure进程异常退出时自动重启
LimitNOFILE=65536提高文件描述符上限。Nacos 要维护大量长连接(gRPC 端口 9848),默认 1024 会成为瓶颈
bash
# 5) 放通端口(若使用 firewalld;已关闭则可跳过)
firewall-cmd --permanent --add-port=8848/tcp
firewall-cmd --permanent --add-port=9848/tcp
firewall-cmd --permanent --add-port=9849/tcp
firewall-cmd --permanent --add-port=7848/tcp
firewall-cmd --reload

# 6) 验证 HTTP 接口
curl -s http://192.168.0.24:8848/nacos/v1/console/health/readiness
# 期望输出:{"status":"UP"}

# 7) 验证端口监听
ss -lntp | grep -E "8848|9848|9849|7848"

这几个端口分别是什么

端口协议用途
8848HTTP控制台与 OpenAPI
9848gRPC客户端连接端口(Nacos 2.x 新增,必须放通)
9849gRPC服务端之间通信
7848HTTPJraft 集群选主(单机模式也会监听)

Nacos 2.x 最常见的连不上问题

Nacos 1.x 只需要放通 88482.x 引入了 gRPC,客户端实际通过 9848 通信

如果只放通 8848,现象是:控制台能打开、readiness 返回 UP,但应用启动时报:

text
com.alibaba.nacos.api.exception.NacosException: Client not connected, current status: STARTING

务必同时放通 9848

5.5 导入若依的配置

若依官方仓库里有 ry-config.sql(或 ry_config_*.sql),包含所有微服务的配置项。

bash
# 方式一:在浏览器操作(推荐,可视化)
# 1. 打开 http://192.168.0.24:8848/nacos
# 2. 首次登录会要求设置密码(Nacos 2.2+ 起不再用默认 nacos/nacos)
# 3. 左侧「配置管理 → 配置列表」
# 4. 右上角「导入配置」→ 选择 ry-config.sql 里的配置 → 选择命名空间 public
# 5. 确认后应看到约 15~20 条配置,dataId 形如:
#    ruoyi-gateway-prod.yml
#    ruoyi-system-prod.yml
#    ruoyi-auth-prod.yml
#    ruoyi-gen-prod.yml
#    ruoyi-job-prod.yml
#    ruoyi-file-prod.yml
#    ruoyi-monitor-prod.yml
#    application-prod.yml(公共配置)

导入后必须逐项核对的配置(这是最容易出错的地方):

配置项需要改成说明
spring.datasource.urljdbc:mysql://192.168.0.22:3306/ry-cloud?...指向你的 MySQL
spring.datasource.username/password你的账号
spring.redis.host192.168.0.23指向你的 Redis
spring.redis.password你的密码
ruoyi.file.domainhttp://192.168.0.11:9300文件服务的对外地址(用节点 IP + NodePort)
ruoyi.file.path/home/ruoyi/uploadPath容器内路径(需配合挂载或改用对象存储)

命名空间与分组怎么选

若依的配置通常放在 public 命名空间的 DEFAULT_GROUP 下。如果你想区分环境

  • 在 Nacos 里新建命名空间 proddev
  • 配置的 dataIdruoyi-gateway-prod.yml 这种带环境后缀的形式
  • 服务端通过 spring.profiles.active=prod 决定加载哪一个

本文档全程使用 prod

5.6 Nacos 验收清单

bash
# 1) 服务端健康
curl -s http://192.168.0.24:8848/nacos/v1/console/health/readiness

# 2) 配置接口能读到数据(需带上鉴权)
curl -s "http://192.168.0.24:8848/nacos/v1/cs/configs?dataId=ruoyi-gateway-prod.yml&group=DEFAULT_GROUP&tenant=" | head -20

# 3) 从 K8s 节点上验证连通性(这一步很关键——业务 Pod 就是从节点出去连的)
#    在 192.168.0.11 上执行:
curl -s -o /dev/null -w "nacos http: %{http_code}\n" http://192.168.0.24:8848/nacos/
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/192.168.0.24/9848' && echo "nacos grpc 9848: OK"

# 4) 从一台临时 Pod 里验证(最终验证)
kubectl run nacos-test --image=m.daocloud.io/docker.io/library/busybox:1.37 --rm -it --restart=Never \
  -- nslookup 192.168.0.24 2>/dev/null; echo "(能执行即说明网络可达)"

必须从"业务所在的位置"验证

在 Nacos 本机 curl localhost:8848 通过,不代表业务能连上。真正的验证点有两个:

  1. K8s 节点(Pod 用 hostNetwork 之外的方式出网时,源 IP 是节点 IP 或 Pod IP)
  2. Pod 内部

很多"本地能连、容器里连不上"的问题,根因是 Pod 出去的源 IP 不在 Nacos 的白名单里,或者 podSubnet192.168.0.0/24 的路由不通。

06 阶段五:三个基础中间件(只给准备要求)

本章的三个组件与本文主题(K8s 部署流程)关系不大,安装方式网上资料很多,这里只说明"必须准备成什么样",避免后面卡住。

6.1 需要准备的清单

IP主机名组件版本要求必须满足的配置要求
192.168.0.22mysqlMySQL8.0.x① 字符集 utf8mb4;② bind-address 允许来自 192.168.0.0/24 的连接;③ 创建 ry-cloudnacos_config 两个库;④ 创建应用专用账号(非 root);⑤ lower_case_table_names=1(可选,避免大小写问题)
192.168.0.23redisRedis7.x① 设置 requirepass;② bind 0.0.0.0(并靠密码与网络隔离保护);③ protected-mode no;④ maxmemory-policy allkeys-lru(可选,防止内存打满)
192.168.0.25gitGitea1.27.x① HTTP 端口 3000;② 关闭注册(DISABLE_REGISTRATION=true);③ 创建组织/用户,把 11 个若依仓库放进去;④ 生成一个 Jenkins 用的访问令牌(Settings → Applications → Generate Token,权限勾 repository: read

6.2 需要在 MySQL 上执行的初始化 SQL

sql
-- 1) 若依业务库
CREATE DATABASE `ry-cloud` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

-- 2) 业务账号(不要用 root 跑应用)
CREATE USER 'ruoyi'@'%' IDENTIFIED BY 'Ruoyi@2026';
GRANT ALL PRIVILEGES ON `ry-cloud`.* TO 'ruoyi'@'%';
FLUSH PRIVILEGES;

这条命令在做什么

  • 导入若依的表结构与初始数据(ry_2021xxxx.sqlquartz.sql 两个文件,来自若依官方仓库)。
  • 表的数量大约 30 张(sys_usersys_menusys_role 等)。
bash
# 3) 导入若依提供的两个 SQL 文件
mysql -uruoyi -pRuoyi@2026 ry-cloud < ry_2025xxxx.sql
mysql -uruoyi -pRuoyi@2026 ry-cloud < quartz.sql

# 4) 验证
mysql -uruoyi -pRuoyi@2026 ry-cloud -e "SELECT COUNT(*) AS tables_count FROM information_schema.tables WHERE table_schema='ry-cloud';"
# 应输出 30 左右

6.3 需要在 Redis 上确认的设置

bash
# 检查关键配置是否生效
redis-cli -h 192.168.0.23 -a '你的密码' CONFIG GET requirepass
redis-cli -h 192.168.0.23 -a '你的密码' CONFIG GET bind
redis-cli -h 192.168.0.23 -a '你的密码' PING
# 应输出 PONG

Redis 的 protected-mode

如果 Redis 设置了密码且 bind 0.0.0.0,需要把 protected-mode 设为 no(或有密码时它一般不会拦截,但不同版本行为有差异)。

最稳的验证方式是从 K8s 节点上连一次

bash
# 在 192.168.0.11 上执行
redis-cli -h 192.168.0.23 -a '你的密码' PING

6.4 需要在 Gitea 上准备的东西

准备项具体做法
仓库目录结构11 个仓库:ruoyi-gatewayruoyi-authruoyi-modules-systemruoyi-modules-genruoyi-modules-jobruoyi-modules-fileruoyi-visual-monitorruoyi-commonruoyi-apiruoyi-dependenciesruoyi-ui
分支策略每个仓库建 prod-k8s 分支(与 Jenkins 的分支过滤规则对应)
Jenkins 访问令牌用户设置 → 应用 → 生成令牌,权限勾选 repository: Read(只需读代码)
组织建议建一个组织(如 ruoyi)统一管理,而不是散在个人账号下

为什么用「每个服务独立仓库」而不是单体仓库

这是本方案的一个前提设定。两种方式各有取舍:

方式优点缺点
单体仓库(monorepo)依赖管理简单,一次 commit 改多个服务改一行代码会触发全部 7 个服务重新构建(除非配置路径过滤);权限粒度粗
独立仓库(multi-repo)只重建改动的服务;每个服务可独立版本化;权限能精确到仓库ruoyi-common 这类公共模块更新后,需要按依赖顺序重新构建下游服务

若依官方提供的就是拆分好的仓库,所以本文档按独立仓库走。代价是需要理解 Maven 的父子依赖关系,见下一章。

07 阶段六:若依微服务的代码与构建产物

7.1 仓库与依赖关系

11 个仓库分成四类:

类别仓库语言/技术在流水线里的处理
基础依赖(不上线)ruoyi-dependenciesMaven POM只发布到 Maven 私服,不构建镜像
ruoyi-commonJava 库同上(若依的公共工具类、常量、注解)
ruoyi-apiJava 库同上(服务间调用用的 Feign 接口定义)
业务服务(上线)ruoyi-gatewaySpring Cloud Gateway构建镜像 + 部署
ruoyi-authSpring Boot构建镜像 + 部署
ruoyi-modules-systemSpring Boot构建镜像 + 部署
ruoyi-modules-genSpring Boot构建镜像 + 部署
ruoyi-modules-jobSpring Boot构建镜像 + 部署
ruoyi-modules-fileSpring Boot构建镜像 + 部署
ruoyi-visual-monitorSpring Boot Admin构建镜像 + 部署
前端(本文档不部署)ruoyi-uiVue构建静态文件,交给 Nginx

独立仓库 + Maven 依赖 = 必须理解"先构建谁"

ruoyi-modules-systempom.xml 里依赖了 ruoyi-commonruoyi-api。如果:

  1. 你改了 ruoyi-common 并推送到仓库
  2. 直接触发 ruoyi-modules-system 的流水线

结果是:它拿到的还是 Maven 仓库里的旧版 ruoyi-common,你的改动不会生效。

三种解决办法:

方案做法适用
A. 本地 install(最简单)在构建机的 Maven 本地仓库里预先 mvn installruoyi-common 等基础包学习环境、改动不频繁
B. 搭建 Maven 私服(推荐)部署 Nexus,把基础包 mvn deploy 上去,业务服务从私服拉多人协作、正式环境
C. 多仓库联合构建Jenkins 用 MultiJob 或 Pipeline 的 build job: 语法,按顺序触发需要严格保证一致性时

本文档采用 方案 A(先手动装好基础包),第 8 章会给出具体命令。方案 B 是进阶方向,需要额外部署 Nexus。

7.2 服务与端口对照表

记住这张表,后面写 YAML 时会反复用到:

服务容器端口建议 NodePort说明
ruoyi-gateway808031080唯一对外入口,所有请求都从这里进
ruoyi-auth920031081认证服务
ruoyi-modules-system920131093系统管理(用户/角色/菜单)
ruoyi-modules-gen920230091代码生成
ruoyi-modules-job920330090定时任务
ruoyi-modules-file930030092文件上传下载
ruoyi-visual-monitor910030095监控(Spring Boot Admin)

为什么 NodePort 不连续

上面这些端口值是真实项目里已经在用的分配30090300913009230095310803108131093)——它们是在使用过程中零散分配的。

建议: 如果是新项目,从一开始就用连续段(如 31080~31086),便于记忆和排查。本文档沿用上面这张表是为了和常见实践对齐。

7.3 每个仓库的 Dockerfile

业务服务的 Dockerfile 都一样,只有 JAR 名不同。以 ruoyi-gateway 为例:

dockerfile
# 基础镜像(用国内加速地址,否则构建会很慢)
FROM m.daocloud.io/docker.io/library/openjdk:17-jdk-slim

# 维护者信息(可选)
LABEL maintainer="ops@example.com"

# 工作目录
WORKDIR /home/ruoyi

# 复制构建产物(注意:JAR 名由 Maven 的 finalName 决定)
COPY ./target/ruoyi-gateway.jar /home/ruoyi/ruoyi-gateway.jar

# 暴露端口(仅声明作用,实际由 K8s 的 Service 决定)
EXPOSE 8080

# 启动命令
ENTRYPOINT ["java", "-jar", "/home/ruoyi/ruoyi-gateway.jar"]

这个 Dockerfile 的要点

说明
FROM ...openjdk:17-jdk-slim用 JDK 17 基础镜像。也可以用 eclipse-temurin:17-jre,体积更小(约 250MB vs 450MB)
COPY ./target/*.jar依赖 Jenkins 先执行 mvn package,镜像构建阶段不要再编译
ENTRYPOINT [...](JSON 数组形式)必须是 JSON 数组形式。写成 ENTRYPOINT java -jar x.jar(shell 形式)会让 /bin/sh 成为 1 号进程,SIGTERM 信号无法传递给 Java 进程,导致 Pod 删除时不会优雅停机,只能等 30 秒后被 SIGKILL 强杀

关于基础镜像的选择

openjdk:17 这个官方镜像已经不再更新(Docker 官方已弃用 openjdk 镜像,改为推荐 eclipse-temurin)。如果你要长期使用,建议换成:

dockerfile
FROM m.daocloud.io/docker.io/library/eclipse-temurin:17-jre

实测该镜像可通过 m.daocloud.io 正常拉取。

7.4 K8s 部署清单

每个业务服务一个 YAML 文件,放在仓库的 k8s/ 目录下。以 gateway 为例:

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ruoyi-gateway-deploy
  namespace: ruoyi
  labels:
    app: ruoyi-gateway
spec:
  replicas: 1                       # 副本数(网关建议 ≥2)
  selector:
    matchLabels:
      app: ruoyi-gateway
  template:
    metadata:
      labels:
        app: ruoyi-gateway
    spec:
      # 关键:声明拉取私有仓库镜像的凭据
      imagePullSecrets:
        - name: harbor-secret
      containers:
        - name: ruoyi-gateway
          # 镜像地址(tag 由 Jenkins 在部署时替换,或由 Kustomize 管理)
          image: 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
            - name: JAVA_OPTS
              value: "-Xms512m -Xmx1024m"
          # 资源限制(必须设,否则一个服务可以把节点内存吃光)
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "1"
              memory: "1024Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: ruoyi-gateway-service
  namespace: ruoyi
  labels:
    app: ruoyi-gateway
spec:
  type: NodePort
  selector:
    app: ruoyi-gateway
  ports:
    - name: http
      port: 8080
      targetPort: 8080
      nodePort: 31080
      protocol: TCP

关键字段说明

字段为什么重要
imagePullSecrets拉私有镜像的凭据。漏掉会报 pull access denied
resources.requests调度依据。所有 Pod 的 requests 总和不能超过节点可分配资源,否则 Pod 会一直 Pending
resources.limits上限保护。必须设,一个内存泄漏的服务会把整个节点的 Pod 一起拖垮
Service labelSelector通过 app 标签把 Service 指向 Pod。Deployment 的 template.metadata.labels 必须包含这个标签,否则 Service 找不到后端(Endpoints 为空)

强烈建议补上健康探针

上面的清单没有 readinessProbe / livenessProbe。这会导致:

  • K8s 认为「Pod 一启动就可用」,在 Java 应用还没初始化完(Spring Boot 启动要 20~60 秒)时就把流量导进来 → 用户看到 502
  • 应用假死(线程池耗尽但进程还在)时不会被重启

补上这两段:

yaml
          # 就绪探针:通过后才把 Pod 加入 Service 的 Endpoints
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3

          # 存活探针:失败后重启容器
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 20
            failureThreshold: 3

前置条件:需要在 Nacos 的配置里开启 actuator 端点:

yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      probes:
        enabled: true        # 开启 /health/readiness 与 /health/liveness

08 阶段七:安装 Jenkins

8.1 在 192.168.0.20 上安装构建工具

Jenkins 需要四样东西:JDK 17、Maven 3.8.9、Docker、Git

bash
# 1) JDK 17
dnf install -y java-17-openjdk java-17-openjdk-devel
java -version

# 2) Git
dnf install -y git
git --version

这条命令在做什么

  • Jenkins 用 JDK 跑自己的进程,同时也用 JDK 执行 Maven 构建。所以构建机上的 JDK 要能跑 Jenkins 本身(Jenkins 2.528 要求 JDK 17 或 21)。
  • git 是 Jenkins 拉代码的必需工具(Jenkins 自带的 git 客户端只是封装,底层还是调用系统的 git)。
bash
# 3) Maven 3.8.9(用官方二进制包,避免 yum 源里的版本不一致)
cd /opt
curl -fLO https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.9/binaries/apache-maven-3.8.9-bin.tar.gz

# 如果上面的地址不存在(Apache 会归档老版本),用这个备用地址:
# curl -fLO https://archive.apache.org/dist/maven/maven-3/3.8.9/binaries/apache-maven-3.8.9-bin.tar.gz

tar -xzf apache-maven-3.8.9-bin.tar.gz -C /opt
ln -sfn /opt/apache-maven-3.8.9 /opt/maven

# 4) 配置 Maven 的国内仓库源(关键:否则拉依赖可能几十分钟)
mkdir -p /root/.m2
cat > /root/.m2/settings.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
  <localRepository>/root/.m2/repository</localRepository>
  <mirrors>
    <!-- 阿里云公共仓库,覆盖 central,并支持 spring 等常用仓库 -->
    <mirror>
      <id>aliyun-public</id>
      <name>Aliyun Public</name>
      <url>https://maven.aliyun.com/repository/public</url>
      <mirrorOf>central</mirrorOf>
    </mirror>
    <mirror>
      <id>aliyun-spring</id>
      <name>Aliyun Spring</name>
      <url>https://maven.aliyun.com/repository/spring</url>
      <mirrorOf>spring-milestones,spring-snapshots</mirrorOf>
    </mirror>
  </mirrors>
</settings>
EOF

# 5) 验证 Maven
export PATH=/opt/maven/bin:$PATH
mvn -v

这条命令在做什么

  • settings.xml<mirrors> 段:把 Maven 的中央仓库请求重定向到阿里云。这是国内构建提速最关键的一步——若依有几十个依赖,不换源可能拉 10 分钟以上。
  • <mirrorOf>central</mirrorOf>:只覆盖中央仓库(central 是 Maven 内置的仓库 id)。不要写成 *,那会连私服也一起代理,导致公司内部依赖拉不到。
  • localRepository:本地仓库路径。jenkins 用户和 root 用户的路径不同,后面配置 Jenkins 工具时要注意(见 8.4)。
bash
# 6) Docker(构建镜像用)
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": ["https://docker.m.daocloud.io"],
  "insecure-registries": ["192.168.0.21:10086", "harbor:10086"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" },
  "storage-driver": "overlay2"
}
EOF

systemctl enable --now docker

# 7) 验证 Docker 与 Harbor 的连通
docker login 192.168.0.21:10086
docker info | grep -E "Registry Mirrors|Insecure"

这条命令在做什么

  • Jenkins 的流水线里要执行 docker builddocker push,所以这台机器必须装 Docker。
  • insecure-registries 和 Harbor 那台机器同理,必须把 Harbor 加入白名单。
  • log-opts 限制日志大小 —— 构建机每天都在 build/push,日志增长极快

Jenkins 容器里能不能用 Docker

本文档的 Jenkins 是 rpm 直接装在宿主机上的,不是跑在容器里,所以直接调用宿主机 Docker 即可,没有"docker in docker"的复杂度。

如果你后来把 Jenkins 容器化,需要把 /var/run/docker.sock 挂进容器,并在容器内装 docker CLI。

8.2 安装 Jenkins

bash
# 1) 查一下清华源里当前可用的版本
curl -s https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/ | grep -o 'jenkins-[0-9.]*-1.1.noarch.rpm' | sort -u | tail -3
# 输出示例:jenkins-2.528.3-1.1.noarch.rpm

# 2) 下载 rpm
cd /root
curl -fLO https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/jenkins-2.528.3-1.1.noarch.rpm

# 3) 安装(dnf 会自动解决依赖)
dnf install -y ./jenkins-2.528.3-1.1.noarch.rpm

# 4) 启动并设置开机自启
systemctl enable --now jenkins

# 5) 查看初始密码
cat /var/lib/jenkins/secrets/initialAdminPassword

# 6) 查看服务状态
systemctl status jenkins --no-pager

这条命令在做什么

  • 清华的 Jenkins 镜像里只有 rpm 文件、没有 jenkins.repo(实测 jenkins.repo 返回 404),所以采用「下载 rpm + dnf 本地安装」的方式。这也更可控——升级时你明确知道装了哪个版本。
  • dnf install -y ./xxx.rpm./ 前缀告诉 dnf 这是个本地文件而不是仓库包名,否则会去仓库找(找不到就报错)。
  • /var/lib/jenkins/secrets/initialAdminPassword:Jenkins 首次启动时生成的一次性解锁密码。
  • Jenkins 的配置文件与数据都在 /var/lib/jenkins,备份时直接备份这个目录即可。
bash
# 7) 放通 8080
firewall-cmd --permanent --add-port=8080/tcp && firewall-cmd --reload

# 8) 从浏览器访问 http://192.168.0.20:8080

加速插件安装(必做)

Jenkins 首次启动会引导你装插件,默认从 updates.jenkins.io 下载,国内极慢。先改了再启动:

bash
# 修改升级站点为清华镜像
sed -i 's#https://updates.jenkins.io/update-center.json#https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json#' \
  /var/lib/jenkins/hudson.model.UpdateCenter.xml

systemctl restart jenkins

/var/lib/jenkins/hudson.model.UpdateCenter.xml 是插件中心的地址配置,改完重启生效。

8.3 必需的插件

进入「系统管理 → 插件管理 → Available」安装以下插件:

插件用途是否必需
Git / Git Client从 Git 仓库拉代码必需(通常已装)
Git Parameter构建时选择分支/标签建议
Pipeline / Workflow 系列使用 Jenkinsfile必需(通常已装)
Multibranch Scan Webhook Trigger支持多分支流水线被 webhook 触发必需
Docker Pipeline在流水线中调用 Docker建议(本文档用 sh 'docker ...',也可不装)
Timestamper给构建日志加时间戳建议(Jenkinsfile 里有 timestamps()
GiteaGitea 的 webhook 与状态回写建议(Gitea 1.27 也可直接用通用 webhook)
Role-based Authorization Strategy细粒度权限可选
Build Timeout防止构建卡死建议

必装但容易漏:Timestamper

Jenkinsfile 里的 options { timestamps() } 依赖 Timestamper 插件。不装的话流水线会直接报错:

text
java.lang.NoSuchMethodError: ... timestamps()

8.4 全局工具与凭据配置

路径:系统管理 → 全局工具配置(Global Tool Configuration)

工具名称(Name)配置方式
JDKJDK-17取消「自动安装」,JAVA_HOME 填 /usr/lib/jvm/java-17-openjdk
MavenMaven-3.8.9取消「自动安装」,MAVEN_HOME 填 /opt/maven
Git默认一般会自动探测到 /usr/bin/git

这里的「名称」必须和 Jenkinsfile 里的一字不差

Jenkinsfile 里写的是:

groovy
tools {
    maven 'Maven-3.8.9'
    jdk 'JDK-17'
}

这两个字符串是按名称精确匹配的。名称写成 maven-3.8.9(小写)或 JDK17(少个横线)都会导致:

text
ERROR: No tool named Maven-3.8.9 found

路径:系统管理 → 凭据(Credentials)→ System → Global credentials → 添加凭据

ID类型内容谁在用
harbor-credentialsUsername with password用户名 robot$ruoyi-cloud+jenkins-robot,密码为机器人 TokenJenkinsfile 里的 HARBOR_CREDENTIALS_ID
gitea-tokenUsername with password(或 Secret text)Gitea 的用户名 + 访问令牌拉取私有仓库代码
kubeconfig-adminSecret fileK8s 的 ~/.kube/config 文件kubectl apply 部署

凭据 ID 不能拼错

Jenkinsfile 里通过 ID 引用凭据:

groovy
withCredentials([usernamePassword(credentialsId: 'harbor-credentials', ...)])

ID 拼错不会在配置阶段报错,而是在流水线执行到那一步时抛 CredentialNotFoundException。所以要么用文档里的 ID,要么同步改 Jenkinsfile。

配置 kubectl:让 Jenkins 能操作集群

bash
# 在 master 上,把 admin.conf 拷贝出来(这一步包含集群的最高权限,注意保管)
# 然后把文件内容保存为 Jenkins 的 kubeconfig-admin 凭据

# 在 Jenkins 机器上验证(临时放置)
mkdir -p /root/.kube
vim /root/.kube/config      # 粘贴 master 上 /etc/kubernetes/admin.conf 的内容

# 验证能连上集群
kubectl get nodes

更安全的做法:用受限的 ServiceAccount 而不是 admin

本文档为降低学习门槛直接用 admin.conf(集群最高权限)。生产的正确做法是:

bash
# 1) 创建一个只能操作 ruoyi 命名空间的 ServiceAccount
kubectl create serviceaccount jenkins-deployer -n ruoyi

# 2) 绑定一个只允许管理该命名空间内资源的 Role
kubectl create role jenkins-deployer-role -n ruoyi \
  --verb=get,list,watch,create,update,patch,delete \
  --resource=deployments,services,configmaps,ingresses,secrets

kubectl create rolebinding jenkins-deployer-binding -n ruoyi \
  --role=jenkins-deployer-role \
  --serviceaccount=ruoyi:jenkins-deployer

# 3) 生成 kubeconfig
kubectl -n ruoyi create token jenkins-deployer --duration=8760h

用这个 Token 组装 kubeconfig,即使泄露也影响不到其他命名空间

09 阶段八:流水线与自动发布

9.1 先在构建机上验证一次完整的手工构建

在写 Jenkinsfile 之前,先手工走通一遍。这样才能分清"是环境问题"还是"是流水线问题"。

bash
# 1) 准备一个工作目录
mkdir -p /root/build-test && cd /root/build-test

# 2) 克隆基础依赖包(三个不上线的库)
for r in ruoyi-dependencies ruoyi-common ruoyi-api; do
  git clone http://192.168.0.25:3000/ruoyi/$r.git
done

# 3) 按顺序安装到本地 Maven 仓库(顺序不能错!)
export PATH=/opt/maven/bin:$PATH

cd ruoyi-dependencies && mvn clean install -Dmaven.test.skip=true && cd ..
cd ruoyi-common       && mvn clean install -Dmaven.test.skip=true && cd ..
cd ruoyi-api          && mvn clean install -Dmaven.test.skip=true && cd ..

这条命令在做什么

  • dependencies → common → api依赖顺序逐个 mvn install
  • install 会把打好的 jar 放进本地仓库 /root/.m2/repository,供后面构建业务服务时引用。
  • -Dmaven.test.skip=true:跳过测试。注意有两个类似的参数
    • -DskipTests:编译测试代码但不运行
    • -Dmaven.test.skip=true连测试代码都不编译(更快,且避免测试代码本身的编译错误)
bash
# 4) 克隆并构建一个业务服务
git clone http://192.168.0.25:3000/ruoyi/ruoyi-gateway.git
cd ruoyi-gateway
mvn clean package -Dmaven.test.skip=true

# 5) 确认产物
ls -lh target/ruoyi-gateway.jar

# 6) 构建镜像
docker build --build-arg JAR_FILE=target/ruoyi-gateway.jar \
  -t 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:test .

# 7) 推送到 Harbor
docker push 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:test

# 8) 验证镜像可以被集群拉取(起一个临时 Pod 试)
kubectl run image-test --image=192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:test \
  --dry-run=client -o yaml | \
  kubectl run image-test --image=192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:test \
  --overrides='{"spec":{"imagePullSecrets":[{"name":"harbor-secret"}]}}' \
  --restart=Never -- sleep 5 -n ruoyi
kubectl get pod image-test -n ruoyi
kubectl delete pod image-test -n ruoyi --force --grace-period=0

这条命令在做什么

  • 这是一次不经过 Jenkins 的完整闭环验证:编译 → 打镜像 → 推送 → 集群拉取。
  • 这一遍走通后,Jenkinsfile 就只是把这几步搬进流水线,排查问题会快得多

先预热 Maven 仓库,让 Jenkins 的首次构建快很多

Jenkins 以 jenkins 用户运行,它的 Maven 本地仓库是 /var/lib/jenkins/.m2/repository,和 root 的不是同一个

bash
# 把 settings.xml 复制给 jenkins 用户
mkdir -p /var/lib/jenkins/.m2
cp /root/.m2/settings.xml /var/lib/jenkins/.m2/settings.xml
chown -R jenkins:jenkins /var/lib/jenkins/.m2

或者直接在 Jenkins 的「全局工具配置」里指定 Maven 的 settings.xml 路径。

9.2 Jenkinsfile(与常见实践一致的 4 阶段)

把下面的文件放到每个业务服务仓库的根目录,文件名 Jenkinsfile

groovy
pipeline {
    agent any

    tools {
        maven 'Maven-3.8.9'
        jdk 'JDK-17'
    }

    environment {
        APP_NAME   = 'ruoyi-gateway'
        NAMESPACE  = 'ruoyi'
        HARBOR_URL = '192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway'
        // 镜像版本用时间戳,保证每次构建都唯一
        VERSION    = sh(script: "date '+%Y%m%d%H%M%S'", returnStdout: true).trim()
        HARBOR_CREDENTIALS_ID = 'harbor-credentials'
        MAVEN_OPTS = '-Dmaven.compiler.source=17 -Dmaven.compiler.target=17 -Dproject.build.sourceEncoding=UTF-8'
    }

    options {
        disableConcurrentBuilds()   // 同一个服务不允许并发构建,避免镜像 tag 冲突
        timestamps()                // 日志带时间戳(需 Timestamper 插件)
        timeout(time: 20, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '20'))
    }

    stages {
        stage('代码编译打包') {
            steps {
                script {
                    sh 'mvn clean package -Dmaven.test.skip=true'
                }
            }
        }

        stage('构建 Docker 镜像') {
            steps {
                script {
                    sh """
                        docker build --build-arg JAR_FILE=target/${APP_NAME}.jar \
                          -t ${HARBOR_URL}:latest .
                        docker tag ${HARBOR_URL}:latest ${HARBOR_URL}:${VERSION}
                    """
                }
            }
        }

        stage('推送镜像到 Harbor') {
            steps {
                script {
                    withCredentials([usernamePassword(
                        credentialsId: HARBOR_CREDENTIALS_ID,
                        usernameVariable: 'HARBOR_USER',
                        passwordVariable: 'HARBOR_PASS')]) {
                        sh """
                            echo "\${HARBOR_PASS}" | docker login 192.168.0.21:10086 \
                              -u "\${HARBOR_USER}" --password-stdin
                            docker push ${HARBOR_URL}:latest
                            docker push ${HARBOR_URL}:${VERSION}
                        """
                    }
                }
            }
        }

        stage('部署到 K8s') {
            steps {
                script {
                    // 把清单里的镜像 tag 替换成本次构建的版本
                    sh """
                        sed -i 's#\\(image: .*\\):latest#\\1:${VERSION}#g' k8s/deployment.yaml
                    """
                    // 确认替换结果
                    sh "grep -n 'image:' k8s/deployment.yaml"

                    // 应用部署
                    sh """
                        kubectl apply -f k8s/deployment.yaml -n ${NAMESPACE}
                        kubectl rollout status deployment/${APP_NAME}-deploy -n ${NAMESPACE} --timeout=180s
                    """
                }
            }
        }
    }

    post {
        success {
            echo "✅ 发布成功:${HARBOR_URL}:${VERSION}"
            sh "docker rmi ${HARBOR_URL}:latest ${HARBOR_URL}:${VERSION} || true"
        }
        failure {
            echo "❌ 构建或部署失败,请检查上方日志"
        }
        always {
            // 清理 workspace 里被 sed 改过的文件,避免影响下次构建
            sh 'git checkout -- k8s/deployment.yaml || true'
        }
    }
}

9.3 逐段解读

toolsenvironment

groovy
tools { maven 'Maven-3.8.9'; jdk 'JDK-17' }
  • 声明本流水线用哪个工具。Jenkins 会在构建时把这些工具的 bin 目录加到 PATH 里。
  • 名称必须与「全局工具配置」里的名称完全一致
groovy
VERSION = sh(script: "date '+%Y%m%d%H%M%S'", returnStdout: true).trim()
  • returnStdout: truesh 返回命令的标准输出而不是退出码。
  • .trim() 不能省:命令输出末尾带换行符,不 trim 会让镜像 tag 变成 ...123456\n,进而导致 docker tag 报错。

options

groovy
disableConcurrentBuilds()
  • 禁止同一 Job 并发。为什么必需:两个构建同时跑会互相覆盖 latest 标签,还会争抢同一个 target/ 目录,产生难以复现的失败。
groovy
timeout(time: 20, unit: 'MINUTES')
  • 超时自动中止。防止构建卡死占着执行器(例如 Maven 卡在下载某个依赖)。

③ 四个 stage 的职责

Stage命令产物
代码编译打包mvn clean package -Dmaven.test.skip=truetarget/<app>.jar
构建 Docker 镜像docker build + docker tag本地镜像(两个 tag)
推送镜像到 Harbordocker push ×2仓库里有 latest<时间戳> 两个 tag
部署到 K8ssed 改 tag + kubectl apply + rollout status集群里的新 Pod

④ 关于 sed. 的转义

groovy
sh """
    sed -i 's#\\(image: .*\\):latest#\\1:${VERSION}#g' k8s/deployment.yaml
"""

这段 sed 有三个容易踩的坑

  1. 分隔符选择:用 # 而不是 /,因为内容里有 /(镜像地址)。
  2. 括号转义:在 Groovy 的 """ 字符串里,\\( 会被传成 \( 给 shell;如果写 \(,Groovy 会把它当成转义字符吃掉,导致 shell 收到 (image: .*):latestsed 报语法错误
  3. 必须用 .* 而不是 .*/:镜像地址里有多层斜杠。

验证方法:在流水线里加一句 sh "grep -n 'image:' k8s/deployment.yaml",把替换结果打出来看。

kubectl rollout status 是最有价值的一行

groovy
kubectl rollout status deployment/${APP_NAME}-deploy -n ${NAMESPACE} --timeout=180s
  • 它会阻塞直到 Deployment 真正完成滚动更新(新 Pod 就绪、旧 Pod 退出)。
  • 如果新版本起不来(镜像错、配置错、端口冲突),这条命令会超时返回非零退出码,流水线标记为失败
  • 不加这一行的后果kubectl apply 一提交就返回成功,流水线显示绿色,但实际 Pod 在疯狂重启——"部署成功"变成了一句谎话

post 段的清理逻辑

groovy
always {
    sh 'git checkout -- k8s/deployment.yaml || true'
}
  • 因为第 4 个 stage 修改了工作区里的 k8s/deployment.yamlsed -i 原地修改),必须还原
  • 不还原的后果:工作区的 git 树永远是 dirty 状态,下次构建时 sed 会基于上一次的结果再次替换,重复执行后 tag 会变得不可预测

这个 sed 本身就是个"技术债"

本文档沿用这种写法是为了与常见实践一致、便于理解。但它有明确缺陷:

  • 文件在构建期间被改动,违反了「构建产物应与源码一一对应」的原则
  • 实际部署的清单与仓库里的清单不一致,出问题时 git diff 看不到真相
  • 没有版本管理:回滚时不知道上一次部署用的是哪个 tag

更好的做法(第 13 章会展开):把镜像 tag 从清单里抽出来,交给 Kustomize 的 images: 字段管理,由 CI 只改 kustomization.yaml 里的一行;或者直接引入 GitOps 工具(Argo CD)来接管「部署」这一步。

9.4 创建流水线任务

推荐用 Multibranch Pipeline(多分支流水线):一个 Job 自动覆盖仓库里的所有分支,不用为每个分支建 Job。

配置项说明
名称ruoyi-gateway与服务名一致
类型Multibranch Pipeline
Branch SourcesGit → http://192.168.0.25:3000/ruoyi/ruoyi-gateway.git凭据选 gitea-token
Behaviors添加 Filter by name (with wildcards)prod*只构建 prod 开头的分支,避免 feature 分支乱构建
Discover Branches保持默认
Build Configurationby Jenkinsfile,脚本路径 Jenkinsfile
Scan Repository Triggers每 5 分钟(或配置 webhook)

为什么用 prod* 而不是 prod-k8s

用通配符意味着将来加 prod-dockerprod-test 这类分支时不用改配置。代价是每个匹配的分支都会被构建——如果分支很多,建议收窄到具体名字。

bash
# 在 Gitea 仓库里配置 webhook(比轮询更及时)
# 仓库 → 设置 → Web 钩子 → 添加 Web 钩子 → Gitea
# 目标 URL: http://192.168.0.20:8080/multibranch-webhook-trigger/invoke?token=<token>
# 触发事件: 推送事件(Push)

这条命令在做什么

  • webhook 让 Gitea 在收到 push 时主动通知 Jenkins,而不是等 Jenkins 每 5 分钟轮询一次。
  • token 参数由 Multibranch Scan Webhook Trigger 插件提供,在 Job 的配置页面可以看到。
  • 注意:不同插件提供的 URL 路径不一样。常见的有:
    • 通用触发:/generic-webhook-trigger/invoke?token=xxx
    • 多分支触发:/multibranch-webhook-trigger/invoke?token=xxx
    • Gitea 插件自带:在 Gitea 服务器配置里填 Jenkins 地址,会自动注册

10 阶段九:端到端验证

10.1 部署全部 7 个服务

先把清单文件准备好(每个服务仓库的 k8s/deployment.yaml 都要有,改成对应服务的名称与端口):

bash
# 在 master 上批量创建命名空间与凭据(如果还没做)
kubectl create namespace ruoyi --dry-run=client -o yaml | kubectl apply -f -

验证顺序很重要——先部署依赖项,再部署业务服务:

bash
# 1) 部署顺序:visual-monitor(不依赖别人)→ auth → modules-* → gateway(最后)
#    每一批部署后等一下,确认起来了再继续

kubectl get pods -n ruoyi -w

为什么网关必须最后部署

ruoyi-gateway 的路由配置里指向 ruoyi-systemruoyi-auth 等服务名。如果它先起来而下游服务还没注册到 Nacos,网关启动时会因为找不到服务实例而快速失败(默认 lb 重试次数用完后退出)。

虽然加了 replicas 和重启策略后最终会恢复正常,但会多几次无谓的重启,日志里全是连接错误,干扰排查。

10.2 全链路验收清单

bash
# ① 所有 Pod 都是 Running 且重启次数正常
kubectl get pods -n ruoyi -o wide
# 关注 RESTARTS 列:刚部署完应该都是 0;如果持续增长,说明进程在崩溃循环

# ② 所有 Service 都有 Endpoints(关键!)
kubectl get endpoints -n ruoyi
# 每个 Service 下面都应该有对应的 Pod IP。
# 如果显示 <none>,说明 Service 的 selector 和 Pod 的 labels 不匹配

# ③ 服务是否都注册到了 Nacos
curl -s "http://192.168.0.24:8848/nacos/v1/ns/catalog/services?pageNo=1&pageSize=50&namespaceId=public" \
  | python3 -m json.tool | grep -E '"name"|"groupName"'
# 应看到 ruoyi-gateway、ruoyi-auth、ruoyi-system 等

# ④ 从集群外访问网关(NodePort)
curl -s -o /dev/null -w "gateway: HTTP %{http_code}\n" http://192.168.0.11:31080/

# ⑤ 完整业务链路:拿验证码 → 登录 → 带 token 查询用户列表
curl -s "http://192.168.0.11:31080/code" | head -c 200
echo

curl -s -X POST "http://192.168.0.11:31080/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}' | head -c 300
echo
# 返回带 token 的 JSON 即为成功

# ⑥ 查看某个服务的日志,确认没有报错
kubectl logs -n ruoyi -l app=ruoyi-gateway --tail=50

# ⑦ 确认连接的用户是 MySQL/Redis/Nacos
#    (在 Nacos 控制台看服务列表、在 MySQL 看连接数)
mysql -h192.168.0.22 -uruoyi -p'Ruoyi@2026' -e "SHOW PROCESSLIST;" | head -10

这些命令在做什么

  • kubectl get endpoints这是最容易被忽略但最关键的一条。Service 有没有正确指向 Pod,全看这里。<none> 表示后端为空,此时请求会直接失败或超时。
  • 第 ⑤ 步的完整登录链路,是唯一能证明"7 个服务真的协同工作"的验证:gateway 转发 → auth 服务验证 → system 服务查数据 → MySQL 返回。任何一环断开都会在这里暴露。

10.3 验证"改代码 → 自动发布"的闭环

bash
# 1) 在开发机克隆一个服务
git clone http://192.168.0.25:3000/ruoyi/ruoyi-modules-system.git
cd ruoyi-modules-system
git checkout prod-k8s

# 2) 改一个可观测的东西(例如在一个 Controller 里加一行日志)
vim src/main/java/com/ruoyi/system/controller/SysUserController.java
# 加一行:log.info("=== BUILD TEST {} ===", System.currentTimeMillis());

# 3) 提交并推送
git add -A
git commit -m "test: 验证自动发布链路"
git push origin prod-k8s

# 4) 打开 Jenkins 页面,应该看到 ruoyi-modules-system 的构建被自动触发
#    4 个 stage 依次变绿

# 5) 构建完成后,确认新 Pod 已就绪
kubectl get pods -n ruoyi -l app=ruoyi-modules-system

# 6) 确认日志里有你新加的那一行
kubectl logs -n ruoyi -l app=ruoyi-modules-system --tail=100 | grep "BUILD TEST"

# 7) 确认 Deployment 用的镜像 tag 是本次构建的时间戳
kubectl get deployment ruoyi-modules-system-deploy -n ruoyi \
  -o jsonpath='{.spec.template.spec.containers[0].image}'
echo

这几步验证了什么

步骤验证的能力
3 → 4webhook / 轮询能正确触发构建
44 个 stage 的脚本无语法与权限问题
5kubectl apply 生效、镜像被集群拉到
6构建产物确实是新代码(这是最本质的验证)
7版本可追溯(tag 能对应到具体一次构建)

11 常见故障排查

11.1 构建阶段

现象原因解决
No tool named Maven-3.8.9 found全局工具配置里名称不一致检查拼写,大小写敏感
Maven 拉依赖极慢或超时没配 settings.xml 的国内镜像按 8.1 配置;注意 Jenkins 用户和 root 用户各一份
Could not resolve dependencies ... ruoyi-common基础包没先 mvn install按 9.1 的顺序先安装三个基础库
docker: command not foundJenkins 服务用户的 PATH 里没有 docker在系统配置的「环境变量」里加 PATH+EXTRA=/usr/bin,或建软链接
permission denied while trying to connect to the Docker daemonjenkins 用户不在 docker 组usermod -aG docker jenkins && systemctl restart jenkins
mvn: command not foundPATH 没包含 Maven检查「全局工具配置」里的 MAVEN_HOME
编译报 source option 5 is no longer supported环境变量 MAVEN_OPTS 没生效确认 environment 段的写法,或直接写进 pom.xml

11.2 镜像阶段

现象原因解决
http: server gave HTTP response to HTTPS clientDocker 没把 Harbor 列入 insecure配置 /etc/docker/daemon.jsoninsecure-registries 并重启
denied: requested access to the resource is denied未登录 / 机器人账号无推送权限docker login 验证;检查 Harbor 机器人账号的权限勾选
COPY failed: no source files were specifiedmvn package 没产出 JAR 或名字不对ls target/ 确认文件名与 Dockerfile 里一致
镜像层特别大(>500MB)基础镜像用了完整 JDKeclipse-temurin:17-jre

11.3 部署阶段

现象原因解决
Pod ImagePullBackOff凭据缺失 / 地址错 / Harbor 不通kubectl describe pod 看 Events;从节点 curl http://192.168.0.21:10086/v2/
Pod Pending节点资源不足 / 有污点kubectl describe pod 看 Events 里的 Insufficienttaint
Pod CrashLoopBackOff应用启动失败kubectl logs --previous 看上一次崩溃的日志
Pod Running 但 0/1 Ready探针失败kubectl describe podReadiness probe failed
Service 的 Endpoints 是 <none>selector 与 labels 不匹配kubectl describe svc 看 Selector;kubectl get pod --show-labels 对比
应用日志报连不上 Nacos9848 端口未放通 / IP 配错从节点 nc -vz 192.168.0.24 9848
应用报 Communications link failureMySQL 不通 / 账号权限 / 密码错从节点用 mysql 客户端连一次
NodePort 访问不通端口没在 30000-32767 / 节点防火墙kubectl get svc 看 NodePort 值;ss -lntp | grep <port>
kubectl rollout status 一直不成功新 Pod 起不来(探针不过/镜像错)同时开一个窗口 kubectl get pods -w

11.4 网络与 DNS

现象原因解决
Pod 内 nslookup kubernetes.default 失败CoreDNS 未就绪 / CNI 异常kubectl get pods -n kube-system | grep coredns
服务间调用报 Connection refused目标服务名写错 / 未注册到 NacosNacos 控制台看服务列表
跨节点 Pod 不通安全组未放通 VXLAN(4789/UDP)检查节点间网络策略
随机超时nf_conntrack 表满调大 net.netfilter.nf_conntrack_max
Pod 拿到 192.168.0.x 的 IPPod 网段与物理网段重叠修改 podSubnet 并重建集群

12 附录

12.1 全部命令速查(按执行顺序)

bash
# ============ 【三台 K8s 节点】系统初始化 ============
dnf update -y && dnf install -y vim wget curl net-tools telnet lsof bash-completion git tar chrony && reboot
# 各自设置主机名后:
cat >> /etc/hosts <<'EOF'
192.168.0.10 k8s-master
192.168.0.11 k8s-node1
192.168.0.12 k8s-node2
192.168.0.20 ci-jenkins
192.168.0.21 harbor
192.168.0.22 mysql
192.168.0.23 redis
192.168.0.24 nacos
192.168.0.25 git
EOF
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab
cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
ip_vs
ip_vs_rr
EOF
modprobe overlay && modprobe br_netfilter && modprobe ip_vs && modprobe ip_vs_rr
cat > /etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
vm.panic_on_oom                     = 0
vm.overcommit_memory                = 1
fs.inotify.max_user_instances       = 8192
fs.inotify.max_user_watches         = 524288
net.netfilter.nf_conntrack_max      = 1048576
EOF
sysctl --system
systemctl enable --now chronyd
systemctl disable --now firewalld
setenforce 0 && sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
dnf makecache && dnf install -y containerd.io
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sed -i 's#sandbox_image = "registry.k8s.io/pause:.*"#sandbox_image = "m.daocloud.io/registry.k8s.io/pause:3.10"#' /etc/containerd/config.toml
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
server = "https://docker.io"

[host."https://m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF
systemctl enable --now containerd
cat > /etc/yum.repos.d/kubernetes.repo <<'EOF'
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/repodata/repomd.xml.key
EOF
dnf makecache && dnf install -y kubelet kubeadm kubectl && systemctl enable kubelet
kubeadm config images pull --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers

# ============ 【master】初始化集群 ============
kubeadm init --config kubeadm-config.yaml --upload-certs
mkdir -p $HOME/.kube && cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
curl -fL -o calico.yaml https://gh-proxy.com/https://raw.githubusercontent.com/projectcalico/calico/v3.32.2/manifests/calico.yaml
sed -i 's|^\( *\)# *- name: CALICO_IPV4POOL_CIDR|\1- name: CALICO_IPV4POOL_CIDR|' calico.yaml
sed -i 's|^\( *\)# *value: "192.168.0.0/16"|\1value: "10.244.0.0/16"|' calico.yaml
kubectl apply -f calico.yaml
kubectl get nodes

# ============ 【worker】加入集群 ============
kubeadm token create --print-join-command   # 在 master 执行,复制输出
kubeadm join 192.168.0.10:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx

# ============ 【harbor】安装 Harbor ============
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable --now docker
cd /opt && curl -fLO https://gh-proxy.com/https://github.com/goharbor/harbor/releases/download/v2.15.2/harbor-offline-installer-v2.15.2.tgz
tar -xzf harbor-offline-installer-v2.15.2.tgz
cd /opt/harbor && cp harbor.yml.tmpl harbor.yml
# 改 hostname: 192.168.0.21、http.port: 10086、注释掉 https 段
./install.sh
docker compose ps

# ============ 【nacos】安装 Nacos ============
dnf install -y java-17-openjdk java-17-openjdk-devel
cd /opt && curl -fLO https://gh-proxy.com/https://github.com/alibaba/nacos/releases/download/2.5.4/nacos-server-2.5.4.tar.gz
tar -xzf nacos-server-2.5.4.tar.gz
# 改 conf/application.properties 的 MySQL 数据源与鉴权密钥
# 建 systemd unit 后:
systemctl enable --now nacos
curl -s http://192.168.0.24:8848/nacos/v1/console/health/readiness

# ============ 【ci-jenkins】安装构建环境 ============
dnf install -y java-17-openjdk java-17-openjdk-devel git
cd /opt && curl -fLO https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.9/binaries/apache-maven-3.8.9-bin.tar.gz
tar -xzf apache-maven-3.8.9-bin.tar.gz && ln -sfn /opt/apache-maven-3.8.9 /opt/maven
mkdir -p /root/.m2 && vi /root/.m2/settings.xml       # 配阿里云镜像
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 配 /etc/docker/daemon.json(insecure-registries + 镜像加速)
systemctl enable --now docker
cd /root && curl -fLO https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/jenkins-2.528.3-1.1.noarch.rpm
dnf install -y ./jenkins-2.528.3-1.1.noarch.rpm
sed -i 's#https://updates.jenkins.io/update-center.json#https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json#' /var/lib/jenkins/hudson.model.UpdateCenter.xml
systemctl enable --now jenkins
cat /var/lib/jenkins/secrets/initialAdminPassword

# ============ 【ci-jenkins】手工验证一次完整构建 ============
export PATH=/opt/maven/bin:$PATH
for r in ruoyi-dependencies ruoyi-common ruoyi-api; do
  git clone http://192.168.0.25:3000/ruoyi/$r.git && (cd $r && mvn clean install -Dmaven.test.skip=true)
done
git clone http://192.168.0.25:3000/ruoyi/ruoyi-gateway.git
cd ruoyi-gateway && mvn clean package -Dmaven.test.skip=true
docker build -t 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:test .
docker push 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:test
kubectl apply -f k8s/deployment.yaml -n ruoyi

# ============ 日常运维 ============
kubectl get nodes -o wide
kubectl get pods -n ruoyi -o wide
kubectl get endpoints -n ruoyi
kubectl logs -n ruoyi -l app=ruoyi-gateway --tail=100 -f
kubectl rollout restart deployment/ruoyi-gateway-deploy -n ruoyi
kubectl rollout undo deployment/ruoyi-gateway-deploy -n ruoyi
kubectl describe pod <pod-name> -n ruoyi

12.2 全部下载地址汇总(写作时实测)

所有地址均在 2026-09 实测可用

下表左列的「标记」指本次探测返回的 HTTP 状态:200/206 均为可正常下载。206 是因为探测时用了 Range 请求(只取前 1KB)。

① 系统与容器运行时

用途地址实测
containerd / Docker(阿里云)https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo200
Docker CE 软件包目录https://mirrors.aliyun.com/docker-ce/linux/centos/9/x86_64/stable/200
containerd 镜像加速(daocloud)https://m.daocloud.io可用

② Kubernetes

用途地址实测
K8s yum 源(v1.36)https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/200
该源的 GPG Keyhttps://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/repodata/repomd.xml.key200
控制平面镜像仓库(阿里云)registry.cn-hangzhou.aliyuncs.com/google_containers实测有 kube-apiserver:v1.36.5
控制平面镜像仓库(daocloud)m.daocloud.io/registry.k8s.io实测同上
pause 镜像m.daocloud.io/registry.k8s.io/pause:3.10206
Calico 清单 v3.32.2https://gh-proxy.com/https://raw.githubusercontent.com/projectcalico/calico/v3.32.2/manifests/calico.yaml206
Calico 镜像m.daocloud.io/docker.io/calico/node:v3.32.2206
metrics-server 清单https://gh-proxy.com/https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml可下载
metrics-server 镜像m.daocloud.io/registry.k8s.io/metrics-server/metrics-server:v0.9.0206
ingress-nginx 清单https://gh-proxy.com/https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml可下载
Helm v4.3.0https://get.helm.sh/helm-v4.3.0-linux-amd64.tar.gz(或华为镜像 https://mirrors.huaweicloud.com/helm/v4.3.0/helm-v4.3.0-linux-amd64.tar.gz206

③ Harbor / Nacos

用途地址实测
Harbor 离线包 v2.15.2https://gh-proxy.com/https://github.com/goharbor/harbor/releases/download/v2.15.2/harbor-offline-installer-v2.15.2.tgz200
Harbor 在线包同上,文件名改 harbor-online-installer-v2.15.2.tgz206
Nacos 2.5.4(gh-proxy)https://gh-proxy.com/https://github.com/alibaba/nacos/releases/download/2.5.4/nacos-server-2.5.4.tar.gz206
Nacos 2.5.4(华为云,直链)https://mirrors.huaweicloud.com/nacos/nacos-2.5.4.tar.gz200

④ Jenkins / Maven / 代码仓库

用途地址实测
Jenkins rpm 目录(清华)https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/200
Jenkins 插件更新中心(清华)https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json可用
Maven 3.8.9(清华)https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.9/binaries/apache-maven-3.8.9-bin.tar.gz可下载
Maven 3.8.9(归档,兜底)https://archive.apache.org/dist/maven/maven-3/3.8.9/binaries/apache-maven-3.8.9-bin.tar.gz可下载
Maven 中央仓库镜像https://maven.aliyun.com/repository/public可用
Gitea v1.27.3https://gh-proxy.com/https://github.com/go-gitea/gitea/releases/download/v1.27.3/gitea-1.27.3-linux-amd64206

⑤ 通用技巧:GitHub 加速前缀

下面三个前缀都可以直接加在 https://github.com/...https://raw.githubusercontent.com/... 前面,实测均可用:

前缀适用场景
https://gh-proxy.com/最稳,release 与 raw 都支持,也支持 git clone
https://ghfast.top/备用
https://cdn.jsdelivr.net/gh/<用户>/<仓库>@<版本>/<路径>只适用于仓库内的文件(不支持 release 附件)

镜像拉取的国内代理前缀:

前缀覆盖的上游
m.daocloud.io/docker.io/Docker Hub
m.daocloud.io/registry.k8s.io/K8s 官方
quay.m.daocloud.io/quay.io
ghcr.m.daocloud.io/ghcr.io
files.m.daocloud.io/github.com/GitHub Release 二进制(如 kubectl、argocd CLI)

已经失效 / 不要用的地址(本次实测排除)

地址结果
swr.cn-north-4.myhuaweicloud.com/ddn-k8s/quay.io/argoproj/argocd404
docker.m.daocloud.io/argoproj/argocd403(该域名只代理 Docker Hub,不代理 quay)
mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/jenkins.repo404(清华源里没有 repo 文件,只有 rpm)
mirrors.aliyun.com/docker-ce/linux/centos/9/docker-ce.repo404(阿里云的 repo 文件路径不带版本号)
download.docker.com国内连接超时

12.3 怎么为你的集群选 K8s 版本

这是本文档最值得带走的方法论。

一句话原则:先看工具链支持到哪,再决定装哪个 K8s

大多数人装 K8s 的习惯是「装最新版」。这在单机软件里没问题,但 K8s 生态有个硬约束:

几乎所有周边工具(Argo CD、Argo Rollouts、Prometheus Operator、Operator SDK 等)的支持策略都是「只覆盖最近 3 个 minor 版本」,与 K8s 官方的 N-2 维护窗口对齐。

后果是:如果你的 K8s 版本太新,周边工具还没适配;太旧,周边工具已经放弃。 一旦踩到这个坑,你会在安装每个工具时都遇到一次版本墙。

正确的选型流程(4 步):

步骤做什么具体操作
列出你必须用的工具例如:Argo CD(CD)、Argo Rollouts(灰度)、metrics-server(HPA)、ingress-nginx
查每个工具的「Tested Kubernetes versions」Argo CD 在官方文档的 operator-manual/installation 页;Argo Rollouts 更直接——读它仓库里的 .github/workflows/*.yaml,里面列了 CI 实际测试的 K8s 版本
取交集找出被所有工具覆盖的 K8s minor 版本
在交集里选「第二新」的那一个最新版刚发布,周边工具的补丁还没跟上;第二新最稳

实例计算(写作时的数据):

text
K8s 官方最新:       v1.37.1   (2026-09-23)
官方维护窗口:       1.35 / 1.36 / 1.37

Argo CD 3.5.3:      覆盖最近 3 个 minor → 1.35 ~ 1.37
Argo Rollouts:      stable 分支测 1.32 ~ 1.35,master 测 1.34 ~ 1.37
metrics-server v0.9:支持 1.30+

交集 → { 1.35 }
再留一档余量 → 选 v1.35.x 或 v1.36.x
本文档选 v1.36.x(也是所有工具都覆盖的版本)

为什么"第二新"是最优解

选择风险
最新版(1.37)部分工具的补丁还没发布,可能遇到未修复的兼容 bug
第二新(1.36)所有主流工具都已适配,且仍在官方维护窗口内
再老一版(1.34)快接近维护窗口边缘,半年后就得被迫升级
更老(≤1.33)已经出窗口,新工具直接不支持,会被迫做一次大版本升级

如果已经踩坑了怎么办:优先升级集群,而不是降级工具。集群升级路径是 1.34 → 1.35 → 1.36 逐个小版本走(K8s 不支持跨 minor 升级),每次升级大约 30 分钟,可以滚动完成。

12.4 这套流程的三个已知短板

本文档交付的是一套能跑通、能理解的方案,但它有两处刻意的简化,和一处固有的缺陷。知道短板在哪,比会用工具更重要。

短板一:sed 修改清单文件

第 9 章的 sed -i 's/:latest/:$VERSION/g'把部署参数写进了构建过程。它造成:

  • 构建期间源码被改动(工作区 git 树 dirty)
  • 部署的清单与仓库不一致(出问题时无法通过 git diff 追溯)
  • 回滚时无法得知目标版本(kubectl rollout undo 只能回退一版,再往前就不知道镜像 tag 是什么了)

改进方向:把「部署参数」从清单里抽出来,交给独立的 GitOps 仓库管理(见文档 ②)。

短板二:CI 承担了 CD 的职责

Jenkins 既在编译代码,又在 kubectl apply。这意味着:

  • Jenkins 必须持有集群的高权限凭据(安全面扩大)
  • 构建与发布耦合(编译失败和发布失败在同一个 Job 里,状态含义模糊)
  • 集群的实际状态没有任何地方记录(谁改了、什么时候改的、改成了什么)

改进方向:CI 只负责「产出镜像 + 更新版本声明」,CD 由 GitOps 控制器持续对齐(见文档 ②)。

短板三:没有灰度能力

本文档用的是 kubectl apply部署是"全量切换":7 个服务一起更新,出问题只能整体回滚。

生产环境真正需要的是:

  • 金丝雀:先给 5% 流量,观察 5 分钟,没问题再全量
  • 蓝绿:新版本完全就绪后再切流量,切换瞬间完成
  • 按指标自动中止:错误率超过阈值自动回滚

这需要 Argo Rollouts 这类专门的渐进式交付控制器(见文档 ③)。

12.5 下一步看什么

如果你想要看这份
让部署这件事也变得可审计、可回滚、自动纠偏(CI/CD 分离)文档 ②《Jenkins 做 CI、Argo CD 做 CD》
理解客户端/服务端类软件的发布(长连接、热更新、多版本共存)文档 ③《客户端 / 服务端类软件发布》
看这套方案在我自己的服务器上是怎么落地的本站「我的环境实施记录」栏目

三句话总结

  1. 顺序不能乱:主机名 → 内核参数 → 容器运行时 → kubeadm → CNI → 中间件 → Jenkins → 流水线。每一步都是下一步的前提,跳步会产生"看起来很怪"的错误。
  2. 网段是最容易埋雷的地方podSubnetserviceSubnet 绝不能和物理网段重叠;这条在文档里出现了三次,因为它真的会浪费你一整晚。
  3. 本文档的终点是"能自动部署",不是"部署得好"sed 改清单、Jenkins 直连集群,这些都是先跑通再优化的阶段性选择。下一步的演进方向在 12.4 已经写清楚了。

基于 VitePress 构建 · 内容为个人学习与工程实践笔记