这些教程会教你如何使用Terraform,但并不会告诉你当60名工程师同时使用它来进行开发时会发生什么。
在学习Terraform的过程中,你通常只会使用一个代码仓库、一个状态文件以及一个环境。你只需在笔记本电脑上运行terraform apply命令,基础设施就会相应地被配置完成。
这种模式在正常情况下运作得很好,但直到你加入一家公司后,才会发现工程师们很少会用笔记本电脑来对生产环境进行操作。你会发现,实际情况与你在练习中遇到的情况往往并不一致。
这篇文章将解释大型工程团队是如何实际使用Terraform的,包括他们使用的代码仓库结构、工作流程、权限管理规则,以及如果没有这些机制会遇到哪些问题。
你将会了解到企业级团队是如何组织代码仓库和状态文件的,他们如何通过GitHub来存储和版本控制可重用的模块,为什么基础设施变更需要通过专门的管道才能应用到生产环境中,他们又是如何检测那些发生在Terraform之外的变更的,以及当出现问题时他们该如何进行恢复。
这里介绍的每一个实践方法,都是因为某个团队在开发过程中遇到了具体的障碍,于是才创造了这些解决方案来克服这些难题的。
先决条件
在阅读这篇文章之前,你应该已经对Terraform有了一定的了解。同时,你也应该知道Git拉取请求和分支合并的工作原理。
这并不是一篇关于Terraform基础知识的介绍文章,而是专门讨论在你掌握了基础知识之后,如何与其他工程师一起共享基础设施资源。
目录
国家腐败是如何发生的
Terraform通过状态文件来记录它所创建的所有资源。该文件会保存每一项资源、每一个ID以及所有的配置信息。当这些信息与云环境中实际存在的资源状态不一致时,就发生了“状态腐败”现象。
人们常常将很多问题归咎于状态文件,但那些在实际生产环境中处理过这类问题的工程师们知道,这些问题通常都可以追溯到少数几种特定的情况,而每种情况都有不同的成因和相应的解决方法。
两名工程师同时运行`terraform apply`命令
在理解这个现象之前,你需要先了解Terraform的工作原理。
当你运行`terraform apply`命令时,实际上会有两件事情分别发生:

首先,Terraform会与AWS进行通信,从而在云环境中创建相应的资源;其次,Terraform会更新状态文件,以记录这些资源的创建情况。
实际上,AWS负责管理真实的基础设施,而状态文件则只是Terraform用来记录这些信息的工具。如果在这个过程中有任何干扰,就会导致这两者之间的数据不一致。
现在来看,当两名工程师同时运行`terraform apply`命令且没有采取任何锁定机制来防止数据冲突时,会发生什么:

Sarah首先打开了状态文件并开始添加一个子网;Marcus也在同一时刻打开了同一个文件,然后开始更新NAT网关的设置。他们都是基于同一个初始状态副本来进行操作的。
Sarah先完成了操作,她的命令在AWS中创建了该子网,并更新了状态文件以记录这一变化。
Marcus随后完成了操作,他的命令更新了AWS中的NAT网关设置;此时,Terraform会使用Marcus开始操作时所读取的状态信息来更新状态文件。
那个版本没有包含Sarah的子网信息,因此更新后的状态文件中已经没有了该子网的记录。

这个子网在AWS系统中确实是存在的,但Terraform的状态文件中已经没有它的记录了。下次执行`terraform plan`命令时,Terraform会认为这个子网从未被创建过,因此会提示重新创建它。
不过状态锁定机制可以防止这种情况发生。Sarah在开始执行操作之前会先获取锁;当Marcus试图执行相同操作时,Terraform会让他等待。
等到Sarah完成操作后,Terraform会更新状态文件并释放锁,此时Marcus再执行操作,这样子网和NAT网关的变更就能被正确地记录下来。
操作被中断
一个GitHub Actions流水线正在对支付基础设施进行修改,包括添加三条新的安全组规则和一个数据库参数组。但在执行过程中,该流水线在运行60分钟后超时了,从而导致任务失败。
以下是这个任务在失败之前实际完成的部分操作:

上面的终端截图显示,在流水线超时之前,三条安全组规则确实已经成功创建了。但由于任务提前终止,数据库参数组的创建过程未能完成,状态文件也没有得到更新。
安全组规则1 → 已成功创建 ✓
安全组规则2 → 已成功创建 ✓
安全组规则3 → 已成功创建 ✓
数据库参数组 → 未成功创建 ✗
状态文件更新 → 从未执行(任务提前失败)
现在,这三条安全组规则确实存在于AWS系统中。但问题在于,Terraform在完成状态文件的更新之前,整个流水线就已经终止了。因此AWS系统知道这些规则的存在,但Terraform的状态文件中却仍然没有这些记录。
在这种情况下,现实情况与状态文件中的信息就不再一致了。
幸运的是,这种情况通常很容易恢复。当流水线再次运行时,Terraform会检查AWS系统中已经存在哪些资源。它会发现那三条安全组规则,因此不会再尝试创建它们;然后它才会去创建那个之前未能完成的数据库参数组。
第二次运行成功完成,状态文件也得到了更新。
这种机制之所以有效,是因为Terraform具有幂等性——再次执行相同的配置只会使基础设施向目标状态靠近,而不会从头开始重新创建所有资源。
不过还有一个小问题需要解决:状态锁的问题。
如果管道在持有锁的状态下被中断,Terraform可能会认为还有另一个应用任务正在运行。此时,下一次管道执行会立即失败,并出现类似这样的错误:

上图显示,由于之前的操作留下了状态锁,导致Terraform apply命令无法执行。错误信息中包含了锁的ID、状态文件的路径以及获取该锁的进程名称。直到锁被释放或手动清除之前,Terraform都不会继续执行后续操作。
在清除锁之前,请确保没有其他Terraform应用任务正在运行。
打开你的CI/CD系统,无论是GitHub Actions、GitLab CI还是Jenkins,都可以查看该环境下的管道执行历史记录:

上图显示,最近四次运行中:一次`terraform-plan`命令成功完成,两次`terraform-apply`命令被标记为“已取消”或“超时”,并且都提示“锁可能已经失效”。第四次`terraform-apply`命令目前仍在运行中,在它完成之前不应清除该锁。
如果之前的`terraform-apply`命令被取消或超时,那么对应的锁就会失效。此时可以使用`terraform force-unlock`命令,并输入错误信息中提到的锁ID来清除该锁。这样,管道就可以正常运行了。
只有在你确定没有任何任务正在运行时,才应该使用`force-unlock`命令。如果在锁仍然有效的情况下尝试清除它,就会导致两个应用任务同时尝试修改相同的状态文件,而这正是设置锁机制所要防止的情况。
有人在错误的环境中执行了Terraform状态管理命令
有一位数据库工程师正在清理测试环境中的旧数据库。
虽然这个数据库仍然存在于AWS系统中,但Terraform应该已经停止了对它的管理。为了达到这个目的,工程师使用了`terraform state rm`命令。
此命令并不会删除AWS中的任何数据,它只是会从Terraform的状态文件中移除与该资源相关的记录。可以将其理解为告诉Terraform:“忽略这个资源的存在,但让它继续正常运行。”
工程师原本打算在测试环境中执行这条命令:
预期效果:测试环境状态 → 忽略旧的测试数据库
但实际上,他们在生产环境中执行了这条命令。
实际结果:生产环境状态 → 忽略了正在运行的支付数据库
最终并没有任何数据被删除,生产环境中的数据库依然正常运行在AWS中。但Terraform已经“忘记”了这个数据库的存在。

现在,Terraform所记录的状态与实际环境中的情况不一致了。下一次执行`terraform plan`命令时,会发现代码中定义了某个数据库,但状态文件中却没有这个数据库的记录,因此Terraform会认为该数据库并不存在,并建议创建一个新的数据库。
如果没有人注意到这一点,Terraform就会在生产环境中创建第二个数据库,这样一来,生产环境中就会同时存在两个数据库,而这两个数据库都不会受到有效的管理,最终会导致非常混乱的局面,也很难进行后续的维护和修复。
`terraform state rm`、`terraform import`以及`terraform state mv`这些命令会立即修改状态文件的内容,而且不会给出任何确认提示。如果在错误的目录中执行这些命令,或者使用了错误的资源地址,那么几秒钟之内就会错误地修改状态文件中的数据。
两个团队管理同一资源
网络维护团队负责管理一个安全组,该安全组控制着对支付数据库的访问权限。当某个新的微服务需要访问数据库时,支付系统维护工程师有两种选择:要么请求网络维护团队添加新的规则,要么自己来管理这个安全组。
他们选择了第二种方案。工程师将现有的安全组导入到支付系统的状态文件中,并为微服务C添加了一条新的访问规则。
从这一刻起,这两个团队都认为自己拥有同一个安全组。

问题在于,Terraform会严格按照每个状态文件中的指令来执行操作。网络维护团队设定的状态文件规定安全组应该允许A和B访问数据库,而支付系统维护团队设定的状态文件则规定安全组应该允许A、B以及微服务C访问数据库。这两种设置不可能同时成立。
当支付系统维护团队应用他们的状态设置时,微服务C确实获得了访问权限。但到了晚上,网络维护团队的配置更新流程被执行了。Terraform读取了网络维护团队设定的状态文件,发现只有A和B被允许访问数据库,于是就更新了安全组设置,使得只有A和B能够访问数据库,而微服务C的访问规则就被悄悄地删除掉了。
没有出现任何错误,两条配置管道都成功通过了审核,而这恰恰使得问题变得非常难以调试。Terraform本身并没有故障,它只是接收到了来自两个不同状态文件的相互矛盾的指令,然后严格按照每个文件的要求进行了操作。
这个问题并不能通过修改Terraform命令来解决。这其实是一个权限分配问题,在任何人执行导入操作之前就应该先确定好相关的责任归属。如果支付团队当初向负责网络配置的团队提交了拉取请求,请求他们在安全组设置中添加相应的规则,那么就会有一个团队负责管理这个安全组,另一个团队则负责维护相应的状态文件,这样就不会出现冲突了。
为什么状态文件会被当作生产环境数据库来处理
状态文件其实类似于会计账本,它记录了Terraform创建的所有配置信息。各个团队之所以会特别对待这个文件,是因为其中往往包含一些敏感信息。
状态文件会以明文形式存储敏感数据,比如数据库密码、API密钥、连接字符串等等。如果在执行配置应用操作时将这些信息传递给了Terraform,它们就会被保存到状态文件中。即使你在Terraform代码中将这些变量标记为敏感信息,这些值仍然会出现在状态文件里。因为Terraform需要这些信息来计算后续配置变更所需的差异。
这意味着:任何能够读取状态文件的人,都可以看到你的数据库密码。
在大型组织中,工程师们通常无法直接访问生产环境的状态文件存储桶。相反,Terraform会通过CI/CD管道来执行操作,而这些管道会使用专门的IAM角色来确保拥有读写状态文件的权限以及执行配置应用的权限。工程师们是通过提交拉取请求和查看配置结果来与基础设施进行交互的,而不会直接修改状态文件。
这种分离机制有助于降低风险,并且能够生成审计痕迹。所有的状态变更都会被管道记录下来,因此可以很容易地追踪到哪些内容在什么时间发生了变化。
企业团队是如何构建他们的Terraform代码仓库的
当你加入一个大型工程组织时,首先会注意到的是那里存在大量的代码仓库。你可能会认为所有基础设施相关的配置都应该存储在一个仓库中,但实际上往往会有几十个仓库。
这些仓库的结构直接反映了权限分配的情况。每个仓库都属于某个特定的团队,而这个团队负责维护该仓库中的所有内容。典型的仓库结构如下所示:

该图展示了两种类型的仓库。第一种属于平台团队,其中包含可重复使用的模块,例如VPC配置、数据库模板和安全组规则等。这些仓库并不会直接生成生产环境中的资源。
第二种仓库则属于各个产品团队,比如支付团队或认证团队。这些团队会使用平台提供的模块来构建自己的基础设施。如果产品团队的仓库中出现了错误,只会影响该团队本身;而如果共享的平台模块出了问题,那么所有依赖该模块的团队都会受到影响。
需要重点理解的是:平台团队的仓库并不直接生成生产资源,它们只是创建一些可重复使用的模块,供各产品团队在构建基础设施时使用。
这种区分非常重要,因为有些仓库仅被一个团队使用,而有些则会被所有团队共享。
如果产品团队的仓库出了问题,通常只会影响该团队;但如果共享的模块出现了错误,那么所有依赖它的团队都会受到影响。

该图清楚地说明了为什么共享仓库比特定于产品的仓库风险更大:payments-infra仓库中的错误只会影响支付团队,而terraform-aws-postgres模块中的错误则会影响所有使用它来配置数据库的团队。terraform-policies仓库中的错误则会影响公司内的所有开发流程。模块被共享的范围越广,一旦出现问题,其影响范围也就越大。
正因为如此,经验丰富的工程师才会格外关注共享模块和策略仓库。
如果支付团队的基础设施出现了问题,那么问题很可能出在支付团队的仓库中。
如果有五个不同的团队同时遇到了相同的问题,那么首先需要检查的就是共享的模块和策略仓库。
团队如何通过分离状态文件来保护彼此
当只有一个人负责管理所有系统时,使用一个统一的状态文件来管理VPC、Kubernetes集群、数据库以及监控配置等是可行的;但当多个团队共享这个状态文件时,问题就会立刻出现。
具体来说,会遇到三个主要问题:
-
影响范围:如果网络配置和数据库配置都保存在同一个状态文件中,那么对网络配置的修改很可能会无意中影响到数据库资源。而将它们分开存储,就可以有效限制故障的影响范围。
-
部署效率:网络基础设施可能每年只会更改几次,而应用程序则可能需要每天进行多次部署。如果多个团队共享同一个状态文件,他们就会互相等待对方完成操作,从而导致部署效率下降。
-
权限冲突:当多个团队共享一个状态文件时,某个团队对某些配置的修改可能会被其他团队无意中覆盖或破坏,从而引发问题。
解决办法是按照所有权边界来划分各个状态文件。一种能够解决这三个问题的结构如下所示:

上图所示的结构中,每个域名都对应一个状态文件,这些文件都存储在“production”文件夹下。
-
“networking”负责管理VPC、子网、路由配置以及NAT网关。
-
“identity”负责处理IAM角色、策略和服务账户相关事宜。
-
“platform”负责管理Kubernetes集群、节点池以及各类插件。
-
“database”负责管理RDS实例、Redis集群以及数据备份工作。
-
“security”负责配置安全组、WAF规则以及证书相关内容。
-
“monitoring”负责使用Prometheus、Grafana等工具进行监控,并设置警报机制。
-
“payments”负责搭建支付服务相关的基础设施。
production/
networking/terraform.tfstate → VPC、子网、路由配置、NAT网关
identity/terraform.tfstate → IAM角色、策略、服务账户
platform/terraform.tfstate → Kubernetes集群、节点池、插件
database/terraform.tfstate → RDS实例、Redis集群、数据备份
security/terraform.tfstate → 安全组、WAF规则、证书
monitoring/terraform.tfstate → Prometheus、Grafana、警报机制
payments/terraform.tfstate → 支付服务基础设施
这只是其中一个例子,并非普遍适用的标准。规模较大的组织通常会进一步细分这些职责。不过原则是相同的:每个状态文件都由一个负责该领域的团队来管理,使用相同的配置流程,以确保冲突不会发生。
规则很简单:每一种资源都应该对应一个状态文件。如果“networking”团队负责配置某个安全组,那么这个安全组就应该存储在“networking”相关的状态文件中。其他团队可以将其作为数据来源进行引用,但不得将其导入自己的状态文件中。这样就可以避免第一节中提到的所有权冲突问题。
为什么有些团队在生产环境中更喜欢使用目录而非工作区
Terraform CLI的工作区功能允许你从一个目录中同时管理开发环境、测试环境和生产环境。每个工作区都会对应一个状态文件,但它们都共享相同的`.tf`配置文件。
infra/
main.tf ← 同一段代码适用于所有环境
variables.tf
terraform.tfstate.d/
dev/
staging/
production/ ← 不同的环境对应不同的状态文件,但使用相同的代码

这种工作空间机制将所有环境都放在一个名为“infra”的目录中。该目录中包含一个名为main.tf的文件,这个文件适用于所有环境。状态数据则分别存储在terraform.tfstate.d目录下,其中dev、staging和production这三个环境各自拥有独立的文件夹,但它们所使用的代码是相同的。
要切换环境,只需执行terraform workspace select production命令,然后应用相应的配置即可。
需要注意的是,切换工作空间是一个需要人工执行的操作。如果当前激活的工作空间错误,那么原本为staging环境准备的更改就可能会被应用到production环境中。
许多团队会选择为长期使用的环境创建单独的目录结构:
environments/
dev/
main.tf ← 具有独立的代码路径
backend.tf ← 指向dev环境的状态数据存储桶
staging/
main.tf ← 具有独立的代码路径
backend.tf ← 指向staging环境的状态数据存储桶
production/
main.tf ← 具有独立的代码路径
backend.tf ← 指向production环境的状态数据存储桶

通过这种目录结构,每个环境都会在environments目录下拥有自己的文件夹。dev、staging和production这三个环境各自都拥有独立的main.tf文件和backend.tf文件,后者分别指向不同的状态数据存储桶。因此,这些环境是完全相互独立的。
如果要针对production环境应用配置,就必须进入production目录。每个环境都有自己的状态数据、后端服务以及执行路径。
这种做法的缺点就是会导致代码重复。不过团队们通常会通过使用共享模块来解决这个问题,这样每个环境的目录中就只会包含与该环境相关的配置信息。
对于那些生命周期较短的环境来说,比如功能分支、预览版本或临时测试环境,工作空间机制仍然非常有用。
团队如何通过GitHub上的模块共享基础设施
当有30个团队各自都需要使用PostgreSQL数据库时,就会出现以下两种情况。
如果没有统一的配置标准,每个团队都会自行编写数据库配置文件。六个月后,当进行安全审计时,会发现:

该图表展示了当4个团队各自独立编写数据库配置文件时,安全审计会发现哪些问题。
Team A将backup_retention_period = 0这个参数设置为了0,这意味着他们的数据库从未被备份过。Team B将storage_encrypted = false设置为false,导致数据以明文形式存储。Team C没有为数据库添加任何标签,因此无法进行成本跟踪。Team D将deletion_protection = false设置为了false,这使得他们的数据库很容易因意外事件而丢失数据。
没有人会故意忽略这些内容,只是当时并没有统一的规范或标准。
通过使用共享模块,平台团队只需编写一次 `postgres` 模块代码。他们会将所有组织要求都包含进去:启用加密功能、进行7天备份、设置监控报警机制、规定必须使用的标签以及开启数据删除保护功能。然后,他们会将这些代码发布到名为 `terraform-aws-postgres` 的GitHub仓库中。
<现在,任何需要使用数据库的团队都会采用这样的编写方式:
module "payments_db" {
source = "git::ssh://github.company.com/platform/terraform-aws-postgres.git?ref=v2.1.0"
name = "payments"
environment = "production"
instance_class = "db.m5.large"
}
<只需要提供这四个参数,其余的所有配置都由该模块自动处理。
<大型组织通常会通过内部注册系统来发布经过审核的模块,这样工程师们就可以直接查找这些模块并了解它们的版本信息,而无需浏览GitHub仓库。此时,引用地址也会发生变化,不再使用完整的Git URL,而是变成这样的形式:
module "payments_db" {
source = "app.terraform.io/mycompany/postgres/aws"
version = "~> 2.1"
}
在模块的源代码地址中,`?ref=v2.1.0` 这一部分并不是装饰性内容。对于有40个团队共同使用同一个模块的情况来说,这一规定能够确保那些出于好意的修改不会导致整个公司出现故障。 <如果没有版本锁定机制,支付团队使用的Postgres模块就会引用最新的代码版本,这意味着每次更新后,所有团队的配置都会发生改变。如果模块的开发者将某个输出变量的名称从 `db_endpoint` 更改为 `database_endpoint`,那么下次其他团队运行 `terraform init` 时,就会自动应用这个更改,但他们的配置文件中仍然会显示旧的变量名 `db_endpoint`,从而导致配置错误。 <这样一来,各种计划都会失败: <版本锁定机制可以避免这种情况。支付团队会继续使用 `v2.1.0` 版本,而模块的开发者则会发布 `v2.2.0` 新版本,并编写变更日志。各团队可以在完成测试后再进行升级。这样一来,就不会有人在不知情的情况下遇到配置问题了。 <这种版本管理方式被称为“语义化版本控制”: 该表格展示了三种版本类型。像v2.1.1这样的补丁版本表示进行了错误修复,进行升级后您的代码不会发生任何变化。像v2.2.0这样的小版本则表示增加了新的可选功能,进行升级后同样不会对代码造成影响。而像v3.0.0这样的大版本则意味着存在可能会破坏现有功能的变更,因此您在升级之前需要阅读变更日志并修改代码。 创建一个Terraform模块可能只需要一个下午的时间,但对其进行长期维护则完全是另一回事。 一位网络工程师需要使用一个VPC模块。平台团队确实有一个这样的模块,但他们的待处理任务列表已经排满了。于是这位工程师创建了一个略有不同的版本。三个月后,另一个团队也做了同样的事情,然后又有一个团队跟着效仿。现在这样的情况就出现了: 没有人故意创建了这么多不同的模块版本。这些变体都是因为“我想快速做一个小小的修改”而产生的。每个变体在安全设置、标签配置以及默认值等方面都存在细微差异。当合规审计要求所有VPC都必须启用流量日志功能时,团队就需要检查这四个不同的模块,才能确定哪些环境符合要求。 那些能够避免这种问题的团队会将他们的模块视为共享服务:明确指定负责维护的团队,通过拉取请求来协同开发,在大版本中进行可能破坏现有功能的变更时会提供迁移指南,而对于已经被弃用的模块则会设定明确的退役日期。通过 那些忽视这些规范的团队最终会面临这样的局面:他们的模块没有人负责维护,没有人愿意去修改它们,也没有人能确定是否可以安全地删除这些模块。 一旦基础设施被分解成不同的状态文件,就会出现一个实际问题:各个团队需要获取其他团队的基础设施相关数据。例如,平台团队的Kubernetes状态信息就需要网络团队的VPC ID,数据库状态信息则需要子网ID,而支付相关的状态信息则又需要数据库的端点地址。 解决这个问题有两种常见的方法。 这种做法确实有效,但也存在局限性。如果要读取其他团队的状态信息,就需要拥有对他们整个状态文件的完整访问权限,而不仅仅是你感兴趣的那些数据。状态文件中会以明文形式存储数据库密码和API密钥,因此这种依赖关系意味着更多的团队会接触到彼此的敏感信息。 另一种方法——也是HashiCorp目前推荐的做法——是通过云服务提供商的API来查询资源,而无需读取其他团队的状态文件: 这种做法不需要团队之间相互访问对方的状态信息,每个团队的状态数据也都保持独立性。不过其缺点在于标签管理的复杂性:负责网络配置的团队必须以数据库团队能够准确理解的方式为VPC添加标签,这就要求各团队在早期就确定统一的命名规范。 许多团队会同时采用这两种方法:对于那些数量较少、且相互依赖关系紧密的资源,会使用本地存储的状态数据;而对于范围更广的资源,则会通过云平台来获取相关信息。 在那些大规模使用Terraform进行生产环境管理的大型组织中,变更命令并不会从某个人的笔记本电脑上发起。如果直接在本地机器上执行变更操作,那么那台机器就必须拥有生产环境的云访问凭证,这既存在安全风险,而且一旦出现问题也无法进行审计追踪。 实际上,生产环境的变更会通过一系列自动化流程来完成。每项变更都会首先在GitHub上提交一个拉取请求,然后由相应的处理流程来执行后续操作: 上图展示了整个流程的八个步骤:工程师首先提交拉取请求,处理流程会先运行terraform validate和格式检查,然后进行安全扫描,接着执行terraform plan命令并将结果附在拉取请求上,审核人员会查看全部输出内容,经过指定人员的批准后才会合并代码,最后处理流程会执行变更操作并记录结果。 当工程师第一次遇到这种情况时,最让他们感到惊讶的是:审核者并不是在批准代码本身,而是在批准计划输出结果,以及那些在云环境中究竟会被创建、修改或删除的具体内容。 有时候,一个代码上的微小更改看起来似乎并无危害,但实际上却可能会产生破坏性的后果。例如,修改某个数据库参数就可能导致资源需要被替换——这意味着Terraform会销毁现有的数据库并重新创建一个新的。 上图显示的计划输出结果表明,aws_db_instance/payments字段必须被替换,这意味着Terraform会销毁现有的数据库并创建一个新的,而不会对现有数据进行处理。 在合并代码之前发现这类问题,这才是审核计划的核心目的——而不是仅仅检查代码本身。 前面我们讨论过模块的所有权归属问题。例如,某个VPC模块可能属于平台开发团队,而数据库基础设施则归数据库维护团队所有。 关键在于要确保这些变更确实能得到负责这些模块的团队的审核。 GitHub通过名为CODEOWNERS的功能解决了这个问题。该功能允许代码仓库指定哪些团队负责管理哪些目录。当有人提交涉及这些文件的拉取请求时,GitHub会自动向相应的团队发送审核请求。 例如,如果某个工程师修改了PostgreSQL模块的相关代码,GitHub就会自动要求平台开发团队进行审批,之后才能合并这些更改。 如果没有CODEOWNERS机制,工程师们就需要自己记住哪些基础设施组件由哪个团队负责管理。 CODEOWNERS机制明确了各团队的职责范围,并能自动触发相应的审核流程。 “基础设施配置差异”指的是Terraform所预测的云环境状态与实际存在的状态之间的差距。 以下这种情况最容易导致这种差异的产生: 这件事被遗忘了,相关事宜也得到了处理,生活继续向前发展。 三个月后,一次常规的Terraform配置应用任务再次执行。Terraform在配置中看到了`db.m5.large`这一选项,但却发现实际上AWS上运行的是`db.m5.4xlarge`。从Terraform的角度来看,数据库的配置规模确实超出了预期,因此它建议将配置改回原来的状态。 没有人注意到这个配置变更带来的影响。应用任务成功执行后,数据库的配置确实被调整成了较小的规模,但用户们很快就开始报告查询速度变慢的问题。团队花费了数小时进行调查,最终才发现问题的根源在于Terraform之前进行的某次配置变更,这次变更实际上撤销了几个月前紧急采取的修复措施。 那些能够妥善处理这类问题的团队,都会定期对所有的生产环境执行`terraform plan`命令。如果`terraform plan`的执行结果返回代码`2`,那就说明发现了配置差异,系统会自动发出警报。此时团队需要决定是执行恢复操作来使配置回到之前的状态,还是更新配置以使其与实际状况相符。无论采取哪种方式,这些变更都应该是显而易见且经过深思熟虑的;而那些被隐藏起来的配置差异,往往会导致更严重的问题。 在几乎所有情况下,只要团队在问题发生之前做好了正确的准备工作,那么配置错误都是可以被纠正的。 那些能在二十分钟内解决问题而非三天后才恢复正常的团队,并不是那些对Terraform技术最为精通的团队,而是那些事先做好了充分准备的团队。 这样就可以将当前的配置状态保存到一个本地文件中。接下来无论你尝试做什么操作,这个备份文件都能为你提供一个可以恢复到的起点。 如果Terraform建议销毁那些实际上仍然存在于云端的资源,那就说明当前的配置状态与实际情况不符;如果它建议创建一些已经存在的资源,那就说明实际配置比预期要先进。无论哪种情况,`terraform plan`的输出结果都能告诉你这种配置差异的具体方向。 任何对使用S3版本控制功能的存储桶进行的写入操作,都会自动生成一个新的版本。如果配置文件损坏或存在错误,你可以列出所有的旧版本,下载最近一个正常版本的文件,然后将其重新应用到系统中: 在恢复配置后,先运行`terraform plan`命令,确认配置内容无误后再执行任何应用操作。 如果在应用操作失败后锁仍然未被释放,请手动清除它: 只有在确认没有正在运行的应用操作之后,才能执行此操作。清除正在使用的锁可能会导致配置状态混乱。 如果某个资源存在于云端,但Terraform已经无法识别它——可能是由于意外执行了`terraform state rm`命令——请直接重新导入该资源,而无需重新创建: 导入资源后,再次运行`terraform plan`命令,以确保没有出现任何意外的变更建议。 本文中提到的各种实践措施,都是针对团队在逐渐增加使用Terraform的过程中所遇到的具体问题而提出的。 状态锁定机制可以防止工程师们互相覆盖彼此的修改内容;状态分离功能有助于减少配置变更带来的影响范围;模块版本管理能确保共享基础设施不会意外出现故障;差异检测功能能够及时发现那些在Terraform之外进行的更改;而“代码所有者制度”则能确保由合适的人来审核相应的变更。 针对不同的问题,采取了不同的解决方案。但这些措施都围绕同一个核心主题:责任归属。 随着团队规模的扩大,许多与Terraform相关的问题其实与基础设施本身关系不大,而是与责任划分问题密切相关。 当多人可以同时修改同一配置状态时,就会发生状态冲突;而当没有人负责维护统一的配置标准时,模块结构就会变得杂乱无章;如果有人进行了更改却没有人负责将Terraform的配置状态与实际环境重新对齐,这些变更就会带来危险。甚至,审核流程中的瓶颈往往也源于对于谁应该批准哪些变更存在不确定性。 理解这一点,会帮助你更好地解读那些不熟悉的Terraform配置仓库。 几十个小的状态文件并不一定意味着设计过于复杂;它们往往代表着不同的责任划分范围。“代码所有者文件”并不是官僚主义的体现,而是一张明确责任归属的地图;而在拉取请求中展示配置计划结果的流程,也不仅仅是自动化工具,它实际上是一种基于基础设施影响而非代码本身来设计的审核机制。 基础设施固然重要,但随着团队规模的扩大,真正让系统保持可维护性的关键在于明确的责任划分。 我每周都会撰写关于DevOps工程、生产系统以及教程中未涉及的内容。如果这些内容对你有帮助, 请订阅我们的通讯吧。团队如何对Terraform模块进行版本管理和发布
payments-infra → 计划失败
analytics-infra → 计划失败
auth-infra → 计划失败
reporting-infra → 计划失败
v2.1.1 → 补丁版本:修复了某些错误,可以安全地升级,无需修改代码。
v2.2.0 → 小型更新:添加了新的可选功能,可以安全地升级,无需修改代码。
v3.0.0 → 大型更新:会导致一些功能发生变化,必须先阅读变更日志,然后再更新代码。
团队如何大规模维护Terraform模块
terraform-aws-vpc ← 原始版本,由平台团队维护
terraform-aws-vpc-v2 ← 由应用团队创建,作者不详
terraform-aws-vpc-shared ← 不清楚哪些环境在使用这个版本
terraform-aws-vpc-prod ↑ 不确定这个版本是否曾经与原始版本有所不同CODEOWNERS文件,每个拉取请求都会自动被转发给相应的审核人员。团队如何在不同状态文件之间共享数据
读取其他团队的状态输出数据
terraform_remote_state数据源可以让一个团队读取另一个团队的状态输出信息。网络团队会将他们的VPC ID和子网ID标记为输出数据,数据库团队则会读取这些数据,并根据这些信息将数据库部署到相应的子网中。网络状态
└── 输出结果:vpc_id、private_subnet_ids
↓
数据库会读取这些信息
└── 然后将RDS部署到相应的子网中直接从云平台查询资源
data "aws_vpc" "main" {
tags = {
Name = "production-vpc"
Environment = "production"
}
}基础设施变更是如何真正投入生产的
工程师提交拉取请求
↓
处理流程:运行terraform validate和格式检查
↓
进行安全扫描(使用Checkov、tfsec等工具)
↓
运行terraform plan命令,并将输出结果作为评论附在拉取请求上
↓
审核人员会查看完整的计划输出内容
↓
经过指定审核人员的批准后,才会合并代码
↓
合并操作会触发应用流程
↓
处理流程:获取状态锁 → 执行变更 → 释放锁 → 记录变更结果
# 必须替换aws_db_instance/payments字段
-/+ resource "aws_db_instance" "payments" {

CODEOWNERS机制是如何确保由相应团队负责审核相关代码的
团队是如何发现基础设施配置出现差异的
周一凌晨3点:生产数据库的CPU使用率突然激增,导致系统故障。
周一凌晨3点15分:工程师在AWS控制台中调整了数据库的大小,从db.m5.large改为db.m5.4xlarge。
周一凌晨3点20分:故障得到解决,工程师结束工作休息。
周一凌晨3点21分:Terraform的状态文件仍然显示数据库大小为db.m5.large。
当配置出现错误时,团队该如何恢复?
第一步:在采取任何行动之前先创建备份。
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).json第二步:运行`terraform plan`命令,查看它提出的建议。
第三步:如果配置文件损坏了,就通过S3版本控制功能来恢复数据。
# 列出所有旧版本
aws s3api list-object-versions \
--bucket mycompany-terraform-state \
--prefix production/database/terraform.tfstate
# 下载特定版本的文件
aws s3api get-object \
--bucket mycompany-terraform-state \
--key production/database/terraform.tfstate \
--version-id "the-version-id-here" \
recovered-state.json
# 将恢复后的文件重新应用到系统中
terraform state push recovered-state.json步骤4:如果管道被阻塞,请清除过期锁。
terraform force-unlock LOCK_ID步骤5:重新导入那些状态异常的资源。
terraform import aws_db_instance.payments db-ABCD1234EFGH5678结论
如果你喜欢阅读这篇文章,我们也可以在 LinkedIn上建立联系。