在上一篇文章中,我们聊了IaC的核心理念,并用Terraform count
参数快速创建了两台服务器。其中有一个非常实际的问题:
# main.tf - 定义云资源
provider "aws" {
region = "us-west-2"
}
# 创建一个VPC
resource "aws_vpc" "app_vpc" {
cidr_block = "10.0.0.0/16"
}
# 创建一个子网
resource "aws_subnet" "app_subnet" {
vpc_id = aws_vpc.app_vpc.id
cidr_block = "10.0.1.0/24"
}
# 创建一个安全组,允许80端口访问
resource "aws_security_group" "web_sg" {
vpc_id = aws_vpc.app_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
# 创建两台EC2实例
resource "aws_instance" "web_server" {
count = 2
ami = "ami-0c55b159cbfafe1f0" # 示例AMI
instance_type = "t2.micro"
subnet_id = aws_subnet.app_subnet.id
security_groups = [aws_security_group.web_sg.name]
tags = {
Name = "WebServer-${count.index + 1}"
}
}
如果我们用
count = 2
创建了WebServer-1
和WebServer-2
,现在我只想删除WebServer-1
,该怎么办?能只按顺序从后往前删吗?
答案是:对于 count
,是的,我们基本上只能从后往前删。直接删除中间的特定资源非常困难且危险。
这正是我们要从 count
的“有序世界”毕业,进入 for_each
的“自由世界”的核心原因。
count
的工作原理:像一个数组
当我们使用 count
时,Terraform 在内部会将这些资源视为一个列表(或数组)。
在我们的例子中,resource "aws_instance" "web_server"
实际上创建了一个资源列表,我们可以通过索引来访问它们:
aws_instance.web_server[0]
(对应WebServer-1
)aws_instance.web_server[1]
(对应WebServer-2
)
当我们想要缩减规模,比如将 count
从 2
改为 1
时,Terraform 的逻辑是: “好的,我需要一个包含1个元素的列表,但现在有2个。我需要删除一个。” 它会选择删除索引最高的那个,也就是 aws_instance.web_server[1]
。
为什么不能直接删除 [0]
呢?
想象一下,如果Terraform允许我们删除 [0]
,那么原来的 [1]
就必须“向前移动”来填补空位,变成新的 [0]
。这个“移动”操作在Terraform看来是一次“销毁和重建”,因为它在状态文件(state file)中的地址变了。这会导致 WebServer-2
被销毁,然后一个新的服务器在 [0]
的位置上被创建,这几乎肯定不是我们想要的结果,因为它会导致服务中断。
因此,count
带来的问题是:资源的身份与其在列表中的顺序(索引)强绑定。这种绑定在需要对集合中特定成员进行操作时,会变得非常僵化和危险。
解决方案:拥抱 for_each
,用“名字”而不是“序号”来管理资源
为了解决这个问题,Terraform 引入了 for_each
这个元参数。for_each
不使用无意义的数字索引,而是使用我们提供的**字符串键(key)**来标识每一个资源。
for_each
接受一个 map
(键值对) 或者 set
(字符串集合)。我们来把之前的例子用 for_each
重写一下。
使用 for_each
的代码示例:
我们不再用 count
,而是提供一个字符串集合,其中每个字符串都是我们服务器的唯一标识符。
# 创建两台EC2实例,使用 for_each
resource "aws_instance" "web_server" {
# for_each 接受一个字符串集合
for_each = toset(["alpha", "beta"]) # 我们给服务器起了两个代号:"alpha" 和 "beta"
ami = "ami-0c55b159cbfafe1f0" # 示例AMI
instance_type = "t2.micro"
subnet_id = aws_subnet.app_subnet.id
security_groups = [aws_security_group.web_sg.name]
tags = {
# each.key 会分别取到 "alpha" 和 "beta"
Name = "WebServer-${each.key}"
}
}
现在,Terraform创建的资源在状态文件中的地址变成了:
aws_instance.web_server["alpha"]
aws_instance.web_server["beta"]
看到了吗?资源的身份不再是 0
或 1
,而是有意义的、由我们自己定义的 "alpha"
和 "beta"
。
如何删除指定的资源?
现在,神奇的时刻到来了。假设我们想删除 alpha
服务器,同时保留 beta
服务器。我们只需要修改 for_each
的集合,把 "alpha"
从里面移除即可:
# ...
# 只保留 "beta",移除了 "alpha"
for_each = toset(["beta"])
# ...
当我们运行 terraform apply
时,Terraform会进行比较:
- 期望状态:需要一个名为
"beta"
的服务器。 - 当前状态:有一个
"alpha"
和一个"beta"
服务器。 - 执行计划:Terraform会精确地计划销毁
aws_instance.web_server["alpha"]
,而对aws_instance.web_server["beta"]
不做任何操作。
问题完美解决!
实战建议:何时使用 count
vs for_each
count
- 适用场景:当我们需要创建一组完全相同、不需要单独管理的无状态资源时。比如,创建5个完全一样的IAM用户策略附件。在这些场景下,我们只关心“数量”,不关心“谁是谁”。
- 缺点:不适用于需要独立生命周期的资源。
for_each
- 适用场景:绝大多数情况下,当我们需要创建一组相似资源时,都应该优先使用
for_each
。特别是对于服务器、数据库、磁盘等有状态或有独立身份的资源。 - 优点:资源身份与配置解耦,可以安全地添加、移除或修改集合中的任意一个成员,而不会影响其他成员。
一个重要的“坑”:从 count
迁移到 for_each
如果我们已经有用 count
创建好的存量资源,直接把代码改成 for_each
会发生什么?Terraform会认为我们要销毁所有旧的(带索引的)资源,然后创建所有新的(带键的)资源,这会导致服务中断!
为了避免这种情况,我们需要使用 terraform state mv
命令来“移动”状态文件中的资源地址,告诉Terraform“旧的 [0]
就是新的 ["alpha"]
”。
假设我们要从 count
迁移到 for_each
,我们需要执行以下步骤:
- 在代码中,将
count
的写法改成for_each
的写法。 - 不要立刻
apply
! - 使用
terraform state mv
命令进行迁移:# 将旧地址 'aws_instance.web_server[0]' 移动到新地址 'aws_instance.web_server["alpha"]' terraform state mv 'aws_instance.web_server[0]' 'aws_instance.web_server["alpha"]' # 将旧地址 'aws_instance.web_server[1]' 移动到新地址 'aws_instance.web_server["beta"]' terraform state mv 'aws_instance.web_server[1]' 'aws_instance.web_server["beta"]'
- 迁移完成后,运行
terraform plan
。如果一切顺利,它应该会显示 “No changes. Your infrastructure matches the configuration.”,证明迁移成功。
总结
总而言之,count
虽然简单直观,但在灵活性和安全性上存在天然的缺陷。for_each
才是现代Terraform实践中管理资源集合的黄金标准。它通过将资源的身份从不稳定的“顺序”解放出来,赋予其稳定的“名字”,从而允许我们像外科手术一样精确、安全地管理基础设施中的每一个成员。
从现在开始,养成优先使用 for_each
的习惯吧,它会让我们在未来的运维工作中避免很多不必要的麻烦!