Terraform
排查 Terraform 问题
与 Terraform 交互的主要方法是 HashiCorp 配置语言 (HCL)。当 Terraform 在您的配置中遇到错误时,它将报告一个错误,其中包含配置中问题的行号和类型。
在本教程中,您将克隆一个包含损坏的 Terraform 配置的仓库,以部署一个 EC2 实例和基础网络。此配置包含有意引入的错误,以便您了解配置语言的故障排除。
先决条件
对于本教程,您需要
- Terraform 0.15.2+ 已本地安装
- 拥有 AWS 账户 且已为 Terraform 配置了凭据
查看 Terraform 故障排除模型

您可能会遇到四种潜在类型的问题:语言、状态、核心和提供程序错误。从最接近用户的错误类型开始
- 语言错误:Terraform 的主要接口是 HashiCorp 配置语言 (HCL),这是一种声明式配置语言。Terraform 核心应用程序会解释配置语言。当 Terraform 在您的配置中遇到语法错误时,它会打印出行号和错误说明。
- 状态错误:Terraform 状态文件存储有关已配置资源的的信息。它将资源映射到您的配置,并跟踪所有相关元数据。如果状态不同步,Terraform 可能会销毁或更改您现有的资源。在排除配置错误后,请查看您的状态。通过刷新、导入或替换资源来确保您的配置同步。
- 核心错误:Terraform 核心应用程序包含所有操作的逻辑。它解释您的配置,管理您的状态文件,构建资源依赖关系图,并与提供程序插件通信。此级别的错误可能是错误。在本教程的后面,您将学习向核心开发团队打开 GitHub issue 的最佳实践。
- 提供程序错误:提供程序插件处理身份验证、API 调用以及将资源映射到服务。在本教程的后面,您将学习向提供程序开发团队打开 GitHub issue 的最佳实践。
克隆 GitHub 示例仓库
克隆 示例 GitHub 仓库。
$ git clone https://github.com/hashicorp-education/learn-terraform-troubleshooting
进入仓库目录。
$ cd learn-terraform-troubleshooting
在您的文件编辑器中打开 main.tf 文件。
此配置包含有意引入的问题,您可能会立即发现一些问题,但现在不要编辑您的配置。在下一节中,您将运行 format 命令以识别错误。
格式化配置
format 命令扫描当前目录中的配置文件,并将您的 Terraform 配置文件重写为 推荐格式。
在您的终端中,运行 terraform fmt 命令。此命令返回两个错误,告知您存在无效字符和无效表达式。这两个错误都发生在第 46 行。
$ terraform fmt
terraform.tfvars
╷
│ Error: Invalid character
│
│ on main.tf line 52, in resource "aws_instance" "web_app":
│ 52: Name = $var.name-learn
│
│ This character is not used within the language.
╵
╷
│ Error: Invalid expression
│
│ on main.tf line 52, in resource "aws_instance" "web_app":
│ 52: Name = $var.name-learn
│
│ Expected the start of an expression, but found an invalid expression token.
╵
Terraform 发现了一些无法解析的问题,并输出了错误,以便您可以手动修复配置。
更正变量插值错误
在 main.tf 中,找到 aws_instance.web_app 资源的第 46 行的 tags 属性。 Name 标签的格式不正确。它试图在没有必要插值语法的的情况下,将字符串附加到 name 输入变量。使用正确的语法更新 Name 属性。
main.tf
##...
tags = {
- Name = $var.name-learn
+ Name = "${var.name}-learn"
}
}
##...
再次运行 terraform fmt,以确保您的变量名符合格式要求。
$ terraform fmt
main.tf
您已解决无效字符和表达式错误,它们不再出现。Terraform 将 "${var.name}-learn" 解析为您的变量名,并使用硬编码的 -learn 字符串附加形成自定义值,采用插值简写形式。
验证您的配置
terraform fmt 仅解析您的 HCL 以查找插值错误或格式错误的资源定义,因此在格式化配置后,您应该使用 terraform validate 来检查您的配置是否符合提供程序的期望。
初始化您的 Terraform 目录,以下载您的配置所需的提供程序。
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 3.24.1"...
- Finding latest version of hashicorp/http...
- Installing hashicorp/aws v5.56.1...
- Installed hashicorp/aws v5.56.1 (signed by HashiCorp)
- Installing hashicorp/http v3.4.3...
- Installed hashicorp/http v3.4.3 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
在您的终端中运行 terraform validate。输出包含一个循环错误,该错误突出显示了两个安全组资源之间的相互依赖关系。
$ terraform validate
╷
│ Error: Cycle: aws_security_group.sg_ping, aws_security_group.sg_8080
│
│
╵
循环错误是 Terraform 依赖树中循环逻辑的实例。Terraform 分析您的基础设施配置中资源之间的依赖关系,以确定执行操作的顺序。
在下一节中,您将更正此依赖图错误。
更正循环错误
您的 aws_security_group 资源在其 security_groups 属性中相互引用。AWS 无法创建安全组,因为它们的配置都引用了另一个尚未存在的组。
删除配置中的相互依赖的安全组规则,留下两个组资源而不带入站属性。
main.tf
resource "aws_security_group" "sg_ping" {
name = "Allow Ping"
- ingress {
- from_port = -1
- to_port = -1
- protocol = "icmp"
- security_groups = [aws_security_group.sg_8080.id]
- }
}
resource "aws_security_group" "sg_8080" {
name = "Allow 8080"
- ingress {
- from_port = 8080
- to_port = 8080
- protocol = "tcp"
- security_groups = [aws_security_group.sg_ping.id]
- }
// connectivity to ubuntu mirrors is required to run `apt-get update` and `apt-get install apache2`
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
不要在 aws_security_group 配置中包含规则,而是使用 aws_security_group_rule 资源并引用安全组 ID。这样可以避免循环错误,因为提供程序将首先创建这两个 aws_security_group 资源,而无需相互依赖的规则。然后它将创建规则,并最后将规则附加到组。
将新的独立规则资源配置添加到 main.tf。
main.tf
resource "aws_security_group_rule" "allow_ping" {
type = "ingress"
from_port = -1
to_port = -1
protocol = "icmp"
security_group_id = aws_security_group.sg_ping.id
source_security_group_id = aws_security_group.sg_8080.id
}
resource "aws_security_group_rule" "allow_8080" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.sg_8080.id
source_security_group_id = aws_security_group.sg_ping.id
}
Terraform 不会在捕获到错误后继续验证。运行 terraform validate 命令以捕获新错误:由于其值中的飞溅表达式 (*) 导致对 for_each 属性的无效引用。
$ terraform validate
╷
│ Error: Invalid reference
│
│ on main.tf line 39, in resource "aws_instance" "web_app":
│ 39: for_each = aws_security_group.*.id
│
│ A reference to a resource type must be followed by at least one attribute
│ access, specifying the resource name.
╵
╷
│ Error: Invalid "each" attribute
│
│ on main.tf line 42, in resource "aws_instance" "web_app":
│ 42: vpc_security_group_ids = [each.id]
│
│ The "each" object does not have an attribute named "id". The supported
│ attributes are each.key and each.value, the current key and value pair of the
│ "for_each" attribute set.
╵
由于上面的 for_each 错误,each 属性中的 vpc_security_group_ids 无法返回 ID。Terraform 没有返回任何安全组 ID,因此 each 对象无效。
检查实例资源的构造。
main.tf
resource "aws_instance" "web_app" {
for_each = aws_security_group.*.id
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
vpc_security_group_ids = [each.id]
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y apache2
sed -i -e 's/80/8080/' /etc/apache2/ports.conf
echo "Hello World" > /var/www/html/index.html
systemctl restart apache2
EOF
tags = {
Name = "${var.name}-learn"
}
}
Terraform 无法在没有其他函数的情况下自动转换类型。
在下一节中,您将更正此表达式和 for_each 错误。
更正 for_each 错误
Terraform 的 for_each 属性允许您根据定义的条件创建一组相似的资源。
在此示例中,您需要创建一组相似的实例,每个实例分配给不同的安全组。Terraform 无法解析 aws_security_group.*.id,因为飞溅表达式 (*) 仅插值列表类型,而 for_each 属性保留给映射类型。本地值可以返回映射类型。
在 main.tf 的第 44 行,将 for_each 属性的值替换为本地值。在第 47 行,将 vpc_security_group_ids 的值替换为来自 for_each 属性的值。最后,更新 tags 属性,为每个实例提供一个唯一的名称。
main.tf
resource "aws_instance" "web_app" {
- for_each = aws_security_group.*.id
+ for_each = local.security_groups
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
- vpc_security_group_ids = [each.id]
+ vpc_security_group_ids = [each.value]
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y apache2
sed -i -e 's/80/8080/' /etc/apache2/ports.conf
echo "Hello World" > /var/www/html/index.html
systemctl restart apache2
EOF
tags = {
- Name = "${var.name}-learn"
+ Name = "${var.name}-learn-${each.key}"
}
}
}
在你的 main.tf 文件中定义本地值。这将把安全组列表转换为映射。
locals {
security_groups = {
sg_ping = aws_security_group.sg_ping.id,
sg_8080 = aws_security_group.sg_8080.id,
}
}
编辑配置文件后,它们可能没有正确格式化。格式化你的配置。
$ terraform fmt
main.tf
你的下一个 terraform validate 操作将产生错误输出,因为你更正了 for_each 的值。你的输出没有捕获 aws_instance.web_app 资源中的多个实例。
验证你的配置以返回错误输出。
$ terraform validate
╷
│ Error: Missing resource instance key
│
│ on outputs.tf line 6, in output "instance_id":
│ 6: value = aws_instance.web_app.id
│
│ Because aws_instance.web_app has "for_each" set, its attributes must be
│ accessed on specific instances.
│
│ For example, to correlate with indices of a referring resource, use:
│ aws_instance.web_app[each.key]
╵
╷
│ Error: Missing resource instance key
│
│ on outputs.tf line 11, in output "instance_public_ip":
│ 11: value = aws_instance.web_app.public_ip
│
│ Because aws_instance.web_app has "for_each" set, its attributes must be
│ accessed on specific instances.
│
│ For example, to correlate with indices of a referring resource, use:
│ aws_instance.web_app[each.key]
╵
╷
│ Error: Missing resource instance key
│
│ on outputs.tf line 16, in output "instance_name":
│ 16: value = aws_instance.web_app.tags
│
│ Because aws_instance.web_app has "for_each" set, its attributes must be
│ accessed on specific instances.
│
│ For example, to correlate with indices of a referring resource, use:
│ aws_instance.web_app[each.key]
╵
在下一节中,你将通过实现一个 for 表达式来定义包含你的实例 ID、IP 地址和名称列表的输出,从而纠正这些错误。
更正你的输出以返回所有值
要更正你的输出,你需要使用 for 表达式来捕获多个资源的元素。
“for”表达式捕获 aws_instance.web_app 的所有元素,并将其存储在一个名为 instance 的临时变量中。然后,Terraform 返回 instance 元素中指定的所有值。在本例中,instance.id、instance.public_ip 和 instance.tags.Name 将返回你创建的每个实例的每个匹配的键值。
打开 outputs.tf 并使用 for 表达式更新输出值。
output "instance_id" {
description = "ID of the EC2 instance"
- value = aws_instance.web_app.id
+ value = [for instance in aws_instance.web_app: instance.id]
}
output "instance_public_ip" {
description = "Public IP address of the EC2 instance"
- value = aws_instance.web_app.public_ip
+ value = [for instance in aws_instance.web_app: instance.public_ip]
}
output "instance_name" {
description = "Tags of the EC2 instance"
- value = aws_instance.web_app.tags
+ value = [for instance in aws_instance.web_app: instance.tags.Name]
}
格式化你的配置。
$ terraform fmt
outputs.tf
验证您的配置
$ terraform validate
Success! The configuration is valid.
应用你的更改
现在你已经更正了 Terraform 配置,运行 terraform apply 来创建你的资源。当提示确认你的更改时,输入 yes。
$ terraform apply
data.http.myip: Reading...
data.http.myip: Read complete after 0s [id=http://ipv4.icanhazip.com]
data.aws_ami.ubuntu: Reading...
data.aws_ami.ubuntu: Read complete after 0s [id=ami-01943ba6b3c809b0e]
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.web_app["sg_8080"] will be created
+ resource "aws_instance" "web_app" {
## ...
Plan: 8 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = [
+ (known after apply),
+ (known after apply),
]
+ instance_name = [
+ "terraform-learn-sg_8080",
+ "terraform-learn-sg_ping",
]
+ instance_public_ip = [
+ (known after apply),
+ (known after apply),
]
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
## ...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
instance_id = [
"i-08cac1508a6baab3b",
"i-056c2e5bc7dba098f",
]
instance_name = [
"terraform-learn-sg_8080",
"terraform-learn-sg_ping",
]
instance_public_ip = [
"52.14.220.20",
"18.224.3.102",
]
错误报告最佳实践
你可能会遇到由于提供程序或应用程序问题而导致的错误。一旦你排除了语言配置错误、版本不匹配或状态差异的可能性,请考虑将你的问题作为错误报告提交给 Terraform 核心团队或 Terraform 提供程序社区。
为了向开发团队或正在处理你的问题社区提供完整的上下文,以下是打开 GitHub issue 的最佳实践。
确认版本控制
确认你正在使用的提供程序和你的环境中 Terraform 的版本。要确认你的提供程序和 Terraform 版本,请运行 version 命令。
$ terraform version
Terraform v1.8.3
on darwin_arm64
+ provider registry.terraform.io/hashicorp/aws v5.56.1
+ provider registry.terraform.io/hashicorp/http v3.4.3
Your version of Terraform is out of date! The latest version
is 1.9.0. You can update by downloading from https://www.terraform.io/downloads.html
你还可以验证你在报告错误之前是否正在使用正确且最新的提供程序版本。如果你的锁定文件指定了旧版本,请考虑更新你的提供程序并再次尝试你的操作。
启用 Terraform 日志记录
Terraform 0.15 及更高版本允许你分别从 Terraform 提供程序和核心应用程序生成日志。Terraform 开发团队需要你的尝试操作的核心日志来排查与核心相关的错误。要启用核心日志记录,请将 TF_LOG_CORE 环境变量设置为适当的 日志级别。对于错误报告,你应该使用 TRACE 级别。
$ export TF_LOG_CORE=TRACE
TRACE 提供最高级别的日志记录,包含开发团队所需的所有信息。 还有其他日志记录级别,但通常保留给寻找特定信息的开发者。
您还可以通过设置 TF_LOG_PROVIDER 环境变量来生成提供程序日志。 通过将这些信息包含在您的错误报告中,提供程序开发团队可以重现和调试特定于提供程序的错误。
$ export TF_LOG_PROVIDER=TRACE
配置好日志记录后,将错误日志的路径设置为环境变量。 如果启用了您的 TF_LOG_CORE 或 TF_LOG_PROVIDER 环境变量,TF_LOG_PATH 变量将创建指定的文件并追加 Terraform 生成的日志。
$ export TF_LOG_PATH=logs.txt
要生成核心和提供程序日志的示例,请运行 terraform refresh 操作。
$ terraform refresh
data.http.myip: Reading...
data.http.myip: Read complete after 0s [id=http://ipv4.icanhazip.com]
data.aws_ami.ubuntu: Reading...
## ...
Outputs:
instance_id = [
"i-08cac1508a6baab3b",
"i-056c2e5bc7dba098f",
]
instance_name = [
"terraform-learn-sg_8080",
"terraform-learn-sg_ping",
]
instance_public_ip = [
"52.14.220.20",
"18.224.3.102",
]
打开并查看 logs.txt 文件。 此日志文件包含 provider.terraform-provider-aws 和 Terraform 核心日志记录。
$ cat logs.txt
##...
2024-07-02T12:40:42.125-0500 [DEBUG] provider: plugin process exited: path=.terraform/providers/registry.terraform.io/hashicorp/aws/5.56.1/darwin_arm64/terraform-provider-aws_v5.56.1_x5 pid=12485
2024-07-02T12:40:42.125-0500 [DEBUG] provider: plugin exited
2024-07-02T12:40:42.125-0500 [TRACE] vertex "provider[\"registry.terraform.io/hashicorp/aws\"] (close)": visit complete
2024-07-02T12:40:42.125-0500 [TRACE] vertex "root": starting visit (*terraform.nodeCloseModule)
2024-07-02T12:40:42.125-0500 [TRACE] vertex "root": does not belong to any module instance
2024-07-02T12:40:42.125-0500 [TRACE] vertex "root": visit complete
2024-07-02T12:40:42.125-0500 [TRACE] LoadSchemas: retrieving schema for provider type "registry.terraform.io/hashicorp/aws"
2024-07-02T12:40:42.125-0500 [TRACE] terraform.contextPlugins: Schema for provider "registry.terraform.io/hashicorp/aws" is in the global cache
2024-07-02T12:40:42.125-0500 [TRACE] LoadSchemas: retrieving schema for provider type "registry.terraform.io/hashicorp/http"
2024-07-02T12:40:42.125-0500 [TRACE] terraform.contextPlugins: Schema for provider "registry.terraform.io/hashicorp/http" is in the global cache
2024-07-02T12:40:42.128-0500 [TRACE] Plan is complete
2024-07-02T12:40:42.128-0500 [TRACE] Plan is not applyable
2024-07-02T12:40:42.128-0500 [TRACE] terraform.contextPlugins: Schema for provider "registry.terraform.io/hashicorp/aws" is in the global cache
2024-07-02T12:40:42.128-0500 [TRACE] LoadSchemas: retrieving schema for provider type "registry.terraform.io/hashicorp/aws"
2024-07-02T12:40:42.128-0500 [TRACE] terraform.contextPlugins: Schema for provider "registry.terraform.io/hashicorp/aws" is in the global cache
2024-07-02T12:40:42.128-0500 [TRACE] LoadSchemas: retrieving schema for provider type "registry.terraform.io/hashicorp/http"
2024-07-02T12:40:42.128-0500 [TRACE] terraform.contextPlugins: Schema for provider "registry.terraform.io/hashicorp/http" is in the global cache
2024-07-02T12:40:42.128-0500 [DEBUG] no planned changes, skipping apply graph check
2024-07-02T12:40:42.128-0500 [INFO] backend/local: refresh calling Refresh
2024-07-02T12:40:42.129-0500 [TRACE] statemgr.Filesystem: creating backup snapshot at terraform.tfstate.backup
2024-07-02T12:40:42.130-0500 [TRACE] statemgr.Filesystem: state has changed since last snapshot, so incrementing serial to 10
2024-07-02T12:40:42.130-0500 [TRACE] statemgr.Filesystem: writing snapshot at terraform.tfstate
2024-07-02T12:40:42.137-0500 [TRACE] statemgr.Filesystem: removing lock metadata file .terraform.tfstate.lock.info
2024-07-02T12:40:42.140-0500 [TRACE] statemgr.Filesystem: unlocking terraform.tfstate using fcntl flock
要删除日志流,请取消设置您不需要的环境变量。 取消设置 Terraform 核心日志记录。 当您重新运行 Terraform 时,Terraform 将仅记录特定于提供程序的的操作。 当您关闭终端会话时,所有环境变量都将被取消设置。
export TF_LOG_CORE=
提交工单
如果您希望在将问题提交到仓库之前获得社区的反馈,请考虑将问题作为 HashiCorp Discuss 论坛中的主题提交。
要创建一个错误审查工单,您必须确定哪个日志流包含您的错误。 在您的 logs.txt 文件中,找到最终的错误消息并将其追溯到来源。 它应该包含 provider-terraform-<PROVIDER-NAME>(如果这是一个提供程序问题)。
确定您的错误来源后,导航到 Terraform 核心 GitHub 仓库或搜索提供程序注册表中的提供程序的 GitHub 仓库。
一些提供程序可能对提交问题有不同的建议,但 Terraform 核心仓库有一个您应该遵循的工单模板,以向团队提供他们所需的信息。
首先,导航到 Terraform GitHub 仓库,并从顶部选项卡中选择“Issues”。

选择“New Issue”。

选择“Get started”并使用错误报告。

熟悉 行为准则。
使用 Terraform 核心模板,填写您收集的信息并记下预期的行为。 完成模板填写后,选择“Submit New Issue”,团队将审核您的工单。
清理资源
销毁您创建的资源。 回复 yes 以确认提示。
$ terraform destroy
下一步
在本教程中,您学习了如何通过更正 EC2 实例和安全组的损坏配置来排查 Terraform 问题。 您通过格式化和验证配置来更正循环错误、变量插值错误和循环错误。 您还学习了如何启用日志记录,以及向 Terraform 和提供程序团队在 GitHub 上报告问题的最佳实践。
有关状态和推荐实践的更多信息,请查看以下教程