
很多人问我天天折腾 Cloudflare 的 CDN、DNS 和各种边缘服务直接在官方面板上点鼠标不就行了为什么非要搞一个专用的系统镜像出来。说实话当你手里有十几个域名、几十条 DNS 记录、隔三差五要刷新缓存、调整隧道路由的时候网页控制台那套点击操作就成了最大的瓶颈。所以我花了一些时间把日常高频操作全部整理成脚本再预装到一个精简的 Linux 系统里做出了一个叫 cloudflare-os 的定制环境。这篇东西就是把整套系统的设计思路、选型理由、构建步骤和踩坑记录完整梳理一遍给同样在跟 Cloudflare API 打交道的朋友做个参考。1. 为什么非要折腾一套Cloudflare专用系统1.1 面板操作的效率瓶颈先说说我自己的实际场景。我手上有几个线上项目还有一批帮朋友维护的站点都挂在 Cloudflare 后面。日常最频繁的操作无非这么几类新增一条 DNS 记录、修改代理状态橙色云朵开不开、清理 CDN 缓存、给某个子域挂上 Tunnel、看一下站点基本流量情况。听起来都很简单但一旦站点数量多起来操作就变得很零碎。每改一条 DNS 记录我得先登录面板、找到对应域名、切到 DNS 页面、找到那条记录、再点编辑整个过程至少二十秒。二十秒看起来不长但一天操作二三十次浪费掉的时间就很可观了。更麻烦的是重复性操作。比如上线一个新环境要批量创建一批子域名的 DNS 记录格式都差不多只是前缀和 IP 不同。这种工作在面板上纯粹是折磨而在命令行里就是几行循环的事。所以我一开始的想法很简单能不能在我自己的电脑上装一些脚本用 curl 直接调 Cloudflare API把这些高频操作全部覆盖掉。1.2 单机脚本的痛点最初我确实就是在自己的 Mac 上写了几个 shell 脚本通过 Cloudflare API v4 完成各种操作。但用了一段时间就发现几个问题。第一个问题是环境不一致。在公司电脑上能用回到家换一台电脑依赖的 jq 没装脚本跑不起来就算装了API Token 的环境变量也要重新配。第二个问题是脚本散落今天写个 update-dns.sh明天写个 purge-cache.sh放到不同目录时间一长自己都找不到。第三个问题是共享困难。团队里其他同事问我怎么弄我说你把这些脚本拷过去再装一下 jq 和 cloudflared再配一下 token……他们直接放弃。这时候就冒出来一个念头与其维护一堆散落的脚本不如做一个精简的定制系统。把需要的工具全部预装好把脚本放到固定目录把 API 凭据的读取方式统一然后把整个系统做成一个镜像。谁需要就直接装这个系统所有工具开箱即用。1.3 cloudflare-os 的定位cloudflare-os 不是一个从零写的操作系统也不是什么 Linux 发行版。它的本质是一个面向 Cloudflare 生态管理场景的定制 Linux 环境。底座还是 Debian但我在上面做了几件事预装所有需要的基础工具包括 cloudflared、curl、jq、python3、git写好一套标准化的脚本来封装 Cloudflare API 的常见操作设计一个统一的配置文件来管理 API 凭据和常用参数再把一些容易踩坑的细节比如限频重试、Token 权限校验、路由冲突检测都融进脚本逻辑里。这个系统解决的核心问题就是把 Cloudflare 的 Web 控制台上那些重复劳动转化为可复用、可版本管理、可分享的命令行工具集。它的适用对象很明确管理多个域名或站点的运维人员、需要批量操作 DNS/CDN 记录的开发者、以及想尝试用 API 自动化管理 Cloudflare 服务的爱好者。2. 基础镜像与核心组件选型2.1 为什么选 Debian 而不是 Alpine第一个需要拍板的问题就是用哪个发行版做底子。Alpine 以体积小著称镜像只有几兆很多 Docker 镜像都喜欢拿它当底座。但我实际做这个系统的时候最终选了 Debian而且是 Debian 的 netinst 版本再手动裁剪不是直接拿一个完整桌面版。原因有几点。Alpine 的包管理器是 apk软件源里的包版本相对较新但更新节奏不太稳定。而 cloudflared 官方对 Debian 系的 .deb 包支持非常好甚至官方文档里推荐的安装方式就是通过 apt 仓库这对我来说意味着版本升级最省心。其次Debian 的用户基数大碰到问题搜索解决方案最容易这一点在排障时比体积优势重要得多。再者Alpine 为了控制体积用了 musl libc而 Cloudflare 的有些二进制虽然官方声称支持 musl但我在实际使用时发现个别情况下还是 glibc 环境下更稳。2.2 核心组件清单整个系统预装的组件没有走多多益善的路线而是严格按需。我在设计时给自己定了个规矩装进来的每个组件都必须能说出它存在的理由而且必须有替代方案。组件版本选型用途说明cloudflared最新稳定版隧道Tunnel管理、DNS 代理这套系统里最关键的外部依赖curl系统自带所有 API 调用的底层传输工具jq系统自带解析 Cloudflare API 返回的 JSON脚本里几乎离不开它python3系统自带一些复杂的脚本逻辑比如批量 DNS 迁移、JSON 拼接、正则校验git系统自带脚本和配置的版本管理也用于从仓库拉取更新htop / iftop系统自带日常排查系统状态用openssl系统自带生成证书请求、查看证书信息等辅助操作注意不是所有人都需要 python3纯 shell 理论上也能完成所有操作。但我的经验是一旦涉及对 JSON 数组做过滤和重组jq 的表达式会变得极其晦涩而 python3 写起来更接近自然语言后期维护的人包括三个月后的自己更容易看懂。2.3 为什么不用 Docker 直接跑容器这是很多朋友问得最多的问题。既然有现成的 Docker 镜像把工具和脚本打包进容器不是更方便吗理论上确实可以我在开发阶段也尝试过用 Docker 做测试环境。但最终没有把容器作为主要交付形态有几个具体原因。第一cloudflared tunnel 这种方式本身需要在本机跑一个常驻进程容器方案必须额外处理进程生命周期、重启策略、日志采集的问题。虽然 docker restart unless-stopped 能解决一部分但增加了不必要的复杂度。第二这套系统的使用场景里有一部分操作直接涉及本机网络配置和系统级资源比如等会儿要说到的本地 DNS 解析、hosts 文件调整、网络命名空间等在容器里做这些会受限。第三我对这套系统的定位是一个可以直接装进 U 盘或虚拟机、随时随地拿来干活的环境不是一个部署到某台服务器的单元。裸金属或虚拟机上直接跑 Debian资源占用低行为也更可预测。3. 从零构建 cloudflare-os关键步骤与脚本逻辑3.1 基础系统安装构建的第一步是装一个最小化的 Debian 系统。我用的是 Debian netinst 镜像安装时在 tasksel 界面只选了SSH server和standard system utilities图形界面完全不装。这一步没什么玄机但有一个建议分区方案里给根分区留够空间不要因为追求精简把分区卡得太死。因为后面要装的 cloudflared 加上日志和脚本缓存占用并不大但如果用户想在这个系统上跑一些额外的分析任务磁盘空间还是宽裕一点好。装完后第一件事永远是更新系统和安装基础工具sudo apt update sudo apt upgrade -y sudo apt install -y curl jq python3 python3-pip git htop iftop openssl ca-certificates这没什么好说的关键是装 cloudflared。官方推荐的方式是把 Cloudflare 的 apt 仓库加进来然后安装。这样做的好处是以后 apt upgrade 就能自动更新 cloudflared不用手动下载二进制。sudo mkdir -p /etc/apt/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /etc/apt/keyrings/cloudflare-main.gpg /dev/null echo deb [signed-by/etc/apt/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflare-main jammy main | sudo tee /etc/apt/sources.list.d/cloudflare-main.list sudo apt update sudo apt install -y cloudflared我标注的发行版名称用了 jammy但这只是一个例子。具体要用哪个代号取决于你 Debian 底座的版本和 Cloudflare 仓库当时的支持情况。我建议装之前先看一眼官方文档确认一下仓库地址的结构避免源配错导致安装失败。3.2 cloudflared 隧道初始化cloudflared 装好后下一步通常就是初始化隧道。隧道的认证方式有两种一种是传统的证书cert.pem方式一种是更细粒度的 Token 方式。我在这个系统里用 Token 方式作为主力因为证书文件的权限管理更麻烦而且一旦泄露影响范围更大。创建隧道的逻辑其实不难关键是要理解认证信息的层级关系。每个 Tunnel 会生成一个专属的 Tunnel ID 和一个 Tunnel Tokencloudflared 通过 Token 连接 Cloudflare 边缘网络。而这个 Token 的获取方式一种是在面板上创建隧道后复制一种是先用cloudflared tunnel login做一次浏览器认证拿到 cert.pem然后用 cert.pem 创建隧道。我推荐的做法是在自己的电脑上完成初始认证生成好 Tunnel 的 JSON 凭据文件然后把这个 JSON 文件安全地传到 cloudflare-os 环境中。原因很简单cloudflared tunnel login需要在浏览器里确认授权而 cloudflare-os 本身往往是没有图形界面的做这一步会很绕。以下是手动在本地建好隧道并导出凭据的流程# 在本地环境执行没有图形界面的机器可以跳过大段浏览器操作 cloudflared tunnel login cloudflared tunnel create my-first-tunnel cloudflared tunnel list创建成功后cloudflared 会在本地生成一个 JSON 凭据文件路径一般在~/.cloudflared/{tunnel-id}.json这个文件就相当于隧道的私钥。把它复制到 cloudflare-os 的/etc/cloudflared/目录下并设置好权限然后写入隧道配置文件/etc/cloudflared/config.ymltunnel: my-first-tunnel credentials-file: /etc/cloudflared/{tunnel-id}.json ingress: - hostname: app.example.com service: http://localhost:8080 - hostname: static.example.com service: http://localhost:8081 - service: http_status:404这个配置的逻辑很简单cloudflared 监听来自 Cloudflare 边缘网络的请求根据请求的 Host 头部决定转发到本机的哪个端口。最后一条 catch-all 规则Service 为http_status:404表示不匹配任何规则的请求直接返回 404。这一步新手特别容易漏掉不写这条 catch-allcloudflared 会启动失败并提示 you must specify at least one origin rule。3.3 API Token 的安全存储整个 cloudflare-os 里最不能出问题的就是 API Token 的存储。Token 的权限要是过宽或者被泄露到不该出现的地方后果远比泄露一个 SSH 私钥严重因为 Token 控制的是一整个域名空间的 API 操作。我的做法首先是权限最小化。到 Cloudflare 面板的 My Profile 下的 API Tokens 页面创建 Token 时只勾选真正需要的权限比如 Zone DNS Edit、Zone Cache Purge Purge其他一律不勾。其次Token 不写进任何脚本文件的明文里而是统一放在/etc/cloudflare/credentials文件里权限设置为 600sudo mkdir -p /etc/cloudflare sudo tee /etc/cloudflare/credentials /dev/null EOF # Cloudflare API credentials for cloudflare-os CF_API_TOKENxxxxxxxxxxxxxxxxxxxxxx CF_ZONE_IDxxxxxxxxxxxxxxxxxxxxxx EOF sudo chmod 600 /etc/cloudflare/credentials脚本读取这个文件的方式是 source而不是在命令行里直接传参数。这样做的好处是任何脚本只要 source 一下这个文件就能拿到凭据但要查看 Token 本身必须 root 权限。另外我强烈建议不要用环境变量的方式把 Token 注入命令行因为 Linux 的/proc/{pid}/environ对同一用户是完全可读的。只要机器上有其他低权限用户就有泄露风险。相比之下一个 600 权限的文件安全得多。3.4 核心脚本的目录组织脚本目录的设计直接决定了这套系统好不好用。我的做法是分级管理根目录是/opt/cloudflare-os/下面分成四个子目录/opt/cloudflare-os/ ├── bin/ # 用户可以直接执行的命令 ├── lib/ # 脚本之间共享的函数库 ├── conf/ # 每个域名的配置参数 └── logs/ # 脚本运行日志之所以这么分是因为最开始我把所有脚本堆在一个目录里结果脚本之间互相依赖要想理解某个脚本怎么工作还得把整个目录都读一遍。分了层之后bin 下面的命令保持简洁无非是调用 lib 里的函数读取 conf 里的域名列表然后干活。4. 日常管理三板斧DNS、缓存、隧道状态4.1 DNS 批量管理脚本DNS 管理是这套系统里使用频率最高的功能。先看怎么用 API 查询某条记录。下面是核心逻辑脚本名字叫cf-dns-list#!/usr/bin/env bash set -euo pipefail source /etc/cloudflare/credentials ZONE_ID${1:?Usage: cf-dns-list zone_name} DNS_RECORDS$(curl -s -X GET https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records \ -H Authorization: Bearer ${CF_API_TOKEN} \ -H Content-Type: application/json) echo ${DNS_RECORDS} | jq -r .result[] | \(.name)\t\(.type)\t\(.content)\t\(.proxied)这里的-r参数很关键让 jq 直接输出纯文本而没有引号方便后续继续用 awk 等工具处理。而批量添加记录的场景更常见。比如现在有一个新的测试环境要上线需要给api、admin、static三个子域名都配上 A 记录指向203.0.113.10。在面板上手工操作要三遍而脚本只需要一个循环#!/usr/bin/env bash set -euo pipefail source /etc/cloudflare/credentials ZONE_ID${1:?Usage: cf-dns-create zone_name} IP${2:?Usage: cf-dns-create zone_name ip} shift 2 for prefix in $; do curl -s -X POST https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records \ -H Authorization: Bearer ${CF_API_TOKEN} \ -H Content-Type: application/json \ --data {\type\:\A\,\name\:\${prefix}\,\content\:\${IP}\,\ttl\:1,\proxied\:true} done关于 TTLCloudflare API 里ttl: 1表示自动模式也就是 TTL 由 Cloudflare 自动调整这个模式下普通记录通常是 300 秒。对大部分场景来说这就是最佳选择不用手动指定一个固定值。另外注意proxied: true这个参数它决定流量是否经过 Cloudflare 的 CDN 和防护。如果只是老实地配一条解析记录、不走 CDN那就设成 false。这一点写脚本时最容易搞混我的建议是默认写 true只有明确需要绕开 Cloudflare 时才改为 false。4.2 缓存清理与预热缓存清理是另一个高频操作。每次发布完新版本如果改动涉及 HTML 或 JS、CSS 这些静态资源旧的 CDN 缓存可能会让用户看到过期内容。以前我必须登录面板点进 Caching 页面点击 Purge Everything现在一条命令搞定#!/usr/bin/env bash set -euo pipefail source /etc/cloudflare/credentials ZONE_ID${1:?Usage: cf-cache-purge zone_name} curl -s -X POST https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache \ -H Authorization: Bearer ${CF_API_TOKEN} \ -H Content-Type: application/json \ --data {purge_everything:true} | jq .successCache Purge 的 API 有两个模式purge_everything表示清理整个域名下的全部缓存还有一个files模式可以只清理指定的 URL 列表。我两种都用。正常情况下上线时用purge_everything最省事但如果是大版本发布缓存量特别大全量清理会让所有静态资源同时回源可能会给源站造成瞬时压力。这种时候我会用files模式只清理具体变更的文件把回源压力控制在一个可控范围。4.3 隧道健康检查与自动重启cloudflared 隧道是一个常驻进程理论上很稳但只要是常驻进程就总会有意外。比如网络波动导致云上连接断开、本地进程被 OOM Killer 干掉、升级后配置不兼容等。与其等用户反馈说网站打不开了再手动处理不如写一个健康检查脚本配合 cron 定期跑。我的检测方法不是本地检查进程状态而是主动请求一条隧道的公网地址看 HTTP 状态码。本地进程存在不代表隧道通着只有从公网请求通到源站服务才说明链路真的没问题。脚本逻辑如下#!/usr/bin/env bash set -euo pipefail HEALTH_URL${1:?Usage: cf-tunnel-health public_url} HTTP_CODE$(curl -s -o /dev/null -w %{http_code} --max-time 10 ${HEALTH_URL}) # 2xx 和 3xx 都认为是正常 if [[ ${HTTP_CODE} -ge 200 ${HTTP_CODE} -lt 400 ]]; then echo $(date) OK: ${HTTP_CODE} /var/log/cloudflare-os/tunnel-health.log else echo $(date) FAIL: ${HTTP_CODE} /var/log/cloudflare-os/tunnel-health.log systemctl restart cloudflared fi注意这里用--max-time 10限制请求超时时间防止极端情况下 curl 卡死让检测脚本本身变成麻烦。cron 里的配置是每两分钟跑一次*/2 * * * * root /opt/cloudflare-os/bin/cf-tunnel-health https://app.example.com/health这个方案的缺点是至少会有一个 10 秒的延迟窗口但考虑到实际场景两分钟内的故障感知已经足够了。5. 踩过的坑:API权限、限频与路由冲突5.1 API Token 权限不足导致的 403这套系统开发过程中最浪费时间的坑就是 API Token 权限配置不完整导致的 403 报错。表面现象很简单调用某个接口返回{success:false,errors:[{code:9109,message:Invalid access token}]}。但如果 Token 本身格式正确只是权限不够返回的错误又不一样可能是 9109Invalid access token也可能是 9104具体取决于服务类型。我的建议是遇到权限相关报错不要急着怀疑 Token 写错了先到 API Tokens 页面检查这个 Token 的权限范围。以我自己的经历为例最初创建的 Token 只勾选了 Zone DNS Edit用了一段时间后想在脚本里加缓存清理功能直接对 purge_cache 接口发请求结果一直 403。排查链路是这样的先确认 Token 没写错curl 请求头无误接着把 API 返回的错误码复制到 Cloudflare 官方文档里搜索文档明确提到 9109 可能由多种原因导致其中一条是 Token 没有对应权限。再去面板看 Token 权限发现果然没有 Zone Cache Purge。解决办法简单粗暴创建一个新 Token把 DNS Edit 和 Cache Purge 都勾上问题解决。5.2 限频 429 与重试退避Cloudflare API v4 的速率限制是按账户维度的不是按 Zone 维度。对于大多数免费套餐用户来说每 5 分钟大约 1200 次请求的限制日常脚本很难触发。但一旦你写了一个批量创建 DNS 记录的脚本在一个循环里创建几百条记录就很有可能踩线。踩线后的返回码是 429同时响应头里会有Retry-After字段告诉你要等多少秒。这个字段很关键。我最初写的脚本没有处理限频逻辑导致批量迁移 DNS 时中途抛出一堆 429脚本直接挂掉还得手动重跑。后来我把重试逻辑统一放到了 lib 里的一个函数function cf_api_request() { local method$1 local path$2 local data${3:-} for attempt in {1..5}; do response$(curl -s -X ${method} https://api.cloudflare.com/client/v4${path} \ -H Authorization: Bearer ${CF_API_TOKEN} \ -H Content-Type: application/json \ --data ${data}) local retry_after retry_after$(echo ${response} | jq -r .errors[0].meta.retry_after // empty) if [[ -n ${retry_after} ]]; then echo Rate limited, waiting ${retry_after}s (attempt ${attempt}) sleep ${retry_after} continue fi break done }这个函数的核心思想是每次请求后检查响应体里是否有重试提示有就 sleep 对应时间再重试最多试 5 次。注意Retry-After这个值不一定在 header 里API 的 JSON 错误信息中有时也有。我在代码里两边都没有写死但逻辑上优先从 JSON 里提取。5.3 泛解析记录与 Tunnel 路由的优先级冲突这是我在使用过程中遇到的最隐蔽的坑也最值得单独拿出来说。你想通过 Tunnel 把dev.example.com和app.example.com都转发到本机不同端口同时 DNS 里有一条*.example.com的泛解析记录指向某个服务器 IP。配置好一切后访问dev.example.com和app.example.com都不走 Tunnel 路由而是直接解析到泛解析记录指向的那台服务器。原因在于 Cloudflare 的解析逻辑泛解析记录wildcard record会匹配所有未显式定义的子域名。Cloudflare 面板上任何子域名的流量走向是以是否有显式 DNS 记录为优先排序的。如果你给某个子域名建了一条显式 A 记录指向某个 IP同时又在 Tunnel 里配置了同名的 hostnameTunnel 配置不会覆盖显式 DNS 记录。除非你把那条显式记录删掉或者把它的代理状态设为仅 DNS即灰色云朵交给 Tunnel 规则去接管。我当时排查了很久一直以为是 cloudflared 的配置问题反复检查 config.yml 里的 hostname 和 service 映射确定无误。最后打开面板的 DNS 页面才发现dev.example.com下静静躺着一列泛解析红色标记。删掉显式 A 记录后Tunnel 马上生效。这个坑的教训就是先看 DNS 记录再查 Tunnel 配置。DNS 的显式记录优先级永远高于模糊匹配。5.4 多账户切换时的凭据管理最后一个值得一提的经验是多账号管理。如果你跟我一样同时帮几个客户维护 Cloudflare 账号每个账号对应不同的域名和 Token那么/etc/cloudflare/credentials这个单一文件的方案就不够用了。我的做法是在 conf 目录里为每个账号建立一个子目录以域名或客户名为标识然后在脚本中增加一个账号参数指定读取哪个凭据文件。比如/opt/cloudflare-os/conf/ ├── client-a/ │ ├── credentials │ └── zones.list └── client-b/ ├── credentials └── zones.list每个子目录里都有一个结构相同但内容不同的 credentials 文件。这样操作时只需要在命令里传入客户标识脚本内部根据标识选择对应的凭据和域名列表。当然这也带来了安全隐患目录数量一多权限管理就容易出问题。我的建议是在整个/opt/cloudflare-os/conf/目录上设置严格的 owner 和 group然后 group 只包含有权限的管理员账号。6. 进阶玩法:接入 Workers 自动化与监控告警6.1 用 Wrangler CLI 批量部署 WorkerDNS 和缓存之外Workers 也是云端自动化的重要环节。cloudflare-os 里预装了 wrangler 命令行工具用于部署和管理 Cloudflare Workers。wrangler 可以通过 npm 安装也可以直接用官方二进制发行版。考虑到这个系统的使用者不一定装 Node.js我推荐用官方独立二进制的方式这样少一个依赖。部署 Worker 的基本流程是把 Worker 代码放在一个固定的工作目录然后用 wrangler 的部署命令发布。在 ci/cd 里最常用的是通过 API Token 的方式认证而 wrangler 支持用环境变量读取 Cloudflare API Token不需要做交互式登录。6.2 通过 API 读取站点指标并做简单告警Cloudflare API v4 提供了一系列站点分析数据接口可以获取请求量、带宽、缓存命中率等基础指标。这套系统里最有价值的脚本之一就是从 API 拉取这些指标再配合简单的判断逻辑做告警。比如某个站点的请求量突然暴跌或者 5xx 错误比例异常飙升脚本就往运维通知渠道里发一条消息。这里提示一个关键细节Cloudflare 分析类接口对传入的时间参数有格式要求泛解析记录和显式记录的优先级冲突、API 权限检查这些表面的细节其实背后反映的是整个 Cloudflare 边缘网络的逻辑:记录解析的优先级、Token 权限模型的边界、API 限频的配额机制。理解了这些机制脚本只是把这些规则自动化了而已这个系统才真正变得好用了。6.3 后续扩展方向这套系统目前已经能覆盖我日常 90% 的 Cloudflare 操作但远没到完善的程度。我接下来想做的几件事也算给读者提供一个扩展思路。第一是引入配置管理工具把/opt/cloudflare-os/下的所有脚本和配置纳入版本管理这样即使整台机器故障也能快速重建。第二是进一步抽象 DNS 记录管理增加导入导出功能方便在不同账号之间迁移解析配置。第三是把健康检查的告警渠道从简单的日志扩展到消息通知服务真正做到故障发生时第一时间感知。不过说实话这些扩展都不是最核心的。最核心的还是那件事理解 Cloudflare 的 API 设计逻辑把它变成你自己的工具。cloudflare-os 本质上是一个载体它承载的是我用命令行管理 Cloudflare 服务的完整工作流。如果你也面临类似的场景不妨从最小的一两个脚本开始慢慢搭建属于你自己的工具集这个过程的收获会比你想象的大得多。