<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Devops · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/devops/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>基础设施即代码是变更日志,不是魔法</title><link>https://www.xbcatm.com/zh/posts/terraform-and-infrastructure-as-code/</link><pubDate>Fri, 13 Nov 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/terraform-and-infrastructure-as-code/</guid><description>用文件描述你的平台,让变更可评审、可复现;价值在于它产出的计划与 diff。</description><content:encoded>&lt;p&gt;声明式基础设施描述期望结果，再由工具计算变更。命令式脚本同样可以查询已有资源、实现幂等性；重复创建不是脚本天生无法避免的命运。Terraform 的价值，在于把配置、资源身份、依赖关系和拟执行操作放进一个共同模型，让评审者能够一起检查。&lt;/p&gt;
&lt;p&gt;因此，值得评审的不只是配置文件的 diff。一次看起来无害的重命名可能改变资源地址，而完全没改过的配置也可能因为有人操作了运行中的系统而产生计划。需要同时读懂「想做什么」以及「工具现在具体提议做什么」。文件可追踪，并不自动等于所有操作都安全。&lt;/p&gt;
&lt;h2 id="计划是提案不是事务保证"&gt;计划是提案，不是事务保证&lt;/h2&gt;
&lt;p&gt;普通 plan 通过 provider 读取受管理对象，把刷新后的观察结果与配置比较，再提出创建、更新、替换或删除操作。未知值、provider 的实现、执行权限，以及计划到执行之间发生的变化，都使它无法保证成功，也不能把输出理解成未来所有 API 调用的完整逐条清单。&lt;/p&gt;
&lt;p&gt;保存计划可以让 apply 使用被评审的那份提案，而不是自动重算另一份，但并不会冻结远端系统。状态发生变化可能使保存的计划失效，执行也可能在完成部分操作后报错。计划文件和状态可能包含敏感信息，应作为受控产物保存，不要为了排错就把完整内容贴到公开日志里。&lt;/p&gt;
&lt;p&gt;成功 apply 之后，如果输入、配置和外部环境稳定，通常应当不再出现新的变更。若持续有差异，就要检查外部控制器、计算属性、provider 缺陷和不断变化的输入。一个干净的计划只说明此次受管理范围内没有拟执行差异，不能证明云账户里所有资源都已被纳入清单，也不能保证业务服务健康。&lt;/p&gt;
&lt;h2 id="状态保存的是身份关系"&gt;状态保存的是身份关系&lt;/h2&gt;
&lt;p&gt;状态把配置地址绑定到真实对象，并保存 Terraform 需要的其他信息。丢失映射不会立刻删除远端资源；之后的计划可能提议创建新对象，而 apply 究竟会建出重复资源，还是遇到名称冲突，取决于 provider 和服务本身。不能把某一次 already exists 错误推广成所有资源的必然结果。&lt;/p&gt;
&lt;p&gt;恢复时应先检查后端历史版本与可用快照。当恢复快照不适合或不可行时，import 可以把受支持的既有对象重新关联到配置地址。除了命令行方式，也有配置驱动的导入流程，并不一定要逐个手工敲命令。导入之后仍需核对资源配置和执行计划，身份绑定完成不意味着下一次 apply 一定没有变化。&lt;/p&gt;
&lt;p&gt;共享状态需要访问控制、可恢复的历史，以及具备合适锁定能力的后端。并非所有后端都支持锁；获取锁失败会使操作停止，是否等待、等待多久还取决于锁超时等命令选项。不要为了绕过另一个合法运行而禁用锁，也不要把状态版本历史等同于完整基础设施审计。后端保存了什么记录、能恢复到哪里，都需要独立确认。&lt;/p&gt;
&lt;h2 id="漂移与重构是两种决定"&gt;漂移与重构是两种决定&lt;/h2&gt;
&lt;p&gt;已经被状态追踪的资源在外部被修改后，需要决定保留新值还是恢复配置值。保留通常意味着更新配置，普通属性漂移不需要重新 import。refresh-only apply 会更新 Terraform 的记录，而不按照配置去改远端对象；它不会替你编辑配置，因此下一次普通计划仍可能提议把外部改动撤回。&lt;/p&gt;
&lt;p&gt;被忽略的属性和 provider 的资源模式，也会影响哪些差异产生操作。控制台变更可能有自己的审计日志，真正的团队问题往往不是「绝无记录」，而是事故处置记录没有关联到下一次部署。把最终要保留的值写清楚，比单纯让一次 refresh 看起来成功更重要。&lt;/p&gt;
&lt;p&gt;如果只改地址，却没有记录新旧地址的关系，就可能得到删除旧对象、创建新对象的提案。&lt;code&gt;moved&lt;/code&gt; 块可以显式说明这种关系，也包括受支持的模块内迁移，所以兼容的重构不必重建资源。不过，伴随发生的其他配置变化仍可能要求替换对象。直接删掉资源块通常会提议销毁，除非明确使用了合适的移除管理流程。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;事故中有人临时调大实例规格，却没同步配置；下一次无关的 apply 随后提议缩小它。应当评审相关操作，并记录长期希望保留的值。计划退出码二表示有非空差异，不专门表示漂移，也不自动代表危险或失败。把所有二都当成故障，容易制造让人忽略的告警噪声。&lt;/p&gt;
&lt;p&gt;面对意外的 destroy，先比较旧地址和新地址，再决定是否批准。面对「资源不见了」，先确认选中的后端和 workspace，而不是立即断言状态丢失。恢复期间要停止并发写入；一份看起来合理的空状态，并不构成重新创建全部资产的依据。把账单、对象标识和配置地址对应起来，才有恢复的基础。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;安装 Terraform 1.4 或更新版本后，在下面新建的一次性目录中执行。内置的 &lt;code&gt;terraform_data&lt;/code&gt; 资源不需要外部 provider、凭据或基础设施。这些 apply 命令只写入本实验的本地状态，不要把示例混入已有的生产配置目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;lab&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$lab&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; main.tf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;HCL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;resource &amp;#34;terraform_data&amp;#34; &amp;#34;old&amp;#34; { input = &amp;#34;kept&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HCL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform init -input&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform validate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform apply -input&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; -auto-approve
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform state show terraform_data.old
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; main.tf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;HCL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;resource &amp;#34;terraform_data&amp;#34; &amp;#34;new&amp;#34; { input = &amp;#34;kept&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HCL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform plan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt;&amp;gt; main.tf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;HCL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;moved {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; from = terraform_data.old
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; to = terraform_data.new
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HCL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform validate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform plan -out&lt;span class="o"&gt;=&lt;/span&gt;moved.tfplan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform apply -input&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; moved.tfplan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform state show terraform_data.new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;terraform plan -detailed-exitcode
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 Terraform 1.13.3 上实际运行后，没有记录关系的重命名会提议创建一个、删除一个；不要执行这个中间计划。加上 &lt;code&gt;moved&lt;/code&gt; 后，计划中的新增、修改、删除数量均为零，迁移前后展示的对象标识保持相同，最后一次计划退出码为零。只有地址迁移的保存计划仍可能返回详细退出码二，因为状态中的地址确实发生了变化。&lt;/p&gt;
&lt;p&gt;这验证了真实 Terraform 客户端的本地计划和地址迁移，没有验证云 provider 导入、远端漂移检测或后端锁。详细退出码的一般含义是：零表示没有变更，一表示错误，二表示存在变更。应该继续读具体动作，不能因为出现二就认定失败，也不能因为资源操作数为零就忽略状态映射的变动。&lt;/p&gt;
&lt;p&gt;实验结束后，先用 &lt;code&gt;cd -&lt;/code&gt; 返回原来的目录，再删除 &lt;code&gt;$lab&lt;/code&gt; 指向的那一个临时目录。其中只保留本实验生成的配置、计划和本地状态；在明确清理前，这些文件仍会留在磁盘上。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state"&gt;Terraform：状态&lt;/a&gt;，地址绑定与状态的作用，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/commands/plan"&gt;Terraform：plan 命令&lt;/a&gt;，计划模式、保存计划、锁选项与退出码，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/remote"&gt;Terraform：远程状态&lt;/a&gt;，共享存储与后端能力，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/locking"&gt;Terraform：状态锁&lt;/a&gt;，后端支持范围与获取锁失败的行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/modules/develop/refactoring"&gt;Terraform：重构模块&lt;/a&gt;，显式 moved 映射与兼容重构，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/import"&gt;Terraform：导入概览&lt;/a&gt;，通过配置接管既有资源，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/import"&gt;Terraform：命令行导入&lt;/a&gt;，另一种命令行导入流程，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/tutorials/state/resource-drift"&gt;Terraform：管理资源漂移&lt;/a&gt;，刷新观察结果与协调配置，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle"&gt;Terraform：生命周期&lt;/a&gt;，忽略属性与移除配置的后果，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/resources/terraform-data"&gt;Terraform：terraform_data&lt;/a&gt;，实验所用的内置本地生命周期资源，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/cli/commands/apply"&gt;Terraform：apply 命令&lt;/a&gt;，执行保存计划与部分失败的语义，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>