实验:RuoYi-Cloud 在 高可用的Kubernetes 上的完整安装步骤(国内镜像版)

本文只包含 若依微服务相关安装,不包含 Kubernetes 集群、Prometheus、Grafana 等平台组件的安装。

本文固定使用当前实验环境已经验证的版本和方式,不追求“最新版本”。


1. 最终部署结构

ruoyi 命名空间
├── ruoyi-mysql
│   ├── StatefulSet
│   ├── PVC:5Gi
│   └── ClusterIP:3306
├── ruoyi-redis
│   ├── StatefulSet
│   ├── PVC:2Gi
│   └── ClusterIP:6379
├── ruoyi-nacos
│   ├── StatefulSet
│   ├── PVC:2Gi
│   ├── ClusterIP:8848、9848
│   └── NodePort:30082 → 控制台 8080
├── ruoyi-gateway
│   ├── Deployment
│   └── ClusterIP:8080
├── ruoyi-auth
│   ├── Deployment
│   └── ClusterIP:9200
├── ruoyi-system
│   ├── Deployment
│   └── ClusterIP:9201
└── ruoyi-ui
    ├── Deployment
    └── NodePort:30081 → Nginx 80

浏览器访问:

若依前端:http://192.168.89.104:30081
Nacos控制台:http://192.168.89.104:30082/index.html

也可以把 .104 换成 .105 或 .106。


2. 本次固定版本

RuoYi-Cloud:springboot3 分支
JDK:17
Helm:3.x
MySQL:5.7.44
Redis:7.2.5-alpine
Nacos:v3.0.2
Node:16.20.2-alpine
Nginx:1.27.5-alpine
StorageClass:local-path

国内镜像统一使用:

m.daocloud.io

实验密码:

MySQL root:RuoYiMysql2026@
Redis:RuoYiRedis2026@
Nacos控制台:nacos / nacos
若依系统:admin / admin123

这些密码只适合实验环境,生产环境必须更换,并使用 Vault、External Secrets 或企业密钥管理系统。


第一阶段:前置检查和源码准备

3. 检查集群、Helm 和 StorageClass

以下命令全部在 k8s-master01 执行:

kubectl get nodes -o wide
helm version --short
kubectl get storageclass

必须满足:

6 个节点全部 Ready
Helm 命令可用
local-path 为默认 StorageClass

4. 创建 ruoyi 命名空间

kubectl create namespace ruoyi \
  --dry-run=client \
  -o yaml \
  | kubectl apply -f -

检查:

kubectl get namespace ruoyi

5. 创建工作目录

mkdir -p "$HOME/ruoyi-k8s-lab"/{charts,source,images,artifacts}

mkdir -p \
  "$HOME/ruoyi-k8s-lab/charts/mysql" \
  "$HOME/ruoyi-k8s-lab/charts/redis" \
  "$HOME/ruoyi-k8s-lab/charts/nacos" \
  "$HOME/ruoyi-k8s-lab/charts/ruoyi-app"

目录用途:

charts/mysql       MySQL Helm Chart
charts/redis       Redis Helm Chart
charts/nacos       Nacos Helm Chart
charts/ruoyi-app   Gateway、Auth、System、UI Helm Chart
source             若依源码
images             镜像构建上下文
artifacts          导出的镜像 tar 包

6. 下载若依 springboot3 源码

sudo apt update
sudo apt install -y git

cd "$HOME/ruoyi-k8s-lab/source"

git clone \
  --depth 1 \
  --branch springboot3 \
  https://gitee.com/y_project/RuoYi-Cloud.git

检查:

cd "$HOME/ruoyi-k8s-lab/source/RuoYi-Cloud"
git branch --show-current
find ./sql -maxdepth 1 -type f -name '*.sql' | sort

应返回分支:

springboot3

本次只导入:

ry_*.sql
ry_config_*.sql

第二阶段:安装 MySQL

7. 创建 MySQL Helm Chart

rm -rf "$HOME/ruoyi-k8s-lab/charts/mysql"
mkdir -p "$HOME/ruoyi-k8s-lab/charts/mysql/templates"
cd "$HOME/ruoyi-k8s-lab/charts/mysql"

7.1 创建 Chart.yaml

cat <<'EOF' > Chart.yaml
apiVersion: v2
name: ruoyi-mysql
description: MySQL for RuoYi Cloud Kubernetes lab
type: application
version: 0.1.0
appVersion: "5.7.44"
EOF

7.2 创建 values-lab.yaml

cat <<'EOF' > values-lab.yaml
fullnameOverride: ruoyi-mysql

image:
  repository: m.daocloud.io/docker.io/library/mysql
  tag: "5.7.44"
  pullPolicy: IfNotPresent

auth:
  rootPassword: "RuoYiMysql2026@"
  database: "ry-cloud"

service:
  port: 3306

persistence:
  storageClass: local-path
  size: 5Gi

mysql:
  innodbBufferPoolSize: 80M

resources:
  requests:
    cpu: 200m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 1Gi
EOF

7.3 创建 templates/secret.yaml

cat <<'EOF' > templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
type: Opaque
stringData:
  mysql-root-password: {{ .Values.auth.rootPassword | quote }}
EOF

7.4 创建 templates/service.yaml

cat <<'EOF' > templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
  ports:
    - name: mysql
      port: {{ .Values.service.port }}
      targetPort: mysql
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}-headless
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  clusterIP: None
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
  ports:
    - name: mysql
      port: {{ .Values.service.port }}
      targetPort: mysql
EOF

7.5 创建 templates/statefulset.yaml

cat <<'EOF' > templates/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  serviceName: {{ .Values.fullnameOverride }}-headless
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.fullnameOverride }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.fullnameOverride }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: mysql
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          args:
            - "--innodb-buffer-pool-size={{ .Values.mysql.innodbBufferPoolSize }}"
            - "--character-set-server=utf8mb4"
            - "--collation-server=utf8mb4_unicode_ci"
            - "--default-time-zone=+8:00"
            - "--lower-case-table-names=1"
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.fullnameOverride }}
                  key: mysql-root-password
            - name: MYSQL_ROOT_HOST
              value: "%"
            - name: MYSQL_DATABASE
              value: {{ .Values.auth.database | quote }}
            - name: TZ
              value: Asia/Shanghai
          ports:
            - name: mysql
              containerPort: 3306
              protocol: TCP
          startupProbe:
            exec:
              command:
                - sh
                - -c
                - mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 30
          readinessProbe:
            exec:
              command:
                - sh
                - -c
                - mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 6
          livenessProbe:
            exec:
              command:
                - sh
                - -c
                - mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
            periodSeconds: 20
            timeoutSeconds: 5
            failureThreshold: 6
          resources:
{{ toYaml .Values.resources | indent 12 }}
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: {{ .Values.persistence.storageClass | quote }}
        resources:
          requests:
            storage: {{ .Values.persistence.size }}
EOF

8. 检查并安装 MySQL

cd "$HOME/ruoyi-k8s-lab"

helm lint ./charts/mysql \
  -f ./charts/mysql/values-lab.yaml

helm template ruoyi-mysql \
  ./charts/mysql \
  --namespace ruoyi \
  -f ./charts/mysql/values-lab.yaml \
  > /tmp/ruoyi-mysql-rendered.yaml

grep -n 'image:' /tmp/ruoyi-mysql-rendered.yaml

正式安装:

helm upgrade --install ruoyi-mysql \
  ./charts/mysql \
  --namespace ruoyi \
  -f ./charts/mysql/values-lab.yaml \
  --wait \
  --timeout 15m

检查:

helm list -n ruoyi
kubectl get pod,svc,pvc -n ruoyi -o wide
kubectl logs -n ruoyi ruoyi-mysql-0 --tail=100

9. 导入若依数据库 SQL

MYSQL_ROOT_PASSWORD="$(
  kubectl get secret ruoyi-mysql \
    -n ruoyi \
    -o jsonpath='{.data.mysql-root-password}' \
  | base64 -d
)"

验证登录:

kubectl exec \
  -n ruoyi \
  ruoyi-mysql-0 \
  -- env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" \
  mysql -uroot \
  -e 'SELECT VERSION(); SHOW DATABASES;'

定位 SQL:

cd "$HOME/ruoyi-k8s-lab/source/RuoYi-Cloud"

RY_SQL="$(
  find ./sql \
    -maxdepth 1 \
    -type f \
    -name 'ry_*.sql' \
    ! -name 'ry_config_*.sql' \
    ! -name 'ry_seata_*.sql' \
  | sort \
  | tail -n 1
)"

RY_CONFIG_SQL="$(
  find ./sql \
    -maxdepth 1 \
    -type f \
    -name 'ry_config_*.sql' \
  | sort \
  | tail -n 1
)"

echo "若依业务SQL:$RY_SQL"
echo "Nacos配置SQL:$RY_CONFIG_SQL"

创建数据库:

kubectl exec \
  -n ruoyi \
  ruoyi-mysql-0 \
  -- env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" \
  mysql -uroot \
  -e '
    CREATE DATABASE IF NOT EXISTS `ry-cloud`
      DEFAULT CHARACTER SET utf8mb4
      COLLATE utf8mb4_unicode_ci;
    CREATE DATABASE IF NOT EXISTS `ry-config`
      DEFAULT CHARACTER SET utf8mb4
      COLLATE utf8mb4_unicode_ci;
  '

导入业务 SQL:

kubectl exec -i \
  -n ruoyi \
  ruoyi-mysql-0 \
  -- env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" \
  mysql \
    -uroot \
    --default-character-set=utf8mb4 \
    ry-cloud \
  < "$RY_SQL"

导入 Nacos 配置 SQL:

kubectl exec -i \
  -n ruoyi \
  ruoyi-mysql-0 \
  -- env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" \
  mysql \
    -uroot \
    --default-character-set=utf8mb4 \
    ry-config \
  < "$RY_CONFIG_SQL"

验证:

kubectl exec \
  -n ruoyi \
  ruoyi-mysql-0 \
  -- env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" \
  mysql -uroot \
  -e '
    SELECT table_schema, COUNT(*) AS table_count
    FROM information_schema.tables
    WHERE table_schema IN ("ry-cloud", "ry-config")
    GROUP BY table_schema;
  '

第三阶段:安装 Redis

10. 创建 Redis Helm Chart

rm -rf "$HOME/ruoyi-k8s-lab/charts/redis"
mkdir -p "$HOME/ruoyi-k8s-lab/charts/redis/templates"
cd "$HOME/ruoyi-k8s-lab/charts/redis"

10.1 创建 Chart.yaml

cat <<'EOF' > Chart.yaml
apiVersion: v2
name: ruoyi-redis
description: Redis for RuoYi Cloud Kubernetes lab
type: application
version: 0.1.0
appVersion: "7.2.5"
EOF

10.2 创建 values-lab.yaml

cat <<'EOF' > values-lab.yaml
fullnameOverride: ruoyi-redis

image:
  repository: m.daocloud.io/docker.io/library/redis
  tag: "7.2.5-alpine"
  pullPolicy: IfNotPresent

auth:
  password: "RuoYiRedis2026@"

service:
  port: 6379

persistence:
  storageClass: local-path
  size: 2Gi

redis:
  appendonly: "yes"
  maxmemory: "128mb"
  maxmemoryPolicy: "allkeys-lru"

resources:
  requests:
    cpu: 50m
    memory: 128Mi
  limits:
    cpu: 300m
    memory: 256Mi
EOF

10.3 创建 templates/secret.yaml

cat <<'EOF' > templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
type: Opaque
stringData:
  redis-password: {{ .Values.auth.password | quote }}
EOF

10.4 创建 templates/service.yaml

cat <<'EOF' > templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: redis
      port: {{ .Values.service.port }}
      targetPort: redis
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}-headless
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  clusterIP: None
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: redis
      port: {{ .Values.service.port }}
      targetPort: redis
EOF

10.5 创建 templates/statefulset.yaml

cat <<'EOF' > templates/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  serviceName: {{ .Values.fullnameOverride }}-headless
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.fullnameOverride }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.fullnameOverride }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: redis
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          args:
            - "--appendonly"
            - {{ .Values.redis.appendonly | quote }}
            - "--dir"
            - "/data"
            - "--requirepass"
            - "$(REDIS_PASSWORD)"
            - "--maxmemory"
            - {{ .Values.redis.maxmemory | quote }}
            - "--maxmemory-policy"
            - {{ .Values.redis.maxmemoryPolicy | quote }}
          env:
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.fullnameOverride }}
                  key: redis-password
          ports:
            - name: redis
              containerPort: 6379
              protocol: TCP
          startupProbe:
            exec:
              command:
                - sh
                - -c
                - REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h 127.0.0.1 ping | grep -q PONG
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30
          readinessProbe:
            exec:
              command:
                - sh
                - -c
                - REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h 127.0.0.1 ping | grep -q PONG
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            exec:
              command:
                - sh
                - -c
                - REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h 127.0.0.1 ping | grep -q PONG
            periodSeconds: 20
            timeoutSeconds: 3
            failureThreshold: 6
          resources:
{{ toYaml .Values.resources | indent 12 }}
          volumeMounts:
            - name: data
              mountPath: /data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: {{ .Values.persistence.storageClass | quote }}
        resources:
          requests:
            storage: {{ .Values.persistence.size }}
EOF

11. 检查并安装 Redis

cd "$HOME/ruoyi-k8s-lab"

helm lint ./charts/redis \
  -f ./charts/redis/values-lab.yaml

helm template ruoyi-redis \
  ./charts/redis \
  --namespace ruoyi \
  -f ./charts/redis/values-lab.yaml \
  > /tmp/ruoyi-redis-rendered.yaml

正式安装:

helm upgrade --install ruoyi-redis \
  ./charts/redis \
  --namespace ruoyi \
  -f ./charts/redis/values-lab.yaml \
  --wait \
  --timeout 10m

检查:

kubectl get pod,svc,pvc -n ruoyi -o wide
kubectl logs -n ruoyi ruoyi-redis-0 --tail=100

读取密码并测试:

REDIS_PASSWORD="$(
  kubectl get secret ruoyi-redis \
    -n ruoyi \
    -o jsonpath='{.data.redis-password}' \
  | base64 -d
)"

kubectl exec \
  -n ruoyi \
  ruoyi-redis-0 \
  -- env REDISCLI_AUTH="$REDIS_PASSWORD" \
  redis-cli ping

应返回:

PONG

写入和读取测试:

kubectl exec \
  -n ruoyi \
  ruoyi-redis-0 \
  -- env REDISCLI_AUTH="$REDIS_PASSWORD" \
  redis-cli SET ruoyi:test redis-ok

kubectl exec \
  -n ruoyi \
  ruoyi-redis-0 \
  -- env REDISCLI_AUTH="$REDIS_PASSWORD" \
  redis-cli GET ruoyi:test

第四阶段:安装 Nacos

12. 检查 NodePort 30082

kubectl get svc -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,NODEPORT:.spec.ports[*].nodePort' \
  | grep -w 30082 || true

没有输出再继续。


13. 三个 Worker 预拉取 Nacos 镜像

分别登录 .104、.105、.106,每台执行:

sudo crictl pull \
  m.daocloud.io/docker.io/nacos/nacos-server:v3.0.2

检查:

sudo crictl images | grep nacos

14. 创建 Nacos Helm Chart

回到 master01:

rm -rf "$HOME/ruoyi-k8s-lab/charts/nacos"
mkdir -p "$HOME/ruoyi-k8s-lab/charts/nacos/templates"
cd "$HOME/ruoyi-k8s-lab/charts/nacos"

14.1 创建 Chart.yaml

cat <<'EOF' > Chart.yaml
apiVersion: v2
name: ruoyi-nacos
description: Nacos for RuoYi Cloud Kubernetes lab
type: application
version: 0.1.0
appVersion: "3.0.2"
EOF

14.2 创建 values-lab.yaml

cat <<'EOF' > values-lab.yaml
fullnameOverride: ruoyi-nacos

image:
  repository: m.daocloud.io/docker.io/nacos/nacos-server
  tag: "v3.0.2"
  pullPolicy: IfNotPresent

mysql:
  host: ruoyi-mysql
  port: 3306
  database: ry-config
  user: root
  existingSecret: ruoyi-mysql
  passwordKey: mysql-root-password

service:
  serverPort: 8848
  consolePort: 8080
  grpcClientPort: 9848

console:
  type: NodePort
  nodePort: 30082

persistence:
  enabled: true
  storageClass: local-path
  size: 2Gi

nacos:
  mode: standalone
  functionMode: all
  auth:
    enabled: true
    token: "UnVvWWlOYWNvc0F1dGhUb2tlbjIwMjYtTGFiLUNoYW5nZU1lLTEyMzQ1Njc4OTA="
    identityKey: "ruoyi-nacos-server"
    identityValue: "ruoyi-nacos-server-2026"
  jvm:
    xms: 512m
    xmx: 512m
    xmn: 256m

resources:
  requests:
    cpu: 200m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 1280Mi
EOF

14.3 创建 templates/secret.yaml

cat <<'EOF' > templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
type: Opaque
stringData:
  nacos-auth-token: {{ .Values.nacos.auth.token | quote }}
  nacos-identity-key: {{ .Values.nacos.auth.identityKey | quote }}
  nacos-identity-value: {{ .Values.nacos.auth.identityValue | quote }}
EOF

14.4 创建 templates/service.yaml

cat <<'EOF' > templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: server
      port: {{ .Values.service.serverPort }}
      targetPort: server
      protocol: TCP
    - name: grpc-client
      port: {{ .Values.service.grpcClientPort }}
      targetPort: grpc-client
      protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}-headless
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  clusterIP: None
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: server
      port: {{ .Values.service.serverPort }}
      targetPort: server
      protocol: TCP
    - name: grpc-client
      port: {{ .Values.service.grpcClientPort }}
      targetPort: grpc-client
      protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.fullnameOverride }}-console
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  type: {{ .Values.console.type }}
  selector:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: console
      port: {{ .Values.service.consolePort }}
      targetPort: console
      nodePort: {{ .Values.console.nodePort }}
      protocol: TCP
EOF

14.5 创建 templates/statefulset.yaml

cat <<'EOF' > templates/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Values.fullnameOverride }}
  labels:
    app.kubernetes.io/name: {{ .Values.fullnameOverride }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  serviceName: {{ .Values.fullnameOverride }}-headless
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.fullnameOverride }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.fullnameOverride }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      terminationGracePeriodSeconds: 60
      initContainers:
        - name: wait-for-mysql
          image: m.daocloud.io/docker.io/library/busybox:1.36.1
          imagePullPolicy: IfNotPresent
          command:
            - sh
            - -c
            - |
              echo "等待 MySQL:{{ .Values.mysql.host }}:{{ .Values.mysql.port }}"
              until nc -z {{ .Values.mysql.host }} {{ .Values.mysql.port }}; do
                echo "MySQL 尚未就绪,3 秒后重试"
                sleep 3
              done
              echo "MySQL 已可以连接"
      containers:
        - name: nacos
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          env:
            - name: MODE
              value: {{ .Values.nacos.mode | quote }}
            - name: FUNCTION_MODE
              value: {{ .Values.nacos.functionMode | quote }}
            - name: NACOS_DEPLOYMENT_TYPE
              value: "merged"
            - name: NACOS_APPLICATION_PORT
              value: {{ .Values.service.serverPort | quote }}
            - name: NACOS_CONSOLE_PORT
              value: {{ .Values.service.consolePort | quote }}
            - name: NACOS_CONSOLE_UI_ENABLED
              value: "true"
            - name: SPRING_DATASOURCE_PLATFORM
              value: "mysql"
            - name: MYSQL_SERVICE_HOST
              value: {{ .Values.mysql.host | quote }}
            - name: MYSQL_SERVICE_PORT
              value: {{ .Values.mysql.port | quote }}
            - name: MYSQL_SERVICE_DB_NAME
              value: {{ .Values.mysql.database | quote }}
            - name: MYSQL_SERVICE_USER
              value: {{ .Values.mysql.user | quote }}
            - name: MYSQL_SERVICE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.mysql.existingSecret }}
                  key: {{ .Values.mysql.passwordKey }}
            - name: MYSQL_DATABASE_NUM
              value: "1"
            - name: MYSQL_SERVICE_DB_PARAM
              value: "characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai"
            - name: NACOS_AUTH_ENABLE
              value: {{ .Values.nacos.auth.enabled | quote }}
            - name: NACOS_AUTH_ADMIN_ENABLE
              value: "true"
            - name: NACOS_AUTH_CONSOLE_ENABLE
              value: "true"
            - name: NACOS_AUTH_TOKEN
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.fullnameOverride }}
                  key: nacos-auth-token
            - name: NACOS_AUTH_IDENTITY_KEY
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.fullnameOverride }}
                  key: nacos-identity-key
            - name: NACOS_AUTH_IDENTITY_VALUE
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.fullnameOverride }}
                  key: nacos-identity-value
            - name: JVM_XMS
              value: {{ .Values.nacos.jvm.xms | quote }}
            - name: JVM_XMX
              value: {{ .Values.nacos.jvm.xmx | quote }}
            - name: JVM_XMN
              value: {{ .Values.nacos.jvm.xmn | quote }}
            - name: TZ
              value: "Asia/Shanghai"
          ports:
            - name: console
              containerPort: 8080
              protocol: TCP
            - name: server
              containerPort: 8848
              protocol: TCP
            - name: grpc-client
              containerPort: 9848
              protocol: TCP
          startupProbe:
            httpGet:
              path: /nacos/v3/admin/core/state/liveness
              port: server
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /nacos/v3/admin/core/state/readiness
              port: server
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 12
          livenessProbe:
            httpGet:
              path: /nacos/v3/admin/core/state/liveness
              port: server
            periodSeconds: 20
            timeoutSeconds: 5
            failureThreshold: 6
          resources:
{{ toYaml .Values.resources | indent 12 }}
{{- if .Values.persistence.enabled }}
          volumeMounts:
            - name: data
              mountPath: /home/nacos/data
{{- end }}
{{- if .Values.persistence.enabled }}
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: {{ .Values.persistence.storageClass | quote }}
        resources:
          requests:
            storage: {{ .Values.persistence.size }}
{{- end }}
EOF

15. 检查并安装 Nacos

cd "$HOME/ruoyi-k8s-lab"

helm lint ./charts/nacos \
  -f ./charts/nacos/values-lab.yaml

helm template ruoyi-nacos \
  ./charts/nacos \
  --namespace ruoyi \
  -f ./charts/nacos/values-lab.yaml \
  > /tmp/ruoyi-nacos-rendered.yaml

安装:

helm upgrade --install ruoyi-nacos \
  ./charts/nacos \
  --namespace ruoyi \
  -f ./charts/nacos/values-lab.yaml \
  --wait \
  --timeout 15m

观察和日志:

kubectl get pod,pvc,svc -n ruoyi -o wide
kubectl logs -n ruoyi ruoyi-nacos-0 --tail=200

浏览器访问:

http://192.168.89.104:30082/index.html

登录:

用户名:nacos
密码:nacos

16. 修改 Nacos 中的 Kubernetes 地址

读取密码:

MYSQL_ROOT_PASSWORD="$(
  kubectl get secret ruoyi-mysql \
    -n ruoyi \
    -o jsonpath='{.data.mysql-root-password}' \
  | base64 -d
)"

REDIS_PASSWORD="$(
  kubectl get secret ruoyi-redis \
    -n ruoyi \
    -o jsonpath='{.data.redis-password}' \
  | base64 -d
)"

在 Nacos 控制台修改以下配置,其他内容保持不变。

ruoyi-gateway-dev.yml

spring:
  data:
    redis:
      host: ruoyi-redis
      port: 6379
      password: RuoYiRedis2026@

ruoyi-auth-dev.yml

spring:
  data:
    redis:
      host: ruoyi-redis
      port: 6379
      password: RuoYiRedis2026@

ruoyi-system-dev.yml

Redis:

spring:
  data:
    redis:
      host: ruoyi-redis
      port: 6379
      password: RuoYiRedis2026@

MySQL 主库:

master:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://ruoyi-mysql:3306/ry-cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
  username: root
  password: RuoYiMysql2026@

每次修改后点击:

发布

第五阶段:编译和制作后端镜像

17. 安装 JDK、Maven 和 Buildah

只在 master01 执行:

sudo apt update
sudo apt install -y \
  openjdk-17-jdk \
  maven \
  buildah

检查:

java -version
mvn -version
sudo buildah version

18. Maven 编译 Gateway、Auth、System

cd "$HOME/ruoyi-k8s-lab/source/RuoYi-Cloud"

export MAVEN_OPTS="-Xms128m -Xmx768m -XX:+UseG1GC"

mvn \
  -DskipTests \
  -T 1 \
  clean package \
  -pl ruoyi-gateway,ruoyi-auth,ruoyi-modules/ruoyi-system \
  -am

成功时应看到:

BUILD SUCCESS

定位 JAR:

GATEWAY_JAR="$(
  find ruoyi-gateway/target \
    -maxdepth 1 \
    -type f \
    -name 'ruoyi-gateway*.jar' \
  | sort \
  | head -n 1
)"

AUTH_JAR="$(
  find ruoyi-auth/target \
    -maxdepth 1 \
    -type f \
    -name 'ruoyi-auth*.jar' \
  | sort \
  | head -n 1
)"

SYSTEM_JAR="$(
  find ruoyi-modules/ruoyi-system/target \
    -maxdepth 1 \
    -type f \
    -name 'ruoyi-modules-system*.jar' \
  | sort \
  | head -n 1
)"

ls -lh "$GATEWAY_JAR" "$AUTH_JAR" "$SYSTEM_JAR"

19. 准备镜像目录和 JAR

mkdir -p \
  "$HOME/ruoyi-k8s-lab/images/gateway" \
  "$HOME/ruoyi-k8s-lab/images/auth" \
  "$HOME/ruoyi-k8s-lab/images/system" \
  "$HOME/ruoyi-k8s-lab/artifacts"

cp "$GATEWAY_JAR" \
  "$HOME/ruoyi-k8s-lab/images/gateway/app.jar"

cp "$AUTH_JAR" \
  "$HOME/ruoyi-k8s-lab/images/auth/app.jar"

cp "$SYSTEM_JAR" \
  "$HOME/ruoyi-k8s-lab/images/system/app.jar"

20. 创建三个 Dockerfile

Gateway:

cat <<'EOF' > "$HOME/ruoyi-k8s-lab/images/gateway/Dockerfile"
FROM m.daocloud.io/docker.io/library/eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY app.jar /app/app.jar
ENV JAVA_OPTS="-Xms128m -Xmx256m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
EOF

Auth:

cat <<'EOF' > "$HOME/ruoyi-k8s-lab/images/auth/Dockerfile"
FROM m.daocloud.io/docker.io/library/eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY app.jar /app/app.jar
ENV JAVA_OPTS="-Xms128m -Xmx256m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
EXPOSE 9200
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
EOF

System:

cat <<'EOF' > "$HOME/ruoyi-k8s-lab/images/system/Dockerfile"
FROM m.daocloud.io/docker.io/library/eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY app.jar /app/app.jar
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
EXPOSE 9201
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
EOF

21. 构建三个后端镜像

sudo buildah bud \
  --format docker \
  --layers \
  -t localhost/ruoyi-gateway:3.6.8-lab \
  "$HOME/ruoyi-k8s-lab/images/gateway"
sudo buildah bud \
  --format docker \
  --layers \
  -t localhost/ruoyi-auth:3.6.8-lab \
  "$HOME/ruoyi-k8s-lab/images/auth"
sudo buildah bud \
  --format docker \
  --layers \
  -t localhost/ruoyi-system:3.6.8-lab \
  "$HOME/ruoyi-k8s-lab/images/system"

检查:

sudo buildah images \
  | grep -E 'ruoyi-(gateway|auth|system)'

22. 导出三个镜像

cd "$HOME/ruoyi-k8s-lab"

sudo buildah push \
  localhost/ruoyi-gateway:3.6.8-lab \
  docker-archive:"$PWD/artifacts/ruoyi-gateway.tar":localhost/ruoyi-gateway:3.6.8-lab

sudo buildah push \
  localhost/ruoyi-auth:3.6.8-lab \
  docker-archive:"$PWD/artifacts/ruoyi-auth.tar":localhost/ruoyi-auth:3.6.8-lab

sudo buildah push \
  localhost/ruoyi-system:3.6.8-lab \
  docker-archive:"$PWD/artifacts/ruoyi-system.tar":localhost/ruoyi-system:3.6.8-lab

sudo chown leecurry:leecurry \
  "$HOME/ruoyi-k8s-lab/artifacts/"*.tar

23. 发送并导入三个 Worker

master01 发送:

for ip in 104 105 106; do
  echo "发送到 192.168.89.${ip}"
  scp \
    "$HOME/ruoyi-k8s-lab/artifacts/"*.tar \
    "leecurry@192.168.89.${ip}:/tmp/"
done

三个 Worker 每台执行:

for image_file in \
  /tmp/ruoyi-gateway.tar \
  /tmp/ruoyi-auth.tar \
  /tmp/ruoyi-system.tar
do
  sudo ctr \
    -n k8s.io \
    images import \
    "$image_file"
done

检查:

sudo crictl images \
  | grep -E 'ruoyi-(gateway|auth|system)'

每台都必须看到三个镜像。


第六阶段:部署 Gateway、Auth、System

24. 创建 ruoyi-app Helm Chart

回到 master01:

rm -rf "$HOME/ruoyi-k8s-lab/charts/ruoyi-app"
mkdir -p "$HOME/ruoyi-k8s-lab/charts/ruoyi-app/templates"
cd "$HOME/ruoyi-k8s-lab/charts/ruoyi-app"

24.1 创建 Chart.yaml

cat <<'EOF' > Chart.yaml
apiVersion: v2
name: ruoyi-app
description: RuoYi Cloud core services for Kubernetes lab
type: application
version: 0.1.0
appVersion: "3.6.8"
EOF

24.2 创建 values-lab.yaml

cat <<'EOF' > values-lab.yaml
global:
  imagePullPolicy: IfNotPresent
  profile: dev
  busyboxImage:
    repository: m.daocloud.io/docker.io/library/busybox
    tag: "1.36.1"
  nacos:
    serverAddr: ruoyi-nacos:8848
    username: nacos
    password: nacos

gateway:
  name: ruoyi-gateway
  replicas: 1
  image:
    repository: localhost/ruoyi-gateway
    tag: "3.6.8-lab"
  port: 8080
  javaOpts: >-
    -Xms128m
    -Xmx256m
    -XX:+UseG1GC
    -Dfile.encoding=UTF-8
    -Duser.timezone=Asia/Shanghai
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi

auth:
  name: ruoyi-auth
  replicas: 1
  image:
    repository: localhost/ruoyi-auth
    tag: "3.6.8-lab"
  port: 9200
  javaOpts: >-
    -Xms128m
    -Xmx256m
    -XX:+UseG1GC
    -Dfile.encoding=UTF-8
    -Duser.timezone=Asia/Shanghai
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi

system:
  name: ruoyi-system
  replicas: 1
  image:
    repository: localhost/ruoyi-system
    tag: "3.6.8-lab"
  port: 9201
  javaOpts: >-
    -Xms256m
    -Xmx512m
    -XX:+UseG1GC
    -Dfile.encoding=UTF-8
    -Duser.timezone=Asia/Shanghai
  resources:
    requests:
      cpu: 150m
      memory: 384Mi
    limits:
      cpu: 700m
      memory: 768Mi
EOF

24.3 创建 templates/secret.yaml

cat <<'EOF' > templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ruoyi-app-nacos
  labels:
    app.kubernetes.io/name: ruoyi-app
    app.kubernetes.io/instance: {{ .Release.Name }}
type: Opaque
stringData:
  username: {{ .Values.global.nacos.username | quote }}
  password: {{ .Values.global.nacos.password | quote }}
EOF

25. 创建 Gateway Deployment

cat <<'EOF' > templates/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.gateway.name }}
  labels:
    app.kubernetes.io/name: {{ .Values.gateway.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  replicas: {{ .Values.gateway.replicas }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.gateway.name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.gateway.name }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      initContainers:
        - name: wait-for-nacos
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-nacos 8848; do echo 等待Nacos; sleep 3; done"]
        - name: wait-for-redis
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-redis 6379; do echo 等待Redis; sleep 3; done"]
      containers:
        - name: gateway
          image: "{{ .Values.gateway.image.repository }}:{{ .Values.gateway.image.tag }}"
          imagePullPolicy: {{ .Values.global.imagePullPolicy }}
          env:
            - name: JAVA_OPTS
              value: {{ .Values.gateway.javaOpts | quote }}
            - name: SPRING_PROFILES_ACTIVE
              value: {{ .Values.global.profile | quote }}
            - name: SPRING_CLOUD_NACOS_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
            - name: SPRING_CLOUD_NACOS_CONFIG_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_CONFIG_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
            - name: SPRING_CLOUD_NACOS_DISCOVERY_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_DISCOVERY_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
          ports:
            - name: http
              containerPort: {{ .Values.gateway.port }}
          startupProbe:
            tcpSocket:
              port: http
            periodSeconds: 5
            failureThreshold: 60
          readinessProbe:
            tcpSocket:
              port: http
            periodSeconds: 10
            failureThreshold: 6
          livenessProbe:
            tcpSocket:
              port: http
            periodSeconds: 20
            failureThreshold: 6
          resources:
{{ toYaml .Values.gateway.resources | indent 12 }}
EOF

26. 创建 Auth Deployment

cat <<'EOF' > templates/auth-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.auth.name }}
  labels:
    app.kubernetes.io/name: {{ .Values.auth.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  replicas: {{ .Values.auth.replicas }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.auth.name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.auth.name }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      initContainers:
        - name: wait-for-nacos
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-nacos 8848; do echo 等待Nacos; sleep 3; done"]
        - name: wait-for-redis
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-redis 6379; do echo 等待Redis; sleep 3; done"]
      containers:
        - name: auth
          image: "{{ .Values.auth.image.repository }}:{{ .Values.auth.image.tag }}"
          imagePullPolicy: {{ .Values.global.imagePullPolicy }}
          env:
            - name: JAVA_OPTS
              value: {{ .Values.auth.javaOpts | quote }}
            - name: SPRING_PROFILES_ACTIVE
              value: {{ .Values.global.profile | quote }}
            - name: SPRING_CLOUD_NACOS_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
            - name: SPRING_CLOUD_NACOS_CONFIG_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_CONFIG_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
            - name: SPRING_CLOUD_NACOS_DISCOVERY_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_DISCOVERY_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
          ports:
            - name: http
              containerPort: {{ .Values.auth.port }}
          startupProbe:
            tcpSocket:
              port: http
            periodSeconds: 5
            failureThreshold: 60
          readinessProbe:
            tcpSocket:
              port: http
            periodSeconds: 10
            failureThreshold: 6
          livenessProbe:
            tcpSocket:
              port: http
            periodSeconds: 20
            failureThreshold: 6
          resources:
{{ toYaml .Values.auth.resources | indent 12 }}
EOF

27. 创建 System Deployment

cat <<'EOF' > templates/system-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.system.name }}
  labels:
    app.kubernetes.io/name: {{ .Values.system.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  replicas: {{ .Values.system.replicas }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.system.name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.system.name }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      initContainers:
        - name: wait-for-nacos
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-nacos 8848; do echo 等待Nacos; sleep 3; done"]
        - name: wait-for-redis
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-redis 6379; do echo 等待Redis; sleep 3; done"]
        - name: wait-for-mysql
          image: "{{ .Values.global.busyboxImage.repository }}:{{ .Values.global.busyboxImage.tag }}"
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "until nc -z ruoyi-mysql 3306; do echo 等待MySQL; sleep 3; done"]
      containers:
        - name: system
          image: "{{ .Values.system.image.repository }}:{{ .Values.system.image.tag }}"
          imagePullPolicy: {{ .Values.global.imagePullPolicy }}
          env:
            - name: JAVA_OPTS
              value: {{ .Values.system.javaOpts | quote }}
            - name: SPRING_PROFILES_ACTIVE
              value: {{ .Values.global.profile | quote }}
            - name: SPRING_CLOUD_NACOS_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR
              value: {{ .Values.global.nacos.serverAddr | quote }}
            - name: SPRING_CLOUD_NACOS_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
            - name: SPRING_CLOUD_NACOS_CONFIG_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_CONFIG_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
            - name: SPRING_CLOUD_NACOS_DISCOVERY_USERNAME
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: username
            - name: SPRING_CLOUD_NACOS_DISCOVERY_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ruoyi-app-nacos
                  key: password
          ports:
            - name: http
              containerPort: {{ .Values.system.port }}
          startupProbe:
            tcpSocket:
              port: http
            periodSeconds: 5
            failureThreshold: 60
          readinessProbe:
            tcpSocket:
              port: http
            periodSeconds: 10
            failureThreshold: 6
          livenessProbe:
            tcpSocket:
              port: http
            periodSeconds: 20
            failureThreshold: 6
          resources:
{{ toYaml .Values.system.resources | indent 12 }}
EOF

28. 创建三个 ClusterIP Service

cat <<'EOF' > templates/services.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.gateway.name }}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: {{ .Values.gateway.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      port: {{ .Values.gateway.port }}
      targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.auth.name }}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: {{ .Values.auth.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      port: {{ .Values.auth.port }}
      targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.system.name }}
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: {{ .Values.system.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      port: {{ .Values.system.port }}
      targetPort: http
EOF

29. 检查并安装后端服务

cd "$HOME/ruoyi-k8s-lab"

helm lint \
  ./charts/ruoyi-app \
  -f ./charts/ruoyi-app/values-lab.yaml

helm template ruoyi-app \
  ./charts/ruoyi-app \
  --namespace ruoyi \
  -f ./charts/ruoyi-app/values-lab.yaml \
  > /tmp/ruoyi-app-rendered.yaml

安装:

helm upgrade --install ruoyi-app \
  ./charts/ruoyi-app \
  --namespace ruoyi \
  -f ./charts/ruoyi-app/values-lab.yaml \
  --wait \
  --timeout 15m

检查:

kubectl get pods -n ruoyi -o wide
kubectl get deployment -n ruoyi
kubectl get service -n ruoyi

查看日志:

kubectl logs -n ruoyi deployment/ruoyi-gateway --tail=200
kubectl logs -n ruoyi deployment/ruoyi-auth --tail=200
kubectl logs -n ruoyi deployment/ruoyi-system --tail=200

Nacos 服务列表应出现:

ruoyi-gateway
ruoyi-auth
ruoyi-system

测试 Gateway:

kubectl run ruoyi-gateway-test \
  -n ruoyi \
  --rm \
  -i \
  --restart=Never \
  --image=m.daocloud.io/docker.io/library/busybox:1.36.1 \
  -- wget -S -O- http://ruoyi-gateway:8080/code

第七阶段:构建和部署 UI

30. 检查 NodePort 30081

kubectl get svc -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,NODEPORT:.spec.ports[*].nodePort' \
  | grep -w 30081 || true

没有输出再继续。


31. 修改前端生产接口前缀

cd "$HOME/ruoyi-k8s-lab/source/RuoYi-Cloud/ruoyi-ui"
if grep -q '^[[:space:]]*VUE_APP_BASE_API' .env.production; then
  sed -ri \
    "s#^[[:space:]]*VUE_APP_BASE_API[[:space:]]*=.*#VUE_APP_BASE_API = '/prod-api'#" \
    .env.production
else
  echo "VUE_APP_BASE_API = '/prod-api'" >> .env.production
fi

检查:

grep -n 'VUE_APP_BASE_API' .env.production

32. 准备 UI 构建目录

UI_SOURCE="$HOME/ruoyi-k8s-lab/source/RuoYi-Cloud/ruoyi-ui"
UI_CONTEXT="$HOME/ruoyi-k8s-lab/images/ui"

rm -rf "$UI_CONTEXT"
mkdir -p "$UI_CONTEXT"

cp -a "$UI_SOURCE"/. "$UI_CONTEXT"/

rm -rf \
  "$UI_CONTEXT/node_modules" \
  "$UI_CONTEXT/dist"

33. 创建 Nginx 配置

cat <<'EOF' > "$UI_CONTEXT/nginx.conf"
server {
    listen 80;
    server_name _;

    charset utf-8;
    root /usr/share/nginx/html;
    index index.html;

    client_max_body_size 100m;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /prod-api/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_connect_timeout 10s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;

        proxy_pass http://ruoyi-gateway:8080/;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
        expires 7d;
        add_header Cache-Control "public";
        try_files $uri =404;
    }
}
EOF

34. 创建 UI Dockerfile

cat <<'EOF' > "$UI_CONTEXT/Dockerfile"
FROM m.daocloud.io/docker.io/library/node:16.20.2-alpine AS builder

WORKDIR /build

COPY package*.json ./

RUN npm config set registry https://registry.npmmirror.com \
    && npm install --legacy-peer-deps

COPY . .

RUN npm run build:prod

FROM m.daocloud.io/docker.io/library/nginx:1.27.5-alpine

RUN rm -f /etc/nginx/conf.d/default.conf

COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /build/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
EOF

35. 构建 UI 镜像

cd "$HOME/ruoyi-k8s-lab/source/RuoYi-Cloud/ruoyi-ui"

UI_VERSION="$(
  python3 - <<'PY'
import json
with open("package.json", encoding="utf-8") as f:
    print(json.load(f)["version"])
PY
)"

UI_TAG="${UI_VERSION}-lab"
UI_CONTEXT="$HOME/ruoyi-k8s-lab/images/ui"

echo "$UI_TAG"

构建:

sudo buildah bud \
  --network=host \
  --format docker \
  --layers \
  -t "localhost/ruoyi-ui:${UI_TAG}" \
  "$UI_CONTEXT"

检查:

sudo buildah images | grep ruoyi-ui

36. 导出并发送 UI 镜像

rm -f "$HOME/ruoyi-k8s-lab/artifacts/ruoyi-ui.tar"

sudo buildah push \
  "localhost/ruoyi-ui:${UI_TAG}" \
  "docker-archive:$HOME/ruoyi-k8s-lab/artifacts/ruoyi-ui.tar:localhost/ruoyi-ui:${UI_TAG}"

sudo chown leecurry:leecurry \
  "$HOME/ruoyi-k8s-lab/artifacts/ruoyi-ui.tar"

发送:

for ip in 104 105 106; do
  scp \
    "$HOME/ruoyi-k8s-lab/artifacts/ruoyi-ui.tar" \
    "leecurry@192.168.89.${ip}:/tmp/"
done

三个 Worker 每台执行:

sudo ctr \
  -n k8s.io \
  images import \
  /tmp/ruoyi-ui.tar

检查:

sudo crictl images | grep ruoyi-ui

37. 给 values-lab.yaml 添加 UI

回到 master01:

cd "$HOME/ruoyi-k8s-lab/charts/ruoyi-app"

重新获取 UI 版本:

UI_VERSION="$(
  python3 - <<'PY'
import json
with open("/home/leecurry/ruoyi-k8s-lab/source/RuoYi-Cloud/ruoyi-ui/package.json", encoding="utf-8") as f:
    print(json.load(f)["version"])
PY
)"

UI_TAG="${UI_VERSION}-lab"

如果 values-lab.yaml 中还没有 ui:,执行:

cat <<EOF >> values-lab.yaml

ui:
  name: ruoyi-ui
  replicas: 1

  image:
    repository: localhost/ruoyi-ui
    tag: "${UI_TAG}"

  port: 80

  service:
    type: NodePort
    nodePort: 30081

  resources:
    requests:
      cpu: 20m
      memory: 64Mi
    limits:
      cpu: 200m
      memory: 128Mi
EOF

更新 Chart 版本:

sed -i 's/^version:.*/version: 0.2.0/' Chart.yaml

38. 创建 templates/ui.yaml

cat <<'EOF' > templates/ui.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.ui.name }}
  labels:
    app.kubernetes.io/name: {{ .Values.ui.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  replicas: {{ .Values.ui.replicas }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.ui.name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.ui.name }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
      containers:
        - name: ui
          image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}"
          imagePullPolicy: {{ .Values.global.imagePullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.ui.port }}
              protocol: TCP
          startupProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 20
            timeoutSeconds: 3
            failureThreshold: 6
          resources:
{{ toYaml .Values.ui.resources | indent 12 }}
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.ui.name }}
  labels:
    app.kubernetes.io/name: {{ .Values.ui.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  type: {{ .Values.ui.service.type }}
  selector:
    app.kubernetes.io/name: {{ .Values.ui.name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: http
      nodePort: {{ .Values.ui.service.nodePort }}
EOF

39. 升级 ruoyi-app

cd "$HOME/ruoyi-k8s-lab"

helm lint \
  ./charts/ruoyi-app \
  -f ./charts/ruoyi-app/values-lab.yaml

helm template ruoyi-app \
  ./charts/ruoyi-app \
  --namespace ruoyi \
  -f ./charts/ruoyi-app/values-lab.yaml \
  > /tmp/ruoyi-app-with-ui.yaml

升级:

helm upgrade --install ruoyi-app \
  ./charts/ruoyi-app \
  --namespace ruoyi \
  -f ./charts/ruoyi-app/values-lab.yaml \
  --atomic \
  --wait \
  --timeout 10m

检查:

kubectl get pods -n ruoyi -o wide
kubectl get svc ruoyi-ui -n ruoyi
helm history ruoyi-app -n ruoyi

第八阶段:最终验证

40. 检查 Helm Release

helm list -n ruoyi

应看到:

ruoyi-mysql   deployed
ruoyi-redis   deployed
ruoyi-nacos   deployed
ruoyi-app     deployed

41. 检查 Pod、Service、PVC

kubectl get pod,svc,pvc -n ruoyi -o wide

所有 Pod 应为:

Running

所有 PVC 应为:

Bound

42. 测试前端和后端

curl -I http://192.168.89.104:30081/

应返回:

HTTP/1.1 200 OK

测试后端代理:

curl -s \
  http://192.168.89.104:30081/prod-api/code \
  | head -c 500

echo

浏览器登录:

地址:http://192.168.89.104:30081
用户名:admin
密码:admin123

第九阶段:VMware 重启后继续使用

不需要重新执行 Helm 安装、SQL 导入或镜像构建。

启动顺序:

master01
master02
master03
worker01
worker02
worker03

等待 2~5 分钟后执行:

kubectl get nodes
kubectl get pods -n ruoyi -o wide
helm list -n ruoyi

访问:

Nacos:http://192.168.89.104:30082/index.html
若依:http://192.168.89.104:30081

第十阶段:常见排错

43. Pod 异常

kubectl get pods -n ruoyi -o wide
kubectl describe pod <Pod名称> -n ruoyi

44. 查看日志

kubectl logs -n ruoyi ruoyi-mysql-0 --tail=200
kubectl logs -n ruoyi ruoyi-redis-0 --tail=200
kubectl logs -n ruoyi ruoyi-nacos-0 --tail=200
kubectl logs -n ruoyi deployment/ruoyi-gateway --tail=200
kubectl logs -n ruoyi deployment/ruoyi-auth --tail=200
kubectl logs -n ruoyi deployment/ruoyi-system --tail=200
kubectl logs -n ruoyi deployment/ruoyi-ui --tail=200

查看容器上一次日志:

kubectl logs -n ruoyi <Pod名称> --previous --tail=200

45. ImagePullBackOff

先看 Pod 在哪个 Worker:

kubectl get pods -n ruoyi -o wide

登录对应 Worker:

sudo crictl images \
  | grep -E 'ruoyi-(gateway|auth|system|ui)'

缺少镜像时重新导入:

sudo ctr -n k8s.io images import /tmp/对应镜像.tar

46. Nacos 注册失败

kubectl logs -n ruoyi deployment/ruoyi-gateway --tail=200
kubectl logs -n ruoyi deployment/ruoyi-auth --tail=200
kubectl logs -n ruoyi deployment/ruoyi-system --tail=200

kubectl get svc ruoyi-nacos -n ruoyi
kubectl get endpointslice -n ruoyi \
  -l kubernetes.io/service-name=ruoyi-nacos

47. MySQL 或 Redis 连接失败

MySQL:

MYSQL_ROOT_PASSWORD="$(
  kubectl get secret ruoyi-mysql \
    -n ruoyi \
    -o jsonpath='{.data.mysql-root-password}' \
  | base64 -d
)"

kubectl exec \
  -n ruoyi \
  ruoyi-mysql-0 \
  -- env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" \
  mysql -uroot -e 'SELECT 1;'

Redis:

REDIS_PASSWORD="$(
  kubectl get secret ruoyi-redis \
    -n ruoyi \
    -o jsonpath='{.data.redis-password}' \
  | base64 -d
)"

kubectl exec \
  -n ruoyi \
  ruoyi-redis-0 \
  -- env REDISCLI_AUTH="$REDIS_PASSWORD" \
  redis-cli ping

48. UI 返回 502

kubectl get svc ruoyi-gateway -n ruoyi
kubectl get endpointslice -n ruoyi \
  -l kubernetes.io/service-name=ruoyi-gateway

从 UI Pod 内测试:

kubectl exec \
  -n ruoyi \
  deployment/ruoyi-ui \
  -- wget -S -O- http://ruoyi-gateway:8080/code

第十一阶段:卸载说明

只卸载业务应用:

helm uninstall ruoyi-app -n ruoyi

卸载 Nacos:

helm uninstall ruoyi-nacos -n ruoyi

卸载 Redis:

helm uninstall ruoyi-redis -n ruoyi

卸载 MySQL:

helm uninstall ruoyi-mysql -n ruoyi

StatefulSet 生成的 PVC 通常不会随 Helm Release 自动删除。

查看:

kubectl get pvc -n ruoyi

只有确认数据不再需要时,才删除:

kubectl delete pvc --all -n ruoyi

删除整个命名空间:

kubectl delete namespace ruoyi

删除命名空间会删除其中所有资源,不要在仍然需要数据时执行。


第十二阶段:当前实验环境限制

当前方案适合学习和实验,但不是完整生产高可用:

MySQL:单副本
Redis:单副本
Nacos:单副本
存储:local-path,本地节点存储
镜像:手工导入三个 Worker
业务服务:初始单副本

后续升级方向:

local-path → Rook-Ceph
手工导入镜像 → Harbor
手工 Buildah 构建 → Jenkins / GitLab CI
手工 Helm 发布 → Argo CD
单副本 → 多副本 + HPA
TCP 探针 → Spring Boot Actuator 健康检查

更多推荐