HashiCorp Terraform 是一个基础设施即代码工具, 允许您以人类可读的配置文件定义云端和本地资源, 这些配置文件可以进行版本控制, 复用和共享.
以上是 terraform 官网对 terraform 的介绍, 它使用可读性相对更高的配置文件来定义云端和本地资源. 这里的云端资源一般指的是由云服务厂商提供的一些对应服务, 例如:AWS EC2, Azure Storage 和 GCP Compute Engine 等, 而对应的本地资源一般指本地云和对应的本地部署平台, 例如:VMware Esxi 和 Proxmox VE 等. 但实际上单纯的裸机服务器也可以通过 shell 脚本来使用 terraform 进行管理.
Terraform 也更像一个集中管理云服务资源的软件, 它有着许多的插件 (在 terraform 中称为 provider), 通过 provider 的引入你可以与各式各样的系统进行集成, 从而实现 “用 terraform 管理资源” 这一目的. 而常见的一些集成 (例如:AWS, GCP 等) 其实就是云服务提供商或 terraform 官方进行维护的 provider.
本文会使用 AWS 进行举例, 说明如何使用 terraform 来部署 AWS 资源.
开始使用
在使用 terraform 部署资源之前, 你需要运行 terraform init
命令来初始 terraform 的运行环境. 而运行 terraform init
的时候你需要确保执行命令的目录已经存在 terraform 配置文件.
通常, 一个最简单的 terraform 配置文件 (terraform.tf) 包含以下内容:
terraform {
required_version = "~> 1.7.1" #用于指定 terraform 版本
backend "s3" {
# 这里没有配置是因为对应的参数会在初始化的命令行中提供, 这样便于在 CI/CD 中使用
}
}
- required_version: 用于确保安装的 terraform 版本能正确解析 terraform 配置文件中的内容并运行.
而老版本的 terraform 执行程序很有可能无法解析版本更新的文件.
这里的
~> 1.7.1
表示允许的 terraform 版本为 1.7.x. 因此 1.7.x 的任何版本都是被允许的. - backend: terraform 在部署时会维护一个当前配置对应的状态文件, 这个状态文件非常地重要. Terraform 会依据这个文件来判断是如何修改云服务资源.
所以, 我们需要妥善的保存状态文件, terraform 也引入了 backend 这个配置块用于配置将状态文件存储在哪里.
backend "s3"
表明我们会使用 AWS S3 来存储生成的状态文件, terraform 还提供了一系列的其他 backend 选项, 例如:存储在本地, GCP, Azure等.- 我们可以从这里了解其他 backend 和对应的配置详情:Terraform Backend Block
初始化命令
在 terraform 配置文件准备就绪后我们可以通过下方的命令来初始化 terraform, 在命令运行后会生成相关的文件并下载配置中定义的 provider.
terraform init -backend-config="bucket=<AWS S3 bucket name>" \
-backend-config="key=<store path in AWS S3>" \
-backend-config="region=<AWS region>"
上面的命令使用了 AWS S3 作为 backend, 因此在执行前需要确保已经成功配置了 AWS 相关的凭证和对应的 S3, 否则会遇到 AWS 错误. 命令运行后, 下方的文件和文件夹将会被创建:
.terraform.lock.hcl
: 存储了 provider 对应的版本和相关的 hash, 它用于校验后续下载的 provider 的版本和完整性. 推荐将.terraform.lock.hcl
文件存入代码库中, 以便于 terraform 在未来的执行中选择同样的 provider 版本, 这在多人合作开发以及 CI/CD 部署时非常有用..terraform
: 这个文件夹中存储了terraform.tfstate
以及下载好的 provider 文件. 请不要将.terraform
文件夹放入存储库中, 尤其要避免存储terraform.tfstate
文件. 因为其中包含了初始化时输入的一些信息, 存储该文件会有密钥泄漏的风险.
在初始化完成后我们便可以使用 terraform plan
来生成执行计划, 并使用 terraform apply -f plan_output
来执行生成的计划.
也可以直接使用 terraform apply
应用定义的资源配置, 在不使用 -f
参数的情况下, 它会隐式的生成 plan 文件并直接应用.
配置文件
在上面的例子中我们提到了 terraform 配置文件, 在真实使用的场景中你可以将 terraform 配置依据不同需求拆分到不同的文件中, 例如:
terraform.tf
中存储 terraform 和 provider 相关的配置variable.tf
中存储定义好的变量aws.tf
中存储 AWS 的资源定义gcp.tf
中存储 GCP 的资源定义
添加 provider
在定义相关资源之前我们还需要了解该如何引入 provider. Provider 可以认为是用于和服务供应商 API 进行集成的插件, 只有通过 provider, 我们定义的资源才能被正确的在相关的平台部署或创建. 此外, 不同的 provider 也会有不同的配置项, 但总的来说它们都有着相同的配置语法.
Provider 同样是定义在 terraform
块中, 你可以看到下方包含了 required_providers
以及 provider "aws"
两个部分.
required_providers
用于指定我们需要的 provider 以及对应的版本provider "aws"
则针对我们引入的 provider 进行一些配置, 例如 AWS 会需要你指定 region 等信息. 由于它们是可选的, 因此也可以不提供.
terraform {
# ... version and backend
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.45.0"
}
}
}
provider "aws" {
}
你可以在这里找到 AWS provider 的具体的使用方式: AWS Provider
资源定义
在引入 provider 后的下一步便是定义资源了, 我们这里以 AWS IAM 为例来简单说明如何定义 IAM role.
data "aws_iam_policy_document" "my_policy_document" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
resource "aws_iam_role" "my_iam_role" {
name = "my_iam_role"
#引用了 my_policy_document 的输出
assume_role_policy = data.aws_iam_policy_document.my_policy_document.json
}
上方定义个了一个名为 my_iam_role
的 IAM role, 它可以由 AWS lambda assume 来执行一些相关的操作. 这里我们会发现上方分别有 data
和 resource
这两种不同的定义:
- 其中
data
表示数据源, 它允许 terraform 从外部检索信息或使用在 terraform 配置中定义的信息, 其他例子详见 信息获取. - 而
resource
通常用于描述一个或多个基础设施对象, 例如网络, 计算实例或 DNS 等. 通常, 使用resource
定义的字换会在执行apply
命令时被创建, 删除或更新.
同时, 我们也可以发现上面的例子中存在资源之间的引用, my_iam_role
就使用了 my_policy_document
中定义的内容. 在更复杂的场景下, 例如: 需要
IAM 先被创建再创建依赖 IAM 的 lambda 函数, 只需在 lambda 定义中引用 IAM 的输出, terraform 就会识别出依赖关系并在 IAM 创建完毕后使用 IAM 的输出创建 lambda.
输入变量
Terraform 支持变量的定义, 这大大方便了我们对资源的定义以及通过 CI/CD 等工具来部署相同类型但配置不同的资源 (例如不同的环境) 等情况.
例如, 我们现在有一个 AWS SQS queue, 针对不同的环境我们会需要不同的 timeout 配置, 那么我们可以使用如下定义:
variable "sqs_queue_timeout_var" {
default = 300
description = "sqs queue timeout variable"
# 用于防止 terraform 在输出中打印 sqs_queue_timeout_var, 可以用于保护一些隐私数据
sensitive = true
}
resource "aws_sqs_queue" "my_sqs_queue" {
name = "my_sqs_queue"
visibility_timeout_seconds = var.sqs_queue_timeout_var
sqs_managed_sse_enabled = false
}
在部署的时候我们可以在通过在命令中传递, 变量文件或是在环境变量来让 terraform 获得相关的值.
# 通过命令传递
terraform apply -var="sqs_queue_timeout_var=600"
# 通过变量文件传递
# 文件内容为: sqs_queue_timeout_var="600"
terraform apply -var-file="variables.tfvars"
# 通过环境变量传递
export TF_VAR_sqs_queue_timeout_var="600"
terraform apply
信息获取
如果资源并不是在当前的 terraform 配置文件中定义的, 但是你又需要获取相关的信息, 这个时候可以使用 data
,
它允许 terraform 从外部检索信息或使用在其他 terraform 配置中定义的信息. 例如:
data "aws_iam_policy_document" "my_policy_document" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
data "aws_ssm_parameter" "dependency_endpoint" {
name = "/dependency/prod/endpoint"
}
my_policy_document
是本地配置的数据的例子, 它可以通过data.aws_iam_policy_document.my_policy_document.json
来获取该定义对应的 JSON 内容. 适用于在多个 IAM role 中复用相同 policy document.dependency_endpoint
是远端数据的获取的例子. 使用data.aws_ssm_parameter.dependency_endpoint.value
可以获取到在 AWS parameter store 中/dependency/prod/endpoint
路径下存储的值.
使用 data
还可以从其他的数据源进行数据的获取, 例如本地文件等. 具体支持哪些数据源取决于是否有相关的 provider 支持.
开始部署
如上面内容提到的, terraform 在部署时会先创建对应的 plan, 随后再 apply 生成的 plan 文件. 同时, terraform 还会将资源的部署状态写入状态文件.
这个状态文件是我们初始化 terraform 时在 -backend-config="key=<store path in AWS S3>"
中指定的, 它存储了当前已部署的资源的状态.
由于在我们的例子中 是使用 AWS S3 作为 backend 的, 因此 terraform 会将状态文件上传至 S3 中, 每次创建 plan 时, terraform 通过对比 AWS 当前资源的状态和状态文件中的内容来计算出哪些资源需要被创建, 更新或删除, 以此创建出 plan 文件.
通常的建议和实践是: 先生成 plan 文件, 并审核生产 plan 文件过程中的输出 (或 plan 文件的内容), 它会告诉你有哪些资源会被创建, 更新或删除. 如果一切都和预期一致,
这个时候再运行 apply
命令来应用生成的 plan 文件.
所以部署的流程如下:
## 初始化 terraform
terraform init -backend-config="bucket=<AWS bucket>" -backend-config="key=<AWS S3 object key>" --backend-config="region=<AWS region>"
# 场景 1: 生成 plan 文件, 并使用 plan 文件进行部署
# 生成 plan 文件
terraform plan -out=tf-plan
# review 流程 (可选)
# 应用 plan 文件
terraform apply -auto-approve tf-plan
#场景 2
# 如果不想生成 plan 文件, 并且想直接部署, 可以使用下方的命令
terraform apply -auto-approve
手动管理资源
有时候我们会有一些手动管理资源的需求, 例如: 要将一个之前手动创建的资源纳入 terraform的管理, 又或是由于部署失败导致需要手动更新当前状态文件的内容.
导入资源
假设当前有一个 ID 为 i-1234567890abcdef0
的 AWS EC2 实例, 如果想要将该实例纳入 terraform 管理, 首先需要在 terraform 配置文件中创建相关的定义:
resource "aws_instance" "my_instance" {
# 配置内容 (确保配置与资源的实际状态一致。)...
}
随后可以通过下方的 import
命令将资源导入至状态文件中.
terraform import aws_instance.my_instance i-1234567890abcdef0
在导入后, 你可以运行 plan
命令来确保导入的资源的状态与配置中定义的一致, 如果一致你将会得到一个空执行计划. 如果不为空你需要重新调整你的 terraform 配置, 或通过
apply
命令来使用配置中定义的内容更新该已有资源.
删除资源
我们仍用 ID 为 i-1234567890abcdef0
的 AWS EC2 实例为例, 假设我们不小心从 AWS console 中删除了该资源, 并且我们不再需要该资源. 那么当我们在 terraform
配置中删掉相关的资源定义后尝试部署, 你会发现相关的部署会直接失败.
这是因为: 在 terraform 的状态文件中仍存在该资源的状态数据, 因此部署的时候会尝试去删除该资源. 但是又由于该资源已经不存在, 因此删除操作会失败进而导致部署失败.
在这种情况下, 需要先确保相关的资源定义已经从 terraform 配置中删除, 然后可以通过 state rm
命令从 terraform 状态文件中移除资源的状态, 命令如下:
terraform state rm aws_instance.my_instance
删除后, 同样建议运行 plan
命令来验证是否已经删除成功, 如果生成的执行计划为空 (或不再包含删除该实例), 那么说明相关的资源已被移除.
回滚与销毁
Terraform 本身并不直接支持回滚功能. 但是可以通过部署旧版本配置来实现类似的效果: 只需将配置文件恢复到之前的版本,
然后运行 terraform apply
进行部署即可.
而销毁数据则更为简单, 如果你只需要销毁部份资源, 那么将对应的资源从 terraform 配置文件中删除, 再进行部署就可以了.
如果你想销毁所有资源, 你可以使用 terraform destroy
命令, 它用于销毁 terraform 管理的所有资源 (基于 provider 实现, 有些资源可能不会被删除).
在通常情况下, 运行 destroy
命令后, 所有的资源都将会被删除.
建议参考 provider 文档来确保删除行为是否有例外, 如有例外, 你需要手动删除相关资源.
与 cloudformation 的对比
由于本文是使用 AWS 进行举例, 因此如果你之前使用过 AWS cloudformation 你可能会疑惑: “为什么不使用 AWS 原生支持的 cloudformation 呢?” 这个问题可以大致分为两种情况来进行回答:
- 只使用了 AWS 作为云服务提供商, 在这种情况下使用两种部署方式区别并不是很大.
- 部署时使用的资源.
- 在触发部署后, cloudformation 会使用 AWS 的资源来监控和部署你定义的资源 (有一定免费额度, 超过会收费).
- terraform 部署则需要你提供一台实例运行部署命令直至部署成功, 失败或超时.
- 部署失败时的处理.
- cloudformation 在部署失败时会尝试回滚.
- terraform 失败时, 你需要手动部署旧版本配置来进行手动回滚, 或者修复配置中的错误来继续部署.
- 回滚失败时的处理.
- cloudformation 某些情况下会回滚失败, 这会导致对应的 cloudformation stack 不再可用, 你需要删掉 stack 来重新部署, 这种情况会大概率会导致服务中断.
- terraform 并没有回滚机制, 因此在出错时需要人工介入处理, 但是不会有需要删除所有资源重新部署这种情况.
- 部署时使用的资源.
- 使用了混合云 (AWS, GCP 等) 来部署服务, 这时 terraform 更有优势.
- cloudformation 只能用于管理 AWS 资源. 因此, 使用混合云的情况下你需要额外设置其他的部署脚本来在其他云上部署服务.
- 而 terraform 则可以轻松支持混合云部署, 并且可以像单云一样定义资源之间的依赖. 因此, 你可以做到使用 GCP 创建出来的资源的 ID 来创建 AWS 资源.
小结
综上所述,terraform 通过简化并结构化地定义基础设施,提升了云资源管理的效率和可维护性。 相比于传统的手动操作或自定义脚本,terraform 提供了一种更加一致、可重复和易于扩展的解决方案。 它不仅支持多云环境的管理,还允许通过资源之间的依赖关系管理复杂基础设施部署流程。