Terraform 是国际著名的开源的资源编排工具,据不完全统计,全球已有超过一百家云厂商及服务提供商支持 Terraform。Terraform 是 HashiCorp 的代码软件基础设施。它允许用户使用高级配置语言定义数据中心基础架构,从中可以创建执行计划以构建 OpenStack 等基础架构,或者在 IBM Cloud,AWS,Microsoft Azure,Google Cloud Platform 等多种云服务中构建基础架构。
2015 年,当 Terraform 第一次进入我的视野,它就像是我的瓦尔哈拉(北欧神话中死亡之神奥丁款待阵亡将士英灵的殿堂)。Terraform 旨在解决复杂基础设施的配置问题——将多个云供应商汇聚在一起——从 AWS 这样的巨头到 Logentries 这样的单一解决方案供应商。
我们的团队认为我们需要一些解决方案来处理基础设施的复杂性问题。对于一个基于 Heroku 和 AWS 的平台来说,水平伸缩到四个 Terraform 似乎是一个完美的解决方案。我们希望有一些东西能够让我们意识到基础设施即代码这个概念的重要性,因为对于一个 DevOps 团队来说,这是必须的。Terraform 功能齐全,但也并非十全十美,仍然有一些问题值得我们注意。
我将列举一些比较大的“坑”,并分享我们是如何填这些坑的。最后,我将会说明,尽管存在这些挑战,但 Terraform 在工具领域仍然有很大的发展空间。
1. 邪恶的状态
首先,Terraform 是有状态的,我个人认为这会带来两个问题:
状态必须始终与基础设施保持同步——这意味着在配置时需要全盘考虑,不能在配置工具之外进行栈的修改。
你必须在某个地方维护状态——必须是一个安全的地方,因为状态可能会带有秘钥之类的东西。
但 Terraform 引入状态是有原因的,状态用来维护文件中定义的资源与在云供应商平台上创建的实际资源之间的映射关系。有了这个,Terraform 就可以为我们提供一些好处:
从云供应商那里处读取状态(状态同步,也称为刷新)可能非常耗时。如果我们可以 100%确定状态是准确的,我们完全可以不进行同步,并立即应用变更。
能够跟踪已创建的资源,可以更轻松地进行重命名和修改结构——这些都是基本的基础设施操作。
Terraform 在应用变更之前会锁定状态,这意味着我们可以确保在应用变更时,没有其他人在做同样的操作。
我认为,在考虑配置工具时,你应该权衡一下上述的几个问题,并搞清楚你的栈是像工作簿之类的东西,每次做出变更后都可以进行重建,还是像是一个生物有机体,需要在它运行过程中做出变更。
2. 难以集成已有的栈
在 Terraform 的早期,很多人抱怨无法将 Terraform 用在已有的栈上。其原因在于,Terraform 无法将已有栈纳入到它的状态管理中。所幸的是,Terraform 通过引入 import 命令解决了这个问题(至少在系统级别)。
但又出现了另一个与此紧密相关的问题——如果你的栈很大,就必须为每个资源多次使用 terraform import 命令。如果不使用一些自动化脚本,这就变成了一个非常耗时且令人感到沮丧的活儿。我们完全可以通过更好的方式来导入这些东西,不过这要求 Terraform 将资源视为树,而不是扁平的结构。在某些情况下,它是完全合理的——比如 heroku_app 和 heroku_domain(或 heroku_drain)。当然,肯定还有很大的改进空间。
3. 复杂的状态修改
在重构基础设施定义时,到最后可能就是重命名资源(修改它们的标识符)或将资源移动到模块中。遗憾的是,Terraform 很难跟踪这些变化,并将它们置于一种未知的错配状态。如果再次运行 apply,会重新创建资源,但这可能不是你想要的。好在 Terraform 提供了 terraform state mv 命令,可用于移动逻辑资源。不好的地方在于,在大多数情况下,你需要大量使用这个命令。
4. 古怪的条件逻辑
Terraform 并非真正的命令式编程语言,有些人不喜欢这一点。但说实话,我并不这么认为——我认为栈的定义应该尽可能是声明性的——这样可以减少定义之间的偏差。另一方面,Terraform 提供的条件逻辑有点古怪。例如,如果要定义条件资源,需要将资源定义为列表,并使用 count 参数来控制它:
resource "heroku_app" "some_app" { count = "${var.create_app}" name = "some-app" ... }
这已经相当具体了,而且你不需要知道 if/else 是怎么回事。
resource "heroku_app" "some_app" { count = "${var.create_app}" name = "some-app" ... } resource "heroku_app" "some_app" { count = "${1 - var.create_app}" name = "some-app" ... }
不过需要注意的是,如果有可能,应该尽量避免使用这样的结构。当然,这不应该成为拒绝使用 Terraform 的理由,只需要知道存在这个问题即可。
在近期发布的一些版本中,可以使用 resource for_each 来简化这个问题。
5. 无法简单地遍历模块
模块的概念是非常棒的——它将可重用的资源集包含在可重用的工件中。我们来看一些经过简化的例子:
app/app.tf
resource "heroku_app" "app" { name = "${var.name}" region = "eu" organization = { "name" = "${var.organization}" } config_vars = ["${var.configuration}"] } resource "heroku_addon" "deploy_hook" { app = "${heroku_app.app.name}" plan = "deployhooks:http" config = { url = "${var.deployhook_url}" } } output "name" { value = "${heroku_app.app.name}" }
可用在服务声明中:
stack/some_service.tf
module "app" { source = "../app" name = "some-service" configuration = { SOME_ENV_VARIABLE = "value" } pipeline_id = "000000-9207-490a-b050-617b01ef79f3" deployhook_url = "https://some.deploy.hook" }
这对我们来说是一个重大的变化,因为我们把很多重复的资源附加到每个应用程序——监控、日志收集、部署钩子等等。但这也带来了一个问题——由于某些原因,相同的工件有时候并不代表相同的资源。也就是说,它们不支持计数参数,但计算参数在应用条件逻辑或按照每个克隆进行服务迭代时却是非常关键的。确切地说,我们不应该这样做:
stack/services.tf(这不是真的)
module "app" { source = "../app" name = "some-service-${var.clone[count.index]}" configuration = "${var.configuration[count.index]}" ... }
我们必须重复每个克隆的定义:
stack/services.tf
module "clone1_app" { source = "../app" name = "some-service-clone1" configuration = "${var.clone1Configuration}" } module "clone2_app" { source = "../app" name = "some-service-clone2" configuration = "${var.clone2Configuration}" }
这个问题也有望在可预见的未来得到解决。
6. 不稳定的资源
作为一款功能丰富的 0.x 版本软件,Terraform 存在一大堆小问题。其中一个问题是,有些资源并不会保持稳定的状态。对我们来说,问题是 SNS 主题订阅策略——我们针对服务所做的所有事情,只要包含订阅了 SNS 的队列,Terraform 就会去修改它们(无论是否有意义)。这样可能会导致混淆,尤其是对于第一次接触 Terraform 的人来说。虽然这个问题是供应商局部问题,并且很可能会得到修复,但在修复之前总归是要注意的。
7. 一些小细节
我们遇到的另一个小问题是无法使用依赖于某些状态的计数值(在模块中),例如:
resource "heroku_app" "sample" { count = "${lookup(var.some_map, "enable_sample_app", 0)}" name = "sample-app" ... }
如果在模块中定义了上述的内容,你将会得到一条这样的消息:无法计算 count 的值…这真的很烦人,特别是当你看到 Hashicorp 的解释,说可以使用 -target 来逐个初始化资源。
8. 如何处理秘钥?
Terraform 文件之所以不容易保存,其中一个原因是不知道该把秘钥保存在哪里。有几种方法可以解决这个问题:
- Hashicorp 使用了他们的 Vault——虽然这是可行的,但它使整个设置变得更加复杂,感觉有点矫枉过正。
- 与 Vault 类似,你可以使用 AWS 的 KMS 来存储秘钥——但它同样有复杂性方面的问题。
- 使用私有 git 存储库,只要电脑不被偷,就不会有什么问题。
- 如果你是在 CD/CI 服务器上运行作业,可以将秘钥保存在环境变量中,但在一个复杂的系统中,可能就难以维护太多的环境变量。
- 你可以将秘钥保存在本地,并使用专门用于配置的机器,但对于有一定规模的团队来说,这样做是不行的。
- 我们的方法是在.tf 文件保存所有秘钥变量,然后使用 git-secret 加密。运行 terraform plan 和 terraform apply 的脚本先用 git-secret 显示秘钥,然后再立即隐藏。虽然这不是一个完美的解决方案,但至少足够简单,减少从本地运行 Terraform 可能会造成的秘钥泄露几率。
9. 托管服务
最初,Hashicorp 和其他公司都没有提供 Terraform 托管服务。作为一款非常复杂的软件(特别是秘密管理),有必要提供一个利基,于是 Terraform Enterprise 出现了。可惜的是,我没有这方面的经验,所以不确定它究竟好不好。但它可能存在的一个问题的是,使用企业模式会导致供应商锁定。
那么我们应该怎么办?
很明显,在选择配置解决方案时,我们需要考虑到 Terraform 存在的一些问题。其中一些问题最终将得到解决,其他一些则是 Hashicorp 必须做出的架构选择。在文中最后,我应该给出我的观点——为什么要选择 Terraform?在我看来,对于某些场景,你根本就没有其他选择,因为很难找到适合这么多云供应商的解决方案。此外,如果你的系统是一个实时系统,每天都有大量重复的基础设施小变更,那么 Terraform 绝对值得考虑。
英文原文: https://www.schibsted.pl/blog/9-reasons-why-terraform-is-a-pain-and-1-why-you-should-still-care/
感谢张婵对本文的审校。
评论