<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Terraform on 亂筆</title>
        <link>https://blog.l3zc.com/tags/terraform/</link>
        <description>Recent content in Terraform on 亂筆</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Tue, 14 Apr 2026 18:06:17 +0800</lastBuildDate><atom:link href="https://blog.l3zc.com/tags/terraform/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Terraform IaC 初体验</title>
            <link>https://blog.l3zc.com/2026/04/iac-with-terraform/</link>
            <pubDate>Thu, 09 Apr 2026 15:13:35 +0800</pubDate>
            <guid>https://blog.l3zc.com/2026/04/iac-with-terraform/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/cover_hu_a8e83f97ed61569c.webp&#34; alt=&#34;Featured image of post Terraform IaC 初体验&#34; /&gt;&lt;p&gt;Terraform 是一款 IaC 工具，所谓 IaC 就是「基础设施即代码」，将我们的基础设施以声明式的代码写出来，随后使用&lt;code&gt;terraform apply&lt;/code&gt;即可完成基础设施的部署，同一份配置，部署出来的一定是一模一样的基础设施（Nix OS 用户狂喜）。&lt;/p&gt;&#xA;&lt;h2 id=&#34;为什么要用-terraform&#34;&gt;为什么要用 Terraform&#xA;&lt;/h2&gt;&lt;p&gt;传统的基础设施管理绝大部分基于人力和各种云服务商的 Dashboard，这就带来了下面这些痛点：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;痛点&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;说明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;难以重现&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;通过 Dashboard 点点点进行配置，容易改错或者改漏，而且难以复现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;环境漂移&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;手动操作导致生产和测试环境配置逐渐偏离，导致极端情况下测试没问题生产直接崩&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;难以扩展&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;新增一套环境需要重复大量手工操作，耗时且容易出错&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;难以审计&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;没有变更记录，&lt;del&gt;出了事不好甩锅&lt;/del&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;难以协作&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;基础设施由少数「懂的人」掌控，谁想要改都得去找这些人，效率低&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;为了解决这些问题，「基础设施即代码」&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;的概念被提了出来，Terraform 就是其中一个著名的解决方案。&lt;/p&gt;&#xA;&lt;p&gt;假设一个现实的使用场景，例如你每年都会购买新的 GCP 的 $300 试用金账号，虽然便宜，但是每年都要重新跑到 GCP 的面板里开机器。而有了 Terraform，想要部署同样配置的机器，只要在每次更换账号之后，将 API Token 替换成新的，随后&lt;code&gt;terraform apply&lt;/code&gt;，只需要几分钟就能开出和你上个账号一模一样的机器、VPC、S3、防火墙配置等等。&lt;/p&gt;&#xA;&lt;p&gt;再比如 Cloudflare DNS 和 Tunnel 的管理和迁移，也只需要将原 Tunnel 的 Ingress Rule 复制到新 Tunnel 的 Ingress Rule。即使将来要迁移到 AliDNS、Route 53 等其他服务商，我们也可以原封不动的将数据复制过去&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;&#xA;&lt;p&gt;尤其是到了多人协作的时候，配合 Git，每次更改都有迹可循，更不需要担心合并冲突，PR 可以自动生成更改预览，出现问题可以立刻回滚到上一个版本，这些都是传统依赖人力去直接操作服务提供商 Dashboard 的做法完全无法做到的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的安装&#34;&gt;Terraform 的安装&#xA;&lt;/h2&gt;&lt;p&gt;Terraform 是用 Go 写的，编译出来的产物自然也是单个可执行文件，所以安装起来也非常容易，Windows 下可以直接使用 Winget 安装：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-pwsh&#34;&gt;winget install Hashicorp.Terraform&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果不想使用 Winget，也可以使用 Scoop 等其他包管理器，或者将预编译的二进制文件放入&lt;code&gt;$PATH&lt;/code&gt;，即可完成安装。&lt;/p&gt;&#xA;&lt;p&gt;如果你在 Linux 环境下，则使用对应的包管理器安装即可，如果使用将二进制文件放入&lt;code&gt;$PATH&lt;/code&gt;的安装方法，则要记得&lt;code&gt;sudo chmod +x terraform&lt;/code&gt;赋予文件的执行权限。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的两个基本概念&#34;&gt;Terraform 的两个基本概念&#xA;&lt;/h2&gt;&lt;h3 id=&#34;providers&#34;&gt;Provider(s)&#xA;&lt;/h3&gt;&lt;p&gt;我们前面已经提到，Terraform 是一个款础设施即代码工具，作为一个工具本身，他并不绑定某个平台，而是通过 Provider(s) 与各个平台对接，要查看有哪些 Provider(s)，可以浏览 &lt;a class=&#34;link&#34; href=&#34;https://registry.terraform.io/browse/providers&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Terraform 的 Registry&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image_hu_463ebc576c22c691.webp&#34; alt=&#34;Terraform 拥有丰富的 Provider(s) 生态&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;状态管理&#34;&gt;状态管理&#xA;&lt;/h3&gt;&lt;p&gt;Terraform 将每次执行基础设施变更操作时的状态信息保存在一个状态文件中，默认情况下会保存在当前工作目录下的&lt;code&gt;terraform.tfstate&lt;/code&gt;文件里&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;，我们也可以修改配置文件自行指定其他的存储后端，例如 S3、Postgres。每次我们执行&lt;code&gt;terraform apply&lt;/code&gt;时，Terraform 都会将目前配置文件声明的状态同现有的状态文件进行比对，从而计算变动的部分，自动确定调整顺序之后操作 Provider 落实状态变动。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的资源导入难题&#34;&gt;Terraform 的资源导入难题&#xA;&lt;/h2&gt;&lt;p&gt;现有资源的导入一直是 Terraform 为人诟病的难题，Hashi Corp. 似乎一直在坚持着一个固执且愚蠢的观点：你所有的基础设施从一开始就应当是用我们的 Terraform 创建的，所以也不存在什么资源导入的问题。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;时间&lt;/th&gt;&#xA;          &lt;th&gt;版本&lt;/th&gt;&#xA;          &lt;th&gt;进展&lt;/th&gt;&#xA;          &lt;th&gt;问题&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2014–2022&lt;/td&gt;&#xA;          &lt;td&gt;v0.x–v1.4&lt;/td&gt;&#xA;          &lt;td&gt;只有 &lt;code&gt;terraform import&lt;/code&gt;，一次一条，而且完全不生成配置&lt;/td&gt;&#xA;          &lt;td&gt;导入完还得自己手写 HCL&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2023.06&lt;/td&gt;&#xA;          &lt;td&gt;v1.5&lt;/td&gt;&#xA;          &lt;td&gt;引入 &lt;code&gt;import&lt;/code&gt; 块和 &lt;code&gt;-generate-config-out&lt;/code&gt;参数，可以生成配置了&lt;/td&gt;&#xA;          &lt;td&gt;然而还是得一条一条写，而且还得自己提供现有资源的 ID&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2024.01&lt;/td&gt;&#xA;          &lt;td&gt;v1.7&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;import&lt;/code&gt; 块支持 &lt;code&gt;for_each&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;批量了，但 ID 还得自己搞&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2024 下半年&lt;/td&gt;&#xA;          &lt;td&gt;v1.12&lt;/td&gt;&#xA;          &lt;td&gt;引入了 &lt;code&gt;terraform query&lt;/code&gt; 和 &lt;code&gt;list&lt;/code&gt; 块，终于实现了自动发现资源的功能&lt;/td&gt;&#xA;          &lt;td&gt;然而这个功能要 Provider 要自己实现，大量 Provider 根本没跟上&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这么多年的时间，社区一直在骂，然而，「我有一堆现有资源，怎么导入 Terraform」这个问题一直没有被认真对待。Terraform 作为「基础设施即代码」工具，设计哲学就是声明式和幂等性&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;，Hashi Corp. 他们笃信「一切以代码声明的状态为准，就应该用 Terraform 从零开始创建资源」。但现实是绝大多数公司都有大量历史存量资源，先有资源、后有代码才是常态。这个矛盾 Hashi Corp. 承认得很晚，&lt;code&gt;v1.12&lt;/code&gt; 的 &lt;code&gt;terraform query&lt;/code&gt; 才算是官方第一次认真面对这个问题——但 Provider 生态的跟进嘛……真的是一言难尽。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的文件结构&#34;&gt;Terraform 的文件结构&#xA;&lt;/h2&gt;&lt;p&gt;Terraform 的文件结构很简单，其主程序运行时会无脑读取工作目录下所有的&lt;code&gt;.tf&lt;/code&gt;文件，只要信息齐全，想给文件取什么名字都可以，比如我的文件结构就长这样：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;❯ tree -a -I .git&#xA;.&#xA;├── .editorconfig&#xA;├── .github&#xA;│   ├── dependabot.yml&#xA;│   └── workflows&#xA;│       ├── terraform-apply.yml&#xA;│       ├── terraform-plan.yml&#xA;│       └── your-fork.yml&#xA;├── .gitignore&#xA;├── .terraform.lock.hcl&#xA;├── cf_dns_zones.tf&#xA;├── cf_tunnel.tf&#xA;├── dns_example_com.tf&#xA;├── dns_example_net.tf&#xA;├── dns_example_top.tf&#xA;├── dns_example_cn.tf&#xA;├── main.tf                 # 基础配置（terraform 块）&#xA;├── moved.tf                # 移动过的资源&#xA;├── provider.tf             # 每个 Provider 的配置&#xA;├── README.md&#xA;├── rename_resources.ps1&#xA;└── variables.tf            # 自定义的变量&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt;用于存放基础配置:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;terraform {&#xA;  required_providers {&#xA;    cloudflare = {&#xA;      source  = &#34;cloudflare/cloudflare&#34;&#xA;      version = &#34;~&gt; 5&#34;&#xA;    }&#xA;&#xA;    tencentcloud = {&#xA;      source  = &#34;tencentcloudstack/tencentcloud&#34;&#xA;      version = &#34;&gt;= 1.81.43&#34;&#xA;    }&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;provider.tf&lt;/code&gt;用于存放每个 Provider 的配置：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;provider &#34;cloudflare&#34; {}&#xA;provider &#34;tencentcloud&#34; {}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里留空是因为我们可以通过环境变量来传递 Credentials，而不是直接将 Credentials 写在配置文件中，例如 Cloudflare 的 Provider 就接受&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;作为&lt;code&gt;api_token&lt;/code&gt;这个变量的替代。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;variables.tf&lt;/code&gt;用于声明自定义的变量：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;variable &#34;cloudflare_zone_example_com&#34; {&#xA;  description = &#34;Cloudflare zone ID for example.com&#34;&#xA;  type        = string&#xA;}&#xA;&#xA;variable &#34;cloudflare_zone_example_top&#34; {&#xA;  description = &#34;Cloudflare zone ID for example.top&#34;&#xA;  type        = string&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这些变量可以通过前缀为&lt;code&gt;TF_VAR_&lt;/code&gt;的环境变量传入，Terraform 也会自动读取&lt;code&gt;terraform.tfvars&lt;/code&gt;文件中的变量，变量的主要用途是在别的配置文件中调用，例如：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_dns_record&#34; &#34;example_cname&#34; {&#xA;  content = &#34;${cloudflare_zero_trust_tunnel_cloudflared.Production_Tunnel.id}.cfargotunnel.com&#34;&#xA;  name    = &#34;example.example.com&#34;&#xA;  proxied = true&#xA;  tags    = []&#xA;  ttl     = 1&#xA;  type    = &#34;CNAME&#34;&#xA;  zone_id = var.cloudflare_zone_id_example_com  #这里调用了cloudflare_zone_example_com这个变量&#xA;  settings = {&#xA;    flatten_cname = false&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;有了这些基础配置，我们就可以运行&lt;code&gt;terraform init&lt;/code&gt;初始化 Terraform 环境并锁定依赖版本，剩下的就都是我们资源的声明了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;将现有资源导入-terraform&#34;&gt;将现有资源导入 Terraform&#xA;&lt;/h2&gt;&lt;p&gt;前文提到过 Terraform 的状态管理这个概念，状态管理虽好，但在我们初始化时也带来了一个问题——在默认状态下，Terraform 的状态文件显然是空的。&lt;/p&gt;&#xA;&lt;p&gt;此时我们需要做的是将云服务提供商上现有资源的状态导入 Terraform，这样 Terraform 才能无缝接手并管理我们的基础设施。相信大家也都看到了前文对 Terraform 资源导入问题的吐槽。以导入 Cloudflare 上托管的 DNS 记录为例，前文提到过，如果 Provider 支持，可以使用 Terraform 在 1.12 版本之后引入的&lt;code&gt;terraform query&lt;/code&gt;，然而大多数情况下，Providers 都是没有跟进这个新功能的，Cloudflare 就属于不支持的那一类。&lt;/p&gt;&#xA;&lt;p&gt;好在虽然 Hashi Corp. 不认真解决问题，各路高强度使用 Terraform 管理基础设施的公司就各显神通。Cloudflare 就维护了名为 &lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/cf-terraforming&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;cf-terraforming&lt;/a&gt; 的导入工具，免去了很多手动导入的麻烦。首先安装&lt;code&gt;cf-terraforming&lt;/code&gt;，这个工具是用 Go 语言编写的，需要在有 Go 语言环境的情况下安装，或者将官方编译好的二进制文件其放入&lt;code&gt;$PATH&lt;/code&gt;并赋予其执行权限。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个工具的用法还是比较简单的，首先你需要以下环境变量：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# 如果你使用 API Token&#xA;export CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&#xA;&#xA;# 如果你使用 API Key&#xA;export CLOUDFLARE_EMAIL=&#39;user@example.com&#39;&#xA;export CLOUDFLARE_API_KEY=&#39;1150bed3f45247b99f7db9696fffa17cbx9&#39;&#xA;&#xA;# 指定需要导入的域名的区域 ID，如果导入的是账户资源（例如 Cloudflare Tunnel）则不需要&#xA;export CLOUDFLARE_ZONE_ID=&#39;81b06ss3228f488fh84e5e993c2dc17&#39;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote class=&#34;alert alert-tip&#34;&gt;&#xA;        &lt;div class=&#34;alert-header&#34;&gt;&#xA;            &lt;span class=&#34;alert-icon&#34;&gt;💡&lt;/span&gt;&#xA;            &lt;span class=&#34;alert-title&#34;&gt;提示&lt;/span&gt;&#xA;        &lt;/div&gt;&#xA;        &lt;div class=&#34;alert-body&#34;&gt;&#xA;            &lt;p&gt;此处的命令假设你正在使用 Bash，如果使用的是与 Bash 语法不兼容的 Shell，则需要做出调整，例如对于 Windows 上的 PowerShell，导入环境变量的语法如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-pwsh&#34;&gt;$env:CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&lt;/code&gt;&lt;/pre&gt;&#xA;        &lt;/div&gt;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;通常我们只需要设置&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;和&lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt;就可以了，在控制台创建 API Token 时，记得赋予这个 Token 必要的权限，本次我们只是导入 DNS 记录，所以只赋予编辑区域 DNS 的权限即可。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image-1_hu_15298d4bd366a377.webp&#34; alt=&#34;赋予操作资源所需要的权限&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;好了，准备工作全部完成，现在可以开始生成配置文件：&lt;/p&gt;&#xA;&lt;p&gt;首先导入账户中域名的配置，也就是&lt;code&gt;cloudflare_zone&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming generate \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --resource-type &#34;cloudflare_zone&#34; &gt; zone.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这一步会在当前目录下生成一个名为&lt;code&gt;zone.tf&lt;/code&gt;的文件，里面会有如下格式的内容：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_zone&#34; &#34;REDACTED&#34; {&#xA;  name                = &#34;REDACTED&#34;&#xA;  paused              = false&#xA;  type                = &#34;full&#34;&#xA;  vanity_name_servers = []&#xA;  account = {&#xA;    id   = &#34;REDACTED&#34;&#xA;    name = &#34;REDACTED&#34;&#xA;  }&#xA;}&#xA;&#xA;resource &#34;cloudflare_zone&#34; &#34;REDACTED&#34; {&#xA;  name                = &#34;REDACTED&#34;&#xA;  paused              = false&#xA;  type                = &#34;full&#34;&#xA;  vanity_name_servers = []&#xA;  account = {&#xA;    id   = &#34;REDACTED&#34;&#xA;    name = &#34;REDACTED&#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;现在域名资源已经导入了，但是里面的配置并没有。接下来，导入域名下的 DNS 记录：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming generate \&#xA;  --zone $CLOUDFLARE_ZONE_ID \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --resource-type &#34;cloudflare_dns_record&#34; &gt;&gt; dns.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这一步会在当前目录下生成一个名为&lt;code&gt;dns.tf&lt;/code&gt;的配置文件，里面会有如下格式的内容：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_5deb14xxxxxb629bf123xxxxxxxc8f_0&#34; {&#xA;  content  = &#34;67.24.33.108&#34;&#xA;  name     = &#34;example.example.com&#34;&#xA;  proxied  = true&#xA;  tags     = []&#xA;  ttl      = 1&#xA;  type     = &#34;A&#34;&#xA;  zone_id  = &#34;81c7f2de8dfxxxxxx52629xxxxxxfc&#34;&#xA;  settings = {}&#xA;}&#xA;&#xA;resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_89xxxxx0bf9cxxxxxx9a_1&#34; {&#xA;  content  = &#34;35.27.108.33&#34;&#xA;  name     = &#34;terraform.example.com&#34;&#xA;  proxied  = true&#xA;  tags     = []&#xA;  ttl      = 1&#xA;  type     = &#34;A&#34;&#xA;  zone_id  = &#34;8xxxxxx7644e428526xxxxxx&#34;&#xA;  settings = {}&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果有多个域名需要导入，则多次分别设置环境变量&lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt;再重复运行命令即可。&lt;/p&gt;&#xA;&lt;p&gt;这里生成的配置文件可以直接使用，也就是我们之后需要的 Terraform 配置文件。然而，此时我们仅仅只是生成了配置文件，但是目前 Terraform 的状态依然是空的，这时要是直接&lt;code&gt;terraform apply&lt;/code&gt;，Terraform 会不管三七二十一将我们刚刚导入的声明一律视为新增资源，然后甩出一大堆「Alredy Exists」报错。所以接下来，我们需要将生成的配置文件导入 Terraform 的&lt;code&gt;terraform.tfstate&lt;/code&gt;状态。&lt;/p&gt;&#xA;&lt;p&gt;Terraform 在 1.5 版本引入了&lt;code&gt;import&lt;/code&gt;块，相比以往一行一行输入命令的方式更加现代。其导入流程是先生成一个包含&lt;code&gt;import&lt;/code&gt;块的&lt;code&gt;.tf&lt;/code&gt;文件，下次进行&lt;code&gt;terraform apply&lt;/code&gt;时，Terraform 就会自动为我们执行导入操作。&lt;/p&gt;&#xA;&lt;p&gt;生成&lt;code&gt;cloudflare_zone&lt;/code&gt;的&lt;code&gt;import&lt;/code&gt;块：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming import \&#xA;  --resource-type &#34;cloudflare_zone&#34; \&#xA;  --modern-import-block \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --zone $CLOUDFLARE_ZONE_ID &gt;&gt; import.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;生成&lt;code&gt;cloudflare_dns_record&lt;/code&gt;的&lt;code&gt;import&lt;/code&gt;块：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming import \&#xA;  --resource-type &#34;cloudflare_dns_record&#34; \&#xA;  --modern-import-block \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --zone $CLOUDFLARE_ZONE_ID &gt;&gt; import.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这一步会在当前目录下生成&lt;code&gt;import.tf&lt;/code&gt;，它包含了所需要的导入信息，作用就是告诉 Terraform 上一步生成的每个&lt;code&gt;resource&lt;/code&gt;块到底对应的是哪一个云服务提供商的资源 ID。这个 ID 是云服务提供商内部标记资源的代码，平常是不会在控制面板上显示的，只有在用 API 特别请求时才会知道。Terraform 在导入过程中需要用到这个 ID 以确认本地的定义对应的云端资源，以实现严格的幂等性。&lt;/p&gt;&#xA;&lt;p&gt;好了，现在我们运行&lt;code&gt;terraform plan&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ terraform plan&#xA;cloudflare_dns_record.minio_a: Refreshing state... [id=xxxxxxxxxxx53]&#xA;cloudflare_zero_trust_tunnel_cloudflared_config.raspberrypi: Refreshing state...&#xA;......&#xA;&#xA;Terraform will perform the following actions:&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_0 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_0&#34; {&#xA;        content     = &#34;67.24.33.108&#34;&#xA;        created_on  = &#34;2026-04-08T10:18:12Z&#34;&#xA;        id          = &#34;5deb14c21xxxxxxx20f1c8f&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:18:12Z&#34;&#xA;        name        = &#34;example.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_1 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_89c149exxxxxxxxxxxba13xxxxxa_1&#34; {&#xA;        content     = &#34;35.27.108.33&#34;&#xA;        created_on  = &#34;2026-04-08T10:17:54Z&#34;&#xA;        id          = &#34;89cxxxxxxxxxxxxxxxxxx09a&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:17:54Z&#34;&#xA;        name        = &#34;terraform.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;81xxxxxxxxxxxxxxxxxxxxxfc&#34;&#xA;    }&#xA;&#xA;Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.&#xA;&#xA;────────────────────────────────────────────────────────────────────────────────────────────────────────&#xA;&#xA;Note: You didn&#39;t use the -out option to save this plan, so Terraform can&#39;t guarantee to take exactly&#xA;these actions if you run &#34;terraform apply&#34; now.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果需要 Add、Change 和 Destroy 的资源数量都是 0，说明我们的导入操作没有问题，直接&lt;code&gt;terraform apply --auto-approve&lt;/code&gt;，资源就导入完成了。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ terraform apply --auto-approve&#xA;cloudflare_dns_record.push_a: Refreshing state... [id=REDACTED]&#xA;&#xA;Terraform will perform the following actions:&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_REDACTED_0 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_0&#34; {&#xA;        content     = &#34;67.24.33.108&#34;&#xA;        created_on  = &#34;2026-04-08T10:18:12Z&#34;&#xA;        id          = &#34;REDACTED&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:18:12Z&#34;&#xA;        name        = &#34;example.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_REDACTED_1 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_1&#34; {&#xA;        content     = &#34;35.27.108.33&#34;&#xA;        created_on  = &#34;2026-04-08T10:17:54Z&#34;&#xA;        id          = &#34;REDACTED&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:17:54Z&#34;&#xA;        name        = &#34;terraform.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Importing... [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Import complete [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Importing... [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Import complete [id=REDACTED/REDACTED]&#xA;&#xA;Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;状态存储和持续集成&#34;&gt;状态存储和持续集成&#xA;&lt;/h2&gt;&lt;p&gt;IaC 的核心价值之一就在于可以轻易实现基于 Git 的多人协作，以及 CI 的持续集成，但是在此之前，又有一个新的问题需要解决——&lt;code&gt;terraform.tfstate&lt;/code&gt;到底放哪里：没人想要换一次环境辛辛苦苦导入的状态就丢一次。&lt;/p&gt;&#xA;&lt;p&gt;Terraform 目前支持以下几种保存状态的后端：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;local&lt;/li&gt;&#xA;&lt;li&gt;remote&lt;/li&gt;&#xA;&lt;li&gt;azurerm&lt;/li&gt;&#xA;&lt;li&gt;consul&lt;/li&gt;&#xA;&lt;li&gt;cos&lt;/li&gt;&#xA;&lt;li&gt;gcs&lt;/li&gt;&#xA;&lt;li&gt;http&lt;/li&gt;&#xA;&lt;li&gt;Kubernetes&lt;/li&gt;&#xA;&lt;li&gt;oci&lt;/li&gt;&#xA;&lt;li&gt;oss&lt;/li&gt;&#xA;&lt;li&gt;pg&lt;/li&gt;&#xA;&lt;li&gt;s3&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;没有特殊需求可以像我一样选择&lt;code&gt;s3&lt;/code&gt;，毕竟 Cloudflare R2 有免费额度，不用白不用。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;terraform {&#xA;  backend &#34;s3&#34; {&#xA;    bucket = &#34;terraform&#34;&#xA;    key    = &#34;terraform.tfstate&#34;&#xA;    region = &#34;auto&#34;&#xA;    endpoints = {&#xA;      s3 = &#34;https://REDACTED.r2.cloudflarestorage.com&#34;&#xA;    }&#xA;&#xA;    # R2 不需要这些 AWS 的验证&#xA;    skip_credentials_validation = true&#xA;    skip_metadata_api_check     = true&#xA;    skip_region_validation      = true&#xA;    skip_requesting_account_id  = true&#xA;    use_path_style              = true&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于 S3 的后端，建议用&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;和&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;两个环境变量来存储 Credentials：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;export AWS_ACCESS_KEY_ID=&#39;REDACTED&#39;&#xA;export AWS_SECRET_ACCESS_KEY=&#39;REDACTED&#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;配置完成，运行&lt;code&gt;terraform init -migrate-state&lt;/code&gt;，配置就成功存储到云上了，以后不论在何处修改配置、运行&lt;code&gt;terraform apply&lt;/code&gt;都无须担心 Terraform 的 State 不同步的问题。&lt;/p&gt;&#xA;&lt;p&gt;接下来就是 Github CI 的配置，其实很简单，无非就是每次&lt;code&gt;git push&lt;/code&gt;时触发一次&lt;code&gt;terraform init&lt;/code&gt;、&lt;code&gt;terraform fmt&lt;/code&gt;和&lt;code&gt;terraform apply&lt;/code&gt;，以下是我的&lt;code&gt;.github/workflows/apply.yml&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: &#34;Terraform Apply&#34;&#xA;&#xA;on:&#xA;  push:&#xA;    branches:&#xA;      - main&#xA;&#xA;env:&#xA;  TF_IN_AUTOMATION: &#34;true&#34;&#xA;  CLOUDFLARE_API_TOKEN: &#34;${{ secrets.CLOUDFLARE_API_TOKEN }}&#34;&#xA;  AWS_ACCESS_KEY_ID: &#34;${{ secrets.AWS_ACCESS_KEY_ID }}&#34;&#xA;  AWS_SECRET_ACCESS_KEY: &#34;${{ secrets.AWS_SECRET_ACCESS_KEY }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_com: &#34;${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}&#xA;&#xA;jobs:&#xA;  terraform:&#xA;    name: &#34;Terraform Apply&#34;&#xA;    runs-on: ubuntu-latest&#xA;    permissions:&#xA;      contents: read&#xA;    concurrency:&#xA;      group: terraform-apply&#xA;      cancel-in-progress: false&#xA;    steps:&#xA;      - name: Checkout&#xA;        uses: actions/checkout@v6&#xA;&#xA;      - name: Setup Terraform&#xA;        uses: hashicorp/setup-terraform@v4&#xA;&#xA;      - name: Terraform Init&#xA;        run: terraform init -input=false&#xA;&#xA;      - name: Terraform Apply&#xA;        run: terraform apply -input=false -auto-approve&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于 PR 则应当让 CI 自动为每次 PR 附上&lt;code&gt;terraform plan&lt;/code&gt;的输出：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: Terraform Plan&#xA;&#xA;on:&#xA;  pull_request:&#xA;    paths:&#xA;      - &#34;**/*.tf&#34;&#xA;      - &#34;.github/workflows/terraform-plan.yml&#34;&#xA;&#xA;permissions:&#xA;  contents: read&#xA;  pull-requests: write&#xA;&#xA;env:&#xA;  TF_IN_AUTOMATION: &#34;true&#34;&#xA;  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}&#xA;  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}&#xA;  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}&#xA;  TF_VAR_cloudflare_zone_id_example_com: &#34;${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}&#xA;&#xA;jobs:&#xA;  plan:&#xA;    name: Terraform Plan&#xA;    runs-on: ubuntu-latest&#xA;    steps:&#xA;      - uses: actions/checkout@v6&#xA;&#xA;      - uses: hashicorp/setup-terraform@v4&#xA;&#xA;      - name: Terraform fmt&#xA;        id: fmt&#xA;        run: terraform fmt -check -recursive&#xA;        continue-on-error: true&#xA;&#xA;      - name: Terraform Init&#xA;        id: init&#xA;        run: terraform init -input=false&#xA;&#xA;      - name: Terraform Validate&#xA;        id: validate&#xA;        run: terraform validate -no-color&#xA;&#xA;      - name: Terraform Plan&#xA;        id: plan&#xA;        run: terraform plan -input=false -no-color&#xA;        continue-on-error: true&#xA;&#xA;      - name: Post Plan to PR&#xA;        uses: actions/github-script@v8&#xA;        with:&#xA;          github-token: ${{ secrets.GITHUB_TOKEN }}&#xA;          script: |&#xA;            const { data: comments } = await github.rest.issues.listComments({&#xA;              owner: context.repo.owner,&#xA;              repo: context.repo.repo,&#xA;              issue_number: context.issue.number,&#xA;            });&#xA;            const botComment = comments.find(c =&gt;&#xA;              c.user.type === &#39;Bot&#39; &amp;&amp; c.body.includes(&#39;&lt;!-- terraform-plan --&gt;&#39;)&#xA;            );&#xA;&#xA;            const planOutput = `${{ steps.plan.outputs.stdout }}`.substring(0, 65000);&#xA;&#xA;            const body = `&lt;!-- terraform-plan --&gt;&#xA;            #### Terraform Plan&#xA;&#xA;            | Step     | Result                            |&#xA;            | -------- | --------------------------------- |&#xA;            | fmt      | \`${{ steps.fmt.outcome }}\`      |&#xA;            | init     | \`${{ steps.init.outcome }}\`     |&#xA;            | validate | \`${{ steps.validate.outcome }}\` |&#xA;            | plan     | \`${{ steps.plan.outcome }}\`     |&#xA;&#xA;            &lt;details&gt;&lt;summary&gt;展开 Plan 详情&lt;/summary&gt;&#xA;&#xA;            \`\`\`terraform&#xA;            ${planOutput}&#xA;            \`\`\`&#xA;            &lt;/details&gt;`;&#xA;&#xA;            if (botComment) {&#xA;              await github.rest.issues.updateComment({&#xA;                owner: context.repo.owner,&#xA;                repo: context.repo.repo,&#xA;                comment_id: botComment.id,&#xA;                body&#xA;              });&#xA;            } else {&#xA;              await github.rest.issues.createComment({&#xA;                issue_number: context.issue.number,&#xA;                owner: context.repo.owner,&#xA;                repo: context.repo.repo,&#xA;                body&#xA;              });&#xA;            }&#xA;&#xA;      - name: Fail if plan failed&#xA;        if: steps.plan.outcome == &#39;failure&#39;&#xA;        run: exit 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image-2_hu_c922b7d4de19a009.webp&#34; alt=&#34;每次 PR 都会有 Plan 的输出&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;后续的工作流程&#34;&gt;后续的工作流程&#xA;&lt;/h2&gt;&lt;p&gt;到这一步，Terraform 的「接管初始化」已经结束了，后面就进入了日常维护阶段。这个阶段其实就三件事：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;创建新资源&lt;/li&gt;&#xA;&lt;li&gt;修改现有的资源&lt;/li&gt;&#xA;&lt;li&gt;删除不再需要的资源&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;常用的操作就两个：&lt;code&gt;terraform plan&lt;/code&gt;和&lt;code&gt;terraform apply&lt;/code&gt;，如果是个人使用，小的改动直接提交就算了；如果是团队协作，每次修改则应当遵循能 PR 就不直接 Commit 的原则。&lt;/p&gt;&#xA;&lt;h3 id=&#34;创建基础设施&#34;&gt;创建基础设施&#xA;&lt;/h3&gt;&lt;p&gt;假设你现在要新增一条 DNS 记录，或者新建一个 Tunnel、一个对象存储桶，流程如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  C1[新建分支&#xA;如 feat/add-minio-record] --&gt; C2[新增 resource 块]&#xA;  C2 --&gt; C3[terraform fmt + validate]&#xA;  C3 --&gt; C4[terraform plan]&#xA;  C4 --&gt; C5{仅新增预期资源?}&#xA;  C5 -- 否 --&gt; C6[修正配置后重跑 plan]&#xA;  C6 --&gt; C4&#xA;  C5 -- 是 --&gt; C7[提交 PR]&#xA;  C7 --&gt; C8[合并后 CI apply]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最理想的&lt;code&gt;plan&lt;/code&gt;输出是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;X to add&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;0 to change&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;0 to destroy&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;如果你只是想加东西，结果出现了&lt;code&gt;to destroy&lt;/code&gt;，那就先别冲动，通常是引用写错、变量搞错，或者不小心改了资源地址，仔细检查是什么地方出了问题。&lt;/p&gt;&#xA;&lt;h3 id=&#34;修改与删除基础设施&#34;&gt;修改与删除基础设施&#xA;&lt;/h3&gt;&lt;p&gt;修改流程和创建类似，但要多一步——评估变更是否会触发重建。&lt;/p&gt;&#xA;&lt;p&gt;因为很多 Provider 字段是&lt;code&gt;ForceNew&lt;/code&gt;，你以为只是改个字段，Terraform 看完说：「好的，删了重建。」这在我们修改 DNS 的这个场景并不是什么很大的问题，但是到了云实例这种资源，如果删除重建势必会造成损失。&lt;/p&gt;&#xA;&lt;p&gt;建议按下面这个顺序来：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  M1[修改 .tf] --&gt; M2[terraform plan]&#xA;  M2 --&gt; M3{出现 replace/destroy?}&#xA;  M3 -- 否 --&gt; M7[确认影响范围]&#xA;  M7 --&gt; M8[terraform apply]&#xA;  M3 -- 是 --&gt; M4[暂停并复核变更]&#xA;  M4 --&gt; M5[必要时加 lifecycle 保护]&#xA;  M5 --&gt; M6[安排变更窗口]&#xA;  M6 --&gt; M8&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对生产环境来说，创建、修改得慢一点并不是什么很大的问题，最应当看重的是操作的正确性，慢一点，不要出错。IaC 不是比手速，IaC 比的是可预期性。&lt;/p&gt;&#xA;&lt;p&gt;如果是删除资源（比如下线某个 DNS 记录、清理废弃 Tunnel），走下面这个流程&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  D1[确认资源已废弃&#xA;  检查业务/监控/脚本依赖] --&gt; D2[删除 resource 块或调整 count/for_each]&#xA;  D2 --&gt; D3[terraform plan]&#xA;  D3 --&gt; D4{to destroy 是否符合预期?}&#xA;  D4 -- 否 --&gt; D5[回滚修改并继续排查依赖]&#xA;  D5 --&gt; D1&#xA;  D4 -- 是 --&gt; D6[准备回滚方案并选低峰窗口]&#xA;  D6 --&gt; D7[PR 审核通过]&#xA;  D7 --&gt; D8[terraform apply]&#xA;  D8 --&gt; D9[删除资源后的可用性检查]&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;日常协作建议&#34;&gt;日常协作建议&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;把 Credentials 放环境变量或 CI Secret，别写进&lt;code&gt;.tf&lt;/code&gt;和仓库&lt;/li&gt;&#xA;&lt;li&gt;对关键资源开启保护策略，防止误删&lt;/li&gt;&#xA;&lt;li&gt;将目录按资源类型拆分&lt;/li&gt;&#xA;&lt;li&gt;定期执行&lt;code&gt;terraform plan&lt;/code&gt;做基础设施漂移检查&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;和校正，避免在面板误操作手动修改&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;虽然这套流程看起来很麻烦，但每一次变更都有记录、可审计、可回滚，最重要的是可复现，这才是 IaC 最有价值的地方。&lt;/p&gt;&#xA;&lt;h2 id=&#34;参考&#34;&gt;参考&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://candinya.com/posts/manage-cloudflare-dns-with-terraform/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;使用 Terraform 管理 CloudFlare 上的 DNS 解析记录 - Candinya&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/tutorials/automation/github-actions&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Automate Terraform with GitHub Actions&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/language/files/tfquery&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Query configuration files&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/language/block/tfquery/list&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;list block reference&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/cf-terraforming&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;cloudflare/cf-terraforming&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developers.cloudflare.com/terraform/advanced-topics/import-cloudflare-resources/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Import Cloudflare resources&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Cloudflare Provider - Terraform Registry&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Infrastructure_as_code&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Infrastructure as Code&lt;/a&gt;，是指采用机器可读的配置文件定义所需要的基础设施的部署方法。&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;每个供应商的配置文件字段命名和格式都有区别，但这可以很容易的编写脚本进行转换。&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;需要特别注意的是，状态文件中可能包含数据库密码、API 密钥等明文存储的敏感信息，因此绝对不要将 &lt;code&gt;.tfstate&lt;/code&gt; 文件提交到公开的代码仓库中。&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;HashiCorp Configuration Language，HashiCorp 自家开发的一种声明式配置语言，旨在兼顾机器可读性与人类可读性。&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:5&#34;&gt;&#xA;&lt;p&gt;幂等性（Idempotence）指计算机系统或接口在接收到同一请求的多次操作时，产生的影响与一次执行的结果相同，不论执行多少次，系统的最终状态始终保持一致。&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:6&#34;&gt;&#xA;&lt;p&gt;如果你仅仅是想让 Terraform 不再管理某个资源，而不是真正在云端销毁它，应该使用 &lt;code&gt;terraform state rm&lt;/code&gt; 命令，而不是在代码中删掉资源块然后 &lt;code&gt;apply&lt;/code&gt;，否则云环境上的真实资源也会被一并销毁。&amp;#160;&lt;a href=&#34;#fnref:6&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:7&#34;&gt;&#xA;&lt;p&gt;基础设施漂移（Infrastructure Drift）是指现实中通过控制台点按等非 IaC 途径修改了基础设施，导致其实际状态与代码中声明的状态不一致的情况。&amp;#160;&lt;a href=&#34;#fnref:7&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item></channel>
</rss>
