增强Terraform体验:为什么我们使用Terragrun

2020-05-08 06:12:38

在Transcend,我们非常重视我们的基础设施及其安全。我们的产品管理其他公司用户的个人数据;我们的基础设施必须是无懈可击的。我们尽可能实现自动化,以降低我们发布的风险,并确保我们的安全标准易于控制和审计。为了实现这些目标,我们结合使用Terragrun和Terraform。

TerraForm(由HashiCorp开发)使人们能够使用代码来配置和管理任何云、基础设施或服务。Terragrut(由HashiCorp的官方合作伙伴Gruntwork提供)包装了Terraform二进制文件,以提供强大的增强功能,从更多的一级表达式到模块依赖项和内置缓存。

Terragrun中的最佳实践在默认情况下鼓励模块的可重用性和可扩展性:它迫使我们做出维护我们的安全和开发原则的良好的技术决策。

Terragrun最初的目的是填补Terraform功能上的一些空白,并不断扩展新的功能。尽管Terraform已经发展到支持更高级的功能集,但它仍有改进的空间。只有Terragrun将以下丰富的功能集带到了桌面上:

为了使Terraform模块可重用,它们需要是可配置的。Terragrun的显式依赖关系无疑是实现这一目标的最有效方式。

虽然使用Terraform远程状态数据源可以在模块之间传递输出值,但使用它可能会导致复杂而冗长的代码。此外,在具有不同状态配置的情况下,这会降低模块的可重用性,并且几乎不会给代码的读者提供关于值来自何处的指导。

Terragrant在显式依赖块中提供了另一种选择,它的功能要强大得多。以下示例使用与Terragrut的依赖关系在具有现有安全组(虚拟防火墙)的现有虚拟私有云(VPC)内的AWS上配置数据库:

1#定义要使用的Terraform源模块:本例中的RDS数据库模块

在Terraform中,我必须定义依赖项,然后在需要它们的每个模块中定义如何摄取它们。这些样板定义可以加起来。

Terragrut知道应该检查这些模块的配置,以了解如何访问它们的状态,这对于跨多个工作区/AWS帐户使用依赖关系非常有用。

在Terraform中,由于状态仅在模块运行后才可用,因此模块的运行顺序很重要(而Terraform不知道该顺序)。操作员需要记录应用程序的顺序。

Terragrun创建依赖关系树,并以正确的顺序运行所有命令,以便在执行时所有必要的依赖关系都可用。

查看Terraform模块注册表上经过验证的模块,会出现一种模式:几乎没有一个模块使用REMOTE_STATE数据源。事实上,托管数十个经过验证的模块的terraform-aws-module Github组织只使用一个REMOTE_STATE依赖项。

这是因为使用REMOTE_STATE会降低模块的可重用性。虽然变量输入可以来自任何地方,但是将REMOTE_STATE块放在模块代码中会将该模块的使用限制为仅当REMOTE_STATE已经存在并且该模块有权访问该状态时才能使用。

此外,Terragrut依赖项内置了依赖项注入,这使得使用像Terratest这样的工具进行测试变得轻而易举。

为了使我们的基础设施保持最新,我们使用流行的CI/CD系统Atlantis。Terragrut创建的依赖层次结构允许我们自动生成Atlantis配置,取代了手动配置的痛苦过程。在Transcend,我们自豪地制作并维护了开源工具terragrun-atlantis-config:我们的Atlantis配置生成器。

在经历了手动且容易出错的更新atlantis.yaml文件的过程之后,我们创建了这个工具。这些文件可能有数万行长,必须在配置中定义每个模块的依赖关系树,定义不当的依赖关系会自动失败。

例如,环境变量使以编程方式填充AWS配置文件值成为可能。它们可以提供后备,并且不鼓励对值进行硬编码。有了环境变量,生活会更美好。

TerraForm没有计划很快支持环境变量(从Mart(一个Terraform维护者)对此Pull请求的响应中可以明显看出),这是有充分理由的!当子模块依赖于从未显式设置的环境变量时,这样做可能会导致非常难以调试的Terraform错误。Terraform的理念并不是说环境变量不好,而是应该显式设置它们,并且只对顶级模块可用。因为Terragrut是一个只处理根模块的包装器,所以它可以并且确实支持环境变量。

如果您使用AWS,您已经为标记声明了多少次变量?为AWS区域部署一个变量怎么样?在提供商中承担的IAM角色如何?您指定了多少个位置具有相同的Terraform Provider版本约束?

我也这么想的。使用Terragrut可以在计划或应用模块之前,使用生成块动态地将Terraform代码添加到模块中,而不是到处移植通用代码。

使用所需的所有变量编写提供程序允许您在需要的任何地方添加该提供程序,只需一个简短的GENERATE块。

说到到处使用标记变量,您定义了多少位置将哪些标记传递给模块?它们添加了有价值的元数据,可用于跟踪从哪些资源最昂贵到资源源代码位置的所有信息。如果您想用创建它的Terraform文件的名称为您的AWS帐户中的每个资源添加一个新标记,需要做多少工作?

有了Terragrant,事情就很简单了。如上所述,使用GENERATE块将标记变量与您需要的其他公共变量一起添加到任何位置,然后在您的父Terragrut文件(每个环境都有一个)中,添加输入:

就像那样,每个单独的资源都会告诉我们管理它的是哪个模块。如果我浏览AWS控制台,发现某些ECS服务因为缺少环境变量而失败,我只需检查该服务上的TerraformPath标记,就可以确切地了解我需要编辑代码库中的哪个文件。

Terragrut的run_cmd函数可在本地外壳中执行任意命令。这在特定任务没有Terraform提供程序(库)的某些特殊情况下很有用。

例如,我们使用AWS IAM身份验证连接到我们的Hashicorp Vault群集。在存储库提供程序内部,我们可以使用iam实例元数据…进行身份验证。但我们需要AWS IAM请求标头来完成身份验证,而获取AWS IAM请求标头是一项复杂的任务。阻力最小的方法是使用外部库,如aws4,这是一个流行的NodeJS库。

幸运的是,因为我可以将变量设置为任意命令输出,所以为了将这个复杂的变量提供给Terraform,我只需要编写一个脚本来生成头,然后将变量指向该输出,就像我在下面的示例Terraform代码中对IAM_REQUEST_HEADERS所做的那样:

Terragrut提供了一个全新的函数read_terragrut_config,允许从外部Terragrut文件导入Terragrun代码。

在Transcend,我们有一些共享环境变量公共子集的ECS容器定义。在公共文件中,我们创建这些共享环境变量映射,然后在容器模块中,我们只需读入公共配置并将其结果与我们的输入合并。

虽然收集我们的环境变量有一些复杂性(它有相当多的依赖项和函数调用),但read_terragrut_config允许我们将复杂性隔离在一个位置,并在其他位置利用其结果。

就像任何一款软件一样,随着时间的推移,我们注意到了一些缺失的功能,不得不绕过一些粗糙的边缘。让我们来看看我们遇到的一些历史问题,以及我们是如何处理它们的:

正如我们前面所展示的,Terragrun依赖项配置块非常强大和有用。但是,由于模块经常依赖于数十个其他模块,每个模块都可能有自己的依赖关系,因此依赖关系树可能会迅速膨胀。一旦我们到达了大约300个顶级模块,其中一些模块有50多个依赖项,它们的Terraform状态需要Terragrun进行查找,运行Terragrun计划命令可能需要10分钟以上的时间。

为了处理此延迟,我们向Terragrut存储库提交了一个Pull Request(PR),该存储库更新了库以使用goroutines并发查找所有状态依赖项。我们在一个周五晚上10点提出了公关,不到一个小时就合并了,第二天早上,它已经在Homebrew上发布了。

有了这些改变,我们最慢的模块现在只需要两分钟就可以计划第一次,一旦Terragrun缓存建立,计划只需要不到一分钟。

在Terraform和Terragrun中,可以在先生成计划文件的情况下进行更改,也可以在不生成计划文件的情况下进行更改。或者:

由于Terragrut实现了如此多的自动化,因此确保应用程序配置不会遇到Terraform的怪癖就变得非常重要:否则,很容易无意中将变量传递给具有planfile的Apply,所有东西都会爆炸。(实际上,它只会出错。感觉就像是在爆炸。)。

Terragrun在应用任何地形之前始终运行Terragrun计划的最佳实践将有助于解决此问题,但在Transcend,我们选择将我们的实践更进一步,以彻底消除该问题。

我们不使用Terraform变量文件,而是使用Hashicorp Vault Provider或通过从应用程序代码直接查询Vault群集,直接从Hashicorp Vault群集加载所有密码值。这样,我们就不会手动共享秘密文件(git中的秘密是不安全的),我们可以对秘密存储的安全性有极高的信心。

由于Terraform和Terragrun将Terraform状态文件存储为纯文本,并且所有机密都可见,这是六年多讨论的持续主题,因此通过将尽可能多的机密转移到Vault中,我们可以确保它们永远不会出现在我们的州中。

这不仅使我们的基础设施代码变得精简(这使得将我们的部署转移到具有Atlantis的CI系统上轻而易举),而且我们的状态文件现在在发生泄漏的情况下更加安全:这是一个巨大的安全胜利!

Terragrun是一个相当冗长的工具,以至于它有史以来最受欢迎的问题是请求干净的日志记录。我们可以产生共鸣。

我们的一些模块为每个计划生成数千个日志。当出现错误时,这些日志可能很有用,但当计划成功时,这些日志往往会过多。

为了解决这个问题,我们自己编写了一个小的Terragrut包装器,在成功运行时,它只显示Terraform计划的输出。当我们的Terraform代码出现问题时,我们的包装器会显示整个Terragrun日志集,这样我们就可以轻松地调试出错误的地方。

系统堆栈中的每个工具都增加了复杂性,既增加了在系统中工作的认知负荷,也增加了新开发人员进入的门槛。一般而言,我们尽量避免复杂性,并尽可能构建精简的代码库。

在Transcend,我们发现Terragrun不仅缩短了我们的代码库几千行,还极大地简化了我们的基础设施代码。我们不再需要重复我们的提供程序、状态或依赖项配置,因此我们代码的逻辑现在完全集中在它应该关注的东西上:我们的开发人员想要的平台资源。

在极少数情况下,Terragrun达不到我们的期望,维护人员对反馈、功能请求和拉请求非常开放,并迅速解决了我们的痛点。

凭借Terragrut为Terraform带来的所有出色功能,我们能够确信,我们部署的每个基础设施都将产生安全、可审核的系统,为我们客户的敏感数据提供强有力的保护。