Recently, I have been continuing to refine the Production automation for the multilingual A Tour of Go project.
Previously, after every new release for the Chinese site, I had to log into the Tencent Cloud EdgeOne console and manually trigger a cache purge:
Content type: Hostname
Purge method: Direct deleteWith only a single site, this was not much of a hassle.
However, as the project expanded to more languages, I progressively automated workflows like Cloudflare, Production deploy, machine acceptance, and browser acceptance. At this point, still having to manually open the EdgeOne console to purge the cache for the Chinese site became an obvious manual bottleneck in the entire release process.
So this time, I decided to bring EdgeOne cache purging into the Production automation as well.
Once I actually started, I realized it was not as simple as “applying for an API Key.” It also involved:
- Whether I should use the primary account API Key;
- How to create a dedicated CAM sub-user;
- How to restrict permissions to the minimum required EdgeOne APIs;
- Why I never saw the SecretKey when the first key was created;
- Why the SecretKey cannot be queried afterward;
- How to securely store the SecretId / SecretKey;
- How to perform read-only API validation before actually executing a cache purge;
- Why mock tests might all pass, yet the real API could still expose a misunderstanding of the fields.
This article documents the entire process.
1. Why not just use the primary account API Key
Initially, I did consider the simplest approach:
Directly create a set of API Keys for the Tencent Cloud primary account.
This would involve the fewest steps and eliminate the need to set up additional CAM users and permission policies.
But upon entering the Tencent Cloud API key management page, the console itself displayed a prominent security warning: primary account keys possess high-level operational permissions over cloud resources and are not recommended for direct programmatic access.

For my needs, the Production program actually only requires:
- Querying EdgeOne zones;
- Querying specific acceleration domains;
- Creating a cache purge task;
- Querying the status of the cache purge task.
There is clearly no need to grant the automation program permissions over the entire Tencent Cloud account.
Therefore, I finally decided:
To create a dedicated CAM sub-user specifically for go-tour-i18n Production.
2. Creating a dedicated CAM sub-user
After logging into Tencent Cloud CAM, I created a new sub-user.
The username I used was:
go-tour-i18n-productionIts purpose is also very specific:
go-tour-i18n EdgeOne Production automationFor the creation method, I selected:
Access resources and receive messages
This user only needs to make programmatic API calls, so I only enabled:
Programmatic accessIt does not need the ability to log in to the daily Tencent Cloud console.
The benefit of this is that the permission boundary is very clear:
Tencent Cloud primary account
↓
CAM sub-user
go-tour-i18n-production
↓
Only used for go-tour-i18n Production automationEven if I need to rotate the API Key in the future, I only have to deal with this dedicated user, leaving the primary account unaffected.
3. Granting only the 4 permissions actually needed for EdgeOne automation
Next was the most important step: customizing the CAM policy.
For the service, I selected:
Edge Security and Acceleration Platform EO (teo)Ultimately, I only authorized 4 API Actions:
DescribeZones
DescribeAccelerationDomains
CreatePurgeTask
DescribePurgeTasks
The responsibilities each of them bears are very clear.
1. DescribeZones
Query the EdgeOne Zone based on the official site name.
The automation program does not hardcode the ZoneId; instead, it first uses:
shuijingwanwq.comto dynamically resolve the official Zone.
2. DescribeAccelerationDomains
After obtaining the Zone, it confirms that the Production hostname to be operated actually belongs to this Zone and is currently in a normal online state.
For example, the Chinese site:
go-dev.shuijingwanwq.com3. CreatePurgeTask
Actually creates the cache purge task.
My original manual operation was:
Hostname
+
Direct deleteTherefore, the API automation must also strictly maintain the same semantics:
Type = purge_host
Method = delete
Targets = [official Production hostname]4. DescribePurgeTasks
CreatePurgeTask does not mean the purge work is finally complete.
The program still needs to query the task status based on the returned JobId until it confirms:
successOnly then can the CDN purge be considered passed.
4. Why the resource scope was still set to “All resources”
A CAM policy can be made even more granular, for example, by restricting it to a specific EdgeOne resource.
However, this is my own personal project with only a few sites at present, and the allowed API Actions have already been reduced to 4, so I chose:
All resourceswithout adding more complex resource-level QCS restrictions.
This does not mean the program can arbitrarily purge any domain.
go-tour-i18n itself has a second layer of restriction:
the hostname can only be read from the official
production/identity.json.
That is, the script does not provide an interface like this:
--hostname arbitrary-example.comNor can it execute:
purge_all
*
wildcard
entire ZoneTherefore, the current security boundary is actually:
CAM:
Only 4 EdgeOne APIs allowed
+
go-tour-i18n:
Only official hostnames in production/identity.json allowedFor this personal project at its current stage, I think this level of complexity is appropriate.
5. Associating the policy with the Production sub-user
After creating the policy, I associated it with:
go-tour-i18n-production
The policy name I used is:
GoTourI18nEdgeOneMaintenanceThe policy was ultimately created and associated successfully.

At this point, the CAM permissions part was complete.
The permission structure can be simplified as:
go-tour-i18n-production
└── GoTourI18nEdgeOneMaintenance
├── DescribeZones
├── DescribeAccelerationDomains
├── CreatePurgeTask
└── DescribePurgeTasks6. First pitfall: when the first key was created, I never saw the SecretKey
Next, I entered the CAM sub-user’s:
API KeysThis was the strangest part of the operation.
When creating the go-tour-i18n-production sub-user, I had already checked:
Programmatic accessAfter the sub-user was created, entering the “API Keys” page showed that the system already had an enabled:
SecretIdThis means the key had actually been created.
But the problem was:
In my memory, there was never a page in the preceding sub-user creation process that allowed me to view or save the SecretKey.
This is not the same as “I saw the SecretKey but forgot to save it.”
At least in my actual operation this time, the experience the process gave me was:
Create CAM sub-user
→ Enable programmatic access
→ Creation complete
→ SecretId already appears on the API Keys pageBut in between, I did not notice any step that displayed the full SecretKey.
Later, when I tried to view the SecretKey, Tencent Cloud popped up a prompt:
The SecretKey is only provided when the key is created and cannot be queried again afterward.

This led to a somewhat awkward state:
SecretId already exists
SecretKey cannot be queried later
and I never saw the SecretKey during the creation processFrom a user experience perspective, I am more inclined to believe this was a UI or flow issue in the console creation process at the time.
Of course, I cannot confirm whether it was a Tencent Cloud frontend bug, an omitted page redirect, or if a very inconspicuous display step was skipped.
But what is certain is:
After the first key was created, I did not have its SecretKey, and I could not query it again later.
Therefore, this key was effectively unusable for my Production automation.
7. Creating a new API Key again, and this time actually seeing the SecretKey
Since the old key’s SecretKey could no longer be retrieved, I did not keep struggling with it and directly clicked:
New KeyThis time, the behavior was very clear.
Tencent Cloud popped up the “Create SecretKey” window and simultaneously displayed:
SecretId
SecretKey
And the page prompted again:
The newly created key only provides the SecretKey during creation and cannot be queried again later. Please save the SecretKey properly.
This time, I immediately saved the full SecretId / SecretKey.
So, based on my actual experience, the more accurate lesson is not:
“The first time, I forgot to save the SecretKey.”
Rather, it should be:
If the page clearly displays the SecretKey after creating an API Key, be sure to save it immediately; if, like my first time, you only see the SecretId after creation and never go through a SecretKey display step, do not expect to be able to view it again later. It is more efficient to just create a new key.
In particular, do not send:
SecretId
SecretKeyto chat tools, Git repositories, or regular documents.
The truly sensitive part is the SecretKey, which should only go into a controlled Production secret file.
8. How to store EdgeOne API credentials on the server
On the Production server, I ultimately used:
/etc/go-tour/edgeone.envThe content structure is only two lines:
TENCENTCLOUD_SECRET_ID=<SecretId>
TENCENTCLOUD_SECRET_KEY=<SecretKey>The file permissions are set to:
root:root
0600The actual check result:
root:root 600 /etc/go-tour/edgeone.envI also ran a structural check that does not output the real values, ultimately yielding:
EDGEONE SECRET CONTRACT: PASSOne important point here is:
Do not cat /etc/go-tour/edgeone.envAt least there is no need to print the real SecretKey to the terminal output just to “confirm the configuration”.
The production program only needs to confirm:
- The file is a regular file;
- owner is root;
- group is root;
- mode is 0600;
- Only the two specified variables exist;
- Both values are non-empty.
9. Second pitfall: do not paste interactive read and subsequent commands all at once
While configuring the server secret, I also hit an interesting little pitfall.
To prevent the SecretKey from entering the shell history, I used:
read -r -s -p 'TENCENTCLOUD_SECRET_KEY: ' ...The approach itself was fine.
The problem was that I pasted the multi-line command containing read into the terminal all at once.
As a result, the shell’s subsequent:
printf
umask
install
rm
statcommands were treated as standard input by read.
The generated:
/etc/go-tour/edgeone.envactually had 19 lines.
The automation preflight failed quickly:
[production-cdn] FAILED: secret file contains an invalid assignmentContinuing to inspect it showed:
lines = 19
contains_cr = TrueThe file even contained:
umask 077
tmp=...
install ...
rm ...These were shell commands that should have been executed.
Finally, I separated the operations:
First execute read
→ Enter SecretId separately
Then execute read -s
→ Enter SecretKey separately
Finally execute the non-interactive file writing commandOnly then was the problem solved.
This pitfall is actually unrelated to Tencent Cloud, but whenever using interactive:
read
password prompt
secret promptit is worth noting:
Do not paste commands that require manual input and subsequent commands all at once.
10. Perform a read-only API preflight first, instead of purging the cache directly
After the credentials were configured, I did not immediately execute:
CreatePurgeTaskInstead, I first added a read-only validation for Production:
scripts/verify-edgeone-authority.sh zh-CNIt only allows calling:
DescribeZones
DescribeAccelerationDomainsIt absolutely will not call:
CreatePurgeTaskThis way, even if the API client is written incorrectly, it will not affect the real CDN cache.
As it turned out, this step was very necessary.
11. Third pitfall: mock tests all green, but the real API still fails
When running the real preflight for the first time, I got:
[production-cdn] FAILED:
EdgeOne zone identity/name/status is not exact and activeThe secret file was correct, and the API permissions did not report AccessDenied.
Continuing to check the code, I found that my custom EdgeOne client incorrectly assumed the Zone response structure was:
Name
Id
StatusThe code was similar to:
zone.get("Name")
zone.get("Id")
zone.get("Status") == "active"But the real DescribeZones returned by Tencent Cloud is:
ZoneName
ZoneId
Type
Status
CnameStatus
ActiveStatus
LockStatus
PausedThis is no longer a simple field spelling issue.
What is more troublesome is that:
Status=activecannot serve as a unified availability check for all site types.
My EdgeOne site uses:
Type=partialwhich means CNAME access.
In this case:
Status=pendingonly means the NS has not been switched, and does not mean the CNAME access site is unavailable.
Therefore, I finally adjusted the Zone validation to be type-aware.
The common requirements are:
ZoneName exact match
ZoneId non-empty
ActiveStatus=active
Paused=false
LockStatus=enableIf:
Type=partialthen it requires:
CnameStatus=finished
Status can be pending / activeIf:
Type=fullthen it requires:
Status=activeUnknown Type:
fail closed12. Why this error was not exposed until the real API
What is more worth reflecting on is:
Before running the real API, all automated tests actually PASSED.
The reason is simple.
The mock fixtures at the time were also built based on the incorrect understanding:
{
"Name": "shuijingwanwq.com",
"Id": "zone-test",
"Status": "active"
}This created a very typical problem:
Wrong specification
↓
Wrong implementation
↓
Wrong mock
↓
Implementation matches mock exactly
↓
Tests all greenThe tests could only prove:
that the code conformed to its own defined assumptions.
But they could not prove:
that these assumptions matched the real third-party API.
This time, it reaffirmed an important principle for me:
For third-party APIs, the request fields, response fields, enums, status semantics, and pagination contracts should be used to build test fixtures directly from the real official schema, and must not be inferred from interface naming conventions.
Especially for Production mutation-related APIs, a read-only real API preflight should be arranged before the actual mutation:
read-only real API preflight13. After the fix, the real EdgeOne authority validation passes
After fixing the Zone contract, I ran again:
scripts/verify-edgeone-authority.sh zh-CNFinally getting:
EDGEONE AUTHORITY PREFLIGHT: PASS
zone_name: shuijingwanwq.com
hostname: go-dev.shuijingwanwq.comThis means the entire real chain below has been validated:
root-only Secret
↓
TC3-HMAC-SHA256
↓
DescribeZones
↓
Exact resolution of official Zone
↓
DescribeAccelerationDomains
↓
Exact validation of Production hostname
↓
PASSNo cache purge was executed during the entire validation process.
This is exactly the effect I wanted:
Prove that the permissions, identity resolution, and API client are all correct first, before allowing the actual Production mutation.
14. The final automated cache purge remains consistent with manual console operations
My original manual operation in the EdgeOne console was:
Content type: Hostname
Purge method: Direct delete
So the API request semantics that the automation ultimately generates are also fixed as:
{
"Type": "purge_host",
"Method": "delete",
"Targets": [
"<正式 Production hostname>"
]
}This does not expose:
purge_all
wildcard
arbitrary hostname
multiple Targets
zone-wide purgeThe target hostname can only come from:
production/identity.jsonFor example:
go-dev.shuijingwanwq.com15. CreatePurgeTask does not immediately mean success
There is another easily overlooked issue in cache purge automation:
CreatePurgeTaskA successful call only means the task has been received by EdgeOne.
The formal Production workflow will continue based on the:
JobIdto call:
DescribePurgeTasksThe status processing rules are similar to:
processing
→ continue waiting
success
→ PASS
failed
timeout
canceled
unknown
→ FAILThat is:
A successful API request response does not equal the completion of the Production cache purge.
16. When the network result is uncertain, do not blindly retry the cache purge
There is an even more troublesome scenario:
CreatePurgeTaskAfter the request is sent, a local:
- timeout;
- connection interruption;
- incomplete response body.
At this point, it is impossible to determine:
whether EdgeOne actually received and created the task?
The most dangerous approach is:
Timeout
→ immediately CreatePurgeTask againBecause this may create duplicate mutations.
The current approach is to first use:
DescribePurgeTasksto query recent tasks, and based on:
ZoneId
Type=purge_host
Target=official hostname
CreateTimeattempt reconciliation.
Only when it can be clearly proven that the first task was not created is a limited retry allowed.
If it remains impossible to confirm:
fail closedrather than pretending it succeeded.
17. After the new API Key is successfully validated, delete the first unusable key
After the newly created API Key completed the real read-only preflight, I returned to Tencent Cloud CAM.
I finally deleted the first key that was no longer usable.
Here I emphasize again:
The first key was not a case of me seeing the SecretKey and forgetting to save it, but rather that during the sub-user creation, I never found the SecretKey display page at all.
Therefore, that key only had a visible SecretId left and was of no practical use to my automation.
After deletion, go-tour-i18n-production ultimately retains only one officially active API Key.

This leaves the credential state quite clean:
1 Production CAM sub-user
1 least-privilege policy
1 valid API Key
1 root-only secret file18. Final structure
After organizing, this EdgeOne Production authority can be summarized as:
Tencent Cloud primary account
↓
CAM sub-user
go-tour-i18n-production
↓
Custom policy
GoTourI18nEdgeOneMaintenance
↓
4 API Actions
↓
API Key
↓
/etc/go-tour/edgeone.env
root:root 0600
↓
read-only Production preflight
↓
exact hostname purgeThe program itself continues to restrict:
Zone
from official production authority
hostname
from production/identity.json
Type
fixed to purge_host
Method
fixed to delete
Targets
fixed to a single official hostname19. The biggest takeaway this time is not “getting an API Key”
Initially, I just wanted to solve a very specific problem:
I did not want to manually enter the Tencent Cloud EdgeOne console and click cache purge every time the Chinese site went live.
But after actually finishing it, I feel the more valuable part was clarifying the entire permission and mutation boundary.
A few practical lessons are especially worth keeping.
Do not use the primary account API Key just to save a few steps
A dedicated CAM sub-user and least-privilege policy take a few more minutes to configure, but are clearly safer for long-term maintenance.
If the page displays the SecretKey, save it immediately
The SecretKey cannot be queried again later.
But my first key this time was more special:
After the sub-user was created, a SecretId already existed, but I never found the SecretKey display page at all.
So if you encounter a similar situation, do not assume you can still query the SecretKey later.
The most hassle-free approach is usually:
Create a new API Key
→ Confirm the page clearly displays SecretId + SecretKey
→ Save it immediately
→ Validate the new Key
→ Delete the old KeyAs for why the SecretKey did not appear the first time, I am now more inclined to believe it was an issue with the Tencent Cloud console flow or UI at the time, but I do not have enough evidence to confirm the specific cause.
Secrets should not go into Git, logs, or regular terminal output
On the Production server, only a root-only 0600 file is kept.
Third-party API mocks must come from the real schema
Otherwise, it is easy to end up with:
Wrong implementation
Wrong test data
Resulting in all tests passingIt is best to have a real read-only preflight before a Production mutation
This time, without the real validation from:
DescribeZones
+
DescribeAccelerationDomainsthe Zone schema error might not have been exposed until the actual cache purge.
Unknown network results and explicit failures are not the same thing
An explicit failure can be safely retried.
When the result is unknown, you should reconcile first, rather than directly repeating the mutation.
Conclusion
After completing the EdgeOne API authority, the CDN cache purge for the Chinese site could finally be incorporated into the go-tour-i18n Production automation.
This step itself is just a small part of optimizing the entire process.
What actually prompted me to start doing this was an official upstream sync for A Tour of Go: when the same change needed to be published to 10 language sites, I found that manual operations that were still acceptable for a single site turned into a massive amount of repetitive work when scaled across multiple locales.
Later, I continued to turn:
publish
CDN purge
shared-assets purge
Production maintenance
machine acceptance
browser acceptanceinto a batched process step by step.
Now, the daily multilingual Production release can be completed serially via a single top-level batch command.
I plan to organize this part of the process into a separate next article.
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan
