用Terraform接管Cloudflare:从手工配置到基础设施即代码

#Terraform #Cloudflare #IaC #DevOps

Table of Contents

在我的多个域名和项目中,Cloudflare一直是我的首选CDN和DNS服务。但随着域名和配置规则的增加,我遇到了一个痛点:手工在Web控制台配置既繁琐又容易出错,而且无法追踪配置变更历史。

当我需要为新项目复制类似的配置时,只能在控制台一条条手工复制。当某个配置出问题时,很难回溯是什么时候、为什么改的。这种"点击式运维"显然不是长久之计。

于是我决定用Terraform将Cloudflare的所有配置纳入版本管理。这篇文章记录了我从手工配置迁移到IaC(Infrastructure as Code)的完整过程,以及踩过的坑。

为什么选择Terraform

什么是IaC

基础设施即代码(Infrastructure as Code)是一种通过代码来定义、部署和管理基础设施的方法。相比传统的手工配置,IaC带来以下优势:

  1. 版本控制:所有配置纳入Git,可以追踪每次变更
  2. 可复现:同样的代码可以在不同环境重复部署
  3. 自动化:通过CI/CD自动应用配置变更
  4. 协作友好:团队成员通过代码协作,而非共享账号
  5. 防止配置漂移:代码即文档,状态可审计

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:

  1. 访问:https://dash.cloudflare.com/profile/api-tokens
  2. 点击"Create Token"
  3. 使用"Edit zone DNS"模板,或自定义权限:
    • Zone - DNS - Edit
    • Zone - Zone - Read
    • Zone - Zone Settings - Edit
  4. 选择需要管理的域名
  5. 创建后保存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验证。这种确定性和可控性,是手工配置无法比拟的。

下一步优化

我计划在以下方面继续优化:

  1. 模块化:将常用配置封装成可复用的模块
  2. 自动化测试:使用Terratest编写基础设施测试
  3. 多环境管理:通过Workspace管理dev/staging/prod
  4. 监控集成:通过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能力。