没有不值得去解决的问题,也没有不值得去学习的技术!

腾讯云 EdgeOne API 自动化:从 CAM 子用户到最小权限 API Key 的完整配置

图8:最终只保留一把启用的新 API Key

作者:

A Tour of Go 多语言翻译项目

图3:访问过去的 Go Tour 简体中文站 tour.go-zh.org 时,当前已经无法正常建立连接。

(1) 从「A Tour of Go 中文版」这个搜索词开始:我决定做一个持续维护的 Go Tour 中文版

本地运行的 A Tour of Go「Methods continued」课程页面,左侧为课程说明,右侧为 Go 代码编辑器及运行结果。

(2) A Tour of Go 中文版项目设计冻结:从 101 页到 103 页,从 Gin 转向 Cobra CLI

A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

(3) A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

go-tour-i18n 项目完成首个简体中文课程页面 welcome/1 的整页翻译、结构校验和本地预览。

(4) A Tour of Go 多语言翻译项目实录:完成首个 zh-CN 页面翻译闭环

图 1:DeepSeek 官方更新日志显示,本次只更新 DeepSeek-V4-Flash,DeepSeek-V4-Pro API 与 APP、Web 模型均未更新

(5) A Tour of Go 中文翻译项目进展:完成前 8 页、修复 present 语法,并暂缓 DeepSeek 对比

图 1:generics/1 简体中文页面本地预览

(6) 一个页面重试五次:使用 GLM-5.2 翻译 A Tour of Go 时遇到的 Token 与 Present 结构问题

图 1:methods/16 简体中文页面浏览器预览

(7) A Tour of Go 中文翻译实录:如何只翻译教学代码注释,而不破坏 Go 代码

图 2:A Tour of Go methods/20「练习:错误」中文候选页面最终渲染效果

(8) A Tour of Go 多语言翻译:当正确的中文语序被受保护标记顺序校验误判

图 3:methods/24 中文页面,静态代码、Bounds 行内代码和链接内的 image.Rectangle 均正确渲染

(9) A Tour of Go 中文翻译完成 7 个代表页校准:最后 3 页又发现了哪些真实问题

图 2:首批 10 个普通页面经过试跑与流程校准后全部进入 ready

(10) A Tour of Go 多语言翻译项目:首批 10 个普通页面试跑,从 2 个 blocked 到全部 ready

图 2:flowcontrol/6 最终中文页面,return 与 v 为适应自然中文语序发生整体换位

(11) A Tour of Go 中文翻译第二批实跑:从 Inline Code 顺序误判到历史响应重新验证

图 1:flowcontrol/6 首次原始输入实验直接通过统一自动校验

(12) A Tour of Go 多语言翻译项目:从 swap 问题重新审视 Token 保护,原始输入与最小保护的一次真实实验

图 7:moretypes/1 最终中文页面预览。左侧课程正文、静态代码块、行内代码和教学注释均正常渲染,右侧官方 Go 示例继续保持原样。

(13) A Tour of Go 多语言翻译实战:第三批 10 页全部 Ready,继续校准 Protected Token 的结构角色

图 4:上游源码确认共有 103 个课程页面,zh-CN 最终达到 ready=103、pending=0、blocked=0

(14) A Tour of Go 多语言翻译项目:zh-CN 103 个课程页面全部 ready,课程正文翻译阶段完成

图 4:GitHub 与 ChatGPT 已成功连接

(15) ChatGPT 连接 GitHub 实测:让 AI 直接读取 go-tour-i18n 仓库参与译文审核

图 3:中文 Go 术语校准完成后,将确定的译法写入 zh-CN glossary

(16) 为什么 channel 最终选择“通道”:A Tour of Go 中文术语统一的一次实践

图 1:技术含义基本正确,但“返回一个返回……”已经明显影响阅读,因此仍被列入 C 类修订

(17) 103 页逐页审核后,只返修 16 页:A Tour of Go 中文正文发布前质量审计

图 2:中文课程页面以及已经完成本地化的编辑器控制区

(18) A Tour of Go 多语言翻译项目:公共 UI 本地化完成,从课程译文走向完整中文界面

图 2:完整 zh-CN 正式投影成功生成,103 个课程页面被重新组装为 7 个 .article

(19) 从 103/103 ready 到完整站点验收:A Tour of Go 中文版补齐正式投影与课程元数据本地化

图 3:从 production bundle 独立启动的 A Tour of Go 简体中文页面,课程正文、公共 UI 和官方 Go 示例均已进入最终发布形态。

(20) A Tour of Go 多语言翻译项目:103 页全部完成后,我终于生成了可独立部署的 zh-CN Production Bundle

图 1:A Tour of Go 简体中文版正式生产页面,远程运行已经成功输出“Hello, 世界”。

(21) A Tour of Go 多语言翻译项目:zh-CN 正式上线,从生产发布到 go.dev Playground 的完整部署记录

图 1:新生产域名 go-dev.shuijingwanwq.com 已正常访问,Go 示例远程运行成功

(22) A Tour of Go 多语言翻译项目:从 go-tour 到 go-dev,完成 EdgeOne、Nginx 与 HTTPS 生产域名迁移

图 1:A Tour of Go 多语言翻译项目正式生产首页

(23) A Tour of Go 多语言翻译项目正式上线:从项目首页、公共 Footer 到生产发布元数据的完整收尾

图 3:GA4 g/collect 请求返回 HTTP 204,并携带当前课程页面地址,确认 Google Analytics 已产生真实事件上报

(24) A Tour of Go 多语言翻译项目:接入 Google Analytics 与百度统计,从 systemd 注入到 EdgeOne 与真实上报验证

图 1:生产环境 robots.txt 与 sitemap.xml 已正确对外提供,Sitemap 共包含 104 个规范 URL。

(25) A Tour of Go 中文站上线后:补齐 robots.txt、Sitemap,并完成五大搜索引擎提交

图 4:发布生产并刷新 EdgeOne 缓存后,再次通过真实手机访问,顶栏项目名称已经完整保持在一行,最终移动端验收通过。

(26) A Tour of Go 多语言翻译项目:修复手机端首页顶栏标题换行,并完成真机验证

图 3:生产自动部署脚本第一次真实运行时,systemd 已经显示 active,但首次 localhost 探测仍然得到 HTTP 000;随后连续 3 次同时满足 active + HTTP 200 后,脚本才判定部署成功,并继续完成公网 HTTP 200 验收。这次真实运行验证了连续应用层健康检查的必要性。

(27) A Tour of Go 多语言翻译项目:从一次 203/EXEC 故障到生产自动部署脚本落地

图 1:阿里云生产环境访问 go.dev Playground 时出现 TLS 握手超时

(28) A Tour of Go 多语言翻译项目:为 Playground 搭建独立的 ZgoCloud 执行代理

图 3:清除 /tour/script.js 缓存后,Run 已直接请求 ZgoCloud /compile 并返回 HTTP 200

(29) A Tour of Go 多语言翻译项目:将生产 Run / Format 切换到 ZgoCloud 执行节点

【图 1:微信中运行 Go 示例时出现 Error communicating with remote server.】

(30) A Tour of Go 中文版手机端运行与格式化偶发失败排查:从间歇性报错到补强 Nginx 可观测性

【图 4:向 golang-dev 公开邮件列表发送 Playground 使用告知邮件并投递成功】

(31) A Tour of Go 多语言项目:为 Playground 代理增加唯一 User-Agent,并主动告知 Go 官方

【图 3:百度剩余 9 个核心 URL 提交成功,remain 归零】

(32) A Tour of Go 搜索引擎收录实测:从 Google 自然索引到百度主动提交与 Bing IndexNow

【图 1:当前正式中文 methods/24 页面,右侧示例代码运行成功】

(33) A Tour of Go 中文翻译完成后,我为什么重新评估 minimal-protect 翻译模式

【图 3:5 个页面 Request SHA256 完全一致,但 Response SHA256 全部不同】

(34) A Tour of Go 翻译实验:更多原始上下文为什么没有更好?从 157 个保护标记到 30 次重复盲评

图 3:3 个代表页 × 4 个评审模型,共 12 次匿名评审的完整排名与第一名票数。

(35) A Tour of Go 翻译质量再评估:minimal-protect 未达预期后,我开始比较 ChatGPT、Codex 与 GLM-5.2

图 2:GitHub 中的 chatgpt-zh-CN-008 重译批次。一个 Batch 同时保存 inputs、raw-responses、candidates、validation 和 retry 记录,GitHub 成为 ChatGPT 与本地翻译流水线之间的数据交接层。

(36) 没有 OpenAI API,我如何用 ChatGPT + GitHub 完成 A Tour of Go 103 页全量重译

图 1:103 个正式课程页面完成最终语义质量审核,结果为 A=103、B/C/D=0,同时不存在缺失、重复或额外页面。A/B/C/D 是本轮项目内部语义质量分级,并不是自动结构 validator 的结果。

(37) 103 页翻完还不能发布:A Tour of Go ChatGPT 译文的语义审核与 Canonical Promotion

图 3:7 个 article endpoint 的 production origin 与 EdgeOne 公网响应全部 exact match,同时新的 ChatGPT 译文标记存在,旧版译文标记已经消失。

(38) 103/103 与 7/7 Exact Match:A Tour of Go ChatGPT 新译文的最终线上验收

【图 2:upstream source preview】

(39) A Tour of Go 多语言翻译项目首次实现 upstream 同步机制:从一次性翻译到长期维护

图 2:A/B/C/D 质量评级规则,展示正式质量 rubric 与 Promotion gate。

(40) A Tour of Go 多语言翻译项目引入 Translation Quality Review:从自动验证到 AI 翻译质量控制

图 4:翻译后 Playground Example,展示中文注释与保持不变的 Go 代码结构。

(41) A Tour of Go 多语言翻译项目实现 Playground Example 本地化:从代码注释翻译到完整学习体验

图 5:公网页面验证广告脚本输出

(42) A Tour of Go 多语言翻译项目接入广告系统:从 AdSense 专用配置到通用 HTML 注入架构

图 3:百度统计单页应用设置

(43) go-dev 单页网站接入统计与 Google AdSense Auto Ads 验证记录

图 1:GitHub 仓库中的多语言翻译项目结构

(44) A Tour of Go 多语言翻译项目:选择日语作为第二语言并启动翻译验证

图 5:模型身份描述不一致

(45) A Tour of Go 多语言翻译项目中的 AI 协作异常排查:一次长期工程会话失效分析

【图 1:ChatGPT GitHub 写入流程调整说明截图】

(46) go-tour-i18n 翻译流程调整:从 ChatGPT GitHub 写入到本地 artifact 导入

图 4 展示了本次异常状态。

(47) go-tour-i18n 翻译自动化流程复盘:已验证的 ChatGPT 翻译能力在新任务中的稳定性问题

【图 6:全部评审结束以后才读取 secret key,完成最终揭盲】

(48) A Tour of Go 翻译质量再评估:Codex High 已接近 ChatGPT High,我决定调整默认翻译引擎

图 4:ja-JP 生产课程页实际运行 Go 示例

(49) A Tour of Go 日语版正式上线:第二门语言 ja-JP 完成生产发布

图 1:ja-JP 上线以后,先提交“新增语言标准化流程”

(50) A Tour of Go 多语言扩展复盘:为第三门语言建立标准化流程

图 1:课程正文结束以后,页面左侧仍然存在大块视觉空白,但 AdSense 并没有在这里插入展示广告。

(51) A Tour of Go 明明有大片空白,为什么 AdSense 就是不显示展示广告?

图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。

(52) 为了 AdSense,要不要放弃 SPA?A Tour of Go 页面架构的一次取舍

图 4:最终的手动课程广告真实展示在课程内容区域内,而不是 footer 之后。

(53) 保留 SPA 以后,广告应该放在哪里?从 AdSense 预览到方案 B

图 1:Google Search Console 中,多个不同的 A Tour of Go 课程 URL 被判断为“重复网页,用户未选定规范网页”。

(54) SPA 的 SEO 老问题:为什么最后还是给 103 个 A Tour of Go 课程页生成了 Prerender HTML

图 2:方案 B 生产环境中,源码已经正常,但课程主体高度异常缩短,footer 提前进入第一屏。

(55) 从源码空白到 AdSense 高度污染:A Tour of Go 方案 B 的生产收尾

图 1:A Tour of Go 德语版 de-DE 已正式运行在生产环境

(56) A Tour of Go de-DE 上线复盘:第三门语言用了约 16 小时,下一门如何压到 8 小时?

图 1:fr-FR 首次 production 验收完成后,我没有立即开始 ko-KR,而是先连续完成新增语言与首次生产流程的优化

(57) 法语版上线后,我没有马上开始韩语:A Tour of Go 新增语言流程的一轮完善与简化

图 1:根据 ko-KR 期间 76 次 Git 提交估算实际工作时间

(58) 原本预计 6 小时,最后用了约 16~18 小时:A Tour of Go 韩语版上线复盘

图 2:英文 source 与当时发生 validation failure 的 ko-KR candidate 对比

(59) Validator 报错,到底该改译文还是改规则?一次 A Tour of Go 韩语翻译实战

图 2:法语 A Tour of Go 仓库目前已经归档,页面仍保留 553 Commits 和 61 Contributors 的历史

(60) 从法语 553 次提交到韩语两代离线:A Tour of Go 社区翻译为什么难以长期维护

【图 4:es-ES 首次 Production 收口后连续完成的流程优化 commit】

(61) 从 es-ES 首次上线复盘:我把 A Tour of Go 新增语言的 Production 流程又收敛了一轮

图 1:旧公网验收链路连续出现 curl 28 超时,最终导致 public-machine 阶段失败

(62) A Tour of Go Production 公网验收踩坑:从跨境 SOCKS 链路改为 zgocloud direct

图 1:两次 Production Machine Acceptance 都已经通过,但 Headless Chrome 分别在 / 和 /tour/ 判定页面没有完成渲染

(63) 页面明明能打开,为什么 Headless Chrome 仍然判定 Production 失败?

图 3:Bing 从 Google Search Console 中识别到新的 it-IT 站点,并同时发现已有的 1 个 sitemap

(64) GSC → Bing Webmaster Tools:多语言站点如何减少重复验证

图 3:英文站 3 小时内出现 565 个 Cloudflare 来源 URL

(65) IndexNow 实战:手工提交长期不显示,Cloudflare Crawler Hints 却很快出现在 Bing Webmaster Tools

【图 1:A Tour of Go 项目首页目前的语言版本列表】

(66) 从 nl-NL 到 tr-TR:A Tour of Go 连续上线三门语言后,我又优化了哪些流程

【图 4:Go local 页面中已经出现 French、German、Korean、Simplified Chinese】

(67) 从邮件无人回复到进入 Go 官方 Go local:我的 4 个 A Tour of Go 翻译站终于被收录

图8:最终只保留一把启用的新 API Key

(68) 腾讯云 EdgeOne API 自动化:从 CAM 子用户到最小权限 API Key 的完整配置

最近我在继续完善 A Tour of Go 多语言项目的 Production 自动化。

以前每次中文站发布新版本后,都需要进入腾讯云 EdgeOne 控制台,手工执行一次缓存刷新:

Plaintext
内容类型: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 密钥管理页面后,控制台本身就给出了非常明显的安全提示:主账号密钥拥有很高的云资源操作权限,不建议直接用于程序化访问。

图1:腾讯云明确提示主账号 API 密钥具有全部云资源控制权限,建议改用子用户密钥
图1:腾讯云明确提示主账号 API 密钥具有全部云资源控制权限,建议改用子用户密钥

对于我的需求来说,Production 程序真正需要的只是:

  1. 查询 EdgeOne 站点;
  2. 查询具体加速域名;
  3. 创建一次缓存清除任务;
  4. 查询缓存清除任务状态。

显然没有必要让自动化程序拿到整个腾讯云账号的权限。

因此最后决定:

为 go-tour-i18n Production 单独创建一个 CAM 子用户。


二、创建专用 CAM 子用户

进入腾讯云 CAM 后,新建一个子用户。

我使用的用户名是:

Plaintext
go-tour-i18n-production

用途也很单一:

Plaintext
go-tour-i18n EdgeOne Production 自动化

创建方式选择:

Plaintext
可访问资源并接收消息
图2:为 Production 自动化单独创建 CAM 子用户,而不是复用主账号
图2:为 Production 自动化单独创建 CAM 子用户,而不是复用主账号

这个用户只需要程序调用 API,因此我只启用了:

Plaintext
编程访问

不需要给它日常腾讯云控制台登录能力。

这样做的好处是权限边界非常清楚:

Plaintext
腾讯云主账号

CAM 子用户
go-tour-i18n-production

只供 go-tour-i18n Production 自动化使用

以后即使需要轮换 API Key,也只需要处理这个专用用户,不影响主账号。


三、只给 EdgeOne 自动化真正需要的 4 个权限

接下来是最重要的一步:自定义 CAM 策略。

服务选择:

Plaintext
边缘安全加速平台 EO(teo)

最终只授权了 4 个 API Action:

Plaintext
DescribeZones
DescribeAccelerationDomains
CreatePurgeTask
DescribePurgeTasks
图3:本次资源范围选择“全部资源”,但 Action 仅保留这 4 个;代码再绑定正式 production hostname
图3:本次资源范围选择“全部资源”,但 Action 仅保留这 4 个;代码再绑定正式 production hostname

它们各自承担的职责很明确。

1. DescribeZones

根据正式站点名称查询 EdgeOne Zone。

自动化程序不会把 ZoneId 写死,而是先通过:

Plaintext
shuijingwanwq.com

动态解析正式 Zone。

2. DescribeAccelerationDomains

拿到 Zone 后,再确认真正需要操作的 Production hostname 确实属于这个 Zone,并且当前处于正常在线状态。

例如中文站:

Plaintext
go-dev.shuijingwanwq.com

3. CreatePurgeTask

真正创建缓存清除任务。

我的人工操作原来是:

Plaintext
Hostname
+
直接删除

因此 API 自动化也必须严格保持相同语义:

Plaintext
Type = purge_host
Method = delete
Targets = [正式 Production hostname]

4. DescribePurgeTasks

CreatePurgeTask 并不意味着清除工作已经最终完成。

程序还需要根据返回的 JobId 查询任务状态,直到确认:

Plaintext
success

才能把 CDN purge 判定为通过。


四、为什么资源范围仍然选择了“全部资源”

CAM 策略还可以继续做到非常细,例如限制到某个具体 EdgeOne 资源。

不过这是我自己的个人项目,目前只有少量站点,而且真正允许的 API Action 已经压缩到了 4 个,所以这里我选择了:

Plaintext
全部资源

而没有继续增加更复杂的资源级 QCS 限制。

这不代表程序可以任意刷新任何域名。

go-tour-i18n 自己还有第二层限制:

hostname 只能从正式的 production/identity.json 中读取。

也就是说,脚本不提供这种接口:

Plaintext
--hostname arbitrary-example.com

更不能执行:

Plaintext
purge_all
*
wildcard
整个 Zone

因此当前安全边界实际上是:

Plaintext
CAM:
只允许 4 个 EdgeOne API

            +

go-tour-i18n:
只允许 production/identity.json 中的正式 hostname

对于目前这个个人项目,我认为这个复杂度比较合适。


五、把策略关联到 Production 子用户

创建好策略后,将它关联到:

Plaintext
go-tour-i18n-production
图4:将 GoTourI18nEdgeOneMaintenance 策略关联到 go-tour-i18n-production
图4:将 GoTourI18nEdgeOneMaintenance 策略关联到 go-tour-i18n-production

策略名称我使用的是:

Plaintext
GoTourI18nEdgeOneMaintenance

最终策略创建并关联成功。

图5:阶段性结果,策略创建并关联成功
图5:阶段性结果,策略创建并关联成功

到这里,CAM 权限部分就完成了。

权限结构可以简化成:

Plaintext
go-tour-i18n-production
└── GoTourI18nEdgeOneMaintenance
    ├── DescribeZones
    ├── DescribeAccelerationDomains
    ├── CreatePurgeTask
    └── DescribePurgeTasks

六、第一个坑:第一次创建出来的 Key,我根本没看到 SecretKey

接下来进入 CAM 子用户的:

Plaintext
API 密钥

这里是我这次操作里最奇怪的一段。

创建 go-tour-i18n-production 子用户时,我已经勾选了:

Plaintext
编程访问

子用户创建完成以后,进入“API 密钥”页面,可以看到系统已经存在一条启用中的:

Plaintext
SecretId

也就是说,密钥实际上已经被创建出来了。

但问题在于:

我印象中,在前面的子用户创建流程里,根本没有出现过让我查看或保存 SecretKey 的页面。

这和“我看到了 SecretKey,但是忘记保存”不是一回事。

至少在我这次实际操作中,流程给我的体验是:

Plaintext
创建 CAM 子用户
→ 启用编程访问
→ 创建完成
→ API 密钥页面已经出现 SecretId

但是中间我没有发现任何一步展示完整 SecretKey。

之后当我尝试查看 SecretKey 时,腾讯云弹出了提示:

SecretKey 只在密钥创建时提供,后续不能再次查询。

图6:SecretKey 创建后不可再次查询的提示
图6:SecretKey 创建后不可再次查询的提示

这就形成了一个有些尴尬的状态:

Plaintext
SecretId 已经存在
SecretKey 后续又不能查询
而创建过程中我又没有看到 SecretKey

从使用体验上看,我更倾向于认为这是当时控制台创建流程里的一个 UI 或流程问题。

当然,我无法确认是腾讯云前端 Bug、页面跳转遗漏,还是某个非常不明显的展示环节被跳过了。

但可以确定的是:

第一把 Key 创建完成以后,我手里没有它的 SecretKey,而且后续也无法再查询。

因此这把 Key 实际上已经无法用于我的 Production 自动化。


七、重新新建一把 API Key,这次才真正看到了 SecretKey

既然旧 Key 的 SecretKey 已经无法获取,我没有继续折腾,而是直接点击:

Plaintext
新建密钥

这一次行为就非常明确了。

腾讯云弹出了“创建 SecretKey”窗口,并且同时显示:

Plaintext
SecretId
SecretKey
图7:重新创建 API 密钥,页面只在创建时展示 SecretId / SecretKey
图7:重新创建 API 密钥,页面只在创建时展示 SecretId / SecretKey

而且页面上还再次提示:

新建的密钥只在创建时提供 SecretKey,后续不可再次查询,请保存好 SecretKey。

这一次我马上保存了完整的 SecretId / SecretKey。

所以结合我的实际经历,更准确的经验不是:

“第一次是因为我忘记保存 SecretKey。”

而应该是:

如果创建 API Key 后页面明确展示了 SecretKey,一定要当场保存;如果像我第一次那样,创建完成后只看到 SecretId,却根本没有经历 SecretKey 展示步骤,也不要指望后面还能重新查看,直接重新创建一把更省事。

这里尤其不要把:

Plaintext
SecretId
SecretKey

发送到聊天工具、Git 仓库或者普通文档。

真正敏感的是 SecretKey,它应该只进入受控的 Production secret 文件。


八、服务器上如何保存 EdgeOne API 凭据

Production 服务器上,我最终使用:

Plaintext
/etc/go-tour/edgeone.env

内容结构只有两行:

Bash
TENCENTCLOUD_SECRET_ID=<SecretId>
TENCENTCLOUD_SECRET_KEY=<SecretKey>

文件权限设置为:

Plaintext
root:root
0600

实际检查结果:

Plaintext
root:root 600 /etc/go-tour/edgeone.env

同时还做了一个不会输出真实值的结构检查,最终得到:

Plaintext
EDGEONE SECRET CONTRACT: PASS

这里很重要的一点是:

Plaintext
不要 cat /etc/go-tour/edgeone.env

至少没有必要为了“确认配置”把真实 SecretKey 打到终端输出里。

正式程序只需要确认:

  • 文件是普通文件;
  • owner 是 root;
  • group 是 root;
  • mode 是 0600;
  • 只存在规定的两个变量;
  • 两个值均非空。

九、第二个坑:不要把交互式 read 和后续命令一次性粘贴

在配置服务器 secret 时,我还踩到了一个很有意思的小坑。

当时为了避免 SecretKey 进入 shell history,我使用了:

Bash
read -r -s -p 'TENCENTCLOUD_SECRET_KEY: ' ...

这个思路本身没问题。

问题是我把包含 read 的多行命令整体一次性粘贴到了终端里。

结果 shell 后面的:

Plaintext
printf
umask
install
rm
stat

等命令,被 read 当成了标准输入内容。

最后生成的:

Plaintext
/etc/go-tour/edgeone.env

居然有 19 行。

自动化 preflight 很快就失败:

Plaintext
[production-cdn] FAILED: secret file contains an invalid assignment

继续检查后发现:

Plaintext
lines = 19
contains_cr = True

文件里甚至出现了:

Plaintext
umask 077
tmp=...
install ...
rm ...

这些本应该执行的 shell 命令。

最后重新分开操作:

Plaintext
先执行 read
→ 单独输入 SecretId

再执行 read -s
→ 单独输入 SecretKey

最后再执行非交互写文件命令

问题才解决。

这个坑其实和腾讯云无关,但以后只要使用交互式:

Plaintext
read
password prompt
secret prompt

都值得注意:

不要把需要等待人工输入的命令和后续命令一次性整体粘贴。


十、先做只读 API preflight,而不是直接清缓存

凭据配置完成以后,我没有马上执行:

Plaintext
CreatePurgeTask

而是先为 Production 增加了一个只读验证:

Bash
scripts/verify-edgeone-authority.sh zh-CN

它只允许调用:

Plaintext
DescribeZones
DescribeAccelerationDomains

绝不会调用:

Plaintext
CreatePurgeTask

这样即使 API client 写错,也不会影响真实 CDN 缓存。

事实证明,这一步非常有必要。


十一、第三个坑:mock tests 全绿,真实 API 仍然失败

第一次运行真实 preflight 时,得到:

Plaintext
[production-cdn] FAILED:
EdgeOne zone identity/name/status is not exact and active

Secret 文件已经正确,API 权限也没有报 AccessDenied。

继续检查代码才发现,自己写的 EdgeOne client 错误地假设 Zone 返回结构是:

Plaintext
Name
Id
Status

代码类似:

Python
zone.get("Name")
zone.get("Id")
zone.get("Status") == "active"

但腾讯云真实 DescribeZones 返回的是:

Plaintext
ZoneName
ZoneId
Type
Status
CnameStatus
ActiveStatus
LockStatus
Paused

这已经不是一个简单的字段拼写问题。

更麻烦的是:

Plaintext
Status=active

也不能作为所有站点类型统一的可用性判断。

我的 EdgeOne 站点使用的是:

Plaintext
Type=partial

也就是 CNAME 接入。

在这种情况下:

Plaintext
Status=pending

只是表示 NS 没有切换,并不代表 CNAME 接入站点不可用。

因此最终把 Zone 判定调整成了类型感知。

公共要求:

Plaintext
ZoneName 精确匹配
ZoneId 非空
ActiveStatus=active
Paused=false
LockStatus=enable

如果:

Plaintext
Type=partial

则要求:

Plaintext
CnameStatus=finished
Status 可以是 pending / active

如果:

Plaintext
Type=full

则要求:

Plaintext
Status=active

未知 Type:

Plaintext
fail closed

十二、为什么这个错误直到真实 API 才暴露

更值得反思的是:

在运行真实 API 之前,所有自动测试其实都是 PASS 的。

原因很简单。

当时 mock fixture 也是按照错误理解构造的:

JSON
{
  "Name": "shuijingwanwq.com",
  "Id": "zone-test",
  "Status": "active"
}

于是形成了一个很典型的问题:

Plaintext
错误 specification

错误 implementation

错误 mock

implementation 与 mock 完全一致

tests 全绿

测试只能证明:

代码符合自己定义的假设。

却不能证明:

这个假设符合真实第三方 API。

这次让我再次确认了一条很重要的原则:

第三方 API 的 request field、response field、enum、status semantics 和 pagination contract,应该直接依据真实官方 schema 建立测试 fixture,不能靠接口命名习惯推断。

特别是 Production mutation 相关 API,更应该在真正 mutation 前安排一次:

Plaintext
read-only real API preflight

十三、修正以后,真实 EdgeOne authority 验证通过

修正 Zone contract 后,再次运行:

Bash
scripts/verify-edgeone-authority.sh zh-CN

最终得到:

Plaintext
EDGEONE AUTHORITY PREFLIGHT: PASS
zone_name: shuijingwanwq.com
hostname: go-dev.shuijingwanwq.com

这意味着下面整条真实链路已经验证:

Plaintext
root-only Secret

TC3-HMAC-SHA256

DescribeZones

精确解析正式 Zone

DescribeAccelerationDomains

精确验证 Production hostname

PASS

整个验证过程中没有执行任何缓存清除。

这正是我想要的效果:

先证明权限、身份解析和 API client 都正确,再允许真正的 Production mutation。


十四、最终自动缓存刷新与控制台人工操作保持一致

原来我在 EdgeOne 控制台中的人工操作是:

Plaintext
内容类型:Hostname
清除方法:直接删除
图9:说明自动化最终要复现的控制台语义
图9:说明自动化最终要复现的控制台语义

所以自动化最终固定生成的 API 请求语义也是:

JSON
{
  "Type": "purge_host",
  "Method": "delete",
  "Targets": [
    "<正式 Production hostname>"
  ]
}

这里没有开放:

Plaintext
purge_all
wildcard
任意 hostname
多个 Targets
zone-wide purge

目标 hostname 只能来自:

Plaintext
production/identity.json

例如:

Plaintext
go-dev.shuijingwanwq.com

十五、CreatePurgeTask 以后还不能直接算成功

缓存刷新自动化里还有一个容易忽略的问题:

Plaintext
CreatePurgeTask

调用成功,只代表任务已经被 EdgeOne 接收。

正式 Production workflow 还会继续根据:

Plaintext
JobId

调用:

Plaintext
DescribePurgeTasks

状态处理规则类似:

Plaintext
processing
→ 继续等待

success
→ PASS

failed
timeout
canceled
unknown
→ FAIL

也就是说:

API 请求返回成功,并不等于 Production cache purge 已完成。


十六、网络结果不确定时,不盲目重复清缓存

还有一种更麻烦的情况:

Plaintext
CreatePurgeTask

请求发出去以后,本地发生:

  • timeout;
  • connection interruption;
  • response body 不完整。

这时无法判断:

EdgeOne 到底有没有收到并创建任务?

最危险的做法是:

Plaintext
超时
→ 马上重新 CreatePurgeTask

因为这样可能重复创建 mutation。

现在的做法是先通过:

Plaintext
DescribePurgeTasks

查询最近任务,并根据:

Plaintext
ZoneId
Type=purge_host
Target=正式 hostname
CreateTime

尝试 reconciliation。

只有能够明确证明第一次没有创建任务时,才允许有限重试。

如果始终无法确认:

Plaintext
fail closed

而不是假装成功。


十七、新 API Key 验证成功后,再删除第一把无法使用的 Key

新创建的 API Key 完成真实只读 preflight 后,我才回到腾讯云 CAM。

最终删除了第一把已经无法正常使用的 Key。

这里再次强调:

第一把不是我看到 SecretKey 后忘记保存,而是创建子用户时我根本没有发现 SecretKey 的展示页面。

因此那把 Key 最终只剩 SecretId 可见,对我的自动化已经没有实际用途。

删除以后,go-tour-i18n-production 最终只保留一把正式使用中的 API Key。

图8:最终只保留一把启用的新 API Key
图8:最终只保留一把启用的新 API Key

这样凭据状态就比较干净:

Plaintext
1 个 Production CAM 子用户
1 个最小权限策略
1 把有效 API Key
1 个 root-only secret 文件

十八、最终结构

整理完成以后,这套 EdgeOne Production authority 可以概括成:

Plaintext
腾讯云主账号

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

程序本身又继续限制:

Plaintext
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。

最省事的办法通常是:

Plaintext
重新新建一把 API Key
→ 确认页面明确展示 SecretId + SecretKey
→ 当场保存
→ 验证新 Key
→ 删除旧 Key

至于第一次为什么没有出现 SecretKey,我现在更倾向于认为是腾讯云当时控制台流程或 UI 上的问题,但没有足够证据确认具体原因。

Secret 不应该进入 Git、日志和普通终端输出

Production server 上只保留 root-only 0600 文件。

第三方 API mock 必须来自真实 schema

否则很容易出现:

Plaintext
实现错了
测试数据也错了
结果测试全部通过

Production mutation 之前最好先有真实 read-only preflight

这次如果没有:

Plaintext
DescribeZones
+
DescribeAccelerationDomains

的真实验证,Zone schema 的错误可能要等到真正缓存刷新时才暴露。

网络结果未知和明确失败不是一回事

明确失败可以安全重试。

结果未知时,应先 reconciliation,而不是直接重复 mutation。


结语

完成 EdgeOne API authority 后,中文站的 CDN 缓存刷新终于也可以纳入 go-tour-i18n 的 Production 自动化。

这一步本身只是整个流程优化中的一小部分。

真正让我开始做这件事的,是一次 A Tour of Go 官方上游同步:当同一个变化需要发布到 10 个语言站后,我发现原来单个站点还能接受的人工操作,放大到多个 locale 后会变成大量重复工作。

后来我继续把:

Plaintext
publish
CDN purge
shared-assets purge
Production maintenance
machine acceptance
browser acceptance

逐步做成了批量化流程。

现在多语言日常 Production release 已经可以通过一个顶层 batch command 串行完成。

这部分过程,我准备再单独整理成下一篇。

从邮件无人回复到进入 Go 官方 Go local:我的 4 个 A Tour of Go 翻译站终于被收录

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 官方无隶属或授权关系。