使用原生脚本实现高可用 Cloudflare 动态 DNS (DDNS) 解析

引言:动态IP环境下的域名解析挑战
在基础设施运维中,服务器拥有动态公网IPv4地址(如住宅宽带、某些VPS服务商的NAT产品)是常见的挑战。DNS记录无法与变化的IP保持同步,将直接导致服务中断。Cloudflare 强大的API为这一难题提供了优雅的解决方案。本文将介绍一套经过生产环境验证的、基于原生Bash脚本和Systemd Timer的Cloudflare DDNS自动更新方案,该方案具备高可靠性、错误处理及详尽的日志记录。
第一部分:配置 Cloudflare API 访问凭证
安全是自动化运维的基石。Cloudflare API Tokens 提供了比全局API密钥更细粒度的权限控制。
-
生成专用API令牌
- 登录 Cloudflare仪表板,点击右上角头像,进入 「我的个人资料」 。
- 切换至 「API令牌」 标签页,点击 「创建令牌」。
- 我们采用 「编辑区域 DNS」 模板,它已预置了所需的最小权限集:
Zone.Zone - 读取Zone.DNS - 编辑
- 在 「区域资源」 部分,选择需要应用此令牌的特定域名(建议),或选择 「所有区域」。
- 确认配置后,点击 「创建令牌」。
-
安全存储令牌 将生成的令牌妥善保存至服务器上的安全路径,并严格限制其访问权限。此令牌仅显示一次。
sudo mkdir -p /etc/cloudflare-ddns echo "您的_API_令牌_字符串" | sudo tee /etc/cloudflare-ddns/token sudo chmod 600 /etc/cloudflare-ddns/token
/etc/cloudflare-ddns/token 文件权限应仅为 root 可读 (chmod 600)。切勿将其提交至版本控制系统或嵌入公开脚本。第二部分:部署智能 DDNS 更新脚本
此脚本的设计遵循了运维最佳实践:幂等性(无论运行多少次,结果一致)、完善的错误处理以及清晰的日志输出。
-
创建脚本文件 在
/usr/local/bin/目录下创建可执行脚本,这是存放本地自定义命令的标准位置。sudo nano /usr/local/bin/cloudflare-ddns.sh -
脚本内容如下 将以下脚本内容复制到文件中。请务必替换
ZONE_NAME和RECORD_NAME变量为您自己的域名和记录。#!/usr/bin/env bash # cloudflare-ddns.sh # example:把 tw.19910812.xyz 的 A 记录更新为当前公网 IPv4(若不存在则创建) # 依赖:curl, jq set -euo pipefail CF_API="https://api.cloudflare.com/client/v4" ZONE_NAME="your_zone_id" # example: 19910812.xyz RECORD_NAME="your_record_name.example.com" # example: tw.19910812.xyz TOKEN_FILE="/etc/cloudflare-ddns/token" if [[ ! -r "$TOKEN_FILE" ]]; then echo "Token file $TOKEN_FILE 不存在或不可读" >&2 exit 1 fi CF_TOKEN=$(cat "$TOKEN_FILE") # 获取当前公网 IPv4(可替换为其它服务) PUBLIC_IP=$(curl -fsS https://api.ipify.org) if [[ -z "$PUBLIC_IP" ]]; then echo "无法获取公网 IP" >&2 exit 1 fi # 1) 获取 zone id ZONE_ID=$(curl -fsS -X GET "$CF_API/zones?name=$ZONE_NAME" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json" \ | jq -r '.result[0].id // empty') if [[ -z "$ZONE_ID" ]]; then echo "未找到 zone $ZONE_NAME,请确认 token 是否有权限或 zone 名称是否正确" >&2 exit 1 fi # 2) 查询目标记录(A) RECORD_JSON=$(curl -fsS -X GET "$CF_API/zones/$ZONE_ID/dns_records?name=$RECORD_NAME&type=A" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json") RECORD_ID=$(echo "$RECORD_JSON" | jq -r '.result[0].id // empty') DNS_IP=$(echo "$RECORD_JSON" | jq -r '.result[0].content // empty') # 如果记录存在,比较 IP;如果不同则更新 if [[ -n "$RECORD_ID" ]]; then if [[ "$PUBLIC_IP" == "$DNS_IP" ]]; then echo "$(date +'%F %T') IP 未变 ($PUBLIC_IP),不需要更新" exit 0 else echo "$(date +'%F %T') IP 变化:$DNS_IP -> $PUBLIC_IP,正在更新..." UPDATE_PAYLOAD=$(jq -n --arg t "A" --arg n "$RECORD_NAME" --arg c "$PUBLIC_IP" '{type:$t,name:$n,content:$c,ttl:120,proxied:false}') RESP=$(curl -fsS -X PUT "$CF_API/zones/$ZONE_ID/dns_records/$RECORD_ID" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json" \ --data "$UPDATE_PAYLOAD") OK=$(echo "$RESP" | jq -r '.success') if [[ "$OK" == "true" ]]; then echo "$(date +'%F %T') 更新成功: $PUBLIC_IP" exit 0 else echo "$(date +'%F %T') 更新失败: $RESP" >&2 exit 1 fi fi else # 记录不存在 -> 创建 echo "$(date +'%F %T') 记录不存在,正在创建 A 记录 $RECORD_NAME -> $PUBLIC_IP" CREATE_PAYLOAD=$(jq -n --arg t "A" --arg n "$RECORD_NAME" --arg c "$PUBLIC_IP" '{type:$t,name:$n,content:$c,ttl:120,proxied:false}') RESP=$(curl -fsS -X POST "$CF_API/zones/$ZONE_ID/dns_records" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json" \ --data "$CREATE_PAYLOAD") OK=$(echo "$RESP" | jq -r '.success') if [[ "$OK" == "true" ]]; then echo "$(date +'%F %T') 创建成功: $PUBLIC_IP" exit 0 else echo "$(date +'%F %T') 创建失败: $RESP" >&2 exit 1 fi fi -
设置脚本权限并首次测试
sudo chmod +x /usr/local/bin/cloudflare-ddns.sh sudo /usr/local/bin/cloudflare-ddns.sh执行后,观察输出。成功的话,您的Cloudflare DNS区域中将立即出现或更新对应的A记录。
第三部分:使用 Systemd Timer 实现可靠定时任务
相较于传统的Cron,Systemd Timer 提供更精细的调度控制、更好的日志集成(通过 journalctl)以及与系统服务的依赖管理。
-
创建 Service 单元文件 (
/etc/systemd/system/cloudflare-ddns.service) 此文件定义了 “要执行的任务”。[Unit] Description=Cloudflare DDNS Updater Service After=network-online.target Wants=network-online.target [Service] Type=oneshot User=root ExecStart=/usr/local/bin/cloudflare-ddns.sh # 增强日志记录,将输出同时送入系统日志 StandardOutput=journal StandardError=journalAfter=network-online.target确保了仅在网络就绪后执行脚本。 -
创建 Timer 单元文件 (
/etc/systemd/system/cloudflare-ddns.timer) 此文件定义了 “何时执行任务”。[Unit] Description=Run Cloudflare DDNS Updater every 5 minutes [Timer] OnBootSec=1min OnUnitActiveSec=5min Persistent=true # 可添加随机延迟,避免所有客户端同时请求 RandomizedDelaySec=30s [Install] WantedBy=timers.targetOnBootSec=1min: 系统启动后1分钟运行第一次。OnUnitActiveSec=5min: 在上次任务激活成功后,每5分钟运行一次。Persistent=true: 如果服务器在计划运行时间点处于关机状态,开机后会尽快补执行一次。
-
启用并启动定时器
sudo systemctl daemon-reload sudo systemctl enable --now cloudflare-ddns.timer -
验证定时器状态
# 查看定时器状态 sudo systemctl status cloudflare-ddns.timer # 查看最近的服务执行日志 sudo journalctl -u cloudflare-ddns.service -n 20 -f
总结与高级应用场景
本方案已在 Debian 12 等主流 Linux 发行版上经过长期验证,稳定可靠。
核心应用场景包括:
- 动态公网IPv4环境:如HKT、HiNet等提供的NAT VPS,其出口IP可能周期性变化。本方案可确保域名始终指向有效的IP。
- 无缝机房迁移:对于使用 搬瓦工多机房切换 等服务的用户,在切换数据中心导致IP变更后,DDNS脚本能自动将域名解析至新IP,实现用户无感知的迁移,极大提升服务的可用性。
- 混合云及边缘计算:在IP不固定的边缘节点部署服务,通过DDNS提供统一的访问入口。
运维价值:
- 自动化:免除手动更新DNS记录的操作负担与人为失误风险。
- 高可用:分钟级的IP同步延迟,最大化服务在线时间。
- 可观测性:通过Systemd Journal,所有执行记录、IP变更历史清晰可查。
您可以根据实际需求,调整脚本中的 TTL 值或Timer的执行频率