写在前面
我有一个域名 raysre.com,放在那儿很久没什么用。最近打算把它变成个人博客——要求很简单:纯静态、不维护服务器、更新成本接近零。
选了 Hugo(Go 写的静态站点生成器,hugo build 整个站点不到 200ms)+ PaperMod 主题(极简,不需要 Webpack/PostCSS 那一套)。托管在腾讯云 COS(对象存储),套一层 EdgeOne(腾讯云的边缘加速产品,对标 Cloudflare),通过 GitHub Actions 推送代码自动上线。
这条路踩了几个坑。COS 的「公有读私有写」和 EdgeOne 的「私有访问授权」组合在一起会产生一个让人摸不着头脑的 AccessDenied。调试过程花了不少时间翻文档,顺带把 EdgeOne 和 COS 里那些没用到但经常被问到的功能也一并理了。
这篇文章是从零到上线的完整记录,加上排查过程、配置解读、概念速查。没用到腾讯云功能的内容放进进阶参考,最后附 FAQ。
整体架构
各组件分工:
| 层级 | 用什么 | 负责 |
|---|---|---|
| 写作 | Markdown | 文章内容,存在 GitHub 仓库里 |
| 构建 | Hugo 0.164.0 + PaperMod | Markdown → HTML/CSS/JS,hugo --minify 压缩输出 |
| 构建环境 | GitHub Actions | 每次 git push main 自动构建+上传 |
| 存储 | 腾讯云 COS(上海,标准存储) | 存 HTML/CSS/JS/图片等静态文件 |
| 加速+安全 | 腾讯云 EdgeOne | 边缘缓存、HTTPS 终结、DDoS 基础防护、www 跳转 |
| DNS | 腾讯云 DNSPod | raysre.com → EdgeOne CNAME |
数据流是单向的:
开发者 git push → GitHub Actions → hugo build → public/ → Python SDK 上传 COS → CDN 刷新
用户访问 raysre.com → EdgeOne 边缘节点(缓存命中)→ 返回缓存 → 用户
|缓存未命中 → 回源 COS → 返回
这个架构对个人博客来说有几个好处:不需要买云服务器,不需要维护 Nginx,COS 存储一个月几毛钱,EdgeOne 免费套餐 10GB/月流量够用。
基础概念速查
配置过程中反复出现的几个概念,放在这里免得后面看不懂。
| 概念 | 一句话 | 你博客里的角色 |
|---|---|---|
| COS 存储桶 | 腾讯云的对象存储,类似 AWS S3 | 存 HTML 等静态文件 |
| 静态网站托管 | COS 的一个功能,让桶像 Web 服务器那样响应 HTTP 请求 | 开了,但 EdgeOne 回源不用它 |
| 默认域名 | COS 给每个桶分配的公网访问域名(xxx.cos.ap-shanghai.myqcloud.com) | 2024 年 1 月后新建的桶不能用它预览网页(官方限制) |
| 静态网站端点 | 另一个域名(xxx.cos-website.ap-shanghai.myqcloud.com),专门用于静态网站访问 | 开了,确认能用 HTTP 访问到页面 |
| EdgeOne 加速域名 | 你自定义的域名接入 EdgeOne 后分配 CNAME(raysre.com.eo.dnse2.com) | DNS CNAME 指到这儿 |
| 回源 | CDN 节点缓存未命中时,向源站拉取文件 | EdgeOne → COS |
| 回源 HOST 头 | CDN 回源请求中带的 Host 头,决定源站识别哪个站点 | 配置错误导致 AccessDenied 的关键字段 |
| CNAME | DNS 里把域名指向另一个域名的记录 | raysre.com CNAME → raysre.com.eo.dnse2.com |
关键理解:COS 有两套域名体系。
| 域名类型 | 格式 | 用在哪 |
|---|---|---|
| 默认域名(API 端点) | bucket.cos.region.myqcloud.com | SDK 上传文件、API 操作 |
| 静态网站端点 | bucket.cos-website.region.myqcloud.com | 浏览器直接访问静态网站 |
EdgeOne 如果用「对象存储源站」模式回源到默认域名,走的不是静态网站逻辑。你用浏览器访问 https://raysre.com/index.html,EdgeOne 回源时相当于向 COS 的 API 端点发起 GET 请求,这时候如果桶是私有读,会直接返回 AccessDenied——这就是后面排查过程中掉进去的坑。
Step 1:Hugo 环境搭建
Hugo 用的是 v0.164.0 extended 版 + PaperMod 主题。注意 PaperMod 是 submodule,克隆仓库时带 --recurse-submodules:
# 新建站点
hugo new site raysre.com --format yaml
cd raysre.com
# PaperMod 主题
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
hugo.toml 核心配置(60 行,去掉了默认配置的冗余项):
baseURL = 'https://raysre.com/' # 生产环境域名,决定所有绝对链接
locale = 'zh-cn'
title = 'DevOps工作笔记'
theme = 'PaperMod'
[params]
description = '工作与生活随记'
author = '阿端'
ShowToc = true # 显示文章目录
ShowBreadCrumbs = true # 面包屑导航
ShowReadingTime = true
ShowWordCount = true
[markup.tableOfContents]
startLevel = 2 # 目录只显示 h2/h3
endLevel = 3
构建命令:
hugo --minify # 构建并压缩,输出到 public/
hugo server -D # 本地预览,--watch 默认开启
备案展示——PaperMod 默认的 footer.html 没有备案号。在 layouts/partials/footer.html 里加了:
<div style="text-align:center;font-size:12px;color:#888;margin-top:10px">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">
沪ICP备2026021650号-1
</a>
<span style="margin:0 8px"></span>
<a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31011502406637"
target="_blank" rel="noopener">
<img src="/images/beian.png" style="display:inline;height:14px;vertical-align:middle" alt="">
沪公网安备 31011502406637 号
</a>
</div>
公安备案图标 beian.png 放 static/images/ 下,构建时会原样复制到 public/images/。
Step 2:COS 存储桶配置
腾讯云 COS 控制台操作清单:
| 配置项 | 设置 | 原因 |
|---|---|---|
| 地域 | 上海(ap-shanghai) | 和 GitHub Actions runner 延迟低,和 EdgeOne 节点同地域回源走内网 |
| 访问权限 | 公有读私有写 | 最早设置;后面排查 AccessDenied 时临时改过,最终改回私有读写+EdgeOne 回源鉴权 |
| 静态网站托管 | 开启,默认首页 index.html | Hugo 生成的就是 index.html |
| 自定义源站域名 | 不配置 | COS 的自定义域名和 EdgeOne 是两条路,用了 EdgeOne 就没必要在 COS 里再配 |
| 防盗链 | Referer 白名单:空 Referer + raysre.com | 防止其他网站直接盗链 COS 默认域名 |
关于访问权限的最终方案:
桶设为私有读写,在 EdgeOne 里开启私有访问授权。EdgeOne 回源 COS 时会带上内部鉴权签名,COS 认这个签名放行。用户在浏览器里直接打 COS 默认域名会返回 403。
这样做的好处:COS 源站不暴露公网入口,只有 EdgeOne 能回源取文件。
Step 3:GitHub Actions 自动部署
deploy.yml 总长 145 行,分 5 个步骤:
| 步骤 | 做什么 | 耗时参考 |
|---|---|---|
| Checkout | 拉仓库 + PaperMod submodule | 3-5s |
| Setup Hugo | 下载 Hugo 0.164.0 extended 二进制 | 2-3s |
| Build | hugo --minify | <1s |
| Deploy to COS | Python SDK 遍历 public/ 上传,带 Content-Type | 10-20s |
| Purge CDN Cache | 调 CDN API 刷新首页缓存 | 1-2s |
Setup Hugo 步骤:从 GitHub Releases 直接下载,不用 peaceiris/actions-hugo 这种第三方 Action——少依赖就是少隐患。指定版本 0.164.0 防止某天自动升版导致构建行为变化。
Deploy 步骤逐段拆解:
# 第一段:MIME 类型映射表
MIME = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.svg': 'image/svg+xml',
# ... 16 种常见格式
}
不设 Content-Type 的话,COS 默认给所有文件 application/octet-stream。浏览器拿到不认识就当成下载,CSS/JS 不生效,页面直接崩。所以每个文件上传时必须带正确的 MIME。
# 第二段:遍历上传
local_dir = 'public'
uploaded = 0
for root, dirs, files in os.walk(local_dir):
for f in files:
local_path = os.path.join(root, f)
cos_key = local_path[len(local_dir):].lstrip('/')
ext = os.path.splitext(f)[1].lower()
content_type = MIME.get(ext, 'application/octet-stream')
with open(local_path, 'rb') as fp:
client.put_object(
Bucket='raysre-1377355589',
Key=cos_key,
Body=fp,
ContentType=content_type
)
uploaded += 1
os.walk() 递归遍历 public/ 下所有文件,cos_key 去掉了 public/ 前缀。比如 public/posts/hello/index.html 上传后 COS Key 就是 posts/hello/index.html,直接和 URL 路径对应。
为什么用 Python SDK 而不是 coscmd:
| 方案 | 问题 |
|---|---|
coscmd CLI | GitHub Actions 默认不带,需要 apt-get 装;配置文件要文本替换密钥,麻烦 |
cos-python-sdk-v5 | pip install 一行搞定,密钥走环境变量,代码即配置 |
Purge CDN 步骤:调腾讯云 CDN API PurgeUrlsCache,签 V3 签名算法(TC3-HMAC-SHA256),刷新首页和 www 两个 URL。Hugo 新构建的文件名含 hash(如 main.abc123.css),自然绕过旧缓存——所以不需要全站刷新,只刷首页让用户拿到新文件引用就够了。
Secrets 配置:TENCENT_SECRET_ID 和 TENCENT_SECRET_KEY 必须放 GitHub Repository secrets(不是 Environment secrets)——走 Environment 的话 deploy.yml 里 ${{ secrets.XXX }} 拿不到值。这个区分是排查早期构建失败时发现的。
Step 4:EdgeOne 配置 & AccessDenied 排查
第一版配置(翻车)
| 配置项 | 值 |
|---|---|
| 加速域名 | raysre.com |
| 源站类型 | 对象存储源站 |
| 源站地址 | 默认域名(raysre-1377355589.cos.ap-shanghai.myqcloud.com) |
| 私有访问授权 | 开启 |
| 回源 HOST 头 | 使用源站域名 |
git push 后 Actions Run #9 成功上传 30+ 个文件但浏览器访问 https://raysre.com/ 返回 AccessDenied XML:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied.</Message>
<Resource>/</Resource>
<RequestId>...</RequestId>
</Error>
而直接访问 COS 静态网站端点 http://raysre-1377355589.cos-website.ap-shanghai.myqcloud.com/ 能正常显示。
定位过程
做了几个测试:
| 测试 | 结果 | 结论 |
|---|---|---|
curl -sv https://raysre.com/ | 403 AccessDenied | EdgeOne 代理层面就返回了 403 |
curl -sI http://cos-website域名/ | 200 + HTML | COS 静态网站本身正常 |
curl -s https://默认域名/index.html | 200 + 完整 HTML | 默认域名公网可访问(当时桶是公有读) |
根因:EdgeOne「私有访问授权」开启时,会构造一个鉴权签名放在回源请求中。但这个签名是针对 COS API 端点的——不是针对静态网站端点的。而用「对象存储源站」模式回源的地址恰恰好是 API 端点地址(默认域名)。COS 收到带签名的请求后发现签名和请求不匹配:API 端点期望的签名格式和静态网站的授权逻辑不一样。
换句话说:公有读桶 + 私有访问授权 = 冲突。公有读桶不需要鉴权就能访问,开了私有访问授权反而让 EdgeOne 构造了一个多此一举的签名,COS 校验失败直接拒。
修复版本(正确配置)
| 配置项 | 旧值 | 新值 |
|---|---|---|
| COS 桶权限 | 公有读 | 私有读写 |
| EdgeOne 私有访问授权 | 开启 | 保持开启 |
| EdgeOne 回源 HOST 头 | 源站域名 | 使用源站域名(不变) |
修复逻辑:先让 EdgeOne 去 COS 完成授权绑定,等 COS 桶策略里出现一条允许 EdgeOne 服务访问的策略后,再把桶改为私有读写。这样 EdgeOne 回源带签名,COS 认这个签名放行。外面直接打 COS 默认域名因为没有签名,直接 403。
附加规则
URL 重写(解决 / 不自动返回 index.html):
| 匹配条件 | 操作 | 目标 |
|---|---|---|
URL Path 等于 / | URL 重写 | /index.html |
这一步是关键。EdgeOne 回源到 COS API 端点时 GET / 不等于 GET /index.html,不像 Nginx 那样有 try_files。不加这个规则首页永远 403。
www 301 重定向:
| 项 | 配置 |
|---|---|
| DNS | 添加 CNAME 记录 www.raysre.com → raysre.com.eo.dnse2.com |
| EdgeOne 域名 | 添加加速域名 www.raysre.com,源站和 raysre.com 相同 |
| EdgeOne 规则引擎 | 匹配 HOST = www.raysre.com → 访问 URL 重定向 301 → https://raysre.com${uri} |
${uri} 是 EdgeOne 内置变量,保留原始路径。比如 www.raysre.com/posts/xxx → raysre.com/posts/xxx,不会全部丟到首页。
Step 5:DNS 切换
DNSPod 控制台里把 raysre.com 和 www.raysre.com 的 A 记录删掉(如果之前指向了服务器 IP),改成 CNAME:
| 记录类型 | 主机记录 | 记录值 | TTL |
|---|---|---|---|
| CNAME | @ | raysre.com.eo.dnse2.com | 600 |
| CNAME | www | raysre.com.eo.dnse2.com | 600 |
TTL 设 600 秒(10 分钟),初次调试时改配置能更快生效。稳定运行后可以调回 3600。
成本估算
以我这个站的实际用量(文章 3 篇、访问量可以忽略):
| 计费项 | 月费(估算) |
|---|---|
| COS 存储(标准存储,<1GB) | ¥0.10 |
| COS 请求费(几千次/月) | ¥0.01 |
| CDN 流量(<10GB/月) | 免费套餐覆盖 |
| CDN 请求费 | 免费套餐覆盖 |
| DNS 解析 | 免费 |
| 总计 | ≈ ¥0.11/月 |
对个人博客来说几乎免费。唯一需要留意的:CDN 免费套餐每月 10GB 国内流量,如果哪天文章被大量访问超额,按量计费部分 ¥0.21/GB——设置好「用量封顶」防止意外。
进阶参考:没用到的腾讯云功能
下面这些是 EdgeOne 和 COS 提供的功能,个人博客场景用不到,但如果你要从静态博客拓展到更复杂的场景,可以参考。
EdgeOne 四层代理(L4 Proxy)
用 TCP/UDP 协议加速,适用场景是游戏加速、IoT 设备通信、实时音视频——不走 HTTP。静态博客只用七层(HTTP/HTTPS)。
EdgeOne 边缘函数(Edge Functions)
在 EdgeOne 边缘节点上跑 JavaScript 代码,类似 Cloudflare Workers。可以做的事:自定义鉴权、A/B 分流、响应头注入、内部重定向。比规则引擎灵活,但需要写代码。如果你要实现「特定 IP 段跳转到维护页」这种逻辑,用边缘函数比规则引擎方便。
回源限频
限制 EdgeOne 向源站发起的请求频率。作用是高并发时保护源站不被击穿。文档里典型的参数是「同一 URL 每秒最多回源 1 次」。个人博客流量小,可以不开。
EdgeOne 用量封顶
设置流量 / 带宽 / QPS 上限,达到后自动限速或返回提示页。建议设一下——免费套餐有额度限制,防止某个月被刷流量产生天价账单。
COS 回源设置
当请求的对象在 COS 桶里找不到时,自动去一个预设地址拉取内容。用于数据热迁移场景(如从旧服务器逐步搬到 COS)。不用于静态博客——所有文件由 GitHub Actions 上传,不会出现 404。
COS 生命周期管理
自动把对象从标准存储切换到低频 / 归档 / 深度归档,或到期后自动删除。如果你的桶存了大量日志需要定期清理,这是一个有用的功能。
COS 智能分层存储
自动在「高频」和「低频」两个访问层之间迁移对象,不需要手动配生命周期。访问模式不可预测时比生命周期省心。
数据万象 CI(图片/音视频处理)
和 COS 绑定的数据处理服务,提供实时图片裁剪、水印、格式转换(比如 ?imageMogr2/thumbnail/200x),以及视频转码、截帧等能力。如果你以后需要在文章里自动生成缩略图、加水印,数据万象是实现方案之一。
COS 标签管理
给对象打标签(如 env=prod, category=blog),方便分类、成本核算和批量操作。
FAQ
Q:为什么 hugo.toml 不用 yaml 格式?
hugo new site --format yaml 生成的就是 TOML(这个参数有点误导,实际上 Hugo 默认生成 TOML)。TOML 比 YAML 更不容易出现缩进错误,而且 Hugo 文档里的示例代码基本是 TOML,复制粘贴改起来方便。
Q:deploy.yml 在 GitHub Actions 里报 ModuleNotFoundError: No module named 'qcloud_cos' 怎么处理?
- run: pip install cos-python-sdk-v5
加在 Deploy 步骤最前面。注意每次 step 的 shell 环境可能不同,pip install 放在同一个 run: 块里,不要跨 step。
Q:如何验证 COS 静态网站端点正常工作?
curl -sI http://raysre-1377355589.cos-website.ap-shanghai.myqcloud.com/index.html
预期返回 200 OK + Content-Type: text/html。如果不是 200,去 COS 控制台检查静态网站托管是否开启、默认首页是否设了 index.html。
Q:HTTPS 证书怎么搞?
EdgeOne 免费提供 DV SSL 证书,接入域名后自动申请,不需要额外操作。如果 www 也需要扫码 HTTPS,申请时勾选多域名即可。
Q:PaperMod 怎么加备案号?
在 layouts/partials/footer.html 里覆盖默认模板。PaperMod 的默认 footer 位于 themes/PaperMod/layouts/partials/footer.html,把内容复制过来,底部追加备案信息。Hugo 发现项目 layouts/ 下同名文件会优先用你的,不会动主题原文件。
Q:文章更新后旧页面能看到吗?
能看到。Hugo 构建时 CSS/JS 文件名含内容 hash,新版本文件名不同。旧文件还在 COS 上,EdgeOne 缓存里旧的 HTML 引用旧 JS——所以 deploy 后加了一步 CDN 刷新。
Q:COS 默认域名能被直接访问吗?
取决于桶权限。如果你设了私有读写+EdgeOne 私有访问授权,直接打 COS 默认域名会 403。顺手加 Referer 白名单可以多做一层防护。
Q:为啥不用 Cloudflare Pages / Vercel?
Cloudflare Pages 免费 500 次/月构建,Vercel 免费 6000 分钟/月。两家托管在国内的访问稳定性时好时坏——不是产品不行,是物理距离在那儿。腾讯云 COS + EdgeOne 面向国内用户,延迟 <50ms,在线率有 SLA。如果你的受众主要在海外,那 Cloudflare Pages 反倒更合适。
Q:文章多了以后构建和上传会变慢吗?
Hugo 官方的基准测试数据:5000 篇文章的站点,hugo build 在 1 秒内完成。瓶颈在上传:os.walk() 顺序串行,文件数到几千时可以考虑用 ThreadPoolExecutor 并行化上传。目前几十个文件串行够用。
写在最后
整个过程踩的最大坑是 EdgeOne 的「私有访问授权」和「公有读桶」的组合——文档里散落在不同页面,没有明确的「桶公有读时不要开私有访问授权」的警告。这篇记录把配置关系理成表格,方便以后查。
不用管服务器、不用打安全补丁、不用半夜被告警吵醒——COS 存文件,EdgeOne 扛流量,GitHub Actions 管发布。代价是没法跑动态内容(评论、登录、后台)。如果你需要这些,WordPress + 云服务器或者 Hugo + Serverless 评论更适合。
如果你也在搭类似的个人博客,上面的 deploy.yml 可以直接拿走用,改个 bucket 名字就行。