Terraform:多云、混合云环境下实现基础设施即代码

思维导图

前言

将基础设施代码化,使用代码对硬件进行管理,在运维领域借用软件领域的最佳实践,将基础设施的运维纳入软件工程的范畴,最终整体改善软件开发和软件交付的过程。

HCL2

在Terraform 0.12版本中,将基础语言从HCL全面升级到HCL2。升级包括对第一类表达式的支持(这样就不需要将变量包装在${...}中了),丰富的类型限制,惰性计算的条件表达式,对null、for_each和for表达式、动态内联块等的支持

不仅可以使用Terraform管理更多其他类型的云平台(例如Alicloud、Oracle Cloud Infrastructure、VMware vSphere等),还可以通过Terraform 将云平台之外的系统作为代码进行管理:包括版本控制系统(例如GitHub、GitLab或BitBucket)、数据存储系统(例如MySQL、PostreSQL或InfluxDB)、监视和警报系统(例如DataDog、New Relic或Grafana)、平台工具(例如Kubernetes、Helm、Heroku、Rundeck或Rightscale),等等

https://github.com/brikis98/terraform-up-and-running-code

Gruntwork的使命是能够10倍地简化软件的开发和部署工作。

为什么使用Terraform

主题

内容

DevOps四大核心价值

文化(culture)、自动化(automation)、度量(measurement)、共享(sharing)

配置漂移

服务器配置多多少少与其他服务器有所不同的问题(configuration drift)

雪花服务器

非标准化配置的服务器(snowflake server)

基础设施即代码

将所有事物都在代码中进行管理,包括服务器、数据库、网络、日志文件、应用程序配置、文档、自动测试、部署过程等

配置管理工具

Chef、Puppet、Ansible、SaltStack

服务开通工具

CloudFormation、Terraform、OpenStack Heat

无主控服务器软件

Ansible、CloudFormation、Heat、Terraform(依赖于云服务提供商的API或基础设施的一部分,不需管理额外组件)

软件交付(software delivery)是指通过一系列工作使代码最终对客户“可见、可用”的过程,例如,将代码运行在生产服务器之上,使代码能够应对数据流量的激增和意外停机中断,保护代码免受攻击者破坏

DevOps的崛起

在开始的一段时间内效果还好,但是随着公司的发展壮大,最终会遇到问题。问题通常是这样的:因为软件发布通过手工完成,随着服务器数量的增加,发布的过程变得缓慢、痛苦和不可预知。运维团队有时也会犯错,最终会导致环境中的每个服务器的配置,多多少少与其他服务器有所不同(此问题被称为configuration drift,即配置漂移),而非标准化配置的雪花服务器(snowflake server)的出现,导致更多的错误发生

DevOps有四大核心价值:文化(culture)、自动化(automation)、度量(measurement)和共享(sharing),有时缩写为CAMS

什么是基础设施即代码

DevOps的一个重要观点是,用户应该将所有事物都在代码中进行管理,包括服务器、数据库、网络、日志文件、应用程序配置、文档、自动测试、部署过程等。

Terraform与其他IaC工具的比较

Chef、Puppet、Ansible和SaltStack都属于配置管理工具,而CloudFormation、Terraform和OpenStack Heat都是服务开通工具。实际上区分并不明显,配置管理工具通常可以进行某种程度的服务开通(例如,使用Ansible部署服务器),服务开通工具通常也可以进行某种程度的配置管理(例如,使用Terraform配置服务器和运行配置脚本)。

默认情况下,Ansible、CloudFormation、Heat和Terraform均为无主控服务器软件。更准确地说,其中一些可能依赖于主控服务器,但是这些服务器已经是你正在使用的基础设施的一部分,而不需要你去管理额外的组件。例如,Terraform使用云服务提供商的API与云平台进行通信,从某种意义上讲,API服务器就扮演着主控服务器的角色,只是它们不需要任何额外的基础设施或额外的身份验证机制(只需要使用已有的API密钥)。

图1-8:Terraform使用无主控服务器模式和无代理软件的架构

服务开通工具+配置管理工具

例如,搭配使用Terraform和Ansible,如图1-9所示。你可以使用Terraform部署所有基础设施,包括网络拓扑(如虚拟私有云VPC、子网、路由表)、数据存储(如MySQL、Redis)、负载均衡器和服务器。然后使用Ansible将应用程序部署在这些服务器之上。

图1-9:搭配使用Terraform和Ansible

服务开通工具+服务器模板工具

例如,搭配使用Terraform和Packer,如图1-10所示。使用Packer将应用程序打包为虚拟机映像。然后使用Terraform部署:运行这些虚拟机映像的服务器,以及其他基础设施,包括网络拓扑(即VPC、子网、路由表)、数据存储(如MySQL、Redis)和负载均衡器。

图1-10:搭配使用Terraform和Packer

服务开通工具+服务器模板+编排工具

例如,搭配使用Terraform、Packer、Docker和Kubernetes,如图1-11所示。你可以使用Packer创建包括Docker和Kubernetes服务的虚拟机映像。然后通过Terraform部署服务器集群,每个服务器都运行此虚拟机映像,以及其余基础设施,包括网络拓扑(即VPC、子网、路由表)、数据存储(如MySQL、Redis)和负载均衡器。

图1-11:搭配使用Terraform、Packer、Docker和Kubernetes

小结

表1-4:流行的IaC工具的使用对比

Terraform入门

主题

详细信息

Terraform资源定义

PROVIDER: 提供商名称(如aws)TYPE: 资源类型(如instance)NAME: 标识符(如my_instance)CONFIG: 资源特定参数

.terraform文件夹

Terraform的临时目录,应加入.gitignore以防上传到版本控制系统

plan命令输出

使用符号标示变更:加号(+)为新增内容,减号(-)为删除内容,波浪号(〜)为修改内容

.gitignore文件内容

忽略.terraform目录和*.tfstate文件,防止存入版本控制系统

表达式

Terraform中返回值的对象,如字符串、数字

引用(Reference)

访问代码其他部分的值,例如资源属性引用(resource attribute reference)

隐式依赖关系

在资源内部引用另一个资源创建的依赖,用于确定资源创建顺序

terraform graph命令

显示资源的依赖关系图

type关键字

用于对用户输入的变量进行类型约束(string、number、bool等)

环境变量命名规范

TF_VAR_<name>,用于设置输入变量的初始值

默认值设定

为输入变量指定默认值,减少命令行参数记忆负担

插值(Interpolation)表达式

在字符串中使用变量引用,如${var.name}

输出变量定义

NAME: 输出变量名VALUE: Terraform表达式CONFIG: 可选参数,包括senstitive

sensitive参数

若为true,防止敏感信息(如密码)在terraform apply日志中显示

terraform output命令

查看指定输出变量的值

部署单个服务器

其中PROVIDER是提供商的名称(例如aws)。TYPE是在该提供商中创建的资源类型(例如instance)。NAME是一个标识符,你可以在整个Terraform代码块范围内通过这个标识符引用该资源(例如my_instance)。CONFIG包括一个或多个特定于该资源的参数或参数组。

在默认情况下,提供商代码将被下载到.terraform文件夹中,该文件夹是Terraform的临时目录(用户或许需要将其添加到.gitignore,以防止将这个临时目录上传到版本控制系统)。

plan命令的输出,类似于UNIX、Linux和Git中用过的diff命令:加号(+)代表任何新添加的内容,减号(-)代表删除的内容,波浪号(〜)代表所有将被修改的内容。

创建一个名为.gitignore的文件,它会告诉Git忽略某些类型的文件,以免你无意中将临时文件存入版本控制系统中。

前面的.gitignore文件的内容,指示Git忽略Terraform临时目录.terraform文件夹,以及Terraform用来存储状态的*.tfstate文件

部署单个Web服务器

Terraform中任何具有返回值的对象都被称为表达式。你已经看到了最简单的表达式类型,如字符串(如"ami-0c55b159cbfafe1f0")和数字(如5)。

引用(reference)是一种特别有用的表达式类型,它使用户可以从代码的其他部分访问该值。如果要访问安全组资源的ID,需要使用资源属性引用(resource attribute reference),该引用的语法如下。

当在一个资源内引用另一个资源时,会创建隐式依赖关系。Terraform可以通过分析这些依赖关系,构建依赖关系图,并使用该关系图自动确定资源的创建顺序。如果你从零部署这个代码,Terraform知道它需要在创建EC2实例之前先创建安全组,因为EC2实例引用了安全组的ID。可以通过运行terraform graph命令显示依赖关系图。

以上输出的格式为DOT图形描述语言,通过使用桌面应用,例如Graphviz,或Web应用GraphvizOnline(见参考资料第2章[20])等工具,可以自动生成一个类似图2-7所示的EC2实例及其安全组的依赖关系图。

部署可配置的Web服务器

type

允许对用户输入的变量类型进行强制约束。Terraform支持许多类型约束,包括string、number、bool、list、map、set、object、tuple和any。如果未指定类型,那么Terraform会设置默认约束类型为any。

还可以使用类型约束创建更复杂的对象和元组结构类型。

也可以通过环境变量来设置输入变量初始值。命名规范是TF_VAR_,其中是你要设置的输入变量的名称。

如果不想在每次运行plan或apply时都记住额外的命令行参数,也可以指定一个默认值。

下面是如何将安全组资源的from_port和to_port参数,设置为变量server_port的值的示例。

在用户数据脚本中设置端口时,最好使用相同的输入变量。要在字符串文字中使用变量引用,需要通过一种被称为插值(interpolation)的表达式,其语法如下。

用户可以在花括号中放置任何有效的变量引用,Terraform会把它转换为字符串。例如,使用以下方法可以将var.server_port的取值作为字符串插入到用户数据中。

Terraform还允许通过使用以下语法来定义输出变量

NAME是输出变量的名字,VALUE是任何你希望输出的Terraform表达式。CONFIG包含两个可选参数。

senstitive

如果此参数设置为true,Terraform在运行terraform apply指令时,不会在日志中记录输出信息。这是一种非常有用的方式,可以用来防止记录输出变量中的敏感信息,例如密码或私钥。

运行terraform output 命令来查看名为的特定输出变量的取值。

Terraform状态

功能

详细信息

Terraform工作区

使用terraform workspace list查看工作区使用terraform workspace select切换工作区

环境和组件隔离

为每个环境(如预发布、生产)和组件(如VPC、服务、数据库)使用单独的Terraform文件夹和状态文件

terraform apply执行

在每个Terraform文件夹中多次运行使用Terragrunt的apply-all命令自动执行

terraform_remote_state数据源

读取其他Terraform状态文件的数据

机密信息保护

使用export命令前留空格避免机密信息存储在Bash历史使用工具(如pass)安全地将机密信息读取到环境变量中

terraform console命令

打开交互式控制台,实验内置函数功能,查询基础设施状态

file函数

读取文件内容并以字符串形式返回

template_file数据源

有两个参数:template(处理的字符串)和vars(变量集合映射),输出属性为rendered

template_file实际操作

在stage/services/webserver-cluster/main.tf添加template_file数据源代码

User Data脚本更新

在stage/services/webserver-cluster/user-data.sh中使用template_file数据源的变量

更新aws_launch_configuration资源

使用template_file数据源的rendered输出变量作为user_data参数

隔离状态文件

你拥有3个可用的工作区,可以通过terraform workspace list命令查看。

并且可以随时使用terraform workspace select命令,在它们之间进行切换。

建议为每个环境(预发布环境、生产环境等)和每个组件(VPC、服务、数据库)使用单独的Terraform文件夹(并因此使用单独的状态文件)

需要在每个文件夹中多次运行terraform apply(请注意,使用Terragrunt,可以通过apply-all命令来自动执行此过程)。

terraform_remote_state数据源 请注意,export命令前故意留有一个空格,这样做可以避免机密信息存储在Bash历史记录中。

还有一种更好的方法可以避免意外将机密信息以纯文本形式存储在磁盘上,即使用命令行友好的机密信息存储区,例如 pass(见参考资料第3章[10])中,使用子进程安全地将机密信息从pass读取到环境变量中。

Web服务器集群代码可以通过使用terraform_remote_state数据源来读取这个状态文件的数据。stage/services/webservercluster/ main.tf中数据源定义如下。

运行terraform console命令打开一个交互式控制台,通过交互式控制台可以很好地实验内置函数的功能。运行Terraform语法,查询基础设施的状态,并立即返回结果。

此函数读取PATH参数中定义的文件,并以字符串形式返回其内容。例如,你可以将你的User Data脚本放入stage/services/webserver-cluster/user-data.sh文件中,并将其内容作为字符串形式加载,如下所示。

难点是,在Web服务器集群的用户数据脚本中,需要Terraform的一些动态数据,包括服务器端口、数据库地址和数据库端口。之前你可以使用Terraform插值,将引用嵌入到Terraform代码的用户数据脚本中。但是这不适用于file函数,你必须通过template_file数据源一起工作。

template_file数据源有两个参数:template,定义将要被处理的字符串vars,是在处理字符串时将要用到的变量集合的映射,它有一个被称为rendered的输出属性,这是对模板进行处理后的结果。下面实际操作一下,将以下template_file数据源代码添加到stage/services/ webserver-cluster/main.tf文件中。

从上面的代码可以看到,template参数指向user_data.sh脚本,vars参数包括3个User Data脚本中需要的变量:服务器端口、数据库地址和数据库端口。要使用这些变量,你需要按以下方式更新stage/services/webserver-cluster/user-data.sh脚本。

最后一步是更新aws_launch_configuration资源的user_data参数,使其指向template_file数据源的rendered输出变量。

使用Terraform模块创建可重用基础设施

主题

详细信息

模块化的好处

在多个环境中重复使用代码,提高代码的可重用性、可维护性和可测试性

模块基础知识

创建modules文件夹,移动stage/services/webserver-cluster文件到modules/services/webserver-cluster

使用模块的语法

NAME: 模块标识符SOURCE: 模块代码路径CONFIG: 模块特定参数

模块的输入参数

在modules/services/webserver-cluster/variables.tf中定义输入变量

预发布环境设置

在stage/services/webserver-cluster/main.tf设置instance_type为"t2.micro",min_size和max_size分别为2

生产环境设置

在live/prod/services/webserver-cluster/main.tf中,使用更高性能的instance_type(如m4.large),将max_size设置为10

模块版本控制

使用Git存储库管理不同的模块版本,通过改变source URL在环境之间切换不同版本

小结

将软件工程的最佳实践应用于基础设施代码,进行代码评审、自动测试,创建版本,安全地在不同环境中测试

图4-3:将代码放入模块中可以在多个环境中重复使用该代码

模块化是编写可重用、可维护和可测试的Terraform代码的关键要素。一旦开始使用,你一定会喜欢上模块并开始尝试:将所有代码功能模块化,在公司中创建模块共享库,使用网上发现的模块,甚至将整个基础设施看成可重复使用的模块的集合。

模块基础知识

创建一个新的名为modules的顶级文件夹,并将所有文件从stage/services/webserver-cluster文件夹移至modules/services/webserver-cluster文件夹。最终具有模块和预发布环境的文件夹结构如图4-4所示。

图4-4:最终具有模块和预发布环境的文件夹结构

打开modules/services/webserver-cluster目录下的main.tf文件,删除provider定义。因为提供商的相关定义应该出现在调用模块的用户代码中,而不是模块本身的配置中。

现在,通过预发布环境使用此模块的语法。

其中,NAME是一个标识符,在整个Terraform代码中可以通过使用该标识符来引用此模块(如web-service),SOURCE是模块代码的路径(如modules/services/webserver-cluster),CONFIG包括一个或多个该模块的特有参数。例如,你可以在stage/services/webservercluster/main.tf中创建一个新文件,通过以下方式调用webserver-cluster模块。

模块的输入

Terraform的模块也可以具有输入参数。要定义它们,可以使用一种你已经熟悉的机制:输入变量。打开modules/services/webserver-cluster/variables.tf并添加3个新的输入变量。

接下来,在modules/services/webserver-cluster/main.tf文件中,使用var.cluster_name代替静态编码名称(如代替terraform-asg-example)。这是对ALB安全组进行的修改。

现在,在预发布环境的stage/services/webserver-cluster/main.tf文件中,需要相应地设置这些新的输入变量。

现在,在预发布环境(stage/services/webserver-cluster/main.tf)中,可以通过将instance_type设置为"t2.micro"和将min_size和max_size分别设置为2,保持小集群和低开销。

另一方面,在生产环境中,可以使用具有更多CPU和内存的instance_type,例如m4.large(请注意,此Instance类型不是AWS免费套餐的一部分。因此,如果只是进行学习且不想产生开销,请继续设置instance_type为"t2.micro"),然后可以将max_size设置为10,允许集群根据负载情况而收缩或增长(不用担心,集群最初只会启动两个实例)。

模块版本控制

图4-6:具有多个存储库的文件布局

要配置此文件夹结构,首先需要将stage、prod和global文件夹移到一个名为live的文件夹中。接下来,将live和modules文件夹配置为独立的Git存储库。以下是将modules文件夹配置为Git存储库的示例。

可以将预发布环境模块和生产环境模块中的source参数指向不同的Git URL,实现模块的版本控制了。如果modules存储库位于GitHub存储库github.com/foo/modules中,以下是live/stage/services/webserver-cluster/main.tf文件中source参数的格式(请注意以下Git URL中的双斜杠是必需的)。

可以通过仅仅更新预发布环境中(live/stage/services/webserver-cluster/main.tf)的source URL来使用这个新版本。

在生产环境中(live/prod/services/webserver-cluster/main.tf),可以继续运行v0.0.1。

小结

通过将基础设施代码定义为模块,可以将软件工程的最佳实践应用于基础设施代码的开发过程。可以通过代码评审和自动测试来验证模块的每次更改;可以为每个模块创建符合语意版本规范的发布;可以在不同的环境中安全地测试模块的不同版本,如果遇到问题,可以恢复到以前的版本。

循环

要在Terraform中完成类似的操作,可以使用count.index变量,获取循环中每次迭代的索引值。

Terraform陷阱

经验教训

详细信息

通过Terraform进行所有操作

一旦基础设施部分由Terraform管理,避免手动更改,以确保代码准确代表基础设施

使用import命令

对已存在的基础设施使用terraform import命令,将其添加到Terraform状态文件中进行管理

始终使用plan命令

运行plan命令以捕获潜在问题,特别注意可能会被错误删除的资源

在销毁前创建

考虑在删除资源前先创建新资源,使用create_before_destroy参数或通过两步手动过程实现

更改资源标识符时更新状态文件

更改资源标识符(如重命名)时,使用terraform state mv命令更新状态文件,而不是手动更改

注意不可变参数

某些资源参数不可更改,更改这些参数会导致Terraform删除旧资源并创建新资源

处理异步和最终一致性API

使用异步和最终一致性API时,等待操作确认完成并更新系统后再进行重试

有两个主要的经验教训。

开始使用Terraform后,任何操作都要通过Terraform进行。

当基础设施的一部分已经由Terraform管理时,切勿手动对其进行更改。否则,不仅将面对各种怪异的Terraform错误,而且还会错过许多使用基础设施即代码(IaC)的好处,因为该代码已经不能准确地代表你的基础设施。

对已经存在的基础设施,请使用import命令。

如果在开始使用Terraform之前,已经创建了基础设施,则可以通过terraform import命令,将基础设施添加到Terraform的状态文件中,以便Terraform可以管理该基础设施。import命令有两个参数。第1个参数是Terraform配置文件中资源的“地址”。这里使用与资源引用相同的语法:_.(如aws_iam_user.existing_user)。第2个参数是特定于资源的ID,用于标识要导入的资源。例如,aws_iam_user资源的ID和用户名称相同(yevgeniy.brikman),而aws_instance资源的ID是EC2实例的ID(i-190e22e5)。在每个资源文档的页面底部,通常都会描述如何导入它。

4个主要的经验。

始终使用plan命令

运行plan命令可以捕获所有这些陷阱。仔细阅读输出结果,尤其注意terraform plan输出提示中的那些将要被删除但是你不想删除的资源。

在销毁前创建

如果确实要替换资源,请仔细考虑是否需要在删除之前先进行创建。如果需要这样,你可以通过create_before_destroy参数来实现。或者,也可以通过两个手动步骤来实现相同的效果:首先,将新资源添加到配置中,运行apply命令;接下来,从配置中删除旧资源,再次运行apply命令。

更改标识符需要更改状态文件

如果要更改与资源关联的标识符(例如,将aws_security_group从instance重命名为cluster_instance),而又不想意外地删除和重建该资源,则需要对Terraform状态文件进行相应地更新。永远不要手动更新Terraform状态文件,而要使用terraformstate命令来完成更新。在重命名标识符时,需要运行terraform state mv命令,该命令具有以下语法。

其中ORIGINAL_REFERENCE是当前对资源的引用表达式,NEW_REFERENCE是要将其移动到的新位置。例如,如果要将aws_security_group从instance重命名为clus ter_instance,则需要运行以下命令。

指示Terraform将以前与aws_security_group.instance关联的状态全部变更为与aws_security_group.cluster_instance相关联。如果在重命名标识符后运行了这个命令,在今后运行terraform plan命令时,将显示没有任何更改。

一些参数是不可变的

许多资源的参数都是不能被更改的。如果更改它们,Terraform将删除旧资源并创建一个新资源来替换它。每个资源的文档通常会说明如果你更改参数会发生什么,因此请养成查阅文档的好习惯。再次强调,请始终使用plan命令,并考虑是否应使用create_before_ destroy策略。

如果使用异步和最终一致性的API,应该等待一段时间,直到该操作已经确认完成并更新整个系统后再重试。

生产级Terraform代码

表6-1:从零开始构建生产级基础设施需要的时间

生产级基础设施模块特点

  1. 模块要小型化
  2. 可组合的模块
  3. 可测试的模块
  4. 可发布的模块
  5. Terraform模块之外的内容

生产级基础设施检查清单

表6-2:生产级基础设施检查清单

生产级基础设施模块特点

模块要小型化

Terraform和IaC的新手通常会在单个文件或单个模块中定义所有基础设施和所有环境(如Dev、Stage、Prod等)。

Clean Code中提到的: 函数的第一个规则是它们应该很小;函数的第二个规则是它们应该更小。

图6-2:将相对复杂的AWS架构重构为许多小型模块

添加一个README.md文件来包含这些指令。这个小小的示例将发挥巨大的作用。在仅有的几个文件和若干行代码中,你实现了如下内容。

手动测试工具

当开发asg-rolling-deploy模块时,基于这段示例代码,可以通过手动方式,反复运行terraform apply和terraform destro命令,检查它是否按预期工作。

自动测试工具

正如你将在第7章中看到的,示例代码和为模块创建自动测试的方法是一样的。我通常建议将测试放入test文件夹。

可执行文档

如果将此示例(包括README.md)提交到版本控制系统中,则团队的其他成员可以通过它来了解模块的工作原理,并在不编写代码的情况下就可以试用模块。这既是培训团队其他成员的一种方式,又可以通过添加自动测试来确保“教材”始终按预期工作。

你在modules文件夹中拥有的每个Terraform模块,都应该在examples文件夹中有一个相对应的示例,并且examples文件夹中的每个示例都应该在test文件夹中有一个相对应的测试。实际上,每个模块可能有多个示例(因此,有多个测试)来展示该模块的不同配置和排列组合方式。例如,为asg-rolling-deploy模块添加其他的示例,展示如何将它与自动缩放策略一起使用、如何将负载均衡器连接到该模块、如何设置自定义标签,等等。

一旦开始定期对模块进行测试,你就会发现另一个非常有用的做法:版本固定(versionpinning)。你应该在所有Terraform模块中,通过required_version参数,调用特定的 Terraform版本。至少需要设定Terraform的主要版本号。

发布模块的另一种方法是,将它们发布到Terraform注册中心。公共的Terraform注册中心位于参考资料第6章[6],其中包括数百个可重复使用的、社区维护的开源模块,适用于AWS、Google Cloud、Azure和许多其他提供商。将模块发布到公共的Terraform注册中心有以下要求。[2]

● 该模块必须存放在公共GitHub存储库。 ● 存储库必须遵循命名规范terraform--,其中PROVIDER指定模块的目标提供商(如aws),而NAME是模块的名称(如vault)。 ● 模块必须遵循特定的文件结构,包括在存储库的根目录中定义Terraform代码、提供README.md、使用main.tf、variables.tf和outputs.tf等约定文件名。 ● 代码库必须使用遵循语义版本规则的Git标签(x.y.z)来进行发布。

如果你的模块满足这些要求,则可以通过使用GitHub账户登录到Terraform注册中心,使用Web UI发布该模块,达到与他人共享的目的。当模块发布到注册中心后,将拥有一个漂亮的界面来显示模块细节,如图6-4所示为Terraform注册中心中的HashiCorp Vault模块。 Terraform注册中心可以自动解析模块的输入和输出,因此那些输入变量和输出变量也将显示在界面中,包括type和description字段,如图6-5所示。

图6-4:Terraform注册中心中的HashiCorp Vault模块

协同

Terraform的黄金法则

Terraform的黄金法则

详细说明

实时存储库的主代码分支应该以1:1的形式完全代表生产环境中实际部署的内容

实时存储库中的Terraform代码应准确反映生产环境的状态,避免进行工具之外的更改

“实际部署的内容”

使用Terraform进行所有更改,避免通过Web UI、手动API调用或其他机制进行修改

“1:1形式代表”

实时存储库的代码应清晰地展示每个环境部署的资源,避免使用Terraform工作区导致的代码和实际部署不一致的情况

“主分支”

生产环境的所有变化应直接合并到主分支(通常是master),并在该分支上执行terraform apply

实时存储库的主代码分支应该以1:1的形式完全代表生产环境中实际部署的内容。

“……实际部署的内容”

确保实时存储库中Terraform的代码能够代表最新目标环境的唯一方法是,永远不要进行工具之外的更改。开始使用Terraform后,请勿通过Web UI、手动API调用或任何其他机制进行更改。正如第5章学习的,工具之外的更改不仅会导致复杂的错误,而且还会抵消许多使用IaC已经带来的优点。

“……1:1形式代表……”

当浏览实时存储库时,通过快速扫描代码,应该可以看出在哪些环境中部署了哪些资源。换句话说,每个资源都应该能找到1:1匹配的,签入实时仓库中的代码行。看起来似乎很浅显的道理却很容易出差错。正如我刚才提到的,一种造成错误的方法是进行工具外的更改,这会导致虽然代码存在,但实时基础设施却是不同的。一种更微妙的错误是由于使用Terraform工作区来管理环境导致的,虽然部署了实时基础设施,但是代码却没有被保存。也就是说,如果使用Terraform工作区部署了3个或30个环境,但实时代码库中也可能只有一个代码副本。仅通过浏览代码,是无法知道实际部署了什么资源的,这将导致错误并使维护变得更加复杂。因此,如第3章的“通过工作区进行隔离”中所述,尽量避免使用工作区来管理环境,而要针对每个环境使用单独的文件和文件夹进行定义,以达到通过浏览实时代码库就可以准确地了解部署环境的目的。在本章的后面,我们将学习如何以少量的复制/粘贴来做到这一点。

“主分支……”

你只需要查看一个分支就可以了解生产环境中实际部署的内容。通常这个分支将是master。这意味着,影响生产环境的所有变化应该直接进入master分支(你可以创建单独分支进行开发,但最终一定要通过pull request合并到master分支)。针对生产环境的部署,应该在master分支上运行terraform apply命令。

Terraform甚至有一个内置的fmt命令,可以自动地重新格式化代码风格。

你可以将这个命令作为提交拦截脚本的一部分来运行,确保所有进入版本控制系统的代码拥有一致的风格

Terragrunt

Terragrunt特点

描述

开源的、基于Terraform的外壳工具

填补了Terraform功能上的空白,提供额外的行为和配置

最少的复制/粘贴,多环境部署

通过terragrunt.hcl文件,在多个环境中部署版本化的Terraform代码

简化的文件布局

使用Terragrunt后的文件布局大量降低实时存储库中的文件和代码行数

配置和部署模块

在modules目录中定义Terraform代码,通过terragrunt.hcl文件配置和部署每个环境的模块

简洁的模块配置

每个模块仅包含一个terragrunt.hcl文件,包含指向模块的指针和特定环境的输入变量

简化的backend配置

通过terragrunt.hcl文件在每个环境中定义backend配置,避免重复定义参数

自动化的模块部署和配置

运行terragrunt apply来自动配置backend和部署模块,支持版本控制和环境隔离

这是一个开源的、基于Terraform的外壳工具,它填补了Terraform功能上的一些空白。本章稍后将会介绍,如何通过最少的复制/粘贴,在多个环境中部署版本化的Terraform代码

Terragrunt将使用指定的命令去调用Terraform,会在基于terragrunt.hcl文件的配置上,增加一些额外的行为。其基本思想是,modules存储库中定义所有相同的Terraform代码,而在实时存储库中,通过terragrunt.hcl文件,提供一种简洁方式来配置和部署每个环境中的各个模块。如图8-5所示为使用Terragrunt后的文件布局,这将大量降低实时存储库中的文件和代码行数。

首先,在modules/data-stores/mysql/main.tf和modules/services/hello-world-app/main.tf文件中,添加provider配置。

图8-5:使用Terragrunt后的文件布局

接下来,在modules/data-stores/mysql/main.tf和modules/services/hello-world-app/main.tf文件中,添加backend配置,但保持config块为空(马上会看到如何使用Terragrunt填补这个空白块)。

提交这些更改并发布模块的新版本。

现在,转到实时存储库,并删除所有以.tf为后缀的文件。用户需要为每个模块,创建一个terragrunt.hcl文件,代替复制/粘贴Terraform代码的工作。例如,这里是live/stage/datastores/mysql/terragrunt.hcl文件。

正如你所看到的,terragrunt.hcl文件使用和Terraform相同的HashiCorp配置语言(HCL)语法。当运行terragrunt apply命令时,代码会找到在terragrunt.hcl文件中的source参数,接下来Terragrunt将执行以下操作。

  1. 检出source中指定的URL的代码到一个临时文件夹。source的参数支持与Terraform模块相同的URL语法,因此你可以使用本地文件路径、Git URL、版本化的Git URL(通过ref参数,如上例所示)等。
  2. 在临时文件夹中运行terraform apply命令,将inputs = { … }代码块中定义的输入变量传递给它。 这种方法的好处在于,实时存储库中的代码将被减少到每个模块仅包含一个terragrunt.hcl文件,该文件包含指向要使用的模块的指针(指向特定的版本),以及为特定环境设置的输入变量。这是所能获得的最简洁的代码。 Terragrunt还可以保持backend配置的简洁。不必为每个模块重复定义bucket、key、dynamodb_table等参数。而是在每个环境下的terragrunt.hcl文件中进行定义。例如,live/stage/terragrunt.hcl文件。

在remote_state代码块中,使用与往常相同的方式配置backend参数,但key值略有不同。key值中使用Terragrunt内置函数path_relative_to_include()。这个函数返回此terragrunt.hcl根文件到包含这个文件的任何子模块之间的相对路径。例如,将这个根文件包含在live/stage/data-stores/mysql/terragrunt.hcl中,只需添加一个include代码块。

在include代码块中,通过使用Terragrunt内置函数find_in_parent_folders()找到根目录的terragrunt.hcl文件。自动从该父文件中继承所有设置,包括remote_state配置。结果是,mysql模块将使用所有来自根文件的相同的backend设置,只是key值将被自动解析为data-stores/mysql/terraform.tfstate。这意味着Terraform状态文件将被保存在与实时存储库相同的文件夹结构中,这将很容易识别哪个模块产生了哪个状态文件。

要部署此模块,请运行terragrunt apply命令。

你可以在日志输出中看到Terragrunt读取了terragrunt.hcl文件,下载了指定的模块,运行terraform init命令来配置backend(如果尚不存在,它甚至会自动创建S3 bucket和DynamoDB表),然后运行terraform apply命令部署所有内容。 现在,通过添加live/stage/services/hello-world-app/terragrunt.hcl文件,并在预发布环境中运行terragrunt apply命令,来部署hello-world-app模块。

该模块使用include代码块从根目录的terragrunt.hcl文件中继承相同的backend设置,而key值正如所期望的那样,将被自动更新为services/hello-world-app/terraform.tfstate。当所有功能在预发布环境中正常工作后,接下来可以在live/prod目录中创建类似的terragrunt.hcl文件,通过在每个模块中运行terragrunt apply命令,将完全相同的v0.0.7版本的工件推广到生产环境中。

将上述各点整合在一起

表8-1:应用程序代码和基础设施代码工作流程比较

图8-6:将版本化的、不可变的工件推广到每个环境