diff --git a/deploy/DEPLOYMENT.md b/deploy/DEPLOYMENT.md index a54a91b..7cfa164 100644 --- a/deploy/DEPLOYMENT.md +++ b/deploy/DEPLOYMENT.md @@ -114,7 +114,7 @@ systemctl restart rke2-server | Git | SCM 检出 | | Credentials / Credentials Binding | 凭据管理与注入 | -### 5.3 凭据(5 个,ID 与 `deploy/Jenkinsfile` 中变量对应) +### 5.3 凭据(6 个,ID 与 `deploy/Jenkinsfile` 中变量对应) | 凭据 ID | 类型 | 来源 / 获取方式 | |---------|------|----------------| @@ -123,6 +123,7 @@ systemctl restart rke2-server | `kra-rke2-128-kubeconfig` | Secret file | 128 节点 `/etc/rancher/rke2/rke2.yaml`,**把 server 的 127.0.0.1 改为 192.168.200.128** 后保存 | | `kra-rke2-129-kubeconfig` | Secret file | 129 节点同上,server 改为 192.168.200.129 | | `kra-mysql-root-password` | Secret text | 自定的 MySQL root 密码(与节点上 MySQL 一致) | +| `kra-jwt-signing-key` | Secret text | 首次生成运行时配置时使用的 JWT 签名密钥;必须非空且为单行文本 | kubeconfig 添加路径:系统管理 → 凭据 → Add Credentials → 类型 Secret file → 上传文件 → 填 ID。 @@ -149,25 +150,25 @@ gitlabBranch(集成/推送触发) | 项 | 说明 | |----|------| | Harbor 拉镜像信任 | 见 §4 第 3 步 `registries.yaml` | -| 应用配置文件 | Jenkins 将 [k8s/config.example.yaml](k8s/config.example.yaml) 渲染为 `-config-template` ConfigMap,初始化容器复制到共享配置 PVC `/data/conf` | -| 配置与上传存储 | `-config`(1Gi)和 `-uploads`(20Gi),均要求存储类支持 `ReadWriteMany` | -| 数据持久化 | `/var/lib//mysql`、`/var/lib//redis`(节点本地目录,当前单节点集群适用;Redis initContainer 需要允许 root 修正 hostPath 权限) | +| 应用配置文件 | Jenkins 将 [k8s/config.example.yaml](k8s/config.example.yaml) 渲染为 `-config-template` Secret,初始化容器首次复制到 `/var/lib//conf/config.yaml` | +| 配置与上传存储 | `/var/lib//conf`、`/var/lib//uploads`;required podAffinity 保证后端与 MySQL 同节点,仅适用于当前单节点方案 | +| 数据持久化 | `/var/lib//mysql`、`/var/lib//redis`(节点本地目录;Redis/后端 initContainer 需要允许 root 修正 hostPath 权限) | | kubectl | 节点用 `/var/lib/rancher/rke2/bin/kubectl`,并 `export KUBECONFIG=/etc/rancher/rke2/rke2.yaml` | **首次部署初始化流程**(kra 框架约定,勿用环境变量注入数据库 DSN): 1. 服务启动时数据库未就绪,首页显示"前往初始化"。 2. 走完 `/init/initdb` 向导:建库建表 + 写入种子数据(默认账号 admin)。 -3. 向导最后一步把数据库连接**写回**共享 PVC 中的 `/data/conf/config.yaml`,配置不会随 Pod 消失。 +3. 向导最后一步把数据库连接**写回**节点目录中的 `/data/conf/config.yaml`,配置不会随 Pod 重建消失。 4. 初始化期间建议先把后端缩容到 1 个副本:`kubectl -n -dev scale deployment/-backend --replicas=1`。初始化完成后再恢复副本数并滚动重启:`kubectl -n -dev scale deployment/-backend --replicas=3`,然后 `kubectl -n -dev rollout status deployment/-backend`。后续手工改配置也需要执行滚动重启。 -> `-config-template` ConfigMap 只负责首次创建配置文件。初始化后,运行时配置以共享 PVC 中的 `/data/conf/config.yaml` 为准;更新 ConfigMap 不会覆盖已有配置,避免覆盖初始化写入的数据库连接。 +> `-config-template` Secret 只负责首次创建配置文件。初始化后,运行时配置以 hostPath 中的 `/data/conf/config.yaml` 为准;更新 Secret 或 Jenkins JWT 凭据不会覆盖已有配置,避免覆盖初始化写入的数据库连接。JWT 后续轮换应通过系统设置完成并滚动重启后端。 ## 7. 首次部署清单(按顺序) 1. Harbor:建 `library` 项目 + robot 账号;证书分发到 131 宿主机与 128/129 节点。 -2. RKE2 集群:确认 `KRA_SHARED_STORAGE_CLASS` 对应的存储类支持 `ReadWriteMany`。 -3. Jenkins:安装插件;添加 §5.3 所列 5 个凭据;按 §5.4 建 `kra` job。 +2. RKE2 集群:确认 `/var/lib//` 所在磁盘容量足够,并允许清单中的 root initContainer 修正目录权限。 +3. Jenkins:安装插件;添加 §5.3 所列 6 个凭据;按 §5.4 建 `kra` job。 4. GitLab:配置 Jenkins 集成(§3)。 5. 推送任意分支触发构建,观察首次构建(有依赖缓存预热,耗时会比后续长)。 6. 初始化向导(§6),完成后浏览器验证。 @@ -189,7 +190,7 @@ curl -I http://192.168.200.129:30080/ # 同上验证 129 | push 后没有构建 | GitLab 集成"最近事件"(hook_logs)看响应码与请求体;确认集成启用且勾选推送 | | 构建失败 | Jenkins 构建日志;先看 `resolved branch` 是否符合预期 | | Pod 镜像拉取失败 | 节点 `registries.yaml`、Harbor robot 是否有效、`harbor-registry` Secret | -| 后端 503 / 初始化异常 | 配置 PVC 是否已绑定、ConfigMap 是否存在、后端日志是否连上 `-mysql:3306` | +| 后端 503 / 初始化异常 | `/var/lib//conf/config.yaml` 权限、配置模板 Secret、后端日志是否连上 `-mysql:3306` | | 登录态全部失效 | 是否在系统设置中更换了 JWT 签名密钥(换值即失效所有 token) | | 构建慢 | 缓存目录是否挂载成功;镜像源(goproxy/npmmirror/daocloud)偶发卡顿需重试 | @@ -197,4 +198,4 @@ curl -I http://192.168.200.129:30080/ # 同上验证 129 - 所有账号密码只存 Jenkins Credentials / 各系统自身,不写入仓库;`configs/config.yaml` 不要提交真实密钥。 - 轮换 Jenkins 密码:GitLab 集成表单改一次即可。 -- 轮换 Harbor robot:更新 Jenkins 凭据后重新部署即可。轮换 MySQL 密码必须先在 MySQL 中执行 `ALTER USER` 并确认连接正常,再更新 Jenkins 凭据并重启相关 Pod;仅更新 Secret 不会修改已初始化 MySQL 数据目录中的 root 密码。JWT 签名密钥通过系统设置修改并保存到共享配置 PVC,随后滚动重启后端;换值会使现有登录态失效。 +- 轮换 Harbor robot:更新 Jenkins 凭据后重新部署即可。轮换 MySQL 密码必须先在 MySQL 中执行 `ALTER USER` 并确认连接正常,再更新 Jenkins 凭据并重启相关 Pod;仅更新 Secret 不会修改已初始化 MySQL 数据目录中的 root 密码。JWT 签名密钥通过系统设置修改并保存到 hostPath 配置文件,随后滚动重启后端;换值会使现有登录态失效。 diff --git a/deploy/Jenkinsfile b/deploy/Jenkinsfile index fa76668..056832c 100644 --- a/deploy/Jenkinsfile +++ b/deploy/Jenkinsfile @@ -19,7 +19,6 @@ pipeline { KRA_BACKEND_NODEPORT = '30081' KRA_MYSQL_IMAGE = 'docker.m.daocloud.io/library/mysql:8.4' KRA_REDIS_IMAGE = 'docker.m.daocloud.io/library/redis:7-alpine' - KRA_SHARED_STORAGE_CLASS = 'longhorn' // 必须支持 ReadWriteMany;按目标集群实际存储类修改 GO_IMAGE = 'docker.m.daocloud.io/golang:1.25' NODE_IMAGE = 'docker.m.daocloud.io/node:20-slim' PNPM_VERSION = '10.15.1' @@ -30,9 +29,10 @@ pipeline { CRED_KUBECONFIG_DEV = 'kra-rke2-128-kubeconfig' CRED_KUBECONFIG_PROD = 'kra-rke2-129-kubeconfig' CRED_MYSQL_PASSWORD = 'kra-mysql-root-password' + CRED_JWT_SIGNING_KEY = 'kra-jwt-signing-key' // k8s 清单渲染白名单:只替换列出的变量,避免误伤清单内 shell 变量(如 probe 里的 $MYSQL_ROOT_PASSWORD) - K8S_RENDER_VARS = '$KRA_APP $KRA_NAMESPACE $KRA_ENV $KRA_BACKEND_IMAGE $KRA_FRONTEND_IMAGE $KRA_REPLICAS $KRA_FRONTEND_NODEPORT $KRA_BACKEND_NODEPORT $KRA_MYSQL_IMAGE $KRA_REDIS_IMAGE $KRA_SHARED_STORAGE_CLASS $BIN_NAME' + K8S_RENDER_VARS = '$KRA_APP $KRA_NAMESPACE $KRA_ENV $KRA_BACKEND_IMAGE $KRA_FRONTEND_IMAGE $KRA_REPLICAS $KRA_FRONTEND_NODEPORT $KRA_BACKEND_NODEPORT $KRA_MYSQL_IMAGE $KRA_REDIS_IMAGE $BIN_NAME' } stages { @@ -75,13 +75,18 @@ pipeline { // job 的 SCM branch spec 固定(如 */main),集成触发时按实际推送分支重新检出, // 保证单 job 同时服务 dev/main 两个环境。 script { - env.KRA_TRIGGER_COMMIT = env.gitlabAfter?.trim() - if (env.KRA_TRIGGER_COMMIT && !(env.KRA_TRIGGER_COMMIT ==~ /[0-9a-fA-F]{40}/)) { - error("Invalid GitLab commit SHA: ${env.KRA_TRIGGER_COMMIT}") + // gitlabAfter 为 push 事件的目标 commit;手动触发时为空。 + // 注意:不能把 null 赋给 env.*(会被转成字符串 "null"),这里用局部变量。 + def triggerCommit = env.gitlabAfter?.trim() + if (triggerCommit && !(triggerCommit ==~ /[0-9a-fA-F]{40}/)) { + error("Invalid GitLab commit SHA: ${triggerCommit}") } - if (env.KRA_TRIGGER_COMMIT && env.KRA_TRIGGER_COMMIT.replace('0', '').isEmpty()) { + if (triggerCommit && triggerCommit.replace('0', '').isEmpty()) { error('GitLab event points to a deleted branch; refusing to deploy') } + if (triggerCommit) { + env.KRA_TRIGGER_COMMIT = triggerCommit + } checkout([$class: 'GitSCM', branches: [[name: "*/${env.KRA_BRANCH}"]], userRemoteConfigs: [[credentialsId: 'gitlab-root-http', url: 'http://192.168.200.130/root/kra.git']]]) @@ -152,6 +157,7 @@ pipeline { steps { withCredentials([file(credentialsId: "${env.KUBECONFIG_CREDENTIALS}", variable: 'KUBECONFIG_FILE'), string(credentialsId: env.CRED_MYSQL_PASSWORD, variable: 'KRA_MYSQL_ROOT_PASSWORD'), + string(credentialsId: env.CRED_JWT_SIGNING_KEY, variable: 'KRA_JWT_SIGNING_KEY'), usernamePassword(credentialsId: env.CRED_HARBOR_PUSH, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')]) { sh ''' set -eu @@ -159,7 +165,6 @@ pipeline { umask 077 TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT - kubectl get storageclass "$KRA_SHARED_STORAGE_CLASS" >/dev/null kubectl create namespace "$KRA_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - DOCKER_CONFIG_DIR="$TMP_DIR/docker" mkdir -p "$DOCKER_CONFIG_DIR" @@ -169,27 +174,43 @@ pipeline { --from-file=.dockerconfigjson="$DOCKER_CONFIG_DIR/config.json" \\ --dry-run=client -o yaml | kubectl apply -f - docker --config "$DOCKER_CONFIG_DIR" logout "$REGISTRY" >/dev/null 2>&1 || true + test -n "$KRA_MYSQL_ROOT_PASSWORD" || { echo 'MySQL root password credential is empty' >&2; exit 1; } printf '%s' "$KRA_MYSQL_ROOT_PASSWORD" > "$TMP_DIR/mysql-root-password" kubectl -n "$KRA_NAMESPACE" create secret generic "${APP_NAME}-runtime" \\ --from-file=mysql-root-password="$TMP_DIR/mysql-root-password" \\ --dry-run=client -o yaml | kubectl apply -f - + test -n "$KRA_JWT_SIGNING_KEY" || { echo 'JWT signing key credential is empty' >&2; exit 1; } + if [ "$(printf '%s' "$KRA_JWT_SIGNING_KEY" | wc -l)" -ne 0 ]; then + echo 'JWT signing key credential must be a single line' >&2 + exit 1 + fi + case "$KRA_JWT_SIGNING_KEY" in + *"'"*) echo "JWT signing key credential must not contain a single quote" >&2; exit 1 ;; + esac CONFIG_TEMPLATE="$TMP_DIR/config.yaml" - envsubst '$APP_NAME $KRA_ENV' < deploy/k8s/config.example.yaml > "$CONFIG_TEMPLATE" - kubectl -n "$KRA_NAMESPACE" create configmap "${APP_NAME}-config-template" \\ + envsubst '$APP_NAME $KRA_ENV $KRA_JWT_SIGNING_KEY' < deploy/k8s/config.example.yaml > "$CONFIG_TEMPLATE" + kubectl -n "$KRA_NAMESPACE" create secret generic "${APP_NAME}-config-template" \\ --from-file=config.yaml="$CONFIG_TEMPLATE" \\ --dry-run=client -o yaml | kubectl apply -f - + kubectl -n "$KRA_NAMESPACE" delete configmap "${APP_NAME}-config-template" --ignore-not-found export KRA_APP="$APP_NAME" export KRA_BACKEND_IMAGE="$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG" export KRA_FRONTEND_IMAGE="$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG" RENDERED_MANIFEST="$TMP_DIR/manifest.yaml" envsubst "$K8S_RENDER_VARS" < deploy/k8s/kra.yaml > "$RENDERED_MANIFEST" + MYSQL_SERVICE_IP=$(kubectl -n "$KRA_NAMESPACE" get service "${APP_NAME}-mysql" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true) + if [ -n "$MYSQL_SERVICE_IP" ] && [ "$MYSQL_SERVICE_IP" != 'None' ]; then + echo "${APP_NAME}-mysql already exists as a ClusterIP Service; delete it during a maintenance window before switching to headless mode" >&2 + exit 1 + fi kubectl apply --dry-run=server -f "$RENDERED_MANIFEST" kubectl apply -f "$RENDERED_MANIFEST" RUNTIME_SECRET_VERSION=$(kubectl -n "$KRA_NAMESPACE" get secret "${APP_NAME}-runtime" -o jsonpath='{.metadata.resourceVersion}') + PATCH_FILE="$TMP_DIR/mysql-patch.json" + printf '{"spec":{"template":{"metadata":{"annotations":{"kra.dev/runtime-secret-version":"%s"}}}}}\n' \\ + "$RUNTIME_SECRET_VERSION" > "$PATCH_FILE" kubectl -n "$KRA_NAMESPACE" patch statefulset "${APP_NAME}-mysql" --type=merge \\ - -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kra.dev/runtime-secret-version\":\"$RUNTIME_SECRET_VERSION\"}}}}}" - kubectl -n "$KRA_NAMESPACE" wait --for=jsonpath='{.status.phase}'=Bound \\ - "pvc/${APP_NAME}-config" "pvc/${APP_NAME}-uploads" --timeout=5m + --patch-file="$PATCH_FILE" kubectl -n "$KRA_NAMESPACE" rollout status statefulset/${APP_NAME}-mysql --timeout=10m kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-redis --timeout=10m kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-backend --timeout=10m diff --git a/deploy/README.md b/deploy/README.md index d3956a5..5115348 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -16,9 +16,9 @@ deploy/ 目录是一套与项目名解耦的部署模板,基于 kra 框架的 - `deploy/Jenkinsfile` 的 `Select environment` 阶段按 `gitlabBranch` 路由:单 job `kra` 同时服务 dev/main 两个环境,构建阶段优先按 push payload 的最终提交 SHA 检出 - 手动构建兜底:`kra` job 手动触发时默认部署 main(不识别 tag,tag 仅作版本标记,不触发部署) -每个环境包含 3 个后端 Pod、3 个前端 Pod、1 个 MySQL Pod 和 1 个 Redis Pod。前端通过 `-backend` ClusterIP Service 访问后端,Kubernetes Service 在 Pod 之间做负载均衡;前端通过 NodePort `30080` 暴露,后端调试入口通过 NodePort `30081` 暴露。后端配置和上传文件使用支持 `ReadWriteMany` 的共享 PVC。 +每个环境包含 3 个后端 Pod、3 个前端 Pod、1 个 MySQL Pod 和 1 个 Redis Pod。前端通过 `-backend` ClusterIP Service 访问后端,Kubernetes Service 在 Pod 之间做负载均衡;前端通过 NodePort `30080` 暴露,后端调试入口通过 NodePort `30081` 暴露。 -MySQL 和 Redis 使用节点本地目录 `/var/lib//mysql`、`/var/lib//redis` 持久化。Redis 部署会先用一次性 root initContainer 修正 hostPath 权限,主进程仍以镜像内的非 root 用户运行。当前两个 Rancher 集群都只有一个节点,这种方式适合当前环境,不提供数据库跨节点高可用。 +MySQL、Redis、后端配置和上传文件都使用 `/var/lib//` 下的节点本地目录。Redis 和后端会先用 root initContainer 修正 hostPath 根目录权限;后端通过 required podAffinity 固定到 MySQL 所在节点。当前两个 Rancher 集群都只有一个节点,这种方式适合当前环境,但不是真正的 RWX,也不提供跨节点高可用;扩展到多节点前必须迁移到 NFS、CephFS 等共享存储。 ## Jenkins 前置条件 @@ -28,6 +28,7 @@ Jenkins 节点需要可用的 Docker CLI/daemon、`kubectl` 和 `envsubst`(Go - `kra-rke2-128-kubeconfig`: 集群 128 的 kubeconfig 文件 - `kra-rke2-129-kubeconfig`: 集群 129 的 kubeconfig 文件 - `kra-mysql-root-password`: 应用专用 MySQL root 密码 +- `kra-jwt-signing-key`: 首次生成配置文件时使用的 JWT 签名密钥 Harbor 项目和 Kubernetes 两个环境都会由流水线自动创建 `harbor-registry` 镜像拉取 Secret。不要把真实密码写入 Git。 @@ -41,7 +42,7 @@ Harbor 项目和 Kubernetes 两个环境都会由流水线自动创建 `harbor-r ### 前置:放置配置文件 -后端启动读取 `/data/conf/config.yaml`。流水线会把 `deploy/k8s/config.example.yaml` 渲染成 ConfigMap,并由初始化容器复制到共享配置 PVC;容器内运行用户为 UID/GID 1000,初始化向导会把数据库连接和 JWT 密钥写回该文件。 +后端启动读取 `/data/conf/config.yaml`。流水线会把 `deploy/k8s/config.example.yaml` 渲染成 Kubernetes Secret,并由初始化容器首次复制到节点目录 `/var/lib//conf`;容器内运行用户为 UID/GID 1000,初始化向导会把数据库连接和 JWT 密钥写回该文件。 ### 初始化 @@ -56,7 +57,7 @@ Harbor 项目和 Kubernetes 两个环境都会由流水线自动创建 `harbor-r | 密码 | Jenkins 凭据 `kra-mysql-root-password` 的值 | | 库名 | `kra`(与应用名一致) | -提交后框架自动建表、写入种子数据(管理员账号、菜单、casbin)并把连接配置写回共享 PVC 中的 `config.yaml`,Pod 重建后配置不丢失。初始化期间建议暂时保持后端为 1 个副本,完成后再扩容到 `KRA_REPLICAS`,避免多个 Pod 同时执行首次初始化。初始化完成后请执行一次后端滚动重启,让所有副本加载新配置。**不要**通过环境变量注入数据库 DSN:那会让应用跳过初始化向导,导致数据库有表无种子数据、无法登录。 +提交后框架自动建表、写入种子数据(管理员账号、菜单、casbin)并把连接配置写回节点目录中的 `config.yaml`,Pod 重建后配置不丢失。初始化期间建议暂时保持后端为 1 个副本,完成后再扩容到 `KRA_REPLICAS`,避免多个 Pod 同时执行首次初始化。初始化完成后请执行一次后端滚动重启,让所有副本加载新配置。**不要**通过环境变量注入数据库 DSN:那会让应用跳过初始化向导,导致数据库有表无种子数据、无法登录。 ## 基于 kra 模板接入新项目 @@ -64,14 +65,13 @@ Harbor 项目和 Kubernetes 两个环境都会由流水线自动创建 `harbor-r | 变量 | 说明 | 默认值(kra 项目) | |---|---|---| -| `APP_NAME` | 应用名,派生所有 K8s 资源名、数据库名、存储声明、镜像后缀 | `kra` | +| `APP_NAME` | 应用名,派生所有 K8s 资源名、数据库名、本地存储路径、镜像后缀 | `kra` | | `IMAGE_REPO` | Harbor 镜像仓库路径 | `library/kra` | | `REGISTRY` | Harbor 地址 | `192.168.200.132` | | `BIN_NAME` | 后端二进制名,须与 `Makefile` 的 `make build` 产物一致 | `kratos-admin` | | `KRA_REPLICAS` | 前后端副本数 | `3` | | `KRA_FRONTEND_NODEPORT` / `KRA_BACKEND_NODEPORT` | NodePort 端口,同一集群多个项目必须错开 | `30080` / `30081` | | `KRA_MYSQL_IMAGE` / `KRA_REDIS_IMAGE` | 中间件镜像 | daocloud mysql:8.4 / redis:7-alpine | -| `KRA_SHARED_STORAGE_CLASS` | 支持 `ReadWriteMany` 的共享存储类 | `longhorn` | | `GO_IMAGE` / `NODE_IMAGE` / `PNPM_VERSION` | 构建镜像与工具版本 | 见文件 | | `CACHE_DIR` | Jenkins 宿主机依赖缓存目录(建议每项目独立) | `/var/lib/kra-ci-cache` | | `CRED_*` | Jenkins 凭据 ID,沿用现有凭据可不改 | 见文件 | @@ -91,7 +91,7 @@ Harbor 项目和 Kubernetes 两个环境都会由流水线自动创建 `harbor-r |---|---| | Deployment/Service | `-backend`、`-frontend`、`-redis` | | StatefulSet/Service | `-mysql` | -| ConfigMap | `-config-template` | +| 配置模板 Secret | `-config-template` | | 运行时 Secret | `-runtime`(key:`mysql-root-password`) | | 数据库名 | `` | diff --git a/deploy/k8s/config.example.yaml b/deploy/k8s/config.example.yaml index 66a34ea..6b14a76 100644 --- a/deploy/k8s/config.example.yaml +++ b/deploy/k8s/config.example.yaml @@ -1,5 +1,5 @@ # 后端配置文件模板。 -# Jenkins 会用 APP_NAME 渲染本文件,并通过 ConfigMap/初始化容器写入共享配置 PVC。 +# Jenkins 会渲染本文件并通过 Secret/初始化容器写入节点共享目录。 # 容器内运行用户为 UID/GID 1000,初始化向导会把运行时配置写回本文件。 # # 说明: @@ -40,7 +40,7 @@ data: admin: router_prefix: "" jwt: - signing_key: "" + signing_key: '${KRA_JWT_SIGNING_KEY}' expires_time: 604800s buffer_time: 86400s issuer: ${APP_NAME} diff --git a/deploy/k8s/kra.yaml b/deploy/k8s/kra.yaml index 6d21935..85b6f0e 100644 --- a/deploy/k8s/kra.yaml +++ b/deploy/k8s/kra.yaml @@ -7,7 +7,6 @@ # KRA_BACKEND_NODEPORT node port for the backend debug service # KRA_MYSQL_IMAGE mysql server image # KRA_REDIS_IMAGE redis server image -# KRA_SHARED_STORAGE_CLASS RWX storage class for config and uploads # KRA_BACKEND_IMAGE backend image built by Jenkins # KRA_FRONTEND_IMAGE frontend image built by Jenkins # BIN_NAME backend binary name inside the image @@ -39,6 +38,7 @@ spec: app.kubernetes.io/name: ${KRA_APP}-mysql app.kubernetes.io/part-of: ${KRA_APP} spec: + automountServiceAccountToken: false containers: - name: mysql image: ${KRA_MYSQL_IMAGE} @@ -129,6 +129,7 @@ spec: app.kubernetes.io/name: ${KRA_APP}-redis app.kubernetes.io/part-of: ${KRA_APP} spec: + automountServiceAccountToken: false initContainers: - name: prepare-redis-storage image: ${KRA_REDIS_IMAGE} @@ -142,12 +143,17 @@ spec: volumeMounts: - name: redis-data mountPath: /data + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: ${KRA_APP}-mysql containers: - name: redis image: ${KRA_REDIS_IMAGE} imagePullPolicy: IfNotPresent - securityContext: - runAsNonRoot: true ports: - name: redis containerPort: 6379 @@ -206,13 +212,14 @@ spec: securityContext: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch + automountServiceAccountToken: false initContainers: - name: initialize-shared-storage image: ${KRA_BACKEND_IMAGE} imagePullPolicy: Always securityContext: - runAsUser: 1000 - runAsGroup: 1000 + runAsUser: 0 + runAsGroup: 0 command: ["sh", "-c"] args: - >- @@ -220,6 +227,7 @@ spec: if [ ! -f /data/conf/config.yaml ]; then cp /bootstrap/config.yaml /data/conf/config.yaml; fi; + chown 1000:1000 /data/conf /data/conf/config.yaml /app/uploads; chmod u+rwX,g+rwX /data/conf /data/conf/config.yaml /app/uploads volumeMounts: - name: config @@ -229,11 +237,17 @@ spec: - name: config-template mountPath: /bootstrap readOnly: true - # /data/conf 由初始化容器从 ConfigMap 初始化到共享 PVC, + # /data/conf 由初始化容器从 Secret 初始化到节点共享目录, # 目录必须可写:初始化向导会把数据库连接写回该文件。 imagePullSecrets: - name: harbor-registry affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: ${KRA_APP}-mysql podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 @@ -287,43 +301,22 @@ spec: cpu: 500m memory: 1Gi volumes: + # 当前是单节点集群;required podAffinity 保证所有后端副本落在 + # MySQL 所在节点,从而共享同一组 hostPath 目录。它不是真正的 RWX。 + # 扩展到多节点前,必须换成 PVC + 真正的共享存储(如 NFS/CephFS)。 - name: config - persistentVolumeClaim: - claimName: ${KRA_APP}-config + hostPath: + path: /var/lib/${KRA_APP}/conf + type: DirectoryOrCreate - name: uploads - persistentVolumeClaim: - claimName: ${KRA_APP}-uploads + hostPath: + path: /var/lib/${KRA_APP}/uploads + type: DirectoryOrCreate - name: config-template - configMap: + secret: name: ${KRA_APP}-config-template --- apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: ${KRA_APP}-config - namespace: ${KRA_NAMESPACE} -spec: - storageClassName: ${KRA_SHARED_STORAGE_CLASS} - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: ${KRA_APP}-uploads - namespace: ${KRA_NAMESPACE} -spec: - storageClassName: ${KRA_SHARED_STORAGE_CLASS} - accessModes: - - ReadWriteMany - resources: - requests: - storage: 20Gi ---- -apiVersion: v1 kind: Service metadata: name: ${KRA_APP}-backend @@ -379,6 +372,7 @@ spec: app.kubernetes.io/name: ${KRA_APP}-frontend app.kubernetes.io/part-of: ${KRA_APP} spec: + automountServiceAccountToken: false imagePullSecrets: - name: harbor-registry affinity: