引言

架构设计

整体的发布流程简化为:本地 Push 源码 -> GitHub Actions 编译打包 -> 触发服务器 apiservice -> 解压并切换目录 -> 刷新 CDN。
在这个流程中,有几个需要处理的细节:

  1. 安全性: apiservice 接口直接暴露在公网上,需要通过 HMAC 算法对消息体进行签名。
  2. 平滑发布: 避免在部署期间出现 404,通过 Linux 软链接 (ln -sfn) 实现新老 Web 目录的原子替换。
  3. 缓存清理: 站点用了 EdgeOne,静态资源更新后如果不主动通知 EdgeOne 过期缓存会造成访问到旧资源。

实现步骤

服务器基础环境

为了遵循权限最小化原则,首先在服务器上为 apiservice 服务创建一个专用的非特权用户及目录:

1
2
3
4
# 创建 apiservice 用户及相关目录
sudo useradd -r -s /bin/sbin/nologin apiservice
sudo mkdir -p /opt/webapp/apiservice /opt/webapp/blog
sudo chown -R apiservice:apiservice /opt/webapp

Web 服务器使用 Caddy,配置反向代理将 /api/deploy 路由转发给后端的 Python 服务,同时托管通过软链接指向的静态页面:

1
2
3
4
5
6
7
api.sinkalex.ac.cn {
...

handle /api/deploy {
reverse_proxy 127.0.0.1:3000
}
}

Python 虚拟环境与依赖安装

为了不污染系统的全局 Python 环境,我们需要为 apiservice 服务单独创建一个虚拟环境(venv)。

1
2
3
4
5
6
7
8
9
10
# 切换到 apiservice 用户
sudo su - -s /bin/bash apiservice
cd /opt/webapp/apiservice

# 创建并激活虚拟环境
python3 -m venv venv
source venv/bin/activate

# 安装后端框架及腾讯云 SDK
pip install fastapi uvicorn python-multipart python-dotenv tencentcloud-sdk-python-teo

腾讯云 CAM 权限配置

由于部署完成后需要自动刷新 EdgeOne CDN 缓存,我们需要调用腾讯云 API。出于安全考虑,不要使用主账号的 API 密钥。

我们需要在腾讯云访问管理(CAM)中创建一个子账号:

  1. 新建用户,访问方式仅勾选 “编程访问”。
  2. 权限策略搜索并勾选 QcloudTEOFullAccess。
  3. 创建成功后,保存好生成的 SecretId 和 SecretKey。

[!NOTE]

在 /opt/webapp/apiservice/ 目录下创建 .env 文件,存入上述密钥以及自定义的 apiservice secret key。
同时将 .env 所有权转交给 apiservice,并将权限设为 600

1
2
3
nano /opt/webapp/apiservice/.env
chown -R apiservice:apiservice /opt/webapp/apiservice/.env
chmod 600 /opt/webapp/apiservice/.env
1
2
3
4
5
6
7
```

### apiservice 服务端实现 (FastAPI)

在同目录下创建 `index.py`,这是接收请求、校验签名并执行部署的核心逻辑:

```python

配置 Systemd 守护进程

为了让 apiservice 服务在后台稳定运行并支持开机自启,我们需要编写一个 systemd 服务单元。退出 apiservice 用户,回到拥有 sudo 权限的账号,创建文件 /etc/systemd/system/hexo-apiservice.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=Hexo apiservice Deployment Service
After=network.target

[Service]
User=apiservice
Group=apiservice
WorkingDirectory=/opt/webapp/apiservice
# 指定虚拟环境的 bin 目录,确保 uvicorn 能正确加载依赖
Environment="PATH=/opt/webapp/apiservice/venv/bin"
ExecStart=/opt/webapp/apiservice/venv/bin/uvicorn index:app --host 127.0.0.1 --port 3000
Restart=always

[Install]
WantedBy=multi-user.target

保存后,重载 systemd 并启动服务:

1
2
3
4
sudo systemctl daemon-reload
sudo systemctl enable --now hexo-apiservice
# 检查服务运行状态
sudo systemctl status hexo-apiservice

GitHub Actions 工作流配置

最后,在 Hexo 仓库根目录新建 .github/workflows/deploy.yml。这里的核心在于通过 Shell 脚本计算与服务端一致的 HMAC 签名,并将其放在 Header 中随文件一同发送。

需要在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中提前配好 DEPLOY_TOKEN(值与服务端的 SECRET_TOKEN 一致)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
name: Deploy Hexo

on:
push:
branches: [ master ]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install and Build
run: |
npm ci || npm install
npx hexo generate

- name: Zip Files
run: |
cd public
zip -r -q ../blog.zip ./*
cd ..

- name: Upload to apiservice
run: |
TIMESTAMP=$(date +%s)
FILE_HASH=$(sha256sum blog.zip | awk '{print $1}')
PAYLOAD="${TIMESTAMP}.${FILE_HASH}"

# 计算 HMAC-SHA256 签名
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.DEPLOY_TOKEN }}" | awk '{print $2}')

curl -X POST -F "file=@blog.zip" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
https:///api/deploy

结语