From 7160e909091fefe5275bf2173a4211ffd57fcf55 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 2 May 2026 20:25:01 +0800 Subject: [PATCH] Guard server provision nginx install --- .../PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md | 10 +- .../Jenkinsfile.production-server-provision | 108 ++++++++++++++++-- 2 files changed, 106 insertions(+), 12 deletions(-) diff --git a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md index 816cacf9..1ff8147f 100644 --- a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md +++ b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md @@ -350,12 +350,16 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - 创建 `/stdb`、`/opt/genarrative`、`/srv/genarrative`、`/etc/genarrative`、`/var/lib/genarrative/maintenance`。 - 安装或更新 SpacetimeDB。 - 安装 systemd unit。 -- 安装 Nginx 配置和维护模式 snippet。 -- 执行 `nginx -t`。 +- 可选安装 Nginx 配置和维护模式 snippet。 +- 安装 Nginx 配置时执行 `nginx -t`。 - 启用并启动 `spacetimedb.service` 与 `genarrative-api.service`。 该流水线属于高风险操作,默认要求人工确认后执行。 -已落地的 Jenkinsfile 为 `jenkins/Jenkinsfile.production-server-provision`。该流水线默认 `DRY_RUN=true`,只打印将执行的初始化动作;真正写入系统用户、目录、systemd、Nginx 配置并启动服务时,必须设置 `DRY_RUN=false` 且勾选 `CONFIRM_PROVISION`。当 `DEPLOY_TARGET=release` 时,还必须勾选 `CONFIRM_RELEASE_DEPLOY_AGENT`,并通过 `linux && genarrative-release-deploy` 调度到独立 release 部署 agent。 +已落地的 Jenkinsfile 为 `jenkins/Jenkinsfile.production-server-provision`。该流水线默认 `DRY_RUN=true`,只打印将执行的初始化动作;真正写入系统用户、目录、systemd、环境文件并启动服务时,必须设置 `DRY_RUN=false` 且勾选 `CONFIRM_PROVISION`。当 `DEPLOY_TARGET=release` 时,还必须勾选 `CONFIRM_RELEASE_DEPLOY_AGENT`,并通过 `linux && genarrative-release-deploy` 调度到独立 release 部署 agent。 + +首次真实初始化默认保持 `INSTALL_NGINX_CONFIG=false`,先完成系统用户、目录、SpacetimeDB、systemd unit 与 `/etc/genarrative/api-server.env` 落盘。等真实域名确定,并且目标机已经存在 `/etc/letsencrypt/live//fullchain.pem` 与 `/etc/letsencrypt/live//privkey.pem` 后,再把 `SERVER_NAME` 从 `genarrative.example.com` 改成真实域名,并设置 `INSTALL_NGINX_CONFIG=true` 安装 Nginx HTTPS 配置。流水线会拒绝在真实初始化中用占位域名写入 Nginx 配置,也会在证书缺失时提前失败。 + +若误用占位域名执行过真实初始化,失败通常发生在 `nginx -t`,错误表现为找不到 `/etc/letsencrypt/live/genarrative.example.com/fullchain.pem` 或 `privkey.pem`。新版初始化在 `INSTALL_NGINX_CONFIG=false` 时会检测并禁用上一轮留下的占位域名 Nginx 配置,避免它继续影响后续 `nginx -t`。 ### Web Build / Deploy diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index 98abf53f..d40f1700 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -26,7 +26,7 @@ pipeline { string(name: 'WEB_LINK', defaultValue: '/srv/genarrative/web', description: 'Nginx 静态站点目录或软链接') string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: 'api-server 环境文件') string(name: 'API_PORT', defaultValue: '8082', description: 'api-server 本机监听端口') - booleanParam(name: 'INSTALL_NGINX_CONFIG', defaultValue: true, description: '安装 Nginx 配置并执行 nginx -t') + booleanParam(name: 'INSTALL_NGINX_CONFIG', defaultValue: false, description: '安装 Nginx 配置并执行 nginx -t;首次初始化且证书未准备好时保持关闭') booleanParam(name: 'ENABLE_SERVICES', defaultValue: true, description: '启用并启动 spacetimedb 与 api-server systemd 服务') } @@ -49,6 +49,9 @@ pipeline { if (!params.SPACETIME_BIN_SOURCE?.trim()) { error('SPACETIME_BIN_SOURCE 不能为空。') } + if (!params.DRY_RUN && params.INSTALL_NGINX_CONFIG && params.SERVER_NAME?.trim() == 'genarrative.example.com') { + error('真实初始化安装 Nginx 配置时必须把 SERVER_NAME 改成真实域名,不能使用 genarrative.example.com 占位值。证书未准备好时请先保持 INSTALL_NGINX_CONFIG=false。') + } } } } @@ -124,6 +127,98 @@ pipeline { deploy/env/api-server.env.example } + validate_nginx_tls() { + local cert_dir="/etc/letsencrypt/live/${SERVER_NAME}" + if [[ "${SERVER_NAME}" == "genarrative.example.com" ]]; then + echo "[server-provision] SERVER_NAME 仍是占位域名,拒绝写入 Nginx HTTPS 配置。请填写真实域名,或先设置 INSTALL_NGINX_CONFIG=false。" >&2 + exit 1 + fi + if [[ ! -f "${cert_dir}/fullchain.pem" || ! -f "${cert_dir}/privkey.pem" ]]; then + echo "[server-provision] 未找到 Nginx HTTPS 证书: ${cert_dir}/fullchain.pem 或 ${cert_dir}/privkey.pem" >&2 + echo "[server-provision] 请先完成证书申请,或首次初始化时设置 INSTALL_NGINX_CONFIG=false,避免写入无法通过 nginx -t 的配置。" >&2 + exit 1 + fi + } + + install_nginx_config_with_rollback() { + local config_target="/etc/nginx/conf.d/genarrative.conf" + local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf" + local rendered_config rendered_snippet config_backup snippet_backup + local had_config="false" + local had_snippet="false" + + run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d + echo "+ render deploy/nginx/genarrative.conf -> ${config_target}" + echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}" + + if [[ "${DRY_RUN}" == "true" ]]; then + echo "+ nginx -t" + return + fi + + validate_nginx_tls + rendered_config="$(mktemp)" + rendered_snippet="$(mktemp)" + config_backup="$(mktemp)" + snippet_backup="$(mktemp)" + render_nginx_config >"${rendered_config}" + cp deploy/nginx/snippets/genarrative-maintenance.conf "${rendered_snippet}" + + if [[ -f "${config_target}" ]]; then + cp -p "${config_target}" "${config_backup}" + had_config="true" + fi + if [[ -f "${snippet_target}" ]]; then + cp -p "${snippet_target}" "${snippet_backup}" + had_snippet="true" + fi + + install -m 0644 "${rendered_config}" "${config_target}" + install -m 0644 "${rendered_snippet}" "${snippet_target}" + + if ! nginx -t; then + echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2 + if [[ "${had_config}" == "true" ]]; then + cp -p "${config_backup}" "${config_target}" + else + rm -f "${config_target}" + fi + if [[ "${had_snippet}" == "true" ]]; then + cp -p "${snippet_backup}" "${snippet_target}" + else + rm -f "${snippet_target}" + fi + rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" + exit 1 + fi + + rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" + } + + cleanup_placeholder_nginx_config() { + local config_target="/etc/nginx/conf.d/genarrative.conf" + local disabled_target + + if [[ ! -f "${config_target}" ]]; then + return + fi + + if ! grep -q "/etc/letsencrypt/live/genarrative.example.com/" "${config_target}"; then + return + fi + + disabled_target="${config_target}.disabled-placeholder-$(date +%Y%m%d%H%M%S)" + echo "[server-provision] 发现上一轮初始化留下的占位域名 Nginx 配置,禁用: ${config_target} -> ${disabled_target}" + if [[ "${DRY_RUN}" != "true" ]]; then + mv "${config_target}" "${disabled_target}" + if command -v nginx >/dev/null 2>&1; then + if ! nginx -t; then + echo "[server-provision] 占位配置已禁用,但 nginx -t 仍失败;请检查其他 Nginx 配置。" >&2 + fi + fi + fi + } + escape_sed_replacement() { printf "%s" "$1" | sed "s/[&|]/\\\\&/g" } @@ -205,14 +300,9 @@ pipeline { fi if [[ "${INSTALL_NGINX_CONFIG}" == "true" ]]; then - run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d - echo "+ render deploy/nginx/genarrative.conf -> /etc/nginx/conf.d/genarrative.conf" - if [[ "${DRY_RUN}" != "true" ]]; then - render_nginx_config >/etc/nginx/conf.d/genarrative.conf - chmod 0644 /etc/nginx/conf.d/genarrative.conf - fi - install_file deploy/nginx/snippets/genarrative-maintenance.conf /etc/nginx/snippets/genarrative-maintenance.conf 0644 - run_cmd nginx -t + install_nginx_config_with_rollback + else + cleanup_placeholder_nginx_config fi run_cmd systemctl daemon-reload