From ce4ed32dfc43a0bd60492f7b91d2edc8ce27c702 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Fri, 4 Sep 2026 17:59:32 +0800 Subject: [PATCH] jenkins --- deploy/DEPLOYMENT.md | 4 +- deploy/Jenkinsfile | 70 ++++++++++++++++++++++++---------- deploy/README.md | 2 + deploy/k8s/config.example.yaml | 3 +- deploy/k8s/kra.yaml | 44 ++++++++++++--------- 5 files changed, 84 insertions(+), 39 deletions(-) diff --git a/deploy/DEPLOYMENT.md b/deploy/DEPLOYMENT.md index 97cdbcf..8afa99c 100644 --- a/deploy/DEPLOYMENT.md +++ b/deploy/DEPLOYMENT.md @@ -160,7 +160,9 @@ gitlabBranch(集成/推送触发) 1. 服务启动时数据库未就绪,首页显示"前往初始化"。 2. 走完 `/init/initdb` 向导:建库建表 + 写入种子数据(默认账号 admin)。 3. 向导最后一步把数据库连接**写回**节点目录中的 `/data/conf/config.yaml`,配置不会随 Pod 重建消失。 -4. 当前 Jenkins 默认将前后端都部署为 1 个副本,适配单节点和 hostPath 存储。初始化完成后如需扩容,必须先迁移到真正的共享存储,再调整 Jenkinsfile 中的 `KRA_REPLICAS` 并重新部署;后续手工改配置也需要执行滚动重启。 +4. +5. +6. 当前 Jenkins 默认将前后端都部署为 1 个副本,MySQL 和 Redis 也默认启动。若环境已有外部 MySQL,可将 `KRA_DEPLOY_MYSQL` 设为 `false`;若不需要 Redis,可将 `KRA_DEPLOY_REDIS` 设为 `false`。关闭时对应工作负载副本数为 0,backend 不等待内置 MySQL,并关闭 Redis 使用。单节点下保持 `KRA_REPLICAS=1`;未来如需扩容,必须先迁移到真正的共享存储,再调整副本数并重新部署。 > `-config-template` Secret 只负责首次创建配置文件。初始化后,运行时配置以 hostPath 中的 `/data/conf/config.yaml` 为准;更新 Secret 或 Jenkins JWT 凭据不会覆盖已有配置,避免覆盖初始化写入的数据库连接。JWT 后续轮换应通过系统设置完成并滚动重启后端。 diff --git a/deploy/Jenkinsfile b/deploy/Jenkinsfile index d69dba4..6d8320a 100644 --- a/deploy/Jenkinsfile +++ b/deploy/Jenkinsfile @@ -16,6 +16,9 @@ pipeline { BIN_NAME = 'kratos-admin' // Makefile build 的产物名 // 当前单节点部署固定使用单副本,避免首次初始化和 hostPath 数据共享产生竞争。 KRA_REPLICAS = '1' + // 中间件按环境开关;关闭时对应工作负载缩容到 0,不创建运行 Pod。 + KRA_DEPLOY_MYSQL = 'true' + KRA_DEPLOY_REDIS = 'true' KRA_FRONTEND_NODEPORT = '30080' KRA_BACKEND_NODEPORT = '30081' KRA_MYSQL_IMAGE = 'docker.m.daocloud.io/library/mysql:8.4' @@ -33,7 +36,7 @@ pipeline { 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 $BIN_NAME' + K8S_RENDER_VARS = '$KRA_APP $KRA_NAMESPACE $KRA_ENV $KRA_BACKEND_IMAGE $KRA_FRONTEND_IMAGE $KRA_REPLICAS $KRA_DEPLOY_MYSQL $KRA_DEPLOY_REDIS $KRA_MYSQL_REPLICAS $KRA_REDIS_REPLICAS $KRA_MYSQL_WAIT_IMAGE $KRA_FRONTEND_NODEPORT $KRA_BACKEND_NODEPORT $KRA_MYSQL_IMAGE $KRA_REDIS_IMAGE $BIN_NAME' } stages { @@ -156,11 +159,23 @@ pipeline { stage('Deploy to Rancher cluster') { 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 ''' + script { + if (!(env.KRA_DEPLOY_MYSQL in ['true', 'false'])) { + error("KRA_DEPLOY_MYSQL must be true or false: ${env.KRA_DEPLOY_MYSQL}") + } + if (!(env.KRA_DEPLOY_REDIS in ['true', 'false'])) { + error("KRA_DEPLOY_REDIS must be true or false: ${env.KRA_DEPLOY_REDIS}") + } + def credentialBindings = [ + file(credentialsId: "${env.KUBECONFIG_CREDENTIALS}", variable: 'KUBECONFIG_FILE'), + string(credentialsId: env.CRED_JWT_SIGNING_KEY, variable: 'KRA_JWT_SIGNING_KEY'), + usernamePassword(credentialsId: env.CRED_HARBOR_PUSH, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD') + ] + if (env.KRA_DEPLOY_MYSQL == 'true') { + credentialBindings << string(credentialsId: env.CRED_MYSQL_PASSWORD, variable: 'KRA_MYSQL_ROOT_PASSWORD') + } + withCredentials(credentialBindings) { + sh ''' set -eu export KUBECONFIG="$KUBECONFIG_FILE" umask 077 @@ -175,11 +190,13 @@ 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 - + if [ "$KRA_DEPLOY_MYSQL" = 'true' ]; then + 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 - + fi 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 @@ -197,26 +214,39 @@ pipeline { 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" + case "$KRA_DEPLOY_MYSQL" in + true) export KRA_MYSQL_REPLICAS=1; export KRA_MYSQL_WAIT_IMAGE="$KRA_MYSQL_IMAGE" ;; + false) export KRA_MYSQL_REPLICAS=0; export KRA_MYSQL_WAIT_IMAGE="$KRA_BACKEND_IMAGE" ;; + *) echo 'KRA_DEPLOY_MYSQL must be true or false' >&2; exit 1 ;; + esac + case "$KRA_DEPLOY_REDIS" in + true) export KRA_REDIS_REPLICAS=1 ;; + false) export KRA_REDIS_REPLICAS=0 ;; + *) echo 'KRA_DEPLOY_REDIS must be true or false' >&2; exit 1 ;; + esac 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 + if [ "$KRA_DEPLOY_MYSQL" = 'true' ] && [ -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 \\ - --patch-file="$PATCH_FILE" - kubectl -n "$KRA_NAMESPACE" rollout status statefulset/${APP_NAME}-mysql --timeout=10m + if [ "$KRA_DEPLOY_MYSQL" = 'true' ]; then + 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 \\ + --patch-file="$PATCH_FILE" + kubectl -n "$KRA_NAMESPACE" rollout status statefulset/${APP_NAME}-mysql --timeout=10m + fi kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-redis --timeout=10m kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-backend --timeout=10m kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-frontend --timeout=10m - ''' + ''' + } } } } diff --git a/deploy/README.md b/deploy/README.md index fbc09ad..c46e8e4 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -70,6 +70,8 @@ Harbor 项目和 Kubernetes 两个环境都会由流水线自动创建 `harbor-r | `REGISTRY` | Harbor 地址 | `192.168.200.132` | | `BIN_NAME` | 后端二进制名,须与 `Makefile` 的 `make build` 产物一致 | `kratos-admin` | | `KRA_REPLICAS` | 前后端副本数(当前单节点部署使用 1) | `1` | +| `KRA_DEPLOY_MYSQL` | 是否启动清单内 MySQL(`true`/`false`,关闭时副本数为 0) | `true` | +| `KRA_DEPLOY_REDIS` | 是否启动清单内 Redis(`true`/`false`,关闭时副本数为 0) | `true` | | `KRA_FRONTEND_NODEPORT` / `KRA_BACKEND_NODEPORT` | NodePort 端口,同一集群多个项目必须错开 | `30080` / `30081` | | `KRA_MYSQL_IMAGE` / `KRA_REDIS_IMAGE` | 中间件镜像 | daocloud mysql:8.4 / redis:7-alpine | | `GO_IMAGE` / `NODE_IMAGE` / `PNPM_VERSION` | 构建镜像与工具版本 | 见文件 | diff --git a/deploy/k8s/config.example.yaml b/deploy/k8s/config.example.yaml index 6b14a76..14b5efe 100644 --- a/deploy/k8s/config.example.yaml +++ b/deploy/k8s/config.example.yaml @@ -5,7 +5,8 @@ # 说明: # - data.database 保持为空:应用启动后首页会出现"前往初始化", # 在初始化页面填写数据库连接后,框架会自动把连接配置写回本文件。 -# - data.redis.addr 指向集群内 Redis Service(-redis:6379)。 +# - data.redis.addr 默认指向集群内 Redis Service(-redis:6379); +# 关闭内置 Redis 时,Jenkins 会同时关闭 admin.system.use_redis。 # - admin.app.env / admin.app.app_id / admin.jwt.issuer 由 Jenkins 按项目和分支渲染。 server: http: diff --git a/deploy/k8s/kra.yaml b/deploy/k8s/kra.yaml index f1bbccc..0d054ef 100644 --- a/deploy/k8s/kra.yaml +++ b/deploy/k8s/kra.yaml @@ -3,6 +3,11 @@ # KRA_NAMESPACE target namespace # KRA_ENV development | production # KRA_REPLICAS replicas per deployment +# KRA_DEPLOY_MYSQL true/false; whether the bundled MySQL runs +# KRA_DEPLOY_REDIS true/false; whether the bundled Redis runs +# KRA_MYSQL_REPLICAS rendered MySQL replica count (0 or 1) +# KRA_REDIS_REPLICAS rendered Redis replica count (0 or 1) +# KRA_MYSQL_WAIT_IMAGE image used by the optional MySQL readiness wait # KRA_FRONTEND_NODEPORT node port for the frontend service # KRA_BACKEND_NODEPORT node port for the backend debug service # KRA_MYSQL_IMAGE mysql server image @@ -28,7 +33,7 @@ metadata: app.kubernetes.io/part-of: ${KRA_APP} spec: serviceName: ${KRA_APP}-mysql - replicas: 1 + replicas: ${KRA_MYSQL_REPLICAS} selector: matchLabels: app.kubernetes.io/name: ${KRA_APP}-mysql @@ -119,7 +124,7 @@ metadata: app.kubernetes.io/name: ${KRA_APP}-redis app.kubernetes.io/part-of: ${KRA_APP} spec: - replicas: 1 + replicas: ${KRA_REDIS_REPLICAS} selector: matchLabels: app.kubernetes.io/name: ${KRA_APP}-redis @@ -145,11 +150,13 @@ spec: mountPath: /data affinity: podAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - topologyKey: kubernetes.io/hostname - labelSelector: - matchLabels: - app.kubernetes.io/name: ${KRA_APP}-mysql + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: ${KRA_APP}-mysql containers: - name: redis image: ${KRA_REDIS_IMAGE} @@ -215,14 +222,15 @@ spec: automountServiceAccountToken: false initContainers: - name: wait-for-mysql - image: ${KRA_MYSQL_IMAGE} + image: ${KRA_MYSQL_WAIT_IMAGE} imagePullPolicy: IfNotPresent command: ["sh", "-c"] args: - >- - until mysqladmin ping -h "${KRA_APP}-mysql" -P 3306 - --connect-timeout=2 --silent; - do sleep 2; done + if [ "${KRA_DEPLOY_MYSQL}" = "true" ]; then + until mysqladmin ping -h "${KRA_APP}-mysql" -P 3306 + --connect-timeout=2 --silent; do sleep 2; done; + fi # 这里只等待 MySQL 服务可达,不要求应用数据库已经初始化;首次部署 # 仍需进入 bootstrap 模式完成 /init/initdb 向导。 - name: initialize-shared-storage @@ -254,11 +262,13 @@ spec: - name: harbor-registry affinity: podAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - topologyKey: kubernetes.io/hostname - labelSelector: - matchLabels: - app.kubernetes.io/name: ${KRA_APP}-mysql + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: ${KRA_APP}-mysql podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 @@ -282,7 +292,7 @@ spec: # JWT 签名密钥由 /data/conf/config.yaml 唯一管理,避免环境变量覆盖管理页面的配置变更; # Redis 地址已在下方 config.yaml 中指定。 - name: KRA_ADMIN_SYSTEM_USE_REDIS - value: "true" + value: "${KRA_DEPLOY_REDIS}" volumeMounts: - name: config mountPath: /data/conf