Terraform AWS 云:健全的基础设施管理
已发表: 2022-03-11编写应用程序只是故事的一部分。 为了使其有价值,它需要部署在可以扩展的地方,它必须以高可用性运行,它需要备份等等。
越来越多的开发人员需要至少掌握这个部署过程。 这表现为,例如,随着系统复杂性的增加,DevOps 成为当今经常需要的角色。 我们不能让自己忽视这些转变,需要了解如何设计易于部署的应用程序。
这也符合我们客户的利益:他们聘请我们作为我们领域的专家,并希望我们提供整个产品,通常是从头到尾。 他们有需求,并且经常忽略他们的业务解决方案运行的堆栈。 最后,重要的是产品的商业价值。
介绍 Terraform
部署和基础设施管理不是一个简单的过程。 除了许多不断变化的领域专业知识之外,我们还需要学习另一种工具或新的工作流程。 如果您一直推迟此操作,那么本文是您了解一种基础架构管理方法的机会。 我希望你最终会对使用 Terraform 更有信心,并更多地了解可能的方法和挑战。 您应该能够使用此工具至少开始管理您的云基础架构的某些部分。
Terraform 是一个抽象层,是的,而且抽象是有漏洞的,我同意。 但最终,我们从事的是解决问题和管理抽象的业务。 这旨在让我们在日常工作中更加清醒。
目标
我将解释 Terraform 是什么,它如何适应整个生态系统,以及它与其他类似工具的比较。 然后,我将向您展示为团队配置多环境和生产就绪的 Terraform 设置所需的步骤。 我将解释编写 Terraform 配置的基础知识——如何使用可共享模块管理复杂性和重复代码。
这些示例都将集中在一个云提供商上:亚马逊网络服务 (AWS)。 这只是我最有经验的云,但所有信息也应该适用于其他云。
我将以一些我希望在开始时就知道的注释结束:一些语法陷阱、怪癖以及 Terraform 不是我选择的工具的情况。
我不会关注语法的细节。 通过阅读 HashiCorp 为 Terraform 提供的精彩文档和指南,您可以快速了解这一点。 我想专注于从一开始可能并不明显的事情,我希望我在开始使用 Terraform 之前就知道这些事情。 希望这将为您指明正确的方向,并允许您将 Terraform 视为在未来项目中使用的工具。
基础设施管理很难
为云中的应用程序设置环境涉及多个步骤。 除非你把它们都写成详细的清单并密切关注它们,否则你会一直犯错; 毕竟,我们是人类。 这些步骤很难分享。 您需要记录大量手动程序,并且文档很快就会过时。 现在将所有这些乘以单个应用程序的环境总数: dev 、 test/qa 、 stage和prod 。 您还需要考虑每个人的安全性。
不是每个工具都有一个我可以使用并忘记复杂性的 UI 吗?
他们确实有 UI——AWS 管理控制台就是一个典型的例子。 但是这些工具在幕后做了很多事情。 在 UI 上单击一次实际上可能会引发一连串难以掌握的变化。 通常无法撤消您在 UI 中所做的操作(通常的“您确定吗?”提示通常是不够的)。 此外,拥有第二双眼睛来检查更改总是一个好主意,但是当我们使用 UI 时,我们需要与此人坐在一起或在更改完成后检查我们的更改,这更像是审核而不是审查。 每个云提供商都有自己的 UI,您需要掌握这些 UI。 UI 被设计为易于使用(不一定简单!),因此容易产生妄想方法,例如“这只是一个微小的调整”,或者您会在 48 小时内忘记的快速生产修补程序。 这种手动点击也很难自动化。
CLI 工具呢?
对于我们的用例,它们会比 UI 工具更好。 但是,您仍然倾向于手动进行更改或编写很容易失控的 bash 脚本。 此外,每个提供商都有自己的 CLI 工具。 使用这些工具,您在提交之前无法看到您的更改。 值得庆幸的是,这不是一个新问题,并且有一些工具可以帮助解决这个问题。
编排与配置管理
我将定义一些用于管理基础设施的工具和实践类别。 这些是编排和配置管理。 您可以将它们大致视为声明式和命令式编程模型(想想 Prolog 与 Python)。 每个人都有自己的优点和缺点,但最好了解它们并为给定的工作应用最好的工具。 此外,编排通常涉及比配置管理更高的抽象级别。
编排
编排更像是一种声明式编程范式。 我喜欢把它想象成管弦乐队的指挥。 指挥家的工作被维基百科(链接)很好地总结为“通过使用手势来指挥多个演奏者或歌手同时表演的艺术”。 指挥传达音乐的节拍和节奏、动态和提示,但实际工作是由擅长演奏乐器的个别音乐家执行的。 没有这样的协调,他们就无法演奏出完美的作品。
这就是 Terraform 适合的地方。 您使用它来执行一个 IT 基础架构:您告诉它要部署什么,然后 Terraform 将它们链接在一起并执行所有必要的 API 调用。 这个领域有类似的工具; 最著名的之一是 AWS Cloud Formation。 与 Terraform 相比,它对恢复和回滚有更好的支持,但在我看来,它的学习曲线更陡峭。 它也与云无关:它仅适用于 AWS。
配置管理
这些实践的补充方面是配置管理方法。 在此范例中,您指定工具必须执行的确切步骤以达到给定的所需配置。 这些步骤本身可能很小并且支持多个操作系统,但您需要积极考虑它们的执行顺序。 他们对当前状态和环境一无所知(除非您对其进行编程),因此,他们会盲目地执行您给他们的任何步骤。 这可能会导致称为配置漂移的问题,您的资源将慢慢与它们最初打算表示的内容不同步,特别是如果您对它们进行了一些手动更改。 他们擅长在单个实例上管理和配置服务。 擅长此工作流的工具示例包括 Chef、Puppet、Ansible 和 Salt。
编排对您的基础架构实施了一种方法,您将资源视为牛,而不是宠物。 当出现问题时,您无需手动“培养”每个 VPS,而是可以用精确的副本替换它们。 我并不是说你根本不关心并重新开始希望最好的事情。
相反,您应该调查并修复代码中的问题,然后部署它。
Ansible(和其他 CM 工具)可用于管理 AWS 基础设施,但这会涉及大量工作并且更容易出错,尤其是当基础设施经常更改且复杂性增加时。
要记住的一件重要事情是编排和配置管理方法不会相互冲突。 它们是兼容的。 在由 Terraform 管理的 AutoScaling 组中拥有一组 EC2 (VPS) 实例是完全可以的,但运行 AWS 应用程序映像 (AMI),它是磁盘的快照,它是通过诸如 Ansible 之类的命令性步骤准备的. Terraform 甚至有一个“提供者”的概念,一旦机器启动,您就可以运行外部的供应工具。
Terraform 文档很好地解释了这一点,并帮助您将 Terraform 置于整个生态系统中。
什么是 Terraform?
它是一个开源工具,由 HashiCorp 创建,允许您将基础架构编码为声明性配置文件,这些文件是版本化和共享的,并且可以查看。
HashiCorp 的名字应该响起——他们也叫 Nomad、Vault、Packer、Vagrant 和 Consul。 如果您使用过任何这些工具,那么您已经了解文档的质量、充满活力的社区以及您可以从他们的解决方案中获得的有用性。
基础设施即代码
Terraform 与平台无关; 您可以使用它来管理裸机服务器或云服务器,例如 AWS、Google Cloud Platform、OpenStack 和 Azure。 在 Terraform 术语中,这些被称为提供者,您可以通过阅读支持的提供者的完整列表来了解规模。 可以同时使用多个提供者,例如,提供者 A 为您配置 VM,但提供者 B 配置和委托 DNS 记录。
这是否意味着只需更改配置文件即可切换云提供商? 不,我什至不认为你会想要那个,至少不是以自动化的方式。 问题是不同的提供者可能有不同的能力、不同的产品、流程、想法等。这意味着你将不得不为不同的提供者使用不同的资源来表达相同的概念。 但是,这仍然可以通过单一、熟悉的配置语法来完成,并且是一个有凝聚力的工作流程的一部分。
Terraform 设置的基本部分
- Terraform 二进制文件本身,您必须安装
- 源代码文件,即您的配置
- 表示 Terraform 管理的资源的状态(本地或远程)(稍后会详细介绍)
编写 Terraform 配置
您可以使用 HCL 语言在*.tf
文件中编写 Terraform 配置代码。 可以选择使用 JSON 格式 ( *.tf.json
),但它针对的是机器和自动生成,而不是人类。 我建议您坚持使用 HCL。 我不会深入研究 HCL 语言的语法; 官方文档在描述如何编写 HCL 以及如何使用变量和插值方面做得非常出色。 我只会提到理解示例所需的最低限度。
在 Terraform 文件中,您主要处理资源和数据源。 资源代表您的基础设施的组件,例如,AWS EC2 实例、RDS 实例、Route53 DNS 记录或安全组中的规则。 它们允许您在云架构中配置和更改它们。
假设您已设置 Terraform,如果您发出terraform apply
,下面的代码将创建一个功能齐全的 EC2 实例(仅显示某些属性):
resource "aws_instance" "bastion" { ami = "ami-db1688a2" # Amazon Linux 2 LTS Candidate AMI 2017.12.0 (HVM), SSD Volume Type - ami-db1688a2 instance_type = "t2.nano" key_name = "${var.key_name}" subnet_ vpc_security_group_ids = ["${aws_security_group.bastion.id}"] monitoring = "false" associate_public_ip_address = "true" disable_api_termination = "true" tags = { Name = "${var.project_tag}-bastion-${var.env}" Env = "${var.env}" Application ApplicationRole = "Bastion Host" Project = "${var.project_tag}" } }
另一方面,有些数据源允许您在不更改给定组件的情况下读取有关给定组件的数据。 您想获取 ACM 颁发的证书的 AWS ID (ARN)? 您使用数据源。 不同之处在于,在配置文件中引用数据源时,数据源以data_
为前缀。
data "aws_acm_certificate" "ssl_cert" { domain = "*.example.com" statuses = ["ISSUED"] }
以上引用了可与 AWS ALB 一起使用的已颁发 ACM SSL 证书。 在你做这一切之前,你需要设置你的环境。
文件夹结构
Terraform 环境(及其状态)由目录分隔。 Terraform 将目录中的所有*.tf
文件加载到一个命名空间中,因此顺序无关紧要。 我推荐以下目录结构:
/terraform/ |---> default_variables.tf (1) /stage/ (2) |---> terraform.tfvars (3) |---> default_variables.tf (4) |---> terraform.tf (5) |---> env_variables.tf (6) /prod/ /<env_name>/
-
default_variables.tf
– 定义所有顶级变量和可选的默认值。 它们可以通过符号链接在每个环境(嵌套目录)中重复使用。 -
/stage/
- 一个保存整个独立环境配置的目录(这里命名为stage
,但它可以是任何东西)。 在此文件夹中所做的任何更改都完全独立于其他环境(env-likeprod
),这是您想要的,以避免因对stage
所做的更改而弄乱生产环境! -
terraform.tfvars
– 定义变量值。.tfvars
文件与.env
文件类似,因为它们保存已定义变量的key=val
对。 例如,这指定了我使用的 AWSprofile
、AWSkey_name
和 AWSkey_path
。 在 Git 中可以忽略。 -
default_variables.tf
– 这是文件 (2) 的符号链接,它允许我们共享与环境无关的变量而无需重复自己。 -
terraform.tf
- 这是每个 env 的主要配置; 它包含配置后端的terraform {}
块。 我也在此处配置提供程序。 -
env_variables.tf
- 该文件包含特定于 env 的变量。 我在 AWS 中使用Env=<env_name>
标记所有资源,因此该文件通常只定义一个变量:env
。
当然,这不是构建环境的唯一方法。 通过实现关注点的明确分离,这对我来说效果很好。
后端配置
我已经提到了 Terraform 状态。 这是 Terraform 工作流程的重要组成部分。 您可能想知道 state 是否真的是必需的。 Terraform 不能一直查询 AWS API 以获取基础设施的实际状态吗? 好吧,如果您考虑一下,Terraform 需要维护它在声明性配置文件中管理的内容与这些文件实际对应的内容(在云提供商的环境中)之间的映射。 请注意,在编写 Terraform 配置文件时,您并不关心例如单个 EC2 实例的 ID 或将为您发布的安全组创建的 ARN。 然而,在内部,Terraform 需要知道给定的资源块代表具有 ID/ARN 的具体资源。 这是检测更改所必需的。 此外,状态用于跟踪资源之间的依赖关系(这也是您通常不必考虑的事情!)。 它们用于构建可以(通常)并行化和执行的图。 与往常一样,我建议您阅读有关 Terraform 状态及其用途的优秀文档。
由于状态是您架构的唯一真实来源,因此您需要确保您和您的团队始终在使用其最新版本,并且您不会因对状态的不同步访问而产生冲突。 相信我,您不想解决状态文件上的合并冲突。
默认情况下,Terraform 将状态存储在磁盘上的文件中,该文件位于(每个 env 的)当前工作目录中,作为terraform.tfstate
文件。 如果您知道自己将是工作中唯一的开发人员,或者只是在学习和试验 Terraform,这没关系。 从技术上讲,您可以使其在团队中工作,因为您可以将状态提交到 VCS 存储库。 但是,您需要确保每个人都在处理最新版本的状态,并且没有人同时更改! 这通常是一个令人头疼的问题,我强烈建议不要这样做。 此外,如果有人加入您的单开发操作,您仍然需要为状态配置替代位置。
幸运的是,这是一个内置于 Terraform 的良好解决方案的问题:所谓的远程状态。 要使远程状态正常工作,您需要使用可用的后端提供程序之一来配置后端。 以下后端示例将基于 AWS S3 和 AWS DynamoDB(AWS NoSQL 数据库)。 你可以只使用 S3,但你会失去状态锁定和一致性检查的机制(不推荐)。 如果您以前只使用本地状态,配置远程后端将为您提供第一次迁移状态的选项,因此您不会丢失任何东西。 您可以在此处阅读有关后端配置的更多信息。
不幸的是,存在先有鸡还是先有蛋的问题:必须手动创建 S3 存储桶和 DynamoDB 表。 Terraform 无法自动创建它们,因为还没有状态! 好吧,有一些像 https://github.com/gruntwork-io/terragrunt 这样的解决方案可以使用 AWS CLI 自动完成,但我不想偏离这篇博文的主题。
有关 S3 和 DynamoDB 后端配置的重要信息包括:
- 在 S3 存储桶上启用版本控制以免受人为错误和墨菲定律的影响。
- DynamoDB 表对读取和写入有速率限制(称为容量)。 如果您对远程状态进行了大量更改,请确保为该表启用 DynamoDB AutoScaling 或配置足够高的 R/W 限制。 否则,Terraform 将在执行大量调用时从 AWS API 收到 HTTP 400 错误。
综上所述,可以将以下后端配置放在terraform.tf
中,以在 S3 和 DynamoDB 上配置远程状态。
terraform { # Sometimes you may want to require a certain version of Terraform required_version = ">= 0.11.7" # Stores remote state, required for distributed teams # Bucket & dynamoDB table have to be created manually if they do not exist # See: https://github.com/hashicorp/terraform/issues/12780 backend "s3" { bucket = "my-company-terraform-state" key = "app-name/stage" region = "eu-west-1" # 5/5 R/W Capacity might not be enough for heavy, burst work (resulting in 400s). Consider enabling Auto Scaling on the table. # See: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ProvisionedThroughput.html dynamodb_table = "terraform-state-lock-table" } }
一次要接受很多内容,但请记住,您为每个环境执行一次,然后就可以忘记它。 如果您需要对状态锁定进行更多控制,可以使用 HashiCorp Terraform Enterprise,但我不会在这里介绍。
提供者
为了让这个后端可以访问并能够与我们的云提供商进行通信,我们需要配置所谓的provider 。 可以将以下块放置在terraform.tf
文件中(对于每个 env):
provider "aws" { profile = "${var.profile}" region = "${var.region}" version = "~> 1.23.0" }
profile
和region
的变量存储在terraform.tfvars
文件中,可以忽略。 profile
变量是指一个命名的配置文件,它使用标准凭证文件保存 AWS 云的安全凭证。 请注意,我还设置了一些版本约束。 您不希望 Terraform 在您不了解每个后端初始化的情况下升级您的提供程序插件。 特别是考虑到 AWS 提供商的 2.x 版本需要仔细升级。
后端初始化
每个后端配置在开始时都需要一个初始化步骤,并且每次对其进行更改时。 初始化还会配置 Terraform 模块(稍后会详细介绍),因此当您添加这些模块时,您还需要重新运行 init 步骤。 此操作可以安全运行多次。 请注意,在后端初始化期间,并非所有变量都可以被 Terraform 读取以配置状态,也不应该出于安全原因(例如,密钥)。 为了克服这个问题,在我们的例子中,使用与默认配置不同的 AWS 配置文件,您可以使用-backend-config
选项并接受k=v
变量对。 这称为部分配置。
terraform init -backend-config=profile=<aws_profile_name>
Terraform 文件共享一个受限于给定目录的范围。 这意味着子文件夹不直接连接到父目录代码。 然而,它定义了一个允许代码重用、复杂性管理和共享的模块。
工作流程
使用 Terraform 代码时的一般工作流程如下:
- 为您的基础架构编写配置。
- 看看它会做出什么实际的改变(
terraform plan
)。 - 或者,执行您在步骤 2 中看到的确切更改(
terraform apply
)。 - 转到 1
地形规划
Terraform plan
命令将向您显示在发出apply
命令时将对您的基础设施进行的更改列表。 多次发布plan
是安全的,因为它本身不会改变任何东西。
如何阅读计划
Terraform 中的对象(资源和数据源)可以通过其完全限定名称轻松识别。
- 对于资源,ID 可能类似于:
<resource_type>.<resource_name>
— 例如,aws_ecs_service.this
。 - 对于模块内的资源,我们有一个额外的模块名称:
module.<module_name>.<resource_type>.<resource_name>
——module.my_service_ecs_service_task.aws_ecs_service.this
。 - 数据源(模块内部和外部):
(module.<module_name>).data.<resource_type>.<resource_name>
—module.my_service_ecs_service_task.data.aws_ecs_task_definition.this
。
资源类型特定于给定的提供者,通常包括其名称 ( aws_…
)。 AWS 可用资源的完整列表可以在文档中找到。
计划将针对给定资源显示五项操作:
-
[+]
添加 - 将创建一个新资源。 -
[-]
Destroy – 资源将被完全销毁。 -
[~]
就地修改 - 将修改资源并更改一个或多个参数。 这通常是安全的。 Terraform 将向您显示将修改哪些参数以及如何修改(如果可能)。 -
[- / +]
– 资源将被删除,然后使用新参数重新创建。 如果对无法就地更改的参数进行了更改,则会发生这种情况。 Terraform 将向您显示哪些更改会强制重新创建资源,并带有以下红色注释:((forces new resource)
。 这是潜在的危险,因为有一段时间资源根本不存在。 它还可以破坏其他连接的依赖项。 我建议解决此类更改,除非您知道后果是什么或不关心停机时间。 -
[<=]
– 将读取数据源。 这是一个只读操作。
以上是一个示例计划。 我所做的更改是:
- 更改了第一个堡垒
instance_type
的 instance_type - 添加了第二个堡垒实例
- 更改了安全组的名称
请注意,最后一个更改是自动检测到父组名称更改的安全组规则。 在我看来,整个计划非常具有可读性。 一些参数值显示为<computed>
——这并不意味着它们会被改变,而是在这个阶段它们不能被检索和呈现(比如尚未创建的SG名称)。 请记住,在plan
命令期间只会显示更改的资源。 任何被省略的都不会被触及。

通常,Terraform 命令接受附加参数。 计划命令最重要的参数是-out
选项,它将您的计划保存在磁盘上。 然后可以通过应用命令执行此保存的计划(与保存的完全相同)。 这非常重要,因为否则可能会发生更改,例如,在您发布plan
和发布apply
之间的同事。 例子:
- 发布
plan
并验证它看起来不错。 - 有人在你不知情的情况下改变了状态。
- 问题
apply
并且——哎呀,它做了一些计划之外的事情!
值得庆幸的是,这个工作流程在 Terraform v0.11.0 中得到了改进。 从这个版本开始, apply
命令会自动为您提供一个您必须随后批准的计划(通过明确输入yes
)。 优点是,如果您应用此计划,它将完全按照呈现的方式执行。
另一个有用的选项是-destroy
,它将向您显示将导致销毁 Terraform 管理的所有资源的计划。 您还可以使用-target
选项将特定资源作为销毁目标。
地形应用
当我们应用给定的计划时,Terraform 会退出并锁定我们的状态以确保排他性。 然后它继续更改资源,最后推送更新的状态。 需要注意的一件事是,某些资源比其他资源需要更长的时间才能完成。 例如,创建 AWS RDS 实例可能需要 12 分钟以上,Terraform 将等待此完成。 显然,Terraform 足够聪明,不会因此阻止所有其他操作。 它创建请求更改的有向图,如果没有相互依赖关系,则使用并行性来加快执行速度。
导入资源
通常,Terraform 和其他“配置即代码”解决方案会逐渐引入到已经存在的环境中。 过渡真的很容易。 基本上,任何未在 Terraform 中定义的东西都是不受管理和不变的。 Terraform 只关心它管理的内容。 当然,这可能会引入问题——例如,如果 Terraform 依赖于存在于其配置之外的某个端点,然后它会被手动销毁。 Terraform 不知道这一点,因此无法在状态更改期间重新创建此资源,这将导致计划执行期间 API 出错。 这不是我担心的事情。 引入 Terraform 的好处远大于坏处。
为了使引入过程更容易一些,Terraform 包含了import
命令。 它用于将已经存在的外部资源引入 Terraform 状态并允许它管理这些资源。 不幸的是,至少在撰写本文时,Terraform 无法为这些导入的模块自动生成配置。 有此功能的计划,但现在,您需要先在 Terraform 中编写资源定义,然后将此资源导入以告诉 Terraform 开始管理它。 一旦导入状态(以及在执行计划期间),您将看到您在.tf
文件中编写的内容与云中实际存在的内容之间的所有差异。 这样,您可以进一步调整配置。 理想情况下,不应该显示任何更改,这意味着 Terraform 配置以 1:1 的比例反映了云中已有的内容。
模块
模块是 Terraform 配置的重要组成部分,我建议您接受并经常使用它们。 它们为您提供了一种重用某些组件的方法。 一个例子是 AWS ECS 集群,它用于运行 Docker 容器。 要使这样的集群正常工作,您需要配置许多单独的资源:集群本身、管理单独 EC2 实例的启动配置、映像的容器存储库、自动缩放组和策略等。 您通常需要为单独的环境和/或应用程序提供单独的集群。
克服这个问题的一种方法是复制和粘贴配置,但这显然是一种短视的解决方案。 做最简单的更新会出错。
模块允许您将所有这些单独的资源封装在一个配置块(称为模块)下。 模块定义了输入和输出,它们是它与“外部世界”(或调用代码)通信的接口。 此外,模块可以嵌套在其他模块中,允许您快速启动整个独立的环境。
本地和远程模块
与状态类似,您可以拥有本地模块或远程模块。 本地模块与 Terraform 配置一起存储(在单独的目录中,在每个环境之外但在同一个存储库中)。 如果您有一个简单的架构并且不共享这些模块,这没关系。
然而,这有局限性。 很难对这些模块进行版本控制并共享它们。 版本控制很重要,因为您可能希望在生产环境中使用 ECS 模块的 v1.0.0,但想在暂存环境中试验 v1.1.0。 如果模块与您的代码一起存储,则对模块代码的每次更改都会反映在每个环境中(一旦运行apply
),这通常是不可取的。
对模块进行版本控制的一种方便方法是将它们全部放在一个单独的存储库中,例如 your-company/terraform-modules。 然后,在 Terraform 配置中引用这些模块时,可以使用 VCS 链接作为源:
module "my-module" { source = "[email protected]:your-company/terraform-modules.git//modules/my-module?ref=v1.1.0" ... }
在这里,我引用了 my-module 的 v1.1.0(特定路径),我可以在不同环境中独立于同一模块的其他版本进行测试。
除此之外,还有模块的可发现性和可共享性问题。 您应该努力编写文档齐全且可重用的模块。 通常,不同的应用程序会有不同的 Terraform 配置,并且可能希望在它们之间共享相同的模块。 如果不将它们提取到单独的存储库中,这将非常困难。
使用模块
通过定义一个特殊的模块块,可以在 Terraform 环境中轻松引用模块。 以下是假设 ECS 模块的此类块的示例:
module "my_service_ecs_cluster" { source = "../modules/ecs_cluster" cluster_name = "my-ecs-service-${var.env}" repository_names = [ "my-ecs-service-${var.env}/api", "my-ecs-service-${var.env}/nginx", "my-ecs-service-${var.env}/docs", ] service_name = "my-ecs-service-${var.env}" ecs_instance_type = "t2.nano" min_size = "1" max_size = "1" use_autoscaling = false alb_target_group_arn = "${module.my_alb.target_group_arn}" subnets = "${local.my_private_subnets}" security_groups = "${aws_security_group.my_ecs.id}" key_name = "${var.key_name}" env_tag = "${var.env}" project_tag = "${var.project_tag}" application_tag = "${var.api_app_tag}" asg_tag = "${var.api_app_tag}-asg" }
传递的所有选项(除了一些全局选项,如source
)在此模块的配置中定义为输入(变量)。
模块注册表
最近 HashiCorp 推出了官方的 Terraform 模块注册表。 这是一个好消息,因为您现在可以从已经开发经过实战测试的模块的社区中汲取知识。 此外,其中一些具有“HashiCorp 已验证模块”徽章,这意味着它们经过审查和积极维护,给您额外的信心。
以前,您要么必须从头开始编写自己的模块(并从错误中吸取教训),要么使用发布在 GitHub 和其他地方的模块,而对它们的行为没有任何保证(除了阅读代码!)
在环境之间共享数据
理想情况下,环境应该完全独立,即使使用不同的 AWS 账户也是如此。 实际上,在某些情况下,一个 Terraform 环境可能会在另一个环境中使用某些信息。 如果您逐渐将架构转换为使用 Terraform,则尤其如此。 One example might be that you have a global
env that provides certain resources to other envs.
Let's say env global
shares data with stage
. For this to work, you can define outputs at the main level of the environment like so:
output "vpc_id" { value = "${module.network.vpc_id}" }
Then, in the stage
environment, you define a datasource that points to the remote state of global
:
data "terraform_remote_state" "global" { backend = "s3" config { bucket = "my-app-terraform-state" key = "terraform/global" region = "${var.region}" dynamodb_table = "terraform-state-lock-table" profile = "${var.profile}" } }
Now, you can use this datasource as any other and access all the values that were defined in global
's outputs:
vpc_
小心的话
Terraform has a lot of pros. I use it daily in production environments and consider it stable enough for such work. Having said that, Terraform is still under active development. Thus, you will stumble on bugs and quirks.
Where to Report Issues and Monitor Changes
First of all, remember: Terraform has a separate core repo and repositories for each provider (eg, AWS). If you encounter issues, make sure to check both the core repo and the separate provider repositories for issues and/or opened pull requests with fixes. GitHub is really the best place to search for bugs and fixes as it is very active and welcoming.
This also means that provider plugins are versioned separately, so make sure you follow their changelogs as well as the core one. Most of the bugs I have encountered were resolved by upgrading the AWS provider which already had a fix.
Can't Cheat Your Way out of Cloud Knowledge
You cannot use Terraform to configure and manage infrastructure if you have no knowledge of how a given provider works. I would say this is a misconception and not a downside, since Terraform has been designed to augment and improve the workflow of configuration management and not to be some magic dust that you randomly sprinkle around and—poof! Environments grow! You still need a solid knowledge of a security model of each cloud, how to write, eg, AWS policies, what resources are available, and how they interact.
Prefer Separate Resources That Are Explicitly linked
There are certain resources—for example, the AWS security group or AWS route table—that allow you to configure the security rules and routes respectively, directly inside their own block. This is tempting, as it looks like less work but in fact will cause you trouble. The problems start when you are changing those rules on subsequent passes. The whole resource will be marked as being changed even if only one route/security rule is being introduced. It also gives implicit ordering to those rules and makes it harder to follow the changes. Thankfully, mixing those both approaches is not allowed now (see the note).
Best-practice example, with explicitly linked resources:
resource "aws_security_group" "my_sg" { name = "${var.app_tag}-my-sg" ... } resource "aws_security_group_rule" "rule_one" { security_group_ ... } resource "aws_security_group_rule" "rule_two" { security_group_ ... }
Terraform plan
Doesn't Always Detect Issues and Conflicts
I already mentioned this in the case where you were managing resources with Terraform that were relying on other, unmanaged infrastructure. But there are more trivial examples—for example, you will get an error if your EC2 instance has Termination Protection enabled, even though plan
would show you it's OK to destroy it. You can argue that this is what Termination Protection has been designed for, and I agree, but there are more examples of things you can do in theory/on plan but when executed will deadlock or error out. For example, you cannot remove a network interface if something is using it—you get a deadlock without an option to gracefully recover.
Syntax Quirks
There are also quirks related to how HCLv1 (the syntax language Terraform uses) has been designed. It has a couple of frustrating quirks. There is work underway to provide an improved version of the parser for HCLv2. The best way to read on the current limitations and the plan to overcome them is this fantastic blog series. In the meantime, there are workarounds for most of those issues. They are not pretty and they will fail once v0.12 comes out, but hey, it is what it is.
When State Update Fails
It sometimes happens that Terraform is not able to correctly push an updated state. This is usually due to underlying network issues. The solution is to retry the state update instead of running apply again, which will fork the state .
Another issue might happen when state lock (the synchronization primitive that prevents multiple users to update the same state) fails to be taken down by Terraform. This involves running terraform force-unlock
with the lock ID to take it down manually.
Thankfully, in case of such problems, Terraform provides you with a good description and steps you need to make to fix it.
Not Everything Is Fun to Manage Through Terraform
There are certain cases where Terraform is not my tool of choice. For example, configuring AWS CodePipeline and CodeBuild projects (AWS equivalent of CI/CD pipeline) is cumbersome when done through Terraform. You need to define each step through very verbose configuration blocks and things like “Login via GitHub” are a lot more complicated than using the UI. Of course, it's still possible if you prefer to have it codified. Well, I guess it's a good candidate for a well-written module!
Same thing goes for managing AWS API Gateway endpoints. In this case, using a dedicated serverless framework would be a better option.
When configuring AWS resources with Terraform, you will find yourself writing a lot of policies. Policies that would otherwise often be auto-generated for you (when using the UI). For those, I'd recommend the AWS Visual Editor and then copying the resulting policy JSON into Terraform.
结论
Using Terraform has been fun and I'll continue doing so. Initial steps can be a bumpy ride, but there are more and more resources that help to ease you in.
I'd definitely recommend taking Terraform for a spin and simply playing with it. Remember, though—be safe and test it out on a non-essential account. If you are eligible for AWS Free Tier, use it as a 12-month free trial. Just be aware it has limitations as to what you can provision. Otherwise, just make sure you spin the cheapest resources, like t3.nano instances.
I highly recommend extensions for Terraform support in various code editors. For Visual Studio Code, there is one with syntax highlighting, formatting, validation and linting support.
It's always valuable to learn new things and evaluate new tools. I found that Terraform helped me immensely in managing my infrastructure. I think working with Terraform will only get easier and more fun, especially once v0.12.0 ships with a major upgrade to the HCL syntax and solve most of the quirks. The traction and community around Terraform are active and vibrant. You can find a lot of great resources on things I didn't manage to cover in a single blogs post, eg, a detailed guide on how to write modules.