地形陷阱

地形陷阱
让我们重点介绍一些陷阱,包括与循环、if 语句和部署技术相关的陷阱,以及影响 Terraform 的更常见问题:

  • count 和 for_each 参数有限制;
  • 限制零停机部署;
  • 再好的计划也可能失败;
  • 重构也有其陷阱;
  • 延迟一致性与延迟是一致的。

count 和 for_each 参数有限制

本章中的示例在循环和条件逻辑中广泛使用了 count 参数和 for_each 表达式。 它们表现良好,但有两个您需要注意的重要限制。

  • Count 和 for_each 不能引用任何资源输出变量。
  • count 和 for_each 不能在模块配置中使用。

count 和 for_each 不能引用任何资源输出变量

假设您需要部署多个 EC2 服务器,并且由于某种原因您不想使用 ASG。 你的代码可能是这样的:

resource "aws_instance" "example_1" {
   count             = 3
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

让我们一一看看。

由于 count 参数设置为静态值,因此此代码将正常运行:当您运行 apply 命令时,它将创建三个 EC2 服务器。 但是,如果您想在当前 AWS 区域内的每个可用区 (AZ) 中部署一台服务器该怎么办? 您可以让代码从 aws_availability_zones 数据源加载区域列表,然后循环遍历每个区域并使用 count 参数和数组索引访问在其中创建一个 EC2 服务器:

resource "aws_instance" "example_2" {
   count                   = length(data.aws_availability_zones.all.names)
   availability_zone   = data.aws_availability_zones.all.names[count.index]
   ami                     = "ami-0c55b159cbfafe1f0"
   instance_type       = "t2.micro"
}

data "aws_availability_zones" "all" {}

该代码也可以正常工作,因为 count 参数可以毫无问题地引用数据源。 但是,如果您需要创建的服务器数量取决于某些资源的输出,会发生什么情况? 为了证明这一点,最简单的方法是使用 random_integer 资源,顾名思义,它返回一个随机整数:

resource "random_integer" "num_instances" {
  min = 1
  max = 3
}

此代码生成 1 到 3 之间的随机数。让我们看看如果我们尝试在 aws_instance 资源的 count 参数中使用此资源的输出会发生什么:

resource "aws_instance" "example_3" {
   count             = random_integer.num_instances.result
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

如果您在此代码上运行 terraform plan,您将收到以下错误:

Error: Invalid count argument

   on main.tf line 30, in resource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.

Terraform 要求在创建或修改任何资源之前的规划阶段计算 count 和 for_each。 这意味着 count 和 for_each 可以引用文字、变量、数据源,甚至资源列表(只要它们的长度可以在调度时确定),但不能引用计算的资源输出变量。

count 和 for_each 不能在模块配置中使用

有一天,您可能会想在模块配置中添加计数参数:

module "count_example" {
     source = "../../../../modules/services/webserver-cluster"

     count = 3

     cluster_name = "terraform-up-and-running-example"
     server_port = 8080
     instance_type = "t2.micro"
}

此代码尝试在模块内使用 count 来创建 webserver-cluster 资源的三个副本。 或者您可能希望通过将其 count 参数设置为 0,基于某些布尔条件来使连接模块成为可选。这可能看起来像是合理的代码,但在运行 terraform plan 时您会收到此错误:

Error: Reserved argument name in module block

   on main.tf line 13, in module "count_example":
   13: count = 3

The name "count" is reserved for use in a future version of Terraform.

不幸的是,从 Terraform 0.12.6 开始,不支持在模块资源中使用 count 或 for_each 。 根据 Terraform 0.12 发行说明 (http://bit.ly/3257bv4),HashiCorp 计划在未来添加此功能,因此根据您阅读本书的时间,它可能已经可用。 为了确定, 请在此处阅读 Terraform 变更日志.

零停机部署的局限性

将 create_before_destroy 块与 ASG 结合使用是创建零停机部署的绝佳解决方案,但有一个警告除外:不支持自动缩放规则。 或者更准确地说,这会在每次部署时将 ASG 大小重置回 min_size,如果您使用自动缩放规则来增加运行的服务器数量,这可能会出现问题。

例如,webserver-cluster 模块包含一对 aws_autoscaling_schedule 资源,这会在上午 9 点将集群中的服务器数量从 11 台增加到 9 台。 如果您在上午 XNUMX 点进行部署,新的 ASG 将仅使用两台服务器而不是十台服务器启动,并保持这种状态直到第二天上午 XNUMX 点。

可以通过多种方式规避此限制。

  • 将 aws_autoscaling_schedule 中的重复参数从 0 9 * * * (“上午 9 点运行”)更改为 0-59 9-17 * * * (“上午 9 点到下午 5 点每分钟运行一次”)。 如果 ASG 已经有十台服务器,再次运行此自动缩放规则不会改变任何内容,这正是我们想要的。 但如果ASG是最近才部署的,这条规则将保证最多一分钟内其服务器数量达到XNUMX台。 这并不是一个完全优雅的方法,从十台服务器到两台服务器的大幅跳跃也会给用户带来问题。
  • 创建一个自定义脚本,该脚本使用 AWS API 来确定 ASG 中活动服务器的数量,使用外部数据源调用该脚本(请参阅第 249 页的“外部数据源”),并将 ASG 的desired_capacity 参数设置为由剧本。 这样,每个新的 ASG 实例将始终以与现有 Terraform 代码相同的容量运行,并使其更难以维护。

当然,Terraform 理想情况下应该内置对零停机部署的支持,但截至 2019 年 XNUMX 月,HashiCorp 团队还没有计划添加此功能(详细信息 - 这里).

正确的计划可能无法成功实施

有时 plan 命令会生成完全正确的部署计划,但 apply 命令会返回错误。 例如,尝试添加 aws_iam_user 资源,其名称与您之前在第 2 章中创建的 IAM 用户所使用的名称相同:

resource "aws_iam_user" "existing_user" {
   # Подставьте сюда имя уже существующего пользователя IAM,
   # чтобы попрактиковаться в использовании команды terraform import
   name = "yevgeniy.brikman"
}

现在,如果您运行 plan 命令,Terraform 将输出一个看似合理的部署计划:

Terraform will perform the following actions:

   # aws_iam_user.existing_user will be created
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (known after apply)
         + force_destroy   = false
         + id                    = (known after apply)
         + name               = "yevgeniy.brikman"
         + path                 = "/"
         + unique_id         = (known after apply)
      }

Plan: 1 to add, 0 to change, 0 to destroy.

如果运行 apply 命令,您将收到以下错误:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:
User with name yevgeniy.brikman already exists.

   on main.tf line 10, in resource "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

当然,问题是具有该名称的 IAM 用户已经存在。 这种情况不仅会发生在 IAM 用户身上,而且会发生在几乎所有资源上。 有人可能手动或使用命令行创建了此资源,但无论哪种方式,匹配 ID 都会导致冲突。 此错误有多种变体,常常让 Terraform 新手感到惊讶。

关键点是 terraform plan 命令仅考虑 Terraform 状态文件中指定的那些资源。 如果以其他方式创建资源(例如,通过单击 AWS 控制台手动创建),它们最终不会出现在状态文件中,因此 Terraform 在执行计划命令时不会考虑它们。 结果,乍一看似乎正确的计划最终会失败。

我们可以从中吸取两个教训。

  • 如果您已经开始使用 Terraform,请不要使用其他任何东西。 如果您的基础设施的一部分是使用 Terraform 进行管理的,您将无法再手动修改它。 否则,您不仅会面临奇怪的 Terraform 错误的风险,而且还会抵消 IaC 的许多好处,因为代码将不再是您的基础设施的准确表示。
  • 如果您已经拥有一些基础设施,请使用导入命令。 如果您开始将 Terraform 与现有基础设施结合使用,则可以使用 terraform import 命令将其添加到状态文件中。 这样 Terraform 将知道需要管理哪些基础设施。 导入命令有两个参数。 第一个是配置文件中的资源地址。 这里的语法与资源链接相同:_。 (如 aws_iam_user.existing_user)。 第二个参数是要导入的资源的ID。 假设资源 ID aws_iam_user 是用户名(例如 yevgeniy.brikman),资源 ID aws_instance 是 EC2 服务器 ID(例如 i-190e22e5)。 如何导入资源通常在其页面底部的文档中指出。

    下面是一个导入命令,用于同步您添加到 Terraform 配置中的 aws_iam_user 资源以及第 2 章中的 IAM 用户(当然,用您的名字替换 yevgeniy.brikman):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform 将调用 AWS API 来查找您的 IAM 用户并在其与 Terraform 配置中的 aws_iam_user.existing_user 资源之间创建状态文件关联。 从现在开始,当您运行 plan 命令时,Terraform 将知道 IAM 用户已经存在,并且不会尝试再次创建它。

    值得注意的是,如果您已经有很多资源想要导入 Terraform,手动编写代码并一次导入每个资源可能会很麻烦。 因此,值得研究像 Terraforming (http://terraforming.dtan4.net/) 这样的工具,它可以自动从您的 AWS 账户导入代码和状态。

    重构也有其陷阱

    重构 是编程中的常见做法,您可以更改代码的内部结构,同时保持外部行为不变。 这是为了使代码更清晰、更整洁、更易于维护。 重构是一项不可或缺的技术,应该经常使用。 但当涉及到 Terraform 或任何其他 IaC 工具时,您必须非常小心一段代码的“外部行为”的含义,否则会出现意想不到的问题。

    例如,一种常见的重构类型是将变量或函数的名称替换为更容易理解的名称。 许多 IDE 内置了对重构的支持,可以自动重命名整个项目中的变量和函数。 在通用编程语言中,这是一个您可能不会想到的微不足道的过程,但在 Terraform 中您必须非常小心,否则您可能会遇到中断。

    例如,webserver-cluster 模块有一个输入变量 cluster_name:

    variable "cluster_name" {
       description = "The name to use for all the cluster resources"
       type          = string
    }

    想象一下,您开始使用此模块来部署一个名为 foo 的微服务。 稍后,您想将服务重命名为 bar。 此更改可能看起来微不足道,但实际上可能会导致服务中断。

    事实上,webserver-cluster 模块在许多资源中使用了 cluster_name 变量,包括两个安全组和 ALB 的 name 参数:

    resource "aws_lb" "example" {
       name                    = var.cluster_name
       load_balancer_type = "application"
       subnets = data.aws_subnet_ids.default.ids
       security_groups      = [aws_security_group.alb.id]
    }

    如果您更改资源的名称参数,Terraform 将删除该资源的旧版本并在其位置创建一个新版本。 但是,如果该资源是 ALB,则在删除它和下载新版本之间,您将没有将流量重定向到 Web 服务器的机制。 同样,如果删除安全组,您的服务器将开始拒绝任何网络流量,直到创建新组。

    您可能感兴趣的另一种重构类型是更改 Terraform ID。 我们以 webserver-cluster 模块中的 aws_security_group 资源为例:

    resource "aws_security_group" "instance" {
      # (...)
    }

    该资源的标识符称为实例。 想象一下,在重构过程中,您决定将其更改为更容易理解的(在您看来)名称 cluster_instance:

    resource "aws_security_group" "cluster_instance" {
       # (...)
    }

    最终会发生什么? 没错:中断。

    Terraform 将每个资源 ID 与云提供商 ID 相关联。 例如,iam_user 与 AWS IAM 用户 ID 关联,aws_instance 与 AWS EC2 服务器 ID 关联。 如果您将资源 ID 更改为 Terraform(例如从实例更改为 cluster_instance,如 aws_security_group 的情况),则它将显示为好像您删除了旧资源并添加了新资源。 如果您应用这些更改,Terraform 将删除旧的安全组并创建一个新的安全组,同时您的服务器开始拒绝任何网络流量。

    您应该从这次讨论中吸取以下四个重要教训。

    • 始终使用计划命令。 它可以揭示所有这些障碍。 仔细检查其输出,并注意 Terraform 计划删除很可能不应删除的资源的情况。
    • 先创建再删除。 如果要替换资源,请在删除原始资源之前仔细考虑是否需要创建替换资源。 如果答案是肯定的,create_before_destroy 可以提供帮助。 通过执行两个步骤可以手动实现相同的结果:首先将新资源添加到配置中并运行 apply 命令,然后从配置中删除旧资源并再次使用 apply 命令。
    • 更改标识符需要更改状态。 如果您想要更改与资源关联的 ID(例如,将 aws_security_group 从实例重命名为 cluster_instance)而不删除资源并创建其新版本,则必须相应地更新 Terraform 状态文件。 切勿手动执行此操作 - 请改用 terraform state 命令。 重命名标识符时,您应该运行 terraform state mv 命令,其语法如下:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE 是引用当前形式的资源的表达式,NEW_REFERENCE 是要将其移动到的位置。 例如,将 aws_security_group 组从 instance 重命名为 cluster_instance 时,需要运行以下命令:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      这告诉 Terraform 之前与 aws_security_group.instance 关联的状态现在应与 aws_security_group.cluster_instance 关联。 如果重命名并运行此命令 terraform plan 后没有显示任何更改,那么您所做的一切都是正确的。

    • 某些设置无法更改。 许多资源的参数是不可更改的。 如果您尝试更改它们,Terraform 将删除旧资源并在其位置创建一个新资源。 每个资源页面通常会指示更改特定设置时会发生什么,因此请务必检查文档。 始终使用 plan 命令并考虑使用 create_before_destroy 策略。

    延迟一致性与延迟一致...

    一些云提供商的 API(例如 AWS)是异步的并且具有延迟一致性。 异步意味着接口可以立即返回响应,而无需等待请求的操作完成。 延迟一致性意味着更改可能需要时间才能传播到整个系统; 发生这种情况时,您的响应可能不一致,并且取决于哪个数据源副本响应您的 API 调用。

    例如,想象一下,您对 AWS 进行 API 调用,要求它创建 EC2 服务器。 API 将几乎立即返回“成功”响应(201 Created),而无需等待服务器本身创建。 如果您尝试立即连接到它,它几乎肯定会失败,因为此时 AWS 仍在初始化资源,或者服务器尚未启动。 此外,如果您再次调用以获取有关此服务器的信息,您可能会收到错误(404 Not Found)。 问题是,有关此 EC2 服务器的信息在变得随处可用之前仍可能在整个 AWS 中传播,您必须等待几秒钟。

    每当您使用具有惰性一致性的异步 API 时,您必须定期重试请求,直到操作完成并通过系统传播。 不幸的是,AWS SDK 没有为此提供任何好的工具,并且 Terraform 项目曾经遭受过许多错误,例如 6813 (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    The subnet ID 'subnet-xxxxxxx' does not exist

    换句话说,您创建一个资源(如子网),然后尝试获取有关它的一些信息(如新创建的子网的 ID),而 Terraform 找不到它。 大多数这些错误(包括 6813)已得到修复,但它们仍然时不时地出现,特别是当 Terraform 添加对新资源类型的支持时。 这很烦人,但在大多数情况下不会造成任何伤害。 当您再次运行 terraform apply 时,一切都应该正常,因为此时信息已经传播到整个系统。

    此摘录摘自叶夫根尼·布里克曼 (Evgeniy Brikman) 的书 “Terraform:代码级别的基础设施”.

来源: habr.com

添加评论