用Terraform接管Cloudflare:从手工配置到基础设施即代码
Table of Contents
前
在我的多个域名和项目中,Cloudflare一直是我的首选CDN和DNS服务。但随着域名和配置规则的增加,我遇到了一个痛点:手工在Web控制台配置既繁琐又容易出错,而且无法追踪配置变更历史。
当我需要为新项目复制类似的配置时,只能在控制台一条条手工复制。当某个配置出问题时,很难回溯是什么时候、为什么改的。这种"点击式运维"显然不是长久之计。
于是我决定用Terraform将Cloudflare的所有配置纳入版本管理。这篇文章记录了我从手工配置迁移到IaC(Infrastructure as Code)的完整过程,以及踩过的坑。
为什么选择Terraform
什么是IaC
基础设施即代码(Infrastructure as Code)是一种通过代码来定义、部署和管理基础设施的方法。相比传统的手工配置,IaC带来以下优势:
- 版本控制:所有配置纳入Git,可以追踪每次变更
- 可复现:同样的代码可以在不同环境重复部署
- 自动化:通过CI/CD自动应用配置变更
- 协作友好:团队成员通过代码协作,而非共享账号
- 防止配置漂移:代码即文档,状态可审计
Terraform vs 其他工具
在IaC工具中,我选择Terraform的原因:
Terraform的优势:
- 多云支持:同一套工具管理Cloudflare、AWS、阿里云等
- 声明式语法:描述"想要什么状态",而非"如何达到"
- 状态管理:自动追踪实际状态与期望状态的差异
- 丰富的Provider:Cloudflare官方维护Provider,支持几乎所有功能
- 社区活跃:问题都能找到解决方案
对比其他方案:
- Cloudflare API:需要自己写脚本,维护成本高
- Pulumi:支持编程语言,但学习曲线陡峭,社区较小
- Ansible:更适合配置管理,不适合声明式基础设施
- ClickOps(手工点击):最简单,但无法规模化
对于管理Cloudflare这样的场景,Terraform的声明式语法和状态管理能力是最佳选择。
环境准备
安装Terraform
在macOS上通过Homebrew安装:
brew install terraform
# 验证安装
terraform version
# Terraform v1.6.0
在Linux上:
# Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
获取Cloudflare API Token
在Cloudflare控制台创建API Token:
- 访问:https://dash.cloudflare.com/profile/api-tokens
- 点击"Create Token"
- 使用"Edit zone DNS"模板,或自定义权限:
- Zone - DNS - Edit
- Zone - Zone - Read
- Zone - Zone Settings - Edit
- 选择需要管理的域名
- 创建后保存Token(只显示一次)
将Token保存到环境变量:
# 添加到 ~/.zshrc 或 ~/.bashrc
export CLOUDFLARE_API_TOKEN="your_api_token_here"
# 或者使用专门的环境变量文件
echo 'CLOUDFLARE_API_TOKEN=your_token' > .env
source .env
这里需要注意的是,不要将Token硬编码在代码中,也不要提交到Git仓库。
创建项目结构
我的Terraform项目结构:
cloudflare-tf/
├── main.tf # 主配置文件
├── variables.tf # 变量定义
├── terraform.tfvars # 变量值(不提交到Git)
├── outputs.tf # 输出定义
├── versions.tf # Provider版本锁定
└── modules/ # 可复用的模块(可选)
├── dns/
└── firewall/
创建项目目录:
mkdir cloudflare-tf
cd cloudflare-tf
从零开始配置
配置Provider
创建 versions.tf 定义Terraform和Provider版本:
terraform {
required_version = ">= 1.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "cloudflare" {
# API token通过环境变量 CLOUDFLARE_API_TOKEN 提供
}
初始化Terraform:
terraform init
这会下载Cloudflare Provider插件。
获取Zone ID
创建 data.tf 获取域名的Zone ID:
# 通过域名查询Zone信息
data "cloudflare_zone" "main" {
name = "example.com"
}
# 输出Zone ID供其他资源使用
output "zone_id" {
value = data.cloudflare_zone.main.id
}
查看Zone ID:
terraform plan
管理DNS记录
在 main.tf 中添加DNS记录:
# A记录
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.main.id
name = "www"
value = "192.0.2.1"
type = "A"
proxied = true # 通过Cloudflare代理
ttl = 1 # 代理时TTL自动设为1
}
# CNAME记录
resource "cloudflare_record" "blog" {
zone_id = data.cloudflare_zone.main.id
name = "blog"
value = "example.com"
type = "CNAME"
proxied = true
}
# TXT记录(SPF验证)
resource "cloudflare_record" "spf" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "v=spf1 include:_spf.google.com ~all"
type = "TXT"
proxied = false
}
# MX记录
resource "cloudflare_record" "mx" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "aspmx.l.google.com"
type = "MX"
priority = 1
proxied = false
}
应用配置:
terraform plan # 预览变更
terraform apply # 应用变更
配置页面规则
Page Rules可以控制缓存、重定向等行为:
resource "cloudflare_page_rule" "redirect_http" {
zone_id = data.cloudflare_zone.main.id
target = "http://*example.com/*"
priority = 1
actions {
always_use_https = true
}
}
resource "cloudflare_page_rule" "cache_static" {
zone_id = data.cloudflare_zone.main.id
target = "example.com/static/*"
priority = 2
actions {
cache_level = "cache_everything"
edge_cache_ttl = 7200
}
}
resource "cloudflare_page_rule" "bypass_api" {
zone_id = data.cloudflare_zone.main.id
target = "api.example.com/*"
priority = 3
actions {
cache_level = "bypass"
}
}
配置防火墙规则
使用Firewall Rules保护你的网站:
# 阻止特定国家的访问
resource "cloudflare_filter" "block_countries" {
zone_id = data.cloudflare_zone.main.id
description = "Block specific countries"
expression = "(ip.geoip.country in {\"CN\" \"RU\"})"
}
resource "cloudflare_firewall_rule" "block_countries" {
zone_id = data.cloudflare_zone.main.id
description = "Block traffic from specific countries"
filter_id = cloudflare_filter.block_countries.id
action = "block"
}
# 速率限制API端点
resource "cloudflare_rate_limit" "api_limit" {
zone_id = data.cloudflare_zone.main.id
threshold = 100
period = 60
match {
request {
url_pattern = "api.example.com/v1/*"
}
}
action {
mode = "challenge"
}
}
SSL/TLS配置
配置SSL模式和证书:
resource "cloudflare_zone_settings_override" "example" {
zone_id = data.cloudflare_zone.main.id
settings {
# SSL模式
ssl = "strict" # off, flexible, full, strict
# 最小TLS版本
min_tls_version = "1.2"
# 启用HTTP/3
http3 = "on"
# 启用0-RTT
zero_rtt = "on"
# 始终使用HTTPS
always_use_https = "on"
# 自动HTTPS重写
automatic_https_rewrites = "on"
# Brotli压缩
brotli = "on"
}
}
导入已有配置
如果你已经在Cloudflare控制台配置了很多资源,可以使用导入功能将它们纳入Terraform管理。
手动导入单个资源
1. 查找资源ID
在Cloudflare API文档中查找资源ID的获取方法,或使用浏览器开发者工具查看网络请求。
例如,查找DNS记录ID:
curl -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" | jq
2. 在Terraform中定义资源
在 main.tf 中添加资源定义(先不填具体值):
resource "cloudflare_record" "imported_www" {
zone_id = data.cloudflare_zone.main.id
name = "www"
value = "placeholder"
type = "A"
proxied = true
}
3. 导入资源状态
terraform import cloudflare_record.imported_www ${ZONE_ID}/${RECORD_ID}
4. 查看实际配置
terraform state show cloudflare_record.imported_www
根据输出更新 .tf 文件中的资源定义。
使用Terraformer批量导入
Terraformer可以批量导入已有资源,但在我的实践中遇到了一些问题。
安装Terraformer:
# macOS
brew install terraformer
# 或手动下载
wget https://github.com/GoogleCloudPlatform/terraformer/releases/download/0.8.24/terraformer-cloudflare-darwin-amd64
chmod +x terraformer-cloudflare-darwin-amd64
mv terraformer-cloudflare-darwin-amd64 /usr/local/bin/terraformer
导入Cloudflare配置:
export CLOUDFLARE_API_TOKEN="your_token"
export CLOUDFLARE_ZONE_ID="your_zone_id"
terraformer import cloudflare \
--resources=dns,page_rules,firewall_rules \
--zone=${CLOUDFLARE_ZONE_ID}
这会生成大量的 .tf 文件和状态文件。
迁移过程中的问题处理
问题1:Provider版本不兼容
现象:执行 terraform plan 时报错:
Error: Incompatible API version with plugin
Plugin version: 4, Client versions: [5]
原因:Terraformer生成的代码使用了旧版本的Provider定义。
解决:替换状态文件中的Provider引用:
terraform state replace-provider -auto-approve \
"registry.terraform.io/-/cloudflare" \
"registry.terraform.io/cloudflare/cloudflare"
并更新 versions.tf 中的Provider配置:
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0" # 使用最新的4.x版本
}
}
}
问题2:资源不兼容
现象:某些资源在新版本Provider中已废弃或API变更。
解决:从状态文件中移除不兼容的资源:
# 列出所有资源
terraform state list
# 移除单个资源
terraform state rm cloudflare_record.old_resource
# 批量移除(使用脚本)
terraform state list | grep "tfer--" | xargs -I {} terraform state rm {}
同时从 .tf 文件中删除对应的资源定义。
问题3:资源命名混乱
Terraformer生成的资源名称往往很长且无意义,例如:
resource "cloudflare_record" "tfer--A_diglp-002E-cf_4ef5c2378c973b50caeb9c229e99e0fe" {
# ...
}
解决:重命名资源以提高可读性:
# 在状态文件中重命名
terraform state mv \
'cloudflare_record.tfer--A_diglp-002E-cf_4ef5c2378c973b50caeb9c229e99e0fe' \
'cloudflare_record.www'
同时在 .tf 文件中更新资源名称。
问题4:资源依赖关系
某些资源之间有依赖关系,需要正确处理:
resource "cloudflare_filter" "rate_limit" {
zone_id = data.cloudflare_zone.main.id
expression = "(http.request.uri.path matches \"^/api/\")"
}
resource "cloudflare_firewall_rule" "rate_limit" {
zone_id = data.cloudflare_zone.main.id
filter_id = cloudflare_filter.rate_limit.id # 依赖Filter
action = "challenge"
}
如果依赖关系错误,使用 depends_on 显式声明:
resource "cloudflare_firewall_rule" "rate_limit" {
# ...
depends_on = [cloudflare_filter.rate_limit]
}
最佳实践
使用变量管理配置
将可变的值提取到 variables.tf:
variable "domain" {
description = "Primary domain name"
type = string
default = "example.com"
}
variable "server_ip" {
description = "Origin server IP"
type = string
}
variable "enable_ddos_protection" {
description = "Enable DDoS protection"
type = bool
default = true
}
在 terraform.tfvars 中赋值:
domain = "mysite.com"
server_ip = "192.0.2.1"
enable_ddos_protection = true
在资源中使用变量:
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.main.id
name = "www"
value = var.server_ip
type = "A"
proxied = true
}
这里需要注意的是,terraform.tfvars 包含敏感信息,应加入 .gitignore。
使用模块复用配置
对于多个域名的相似配置,可以创建可复用的模块。
创建 modules/domain/main.tf:
variable "domain" {
type = string
}
variable "server_ip" {
type = string
}
data "cloudflare_zone" "this" {
name = var.domain
}
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.this.id
name = "www"
value = var.server_ip
type = "A"
proxied = true
}
resource "cloudflare_record" "root" {
zone_id = data.cloudflare_zone.this.id
name = "@"
value = var.server_ip
type = "A"
proxied = true
}
在主配置中使用模块:
module "site1" {
source = "./modules/domain"
domain = "site1.com"
server_ip = "192.0.2.1"
}
module "site2" {
source = "./modules/domain"
domain = "site2.com"
server_ip = "192.0.2.2"
}
使用Remote State
对于团队协作,将状态文件存储在远程后端:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "cloudflare/terraform.tfstate"
region = "us-west-2"
# 启用状态锁定
dynamodb_table = "terraform-locks"
encrypt = true
}
}
或使用Terraform Cloud:
terraform {
cloud {
organization = "my-org"
workspaces {
name = "cloudflare-prod"
}
}
}
版本控制最佳实践
.gitignore 配置:
# Terraform状态文件(如果不用Remote State)
*.tfstate
*.tfstate.*
# 敏感变量
terraform.tfvars
*.auto.tfvars
# Terraform目录
.terraform/
.terraform.lock.hcl
# 崩溃日志
crash.log
# 临时文件
*.tfplan
提交规范:
# 每次变更前先格式化
terraform fmt
# 验证配置
terraform validate
# 提交前plan
terraform plan -out=tfplan
# 确认无误后提交
git add *.tf
git commit -m "feat: add DNS records for new subdomain"
CI/CD集成
在GitHub Actions中自动化Terraform:
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.0
- name: Terraform Init
run: terraform init
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
实战收益
自从使用Terraform管理Cloudflare后,我获得了以下收益:
效率提升
- 配置复用:新域名的配置从30分钟降低到5分钟(复制模块即可)
- 批量修改:修改多个域名的相同配置,只需改一处代码
- 错误减少:通过
terraform plan预览变更,避免误操作
可维护性
- 版本控制:所有配置变更都有Git历史记录
- 文档化:代码本身就是最准确的文档
- 回滚简单:
git revert+terraform apply即可回滚
协作友好
- 团队协作:通过PR进行配置变更审核
- 权限控制:不需要共享Cloudflare账号,通过API Token控制权限
- 知识传承:新成员通过代码快速了解基础设施
成本节约
虽然Terraform本身免费,但带来的时间节约是实实在在的:
- 手工配置:每个域名约30分钟
- Terraform:首次设置2小时,后续每个域名5分钟
- 10个域名就能收回投资
更重要的是,避免了因误操作导致的服务中断成本。
后
使用Terraform管理Cloudflare是我从ClickOps走向DevOps的重要一步。虽然初期有学习成本,但长期收益是显而易见的。
基础设施即代码的核心价值,不仅在于自动化,更在于将隐性知识(如何配置)转化为显性知识(代码),让基础设施变得可理解、可复现、可演进。
现在,我的所有Cloudflare配置都在Git仓库中,任何变更都要经过review和CI验证。这种确定性和可控性,是手工配置无法比拟的。
下一步优化
我计划在以下方面继续优化:
- 模块化:将常用配置封装成可复用的模块
- 自动化测试:使用Terratest编写基础设施测试
- 多环境管理:通过Workspace管理dev/staging/prod
- 监控集成:通过Terraform配置Cloudflare Analytics和日志推送
扩展阅读
如果你也想尝试IaC,推荐以下资源:
官方文档:
- Terraform文档:https://www.terraform.io/docs
- Cloudflare Provider文档:https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs
学习资源:
- Terraform入门教程:https://learn.hashicorp.com/terraform
- Terraformer导入工具:https://github.com/GoogleCloudPlatform/terraformer
替代方案:
- Pulumi:https://www.pulumi.com/ (支持真正的编程语言)
- OpenTofu:https://opentofu.org/ (Terraform的开源分支)
工具推荐:
- terraform-docs:自动生成模块文档
- tflint:Terraform代码静态分析
- checkov:安全和合规性扫描
就这样!希望这篇文章能帮助你开始用Terraform管理Cloudflare。记住,最好的学习方式是实践——选择一个小项目开始,逐步扩展你的IaC能力。