跳至主要内容

自定义条件

您可以为配置中的几种类型的对象创建生成自定义错误消息的条件。例如,您可以为输入变量添加一个条件,以检查传入的图像 ID 格式是否正确。自定义条件可以捕获假设,帮助未来的维护人员理解配置的设计和意图。它们还可以更早地在上下文中返回有关错误的有用信息,帮助使用者更轻松地诊断其配置中的问题。

此页面解释以下内容

为您的用例选择自定义条件

OpenTofu 的不同自定义条件最适合各种情况。使用以下广泛指南为您的用例选择最佳自定义条件

  1. 带有断言的检查块 验证您的整个基础设施。此外,检查块不会阻止或阻塞 OpenTofu 操作的整体执行。
  2. 验证条件输出后置条件 可以确保配置的输入和输出满足特定要求。
  3. 资源 先决条件和后置条件 可以验证 OpenTofu 是否以可预测的结果生成配置。

有关何时使用某些自定义条件的更多信息,请参阅 先决条件和后置条件的选择选择检查或其他自定义条件

输入变量验证

variable 块中添加一个或多个 validation 块以指定自定义条件。每个验证都需要一个 condition 参数,该表达式必须使用变量的值在值有效时返回 true,或在值无效时返回 false。表达式只能引用包含的变量,并且不得产生错误。

如果条件计算结果为 false,则 OpenTofu 会生成一个 错误消息,其中包含 error_message 表达式的结果。如果声明多个验证,则 OpenTofu 会返回所有失败条件的错误消息。

以下示例检查 AMI ID 是否具有有效的语法。

代码块
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

如果表达式的失败决定了验证决策,请使用 can 函数,如下例所示。

代码块
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
# regex(...) fails if it cannot find a match
condition = can(regex("^ami-", var.image_id))
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

先决条件和后置条件

使用 preconditionpostcondition 块为资源、数据源和输出创建自定义规则。

OpenTofu 在评估与其关联的对象之前检查先决条件,并在评估对象之后检查后置条件。OpenTofu 会尽早评估自定义条件,但必须将依赖于未知值的条件推迟到应用阶段。有关更多详细信息,请参阅 仅在应用期间检查的条件

用法

每个先决条件和后置条件都需要一个 condition 参数。这是一个必须在条件满足时返回 true 或在条件无效时返回 false 的表达式。表达式可以引用同一模块中的任何其他对象,只要引用不会创建循环依赖关系即可。资源后置条件还可以使用 self 对象 来引用配置它们的资源的每个实例的属性。

如果条件计算结果为 false,则 OpenTofu 会生成一个 错误消息,其中包含 error_message 表达式的结果。如果声明多个先决条件或后置条件,则 OpenTofu 会返回所有失败条件的错误消息。

以下示例使用后置条件来检测调用者是否意外提供了适用于错误系统组件的 AMI。

代码块
data "aws_ami" "example" {
id = var.aws_ami_id

lifecycle {
# The AMI ID must refer to an existing AMI that has the tag "nomad-server".
postcondition {
condition = self.tags["Component"] == "nomad-server"
error_message = "tags[\"Component\"] must be \"nomad-server\"."
}
}
}

资源和数据源

resourcedata 块内部的 lifecycle 块可以包含 preconditionpostcondition 块。

  • OpenTofu 在评估现有的 countfor_each 参数后评估 precondition 块。这允许 OpenTofu 为每个实例分别评估先决条件,然后使 each.keycount.index 等可用于这些条件。OpenTofu 还在评估资源的配置参数之前评估先决条件。先决条件可能优先于参数评估错误。
  • OpenTofu 在计划和应用对受管资源的更改后,或在从数据源读取后评估 postcondition 块。后置条件失败会阻止对依赖于失败资源的其他资源进行更改。

在大多数情况下,我们不建议在同一个配置中同时包含表示相同对象的 `data` 块和 `resource` 块。这样做可能会阻止 OpenTofu 理解 `data` 块的结果可能会受到 `resource` 块更改的影响。但是,当您需要检查 `resource` 块的结果(该资源本身没有直接导出)时,只要将检查作为 `data` 块的直接 `postcondition`,就可以安全地使用 `data` 块来检查该对象。这告诉 OpenTofu `data` 块充当对在其他地方定义的对象的检查,从而允许 OpenTofu 按正确的顺序执行操作。

输出

一个 `output` 块可以包含一个 `precondition` 块。

前置条件可以起到与输入变量 `validation` 块对称的作用。输入变量验证检查模块对其输入所做的假设,而前置条件则检查模块对其输出所做的保证。您可以使用前置条件来防止 OpenTofu 在状态中保存无效的新输出值。您还可以使用它们来保留来自先前应用的有效输出值(如果适用)。

OpenTofu 在评估 `value` 表达式以最终确定结果之前,会先评估输出值的前置条件。前置条件可能优先于 `value` 表达式中潜在的错误。

示例

以下示例显示了前置条件和后置条件的用例。前置条件和后置条件声明了以下假设和保证。

  • **AMI ID 必须引用包含 `x86_64` 架构操作系统的 AMI。** 前置条件将检测调用者是否意外构建了针对不同架构的 AMI,该 AMI 可能无法运行此虚拟机旨在托管的软件。

  • **EC2 实例必须分配一个公共 DNS 主机名。** 在 Amazon Web Services 中,只有当 EC2 实例属于以特定方式配置的虚拟网络时,才会分配公共 DNS 主机名。后置条件将检测所选虚拟网络是否配置不正确,提示用户调试网络设置。

  • **EC2 实例将拥有一个加密的根卷。** 前置条件确保根卷已加密,即使在此 EC2 实例中运行的软件可能仍然可以在未加密的卷上按预期运行。这允许 OpenTofu 立即产生错误,在任何其他组件依赖新 EC2 实例之前。

代码块

data "aws_ami" "example" {
owners = ["amazon"]

filter {
name = "image-id"
values = ["ami-abc123"]
}
}

resource "aws_instance" "example" {
instance_type = "t3.micro"
ami = data.aws_ami.example.id

lifecycle {
# The AMI ID must refer to an AMI that contains an operating system
# for the `x86_64` architecture.
precondition {
condition = data.aws_ami.example.architecture == "x86_64"
error_message = "The selected AMI must be for the x86_64 architecture."
}

# The EC2 instance must be allocated a public DNS hostname.
postcondition {
condition = self.public_dns != ""
error_message = "EC2 instance must be in a VPC that has public DNS hostnames enabled."
}
}
}

data "aws_ebs_volume" "example" {
# Use data resources that refer to other resources to
# load extra data that isn't directly exported by a resource.
#
# Read the details about the root storage volume for the EC2 instance
# declared by aws_instance.example, using the exported ID.

filter {
name = "volume-id"
values = [aws_instance.example.root_block_device.volume_id]
}

# Whenever a data resource is verifying the result of a managed resource
# declared in the same configuration, you MUST write the checks as
# postconditions of the data resource. This ensures OpenTofu will wait
# to read the data resource until after any changes to the managed resource
# have completed.
lifecycle {
# The EC2 instance will have an encrypted root volume.
postcondition {
condition = self.encrypted
error_message = "The server's root volume is not encrypted."
}
}
}

output "api_base_url" {
value = "https://${aws_instance.example.private_dns}:8433/"
}

在前置条件和后置条件之间进行选择

您通常可以将验证检查实现为生成数据的资源的后置条件,或者作为使用数据的资源或输出值的前置条件。要确定哪种方法最合适,请考虑检查是表示假设还是保证。

使用前置条件表示假设

假设是在特定资源的配置可使用时必须为真的条件。例如,`aws_instance` 配置可以假设给定的 AMI 将始终配置为 `x86_64` CPU 架构。

我们建议使用前置条件表示假设,以便未来的维护人员可以在依赖该条件的其他表达式附近找到它们。这让他们可以更多地了解该资源的预期允许内容。

使用后置条件表示保证

保证是对象的一个特征或行为,配置的其余部分应该能够依赖它。例如,`aws_instance` 配置可以保证 EC2 实例将在分配给它私有 DNS 记录的网络中运行。

我们建议使用后置条件表示保证,以便未来的维护人员可以在负责实现这些保证的资源配置附近找到它们。这使他们能够更轻松地确定在更改配置时应保留哪些行为。

其他决策因素

在创建前置条件和后置条件时,您还应该考虑以下问题。

  • 哪个资源或输出值最有助于在错误消息中报告?OpenTofu 将始终在声明条件的位置报告错误。
  • 哪种方法更方便?如果特定资源具有许多依赖项,并且所有依赖项都对该资源做出了假设,那么将该假设一次声明为该资源的后置条件可能比在每个依赖项上多次声明为前置条件更实用。
  • 将相同或类似的条件声明为前置条件和后置条件是否有帮助?如果后置条件位于与前置条件不同的模块中,这可能很有用,因为它允许模块在独立发展时相互验证。

使用断言进行检查

检查块 可以验证基础设施,不受常规资源生命周期的限制。您可以通过 `assert` 块添加自定义条件,这些条件在计划和应用阶段结束时执行,并生成警告以通知您基础设施中的问题。

您可以在 `check` 块中添加一个或多个 `assert` 块来验证自定义条件。每个断言都需要一个 `condition` 参数,这是一个布尔表达式,如果预期的假设或保证得到满足,则应返回 `true`,否则返回 `false`。您的 `condition` 表达式可以引用 `check` 块周围可用的任何资源、数据源或变量。

以下示例使用带断言的检查块来验证 OpenTofu 网站是否正常。

代码块
check "health_check" {
data "http" "opentofu_org" {
url = "https://www.opentofu.org"
}

assert {
condition = data.http.opentofu_org.status_code == 200
error_message = "${data.http.opentofu_org.url} returned an unhealthy status code"
}
}

如果条件计算结果为 `false`,OpenTofu 将生成一条 错误消息,其中包含 `error_message` 表达式的结果。如果声明了多个断言,OpenTofu 将返回所有失败条件的错误消息。

在云后端中进行持续验证

云后端可以自动检查工作区配置中的检查在 OpenTofu 配置基础设施后是否继续通过。例如,您可以编写一个 `check` 来持续监控 API 网关证书的有效性。当条件失败时,持续验证会提醒您,以便您可以更新证书并避免下次更新基础设施时出现错误。

条件表达式

检查断言、输入变量验证、前置条件和后置条件都需要一个 `condition` 参数。这是一个布尔表达式,如果预期的假设或保证得到满足,则应返回 `true`,否则返回 `false`。

只要表达式有效并返回布尔结果,您就可以在条件中使用任何 OpenTofu 的内置函数或语言运算符。以下语言特性在编写条件表达式时特别有用。

逻辑运算符

使用逻辑运算符 `&&`(AND)、`||`(OR)和 `!`(NOT)将多个条件组合在一起。

代码块
  condition = var.name != "" && lower(var.name) == var.name

您还可以使用算术运算符(例如 `a + b`)、相等运算符(例如 `a == b`)和比较运算符(例如 `a < b`)。有关详细信息,请参阅 算术和逻辑运算符

`contains` 函数

使用 `contains` 函数 测试给定值是否是一组预定义有效值之一。

代码块
  condition = contains(["STAGE", "PROD"], var.environment)

`length` 函数

使用 `length` 函数 测试集合的长度并要求非空列表或映射。

代码块
  condition = length(var.items) != 0

这比使用 `==` 或 `!=` 直接与另一个集合进行比较更好。这是因为比较运算符只有在两个操作数具有完全相同的类型时才能返回 `true`,这对于空集合通常是不明确的。

`for` 表达式

`for` 表达式 与 `alltrue` 和 `anytrue` 函数结合使用,以测试条件是否对集合的所有元素或任何元素成立。

代码块
  condition = alltrue([
for v in var.instances : contains(["t2.micro", "m3.medium"], v.type)
])

`can` 函数

使用 `can` 函数 简洁地将表达式的有效性用作条件。如果给定的表达式成功评估,则返回 `true`;如果返回任何错误,则返回 `false`,因此您可以使用各种其他通常返回错误的函数作为条件表达式的一部分。

例如,您可以将canregex一起使用来测试字符串是否与特定模式匹配,因为当regex遇到不匹配的字符串时会返回错误。

代码块
  condition = can(regex("^[a-z]+$", var.name))

您还可以将can与类型转换函数一起使用,以测试某个值是否可以转换为某个类型或类型约束。

代码块
  # This remote output value must have a value that can
# be used as a string, which includes strings themselves
# but also allows numbers and boolean values.
condition = can(tostring(data.terraform_remote_state.example.outputs["name"]))
代码块
  # This remote output value must be convertible to a list
# type of with element type.
condition = can(tolist(data.terraform_remote_state.example.outputs["items"]))

您还可以将can与属性访问或索引运算符一起使用,以测试集合或结构化值是否具有特定的元素或索引。

代码块
  # var.example must have an attribute named "foo"
condition = can(var.example.foo)
代码块
  # var.example must be a sequence with at least one element
condition = can(var.example[0])
# (although it would typically be clearer to write this as a
# test like length(var.example) > 0 to better represent the
# intent of the condition.)

self对象

在后置条件块中使用self对象来引用正在评估的实例的属性。

代码块
resource "aws_instance" "example" {
instance_type = "t2.micro"
ami = "ami-abc123"

lifecycle {
postcondition {
condition = self.instance_state == "running"
error_message = "EC2 instance must be running."
}
}
}

eachcount对象

在设置了for_eachcount的块中,使用eachcount对象来引用在链中扩展的其他资源。

代码块
variable "vpc_cidrs" {
type = set(string)
}

data "aws_vpc" "example" {
for_each = var.vpc_cidrs

filter {
name = "cidr"
values = [each.key]
}
}

resource "aws_internet_gateway" "example" {
for_each = data.aws_vpc.example
vpc_id = each.value.id

lifecycle {
precondition {
condition = data.aws_vpc.example[each.key].state == "available"
error_message = "VPC ${each.key} must be available."
}
}
}

错误消息

输入变量验证、前置条件和后置条件都必须包含error_message参数。此参数包含OpenTofu在检测到未满足的条件时将作为错误消息一部分包含的文本。

代码块
Error: Resource postcondition failed

with data.aws_ami.example,
on ec2.tf line 19, in data "aws_ami" "example":
72: condition = self.tags["Component"] == "nomad-server"
|----------------
| self.tags["Component"] is "consul-server"

The selected AMI must be tagged with the Component value "nomad-server".

error_message参数可以是任何计算结果为字符串的表达式。这包括字面字符串、heredoc和模板表达式。您可以使用format函数nulllistmap类型的项转换为格式化的字符串。支持多行错误消息,并且具有前导空格的行不会进行断行。

我们建议将错误消息编写为一个或多个完整的句子,其风格类似于OpenTofu自己的错误消息。OpenTofu将在检测到问题的资源名称以及条件表达式中包含的任何外部值旁边显示该消息。

仅在应用期间检查的条件

OpenTofu尽早评估自定义条件。

输入变量验证只能引用变量值,因此OpenTofu始终立即评估它们。检查断言、前置条件和后置条件取决于OpenTofu在应用配置之前或之后是否知道与条件关联的值。

  • 应用前已知:OpenTofu在计划阶段检查条件。例如,只要图像 ID 不是从另一个资源生成,OpenTofu 就可以在计划期间知道其值。
  • 应用后已知:OpenTofu会延迟检查该条件,直到应用阶段。例如,AWS 仅在启动 EC2 实例时分配根卷 ID,因此 OpenTofu 只能在应用后知道此值。

在应用阶段,失败的前置条件将阻止OpenTofu实施与关联资源相关的计划操作。但是,失败的后置条件将在OpenTofu已实施这些操作后停止处理。失败的后置条件将阻止任何依赖于该资源的进一步下游操作,但不会撤消OpenTofu已采取的操作。

OpenTofu在创建完整配置的初始阶段通常比应用后续更改时拥有更少的信息。因此,OpenTofu可能会在应用初始创建期间检查条件,然后在后续更新期间在计划期间检查它们。