Files
blog/CNB构建落地方案.md
T
zqlit e0218b42bb docs: 迁 CNB 前的架构决策文档 + 构建骨架
- 决策文档:架构总览 / 代码源与构建平台选型 / CNB构建落地方案 /
  EdgeOne双区域 / Gitee / 阿里云ESA / 精简方案
- CNB 构建骨架:deploy/Dockerfile、bin/linux/hugo
  (84MB,linux/amd64 extended 0.128.2,供 CNB 容器 COPY 用)
- scripts/setup-cnb-remotes.sh:双远端切换(CNB 主仓 + GitHub 备份)
- .gitignore 补 .workbuddy/(工作数据不入库)
2026-10-04 12:40:17 +08:00

437 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CNB 构建落地方案:双 CDN 架构不动,砍掉中转链
> 状态:2026-10-04 · 待你确认后落地
> 回答的问题:「CNB 构建的前提下,能不能继续现在的架构(境外 EdgeOne + 境内多吉云 → 又拍云源站)?」
---
## 一句话结论
**能,而且一个字都不用改。** 域名解析、备案、EdgeOne 项目、多吉云配置、又拍云存储全部保持原样。
CNB 只替换「构建 + 搬运」这一段,顺带砍掉广州中转机上那个每分钟轮询的 `sync.sh`。
---
## 零、GitHub 依赖审计:全链路零依赖
回答「GitHub 账号被标记了,还能连上 CNB 吗」——**能,而且这条链路从头到尾不需要 GitHub。**
逐环节核实(来源:腾讯云官方文档「社区版快速入门」「个人中心」、CNB 官方 Wiki):
| 环节 | CNB 的做法 | 碰 GitHub 吗 |
|---|---|---|
| 注册 / 登录 | **微信扫码**,扫码即自动注册,无邮箱验证、无登录密码 | ❌ |
| 实名认证 | 微信扫码授权手机号 | ❌ |
| 建组织 / 仓库 | CNB 内建(无「个人私有仓库」,仓库必须挂在组织下) | ❌ |
| 导入代码 | 标准 git:`git push --mirror <cnb-url>`,源仓可以是**自建 Gitea** | ❌ |
| 拉构建依赖 | CNB 自带「国内外依赖源默认加速」(腾讯云 CDN + Docker/NPM/Maven 镜像) | ❌ |
| hugo 二进制 | **已随仓库携带**(`bin/linux/hugo`),构建时零跨境下载 | ❌ |
| 部署 | EdgeOne API + 又拍云 upx v0 | ❌ |
官方原文:
> 云原生构建默认使用微信扫码注册登录,未注册用户在扫码登录后,将自动完成注册。
> —— 腾讯云文档《社区版快速入门》
> 云原生构建为微信扫码登录注册,没有登录密码。
> —— 腾讯云文档《个人中心》
### 唯一会碰到 GitHub 的两个场景,你都不走
1. **用 CNB 的「从 GitHub 导入」一键迁移工具** —— 那个需要 `GITHUB_TOKEN`。
**但你从自建 Gitea 迁,用 `git push --mirror`,不需要它。**
2. **构建时在线拉 GitHub 资源** —— 这正是**已经提前拆掉**的雷:hugo 二进制改为随仓库携带。
### 顺带纠正一个概念
**「账号被标记」≠「IP 被墙」。** 标记是账号级限制;匿名 HTTP 访问 github.com 的公开资源不受影响
(实测国内节点 `curl` hugo release 仍返回 302 正常)。所以即便真要在构建里拉 GitHub,也只是**慢**,不是**不通**。
### 你真正要做的三件准备(都与 GitHub 无关)
1. 微信扫码注册 CNB
2. 微信扫码完成**实名认证**(官方说明:未实名用户禁止写行为 —— 创建仓库 / 推送代码)
3. 先建一个**组织**,仓库建在组织下
---
## 一、决定性发现:答案你自己早就写在代码里了
`deploy.yml` 第 290 行起,那段被 `if: false` 停用的 `Upload to UpYun`,注释原话:
```
│ 2026-10-03 起永久跳过海外直传:本 runner 在境外,对 UpYun v0 接口
│ 无论 -w 10/--strong 还是 -w 3 均为 70 秒左右 EOF,3 轮重试全败,
│ 每次部署白耗 5 分钟(2026-10-03 build #25 日志实锤)。
│ 国内线路由腾讯云广州中转机兜底(1Panel 计划任务「又拍云同步(国内
│ 中转)」每分钟比对 COS build hash → 下载 355M → upx sync →
│ 又拍云 purge → 多吉云刷新,实测 5 分钟内完成且它自己会刷 CDN)。
│ 将来若把 runner 挪到国内,把下面的 if: false 删掉即可恢复。
```
**当初被迫上中转机,唯一的原因是「runner 在境外」。**
链路是这样被逼出来的:
| 环节 | 为什么存在 |
|---|---|
| runner 在境外 | Gitea 跑在境外 VPS |
| 直传又拍云失败 | 境外 → 又拍云 v0 接口,**跨境 + 并发敏感 → 70 秒 EOF,必败** |
| COS 中转 | 境外的产物,国内机器得有个地方取 |
| 广州中转机 + sync.sh | 只有它在国内,能直传又拍云 |
**CNB 的构建节点就在国内(腾讯云)。** 那句注释的复活条件直接成立 —— 中转机、COS、artifact 传递全都失去了存在理由。
---
## 二、目标架构
```
写作 / 本地
│ git push
▼
CNB(腾讯云,构建节点在国内 4 核 8 GiB)
│ 一次构建,三条出口
├──► 又拍云存储(境内源站) upx sync + upx purge
│ │ 多吉云回源自动跟随
│ └──► 多吉云 CDN 刷新(API 精确刷新)
│
└──► EdgeOne Pages(境外) npx edgeone pages deploy --area overseas
```
**保留不动**(零改动):
- 又拍云存储 `imzql`(境内源站)
- 多吉云 CDN(境内解析,回源又拍云)
- EdgeOne Pages 项目 `hugo-blog`(境外解析,`--area overseas`)
- 域名解析、ICP 备案、Ying 主题、content 内容
**替换掉**:
- Gitea Actions `build` job → CNB 流水线
- act_runner → 不需要
- COS / Gitea artifact 传递 → 不需要(含那串 v3/v4 坑)
- 广州中转机 `sync.sh` 轮询 → 不需要
- `deploy.yml` 里 8 处 `github.server_url` 兼容分支 → 不需要(另写 `.cnb.yml`)
---
## 三、CNB 能力核对(全部官方文档核实)
| 能力 | 值 | 对本项目的意义 |
|---|---|---|
| 构建节点 | 默认 8 核 16 GiB(**内存 = CPU 核数 × 2**) | 全面优于广州机 2 核 1.9 G |
| 免费额度 | **160 核时/月**(0.125 元/核时超出) | 见下方换算 |
| 构建最大时长 | 20 小时;单任务无输出 10 分钟 / 超 1 小时触发超时(可声明,上限 12 小时) | 传 372MB 绰绰有余 |
| 自定义环境 | `docker.image` 或 `docker.build`(Dockerfile) | 可固定 hugo 0.128.2 extended |
| 密钥管理 | 密钥仓库 + `imports` 导入环境变量 | 又拍云/多吉云/EdgeOne 凭据不落仓库 |
| 定时任务 | 支持 cron,最小间隔 5 分钟,时区 Asia/Shanghai | 可替代每日定时构建 |
| **出网限制** | **无白名单限制**(官方:出口 IP 动态,明确不建议白名单) | 又拍云 / 多吉云 / EdgeOne API 都可直连 |
| Docker-in-Docker | 支持(`services: docker`) | 备用 |
| 官方集成 | 腾讯云官方有 **CNB → EdgeOne** 的部署文档示例(`npx edgeone makers deploy`) | 已验证路径 |
### 额度换算(关键决策数字)
| 规格 | 内存 | 每次构建耗(按 10 分钟) | 160 核时可用次数 |
|---|---|---|---|
| 8 核 | 16 GiB | 1.33 核时 | **120 次/月** |
| **4 核** | **8 GiB** | **0.67 核时** | **240 次/月** |
| 2 核 | 4 GiB | 0.33 核时 | 480 次/月 |
你的触发频率是「每天 1 次定时 + 每次发文」≈ **60–90 次/月**。
**建议用 4 核 8 GiB** —— 240 次/月的余量,即使构建耗时翻倍到 20 分钟也还有 120 次。hugo 构建吃 CPU(4 核够并行编译),`upx sync` 吃网络不吃 CPU。
---
## 四、落地骨架
### 4.0 第一步:仓库迁移与「双远端推送」(CNB 主仓 + GitHub 备份)
**目标形态**
```
origin → https://cnb.cool/<组织>/<仓库> ← 主仓(fetch + push),CNB 构建由它触发
gh → git@github.com:zqlit/blog.git ← 备份(push),只存档不参与构建
gitea → 自建 Gitea ← 暂时留着当只读参考,链路稳定后手工删
```
一条命令推两段:`git pushall`(等价于 `git push origin main; git push gh main`)。
**★ 为什么不用「一个 remote 挂多个 pushurl」**
现有 `origin` 就是那种写法(pushurl 同时挂了 GitHub 和 自建 Gitea)。但它有个被忽视的缺陷,我做了实测:
| 场景 | 结果 |
|---|---|
| 两个 pushurl 都可用 | ✅ 两个都收到,一次 push 搞定 |
| **第一个失败** | ❌ **git 立即中止,第二个根本不会被推** |
也就是说:**CNB 推失败时,GitHub 那份备份也不会更新** —— 备份的意义就没了。
所以拆成两个独立 remote,别名里用 `;` 而不是 `&&`,保证互不阻塞。
**脚本**(已就位,可直接用)
```bash
bash scripts/setup-cnb-remotes.sh https://cnb.cool/<组织>/<仓库>
```
它会先把你现有的远端配置(含明文凭据)备份到 `.workbuddy-backup/git-remotes.<时间戳>.txt`,
再重建 `origin`/`gh` 并注册 `pushall` 别名 —— 可回退。
**★ CNB 不支持 SSH(官方明确)**
> SSH Not Supported —— 平台不支持 SSH 协议访问。
> 认证方式:用户名**固定填 `cnb`**,密码填**访问令牌**。
> —— docs.cnb.cool《Git Address and Authentication Instructions》
这跟 GitHub 的 SSH 不同,所以两段要各用各的认证:
```bash
git config --global credential.helper manager # Windows 的 Git Credential Manager
# 首次 push 时:用户名 cnb,密码 = 访问令牌(个人设置 → 访问令牌 → 勾「代码仓库/读写」)
```
仓库地址直接用仓库页地址即可(`https://cnb.cool/组/仓`),带不带 `.git` 都支持。
**建仓顺序不能乱**(官方要求)
1. 微信扫码注册 → 2. 微信扫码**实名认证**(未实名禁止建仓/推码)→ 3. 先建**组织**(CNB 无「个人私有仓库」,仓库必须挂在组织下)→ 4. 建仓库 → 5. 跑上面的脚本 + push
### ⚠️ 推 CNB 之前必须先知道的敏感文件(已审计)
这些文件**当前就被 git 跟踪**,会在首次 push 时连**全部历史**一起进 CNB:
| 文件 | 内容 | 状态 |
|---|---|---|
| `write-server/nginx/ssl/privkey.pem` | **真实 TLS 私钥**(EC P-256,`BEGIN PRIVATE KEY`),2026-06-22 起在库 | 已跟踪 |
| `.env` | `SILICONFLOW_API_KEY`、`ZHIPU_API_KEY` | 已跟踪 |
| `GITEA_SECRETS.md` | Gitea 相关凭据说明 | 已跟踪 |
| `content/posts/2024/.../setup-secrets.png` | 疑似密钥配置截图 | 已跟踪 |
**好消息**:已核实 `https://github.com/zqlit/blog` 返回 **Page not found**,仓库不可公开访问
→ **私钥没有被公开暴露**,无需紧急处置。
**但两件事建议顺手做掉**:
1. **CNB 仓库建成「私有」** —— 这些文件在私有仓里影响可控
2. **`git rm --cached` 停止继续跟踪**(历史里的删不掉,但至少不再新增):
```bash
git rm --cached .env GITEA_SECRETS.md write-server/nginx/ssl/privkey.pem write-server/nginx/ssl/fullchain.pem
```
⚠️ 注意:`.gitignore` 里其实**已经有** `.env` / `.env.*` 规则 —— 它们失效的原因不是规则写错,
而是**忽略规则对「已跟踪」文件无效**,所以必须显式 `git rm --cached`。
(另:`.gitignore` 里那条 `write\.env` 是写错的 —— gitignore 中 `\` 是转义符,
它实际匹配文件名 `write.env` 而非 `write/.env`;幸好 `write/.gitignore` 里有 `.env*` 兜住了。)
**不建议现在做历史清理**(`git filter-repo`):会重写 955 个提交、让 GitHub 备份随之分叉,
收益(去掉一个未公开的私钥)远小于风险。正确做法是**轮换凭据**而不是重写历史。
---
### 4.1 `deploy/Dockerfile`(构建环境)
CNB 默认镜像不含 hugo / upx,写一个专属镜像固定版本:
```dockerfile
FROM node:22-bookworm-slim
# 换国内 apt 源(CNB 只明示加速 Docker/NPM/Maven,apt 默认源在国内节点可能慢)
RUN set -eux; \
for f in /etc/apt/sources.list /etc/apt/sources.list.d/debian.sources; do \
if [ -f "$f" ]; then \
sed -i -e 's|deb.debian.org|mirrors.aliyun.com|g' \
-e 's|security.debian.org|mirrors.aliyun.com|g' "$f"; \
fi; \
done; \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl tar zstd python3 xz-utils git \
&& rm -rf /var/lib/apt/lists/*
# Hugo extended 0.128.2(linux/amd64)——★ 随仓库携带,构建时零跨境下载
COPY bin/linux/hugo /usr/local/bin/hugo
RUN chmod +x /usr/local/bin/hugo && hugo version
# 又拍云 upx
RUN set -eux; \
curl -fsSL -o /tmp/upx.tgz \
"https://collection.b0.upaiyun.com/softwares/upx/upx_0.4.9_linux_amd64.tar.gz"; \
tar -xzf /tmp/upx.tgz -C /usr/local/bin upx; \
chmod +x /usr/local/bin/upx; \
rm /tmp/upx.tgz
# EdgeOne CLI
RUN npm i -g edgeone@latest
CMD ["hugo", "version"]
```
> ✅ 已实测(2026-10-04):
> 1. **hugo 二进制改为随仓库携带**(`bin/linux/hugo`,84MB,进 git 后仅 24MB)。实测国内节点直连 GitHub 拉这个 release 要 **127 秒**(本机只要 7 秒),而每次构建都要付这个成本 → 自带更稳。
> 2. 又拍云 `upx` 的下载源是 `collection.b0.upaiyun.com`(国内 CDN),直连稳定,保留在线安装。
> 3. **架构不能混用**:`E:\Hugo\hugo.exe` 是 `windows/amd64`(本地预览用),CNB 构建节点是 `linux/amd64`(`bin/linux/hugo`),两者互不替代。
> 4. 若以后主仓改选 **Gitee(单仓 500MB)**,这 84MB(git 内 24MB)会挤占配额,届时改用 CNB/又拍云对象存储托管该二进制,别放仓库。
> 5. **apt 源改阿里云镜像**:CNB 官方只明示加速「Docker / NPM / Maven 镜像」,Debian 默认 apt 源不在承诺范围内,而构建节点在国内 → 换阿里云兜底,避免构建卡在 `apt-get update`(首轮构建时留意这一步耗时)。
### 4.2 `.cnb.yml`(流水线)
```yaml
main:
push:
- name: 构建并发布(境内外双线路)
imports:
# 密钥仓库(私有),存放 UPYUN_* / DOGE_* / EDGEONE_API_TOKEN
- https://cnb.cool/<你的组织>/<密钥仓库>/-/blob/main/secrets.yml
runner:
cpus: 4 # 4 核 → 8 GiB
docker:
build: deploy/Dockerfile
stages:
- name: Hugo 构建
script: |
set -euo pipefail
hugo version
python3 scripts/add_draft_to_hidden.py
rm -rf public resources/_gen
hugo --minify --gc
BUILD_HASH=$(find public -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')
echo "$BUILD_HASH" > public/.build_hash
echo "$BUILD_HASH" > public/build_hash.txt
echo "$BUILD_HASH" > .build_hash.out
echo "✅ 构建完成 hash=${BUILD_HASH:0:12} 文件数=$(find public -type f | wc -l)"
- name: 同步到又拍云(境内源站)
script: |
set -euo pipefail
upx login "$UPYUN_SERVICE" "$UPYUN_OPERATOR" "$UPYUN_PASSWORD"
upx sync public/ / -w 5 --delete
upx put public/.build_hash /__build_hash || true
echo "✅ 又拍云同步完成"
- name: 刷新又拍云 CDN
script: |
set -euo pipefail
# ★ 必须刷固定入口页:只刷首页会让 sitemap.xml / rss.xml /
# archives.html 一直发 CDN 里的旧副本(2026-10-01 实测复现)
{ echo "https://usj.cc/";
for p in sitemap.xml rss.xml archives.html posts.html comment.html links.html; do
echo "https://usj.cc/$p"
done
} > /tmp/purge.txt
upx purge --list /tmp/purge.txt \
|| echo "⚠️ purge 失败,降级不影响源站已更新"
- name: 刷新多吉云 CDN
script: |
set -euo pipefail
python3 - <<'PY'
import hmac, hashlib, json, os, urllib.request
AK, SK = os.environ['DOGE_AK'], os.environ['DOGE_SK']
SITE = 'https://usj.cc'
P = '/cdn/refresh/add.json'
urls = [SITE + '/', SITE + '/sitemap.xml', SITE + '/rss.xml',
SITE + '/archives.html', SITE + '/posts.html']
def refresh(rtype, u, label):
body = json.dumps({'rtype': rtype, 'urls': json.dumps(u)})
sig = hmac.new(SK.encode(), (P + '\n' + body).encode(), hashlib.sha1).hexdigest()
req = urllib.request.Request(
'https://api.dogecloud.com' + P, data=body.encode(),
headers={'Authorization': 'TOKEN %s:%s' % (AK, sig),
'Content-Type': 'application/json'}, method='POST')
with urllib.request.urlopen(req, timeout=30) as r:
print('%s -> %s' % (label, r.read().decode()[:200]))
refresh('path', [SITE + '/'], '目录刷新')
refresh('url', urls, '精确刷新 %d 条' % len(urls))
PY
- name: 部署 EdgeOne(境外线路)
script: |
set -euo pipefail
test -n "$EDGEONE_API_TOKEN" || { echo "缺 EDGEONE_API_TOKEN"; exit 1; }
npx edgeone pages deploy ./public -n hugo-blog -t "$EDGEONE_API_TOKEN" --area overseas
- name: 上报部署状态
script: |
set -euo pipefail
H=$(cat .build_hash.out)
curl -s --max-time 20 -X POST \
"https://api.200181.xyz/api/deploy-status?token=${RSS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"hash\":\"$H\",\"phase\":\"built\"}" || true
curl -s --max-time 20 -X POST \
"https://api.200181.xyz/api/deploy-notify?token=${RSS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"hash\":\"$H\"}" || true
```
---
## 五、两个必须复刻的细节(`sync.sh` 里的血泪)
### 5.1 purge 不能只刷首页
`sync.sh` 的注释记录了一个实测坑:
> 历史写法只有 `upx purge "$SITE/"`,即只刷首页 —— `sitemap.xml` / `rss.xml` /
> `archives.html` / `posts.html` 这些「老路径」会一直发 CDN 里的旧副本。
> 2026-10-01 实测复现:中转机产物 sitemap.xml 已含新文章(22812B),
> CDN 上那份却仍是 7/24 的 22592B;手动 purge 后立刻变新。
所以骨架里固定刷「首页 + 6 个入口页」。**进阶做法**(完整复刻 `sync.sh` 的精确刷新):
把文件清单存到又拍云 `/__manifest.txt`,每次 CNB 拉下来与本次产物比对,得出「变化 URL」精确刷新。
好处是新增文章时只刷那几页;代价是多维护一个清单文件。**建议先跑固定入口版,观察 1–2 周再决定要不要加。**
### 5.2 又拍云并发参数
`sync.sh` 用 `-w 10 --strong`。当时失败是因为**跨境**(境外 runner 直传),国内直传理论无此问题。
但建议首次仍保守用 `-w 5`(去掉 `--strong`,它会把请求量放大),实测稳定后再往上调。
---
## 六、砍掉清单
| 砍掉 | 说明 |
|---|---|
| act_runner | 不再需要自建 runner |
| Gitea artifact 传递链 | 含 `upload-artifact@v3/v4` 那串坑 |
| 腾讯云 COS | 中转存储不再需要 |
| 广州中转机 `sync.sh` + 1Panel 定时任务 | **注意:只停任务,机器上还有 mysql/openresty 等 7 个容器** |
| `deploy.yml` 的 8 处 `github.server_url` 补丁分支 | `.cnb.yml` 重新写 |
| 两套冗余通知(TG/飞书/邮件三选一) | 独立小优化 |
**发布链路:7 环节 → 3 环节**(写作 → CNB 构建 → 双线上线)
**自维护构建节点:1 台 → 0 台**
---
## 七、前置动作与待核实项
### 必须先确认(按重要性)
1. **★ EdgeOne 从国内上传 372MB 的耗时**。现在部署是从境外 runner 上传(同区域快),改到 CNB(国内)后是跨境上传到 EdgeOne 国际站。**这是本方案最大的未知数**,必须实测一次。
2. **★ CNB 构建节点能否顺畅拉取 `github.com` 的 hugo release**。不行就换国内镜像源,或把 hugo 二进制预置进 CNB 制品库。
3. **又拍云 v0 接口从 CNB 出口的真实表现**。国内直传预期无碍,但需实测确认(这是整个方案的立论基础,务必第一次就验)。
4. 主仓放哪:
- **CNB 当主仓** → 最简,Gitea 可退役,写作后台改 `origin`
- **Gitea 当主仓 + push mirror 到 CNB** → 写作零改动,代价是多留一台机器
5. 多吉云刷新配额(`sync.sh` 里有 200 条截断逻辑,CNB 版本固定入口页不触及)。
### 建议的验证步骤(不动任何现有东西)
1. 建一个 CNB 测试仓库(或就用一个分支),放最小化 `.cnb.yml` + Dockerfile
2. 跑通「hugo build」→ 确认镜像与构建时长
3. 单独测「`upx sync` 到一个测试 bucket」→ 确认国内直传可行
4. 单独测「`edgeone pages deploy` 到测试项目」→ 拿到跨境上传耗时
5. 四项全绿,才动正式仓库
---
## 八、与之前几份方案的关系
| 方案 | 结论 | 与本方案的关系 |
|---|---|---|
| `代码源与构建平台选型.md` | CNB 首选(100GiB 仓库 + 160 核时) | 本方案是它的落地细化 |
| `Gitee方案评估.md` | 可用,但需瘦身 588MB → 500MB | **用 CNB 就不用瘦身** |
| `EdgeOne双区域方案评估.md` | 建议 `-a global` 或双项目 | 双区域照旧,只是构建方换成 CNB |
| `阿里云ESA评估.md` | 2000 文件上限卡死 | 不适用 |
| `精简方案-只留CF和Hugo.md` | 只留 CF 会丢又拍云+多吉云 | **本方案保留双 CDN,复杂度收益同样大** |
**核心区别**:之前几份在讨论「CDN 选谁」;本方案证明**你不需要换 CDN,只需要换掉「构建 + 搬运」那一层**。而这一层恰恰是复杂度的大头。