最近我在继续完善 A Tour of Go 多语言项目的 Production 自动化。
以前每次中文站发布新版本后,都需要进入腾讯云 EdgeOne 控制台,手工执行一次缓存刷新:
内容类型:Hostname
清除方法:直接删除单独一个站点时,这个操作并不麻烦。
但随着项目逐渐扩展到更多语言,我已经把 Cloudflare、Production deploy、machine acceptance、browser acceptance 等流程陆续自动化。此时中文站还需要人工打开 EdgeOne 控制台清缓存,就成了整个发布流程里比较明显的一处人工断点。
于是这次决定把 EdgeOne 缓存刷新也纳入 Production 自动化。
真正开始做以后,我发现事情并不只是“申请一个 API Key”这么简单,其中还涉及:
- 是否应该使用主账号 API Key;
- 如何建立专用 CAM 子用户;
- 如何限制到最少的 EdgeOne API 权限;
- 为什么第一次创建出来的密钥,我根本没有看到 SecretKey;
- SecretKey 后续为什么又无法查询;
- 如何安全保存 SecretId / SecretKey;
- 如何在真正执行缓存刷新前做只读 API 验证;
- 为什么 mock tests 全部通过,真实 API 仍然可能暴露字段理解错误。
这篇文章把整个过程记录下来。
一、为什么不直接使用主账号 API Key
最开始我确实考虑过一个最简单的方案:
直接给腾讯云主账号创建一组 API Key。
这样操作步骤最少,也不用额外建立 CAM 用户和权限策略。
但进入腾讯云 API 密钥管理页面后,控制台本身就给出了非常明显的安全提示:主账号密钥拥有很高的云资源操作权限,不建议直接用于程序化访问。

对于我的需求来说,Production 程序真正需要的只是:
- 查询 EdgeOne 站点;
- 查询具体加速域名;
- 创建一次缓存清除任务;
- 查询缓存清除任务状态。
显然没有必要让自动化程序拿到整个腾讯云账号的权限。
因此最后决定:
为 go-tour-i18n Production 单独创建一个 CAM 子用户。
二、创建专用 CAM 子用户
进入腾讯云 CAM 后,新建一个子用户。
我使用的用户名是:
go-tour-i18n-production用途也很单一:
go-tour-i18n EdgeOne Production 自动化创建方式选择:
可访问资源并接收消息
这个用户只需要程序调用 API,因此我只启用了:
编程访问不需要给它日常腾讯云控制台登录能力。
这样做的好处是权限边界非常清楚:
腾讯云主账号
↓
CAM 子用户
go-tour-i18n-production
↓
只供 go-tour-i18n Production 自动化使用以后即使需要轮换 API Key,也只需要处理这个专用用户,不影响主账号。
三、只给 EdgeOne 自动化真正需要的 4 个权限
接下来是最重要的一步:自定义 CAM 策略。
服务选择:
边缘安全加速平台 EO(teo)最终只授权了 4 个 API Action:
DescribeZones
DescribeAccelerationDomains
CreatePurgeTask
DescribePurgeTasks
它们各自承担的职责很明确。
1. DescribeZones
根据正式站点名称查询 EdgeOne Zone。
自动化程序不会把 ZoneId 写死,而是先通过:
shuijingwanwq.com动态解析正式 Zone。
2. DescribeAccelerationDomains
拿到 Zone 后,再确认真正需要操作的 Production hostname 确实属于这个 Zone,并且当前处于正常在线状态。
例如中文站:
go-dev.shuijingwanwq.com3. CreatePurgeTask
真正创建缓存清除任务。
我的人工操作原来是:
Hostname
+
直接删除因此 API 自动化也必须严格保持相同语义:
Type = purge_host
Method = delete
Targets = [正式 Production hostname]4. DescribePurgeTasks
CreatePurgeTask 并不意味着清除工作已经最终完成。
程序还需要根据返回的 JobId 查询任务状态,直到确认:
success才能把 CDN purge 判定为通过。
四、为什么资源范围仍然选择了“全部资源”
CAM 策略还可以继续做到非常细,例如限制到某个具体 EdgeOne 资源。
不过这是我自己的个人项目,目前只有少量站点,而且真正允许的 API Action 已经压缩到了 4 个,所以这里我选择了:
全部资源而没有继续增加更复杂的资源级 QCS 限制。
这不代表程序可以任意刷新任何域名。
go-tour-i18n 自己还有第二层限制:
hostname 只能从正式的
production/identity.json中读取。
也就是说,脚本不提供这种接口:
--hostname arbitrary-example.com更不能执行:
purge_all
*
wildcard
整个 Zone因此当前安全边界实际上是:
CAM:
只允许 4 个 EdgeOne API
+
go-tour-i18n:
只允许 production/identity.json 中的正式 hostname对于目前这个个人项目,我认为这个复杂度比较合适。
五、把策略关联到 Production 子用户
创建好策略后,将它关联到:
go-tour-i18n-production
策略名称我使用的是:
GoTourI18nEdgeOneMaintenance最终策略创建并关联成功。

到这里,CAM 权限部分就完成了。
权限结构可以简化成:
go-tour-i18n-production
└── GoTourI18nEdgeOneMaintenance
├── DescribeZones
├── DescribeAccelerationDomains
├── CreatePurgeTask
└── DescribePurgeTasks六、第一个坑:第一次创建出来的 Key,我根本没看到 SecretKey
接下来进入 CAM 子用户的:
API 密钥这里是我这次操作里最奇怪的一段。
创建 go-tour-i18n-production 子用户时,我已经勾选了:
编程访问子用户创建完成以后,进入“API 密钥”页面,可以看到系统已经存在一条启用中的:
SecretId也就是说,密钥实际上已经被创建出来了。
但问题在于:
我印象中,在前面的子用户创建流程里,根本没有出现过让我查看或保存 SecretKey 的页面。
这和“我看到了 SecretKey,但是忘记保存”不是一回事。
至少在我这次实际操作中,流程给我的体验是:
创建 CAM 子用户
→ 启用编程访问
→ 创建完成
→ API 密钥页面已经出现 SecretId但是中间我没有发现任何一步展示完整 SecretKey。
之后当我尝试查看 SecretKey 时,腾讯云弹出了提示:
SecretKey 只在密钥创建时提供,后续不能再次查询。

这就形成了一个有些尴尬的状态:
SecretId 已经存在
SecretKey 后续又不能查询
而创建过程中我又没有看到 SecretKey从使用体验上看,我更倾向于认为这是当时控制台创建流程里的一个 UI 或流程问题。
当然,我无法确认是腾讯云前端 Bug、页面跳转遗漏,还是某个非常不明显的展示环节被跳过了。
但可以确定的是:
第一把 Key 创建完成以后,我手里没有它的 SecretKey,而且后续也无法再查询。
因此这把 Key 实际上已经无法用于我的 Production 自动化。
七、重新新建一把 API Key,这次才真正看到了 SecretKey
既然旧 Key 的 SecretKey 已经无法获取,我没有继续折腾,而是直接点击:
新建密钥这一次行为就非常明确了。
腾讯云弹出了“创建 SecretKey”窗口,并且同时显示:
SecretId
SecretKey
而且页面上还再次提示:
新建的密钥只在创建时提供 SecretKey,后续不可再次查询,请保存好 SecretKey。
这一次我马上保存了完整的 SecretId / SecretKey。
所以结合我的实际经历,更准确的经验不是:
“第一次是因为我忘记保存 SecretKey。”
而应该是:
如果创建 API Key 后页面明确展示了 SecretKey,一定要当场保存;如果像我第一次那样,创建完成后只看到 SecretId,却根本没有经历 SecretKey 展示步骤,也不要指望后面还能重新查看,直接重新创建一把更省事。
这里尤其不要把:
SecretId
SecretKey发送到聊天工具、Git 仓库或者普通文档。
真正敏感的是 SecretKey,它应该只进入受控的 Production secret 文件。
八、服务器上如何保存 EdgeOne API 凭据
Production 服务器上,我最终使用:
/etc/go-tour/edgeone.env内容结构只有两行:
TENCENTCLOUD_SECRET_ID=<SecretId>
TENCENTCLOUD_SECRET_KEY=<SecretKey>文件权限设置为:
root:root
0600实际检查结果:
root:root 600 /etc/go-tour/edgeone.env同时还做了一个不会输出真实值的结构检查,最终得到:
EDGEONE SECRET CONTRACT: PASS这里很重要的一点是:
不要 cat /etc/go-tour/edgeone.env至少没有必要为了“确认配置”把真实 SecretKey 打到终端输出里。
正式程序只需要确认:
- 文件是普通文件;
- owner 是 root;
- group 是 root;
- mode 是 0600;
- 只存在规定的两个变量;
- 两个值均非空。
九、第二个坑:不要把交互式 read 和后续命令一次性粘贴
在配置服务器 secret 时,我还踩到了一个很有意思的小坑。
当时为了避免 SecretKey 进入 shell history,我使用了:
read -r -s -p 'TENCENTCLOUD_SECRET_KEY: ' ...这个思路本身没问题。
问题是我把包含 read 的多行命令整体一次性粘贴到了终端里。
结果 shell 后面的:
printf
umask
install
rm
stat等命令,被 read 当成了标准输入内容。
最后生成的:
/etc/go-tour/edgeone.env居然有 19 行。
自动化 preflight 很快就失败:
[production-cdn] FAILED: secret file contains an invalid assignment继续检查后发现:
lines = 19
contains_cr = True文件里甚至出现了:
umask 077
tmp=...
install ...
rm ...这些本应该执行的 shell 命令。
最后重新分开操作:
先执行 read
→ 单独输入 SecretId
再执行 read -s
→ 单独输入 SecretKey
最后再执行非交互写文件命令问题才解决。
这个坑其实和腾讯云无关,但以后只要使用交互式:
read
password prompt
secret prompt都值得注意:
不要把需要等待人工输入的命令和后续命令一次性整体粘贴。
十、先做只读 API preflight,而不是直接清缓存
凭据配置完成以后,我没有马上执行:
CreatePurgeTask而是先为 Production 增加了一个只读验证:
scripts/verify-edgeone-authority.sh zh-CN它只允许调用:
DescribeZones
DescribeAccelerationDomains绝不会调用:
CreatePurgeTask这样即使 API client 写错,也不会影响真实 CDN 缓存。
事实证明,这一步非常有必要。
十一、第三个坑:mock tests 全绿,真实 API 仍然失败
第一次运行真实 preflight 时,得到:
[production-cdn] FAILED:
EdgeOne zone identity/name/status is not exact and activeSecret 文件已经正确,API 权限也没有报 AccessDenied。
继续检查代码才发现,自己写的 EdgeOne client 错误地假设 Zone 返回结构是:
Name
Id
Status代码类似:
zone.get("Name")
zone.get("Id")
zone.get("Status") == "active"但腾讯云真实 DescribeZones 返回的是:
ZoneName
ZoneId
Type
Status
CnameStatus
ActiveStatus
LockStatus
Paused这已经不是一个简单的字段拼写问题。
更麻烦的是:
Status=active也不能作为所有站点类型统一的可用性判断。
我的 EdgeOne 站点使用的是:
Type=partial也就是 CNAME 接入。
在这种情况下:
Status=pending只是表示 NS 没有切换,并不代表 CNAME 接入站点不可用。
因此最终把 Zone 判定调整成了类型感知。
公共要求:
ZoneName 精确匹配
ZoneId 非空
ActiveStatus=active
Paused=false
LockStatus=enable如果:
Type=partial则要求:
CnameStatus=finished
Status 可以是 pending / active如果:
Type=full则要求:
Status=active未知 Type:
fail closed十二、为什么这个错误直到真实 API 才暴露
更值得反思的是:
在运行真实 API 之前,所有自动测试其实都是 PASS 的。
原因很简单。
当时 mock fixture 也是按照错误理解构造的:
{
"Name": "shuijingwanwq.com",
"Id": "zone-test",
"Status": "active"
}于是形成了一个很典型的问题:
错误 specification
↓
错误 implementation
↓
错误 mock
↓
implementation 与 mock 完全一致
↓
tests 全绿测试只能证明:
代码符合自己定义的假设。
却不能证明:
这个假设符合真实第三方 API。
这次让我再次确认了一条很重要的原则:
第三方 API 的 request field、response field、enum、status semantics 和 pagination contract,应该直接依据真实官方 schema 建立测试 fixture,不能靠接口命名习惯推断。
特别是 Production mutation 相关 API,更应该在真正 mutation 前安排一次:
read-only real API preflight十三、修正以后,真实 EdgeOne authority 验证通过
修正 Zone contract 后,再次运行:
scripts/verify-edgeone-authority.sh zh-CN最终得到:
EDGEONE AUTHORITY PREFLIGHT: PASS
zone_name: shuijingwanwq.com
hostname: go-dev.shuijingwanwq.com这意味着下面整条真实链路已经验证:
root-only Secret
↓
TC3-HMAC-SHA256
↓
DescribeZones
↓
精确解析正式 Zone
↓
DescribeAccelerationDomains
↓
精确验证 Production hostname
↓
PASS整个验证过程中没有执行任何缓存清除。
这正是我想要的效果:
先证明权限、身份解析和 API client 都正确,再允许真正的 Production mutation。
十四、最终自动缓存刷新与控制台人工操作保持一致
原来我在 EdgeOne 控制台中的人工操作是:
内容类型:Hostname
清除方法:直接删除
所以自动化最终固定生成的 API 请求语义也是:
{
"Type": "purge_host",
"Method": "delete",
"Targets": [
"<正式 Production hostname>"
]
}这里没有开放:
purge_all
wildcard
任意 hostname
多个 Targets
zone-wide purge目标 hostname 只能来自:
production/identity.json例如:
go-dev.shuijingwanwq.com十五、CreatePurgeTask 以后还不能直接算成功
缓存刷新自动化里还有一个容易忽略的问题:
CreatePurgeTask调用成功,只代表任务已经被 EdgeOne 接收。
正式 Production workflow 还会继续根据:
JobId调用:
DescribePurgeTasks状态处理规则类似:
processing
→ 继续等待
success
→ PASS
failed
timeout
canceled
unknown
→ FAIL也就是说:
API 请求返回成功,并不等于 Production cache purge 已完成。
十六、网络结果不确定时,不盲目重复清缓存
还有一种更麻烦的情况:
CreatePurgeTask请求发出去以后,本地发生:
- timeout;
- connection interruption;
- response body 不完整。
这时无法判断:
EdgeOne 到底有没有收到并创建任务?
最危险的做法是:
超时
→ 马上重新 CreatePurgeTask因为这样可能重复创建 mutation。
现在的做法是先通过:
DescribePurgeTasks查询最近任务,并根据:
ZoneId
Type=purge_host
Target=正式 hostname
CreateTime尝试 reconciliation。
只有能够明确证明第一次没有创建任务时,才允许有限重试。
如果始终无法确认:
fail closed而不是假装成功。
十七、新 API Key 验证成功后,再删除第一把无法使用的 Key
新创建的 API Key 完成真实只读 preflight 后,我才回到腾讯云 CAM。
最终删除了第一把已经无法正常使用的 Key。
这里再次强调:
第一把不是我看到 SecretKey 后忘记保存,而是创建子用户时我根本没有发现 SecretKey 的展示页面。
因此那把 Key 最终只剩 SecretId 可见,对我的自动化已经没有实际用途。
删除以后,go-tour-i18n-production 最终只保留一把正式使用中的 API Key。

这样凭据状态就比较干净:
1 个 Production CAM 子用户
1 个最小权限策略
1 把有效 API Key
1 个 root-only secret 文件十八、最终结构
整理完成以后,这套 EdgeOne Production authority 可以概括成:
腾讯云主账号
↓
CAM 子用户
go-tour-i18n-production
↓
自定义策略
GoTourI18nEdgeOneMaintenance
↓
4 个 API Action
↓
API Key
↓
/etc/go-tour/edgeone.env
root:root 0600
↓
read-only Production preflight
↓
exact hostname purge程序本身又继续限制:
Zone
来自正式 production authority
hostname
来自 production/identity.json
Type
固定 purge_host
Method
固定 delete
Targets
固定单一正式 hostname十九、这次最大的收获不是“拿到了一个 API Key”
最开始我只是想解决一个非常具体的问题:
不想每次中文站上线后,再手工进入腾讯云 EdgeOne 控制台点一次缓存清除。
但真正做完以后,我觉得更有价值的是把整个权限和 mutation 边界明确了。
几个实际经验尤其值得保留。
不要为了省几步直接使用主账号 API Key
专用 CAM 子用户和最小权限策略多几分钟配置,但长期维护明显更安全。
如果页面展示了 SecretKey,要当场保存
SecretKey 后续不能再次查询。
但这次我的第一把 Key 更特殊:
子用户创建完成以后已经存在 SecretId,但我根本没有发现 SecretKey 展示页面。
所以如果遇到类似情况,不要假设以后还能查询 SecretKey。
最省事的办法通常是:
重新新建一把 API Key
→ 确认页面明确展示 SecretId + SecretKey
→ 当场保存
→ 验证新 Key
→ 删除旧 Key至于第一次为什么没有出现 SecretKey,我现在更倾向于认为是腾讯云当时控制台流程或 UI 上的问题,但没有足够证据确认具体原因。
Secret 不应该进入 Git、日志和普通终端输出
Production server 上只保留 root-only 0600 文件。
第三方 API mock 必须来自真实 schema
否则很容易出现:
实现错了
测试数据也错了
结果测试全部通过Production mutation 之前最好先有真实 read-only preflight
这次如果没有:
DescribeZones
+
DescribeAccelerationDomains的真实验证,Zone schema 的错误可能要等到真正缓存刷新时才暴露。
网络结果未知和明确失败不是一回事
明确失败可以安全重试。
结果未知时,应先 reconciliation,而不是直接重复 mutation。
结语
完成 EdgeOne API authority 后,中文站的 CDN 缓存刷新终于也可以纳入 go-tour-i18n 的 Production 自动化。
这一步本身只是整个流程优化中的一小部分。
真正让我开始做这件事的,是一次 A Tour of Go 官方上游同步:当同一个变化需要发布到 10 个语言站后,我发现原来单个站点还能接受的人工操作,放大到多个 locale 后会变成大量重复工作。
后来我继续把:
publish
CDN purge
shared-assets purge
Production maintenance
machine acceptance
browser acceptance逐步做成了批量化流程。
现在多语言日常 Production release 已经可以通过一个顶层 batch command 串行完成。
这部分过程,我准备再单独整理成下一篇。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ Brazilian Portuguese — Português (Brasil)
✅ Dutch — Nederlands
✅ French — Français
✅ German — Deutsch
✅ Italian — Italiano
✅ Japanese — 日本語
✅ Korean — 한국어
✅ Simplified Chinese — 简体中文
✅ Spanish — Español
✅ Turkish — Türkçe
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
项目正在持续扩展更多语言版本,并长期维护翻译质量、生产发布与后续更新。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。
