以前の出来事ですが、TF素人だった私は下記のようなTFを書いてました。
locals {
sg_ingress_rules = [
{
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 443
to_port = 443
},
{
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 6666
to_port = 6666
},
]
}
resource "aws_security_group" "this" {
name = "${local.prefix}-sg"
vpc_id = var.vpc_id
tags = {
Name = "${local.prefix}-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "this" {
for_each = { for idx, rule in local.sg_ingress_rules : idx => rule }
security_group_id = aws_security_group.this.id
cidr_ipv4 = each.value.cidr_ipv4
from_port = each.value.from_port
to_port = each.value.to_port
ip_protocol = each.value.ip_protocol
}
このTFどこが悪いかお分かりになりますか?
正解は...
resource "aws_vpc_security_group_ingress_rule" "this" {
for_each = { for idx, rule in local.sg_ingress_rules : idx => rule } #<===== ココ
security_group_id = aws_security_group.this.id
cidr_ipv4 = each.value.cidr_ipv4
from_port = each.value.from_port
to_port = each.value.to_port
ip_protocol = each.value.ip_protocol
}
なぜでしょうか?
まず、とりあえずこれ、applyしたらリソースは普通にできます。
では次に、このsgのingress ruleをport 3306も許可するように更新しようとします。
locals {
sg_ingress_rules = [
{
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 443
to_port = 443
},
# Newly added!!
{
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 3306
to_port = 3306
},
{
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 6666
to_port = 6666
},
]
}
Planを流すと下記のようになるでしょう。
+ create
~ update in-place
OpenTofu will perform the following actions:
# aws_vpc_security_group_ingress_rule.this["1"] will be updated in-place
~ resource "aws_vpc_security_group_ingress_rule" "this" {
~ from_port = 6666 -> 3306
id = "sgr-<UUID1>"
~ to_port = 6666 -> 3306
# (7 unchanged attributes hidden)
}
# aws_vpc_security_group_ingress_rule.this["2"] will be created
+ resource "aws_vpc_security_group_ingress_rule" "this" {
+ arn = (known after apply)
+ cidr_ipv4 = "0.0.0.0/0"
+ from_port = 6666
+ id = (known after apply)
+ ip_protocol = "tcp"
+ region = "ap-northeast-1"
+ security_group_id = "sg-<UUID2>"
+ security_group_rule_id = (known after apply)
+ tags_all = {}
+ to_port = 6666
}
Plan: 1 to add, 1 to change, 0 to destroy.
たぶん、ここで注意深い人は、あ、1 to changeを見て、これはなんか嫌な予感がすると気づくはずです。
しかし、ミーティング前にPRをマージを完了させてタスクをdoneにしたかった私は急いでマージをクリック、applyのログを眺めていると...
╷
╷
│ Error: creating VPC Security Group Rule
│
│ with aws_vpc_security_group_ingress_rule.this["2"],
│ on security_groups.tf line N, in resource "aws_vpc_security_group_ingress_rule" "this":
│ N: resource "aws_vpc_security_group_ingress_rule" "this" {
│
│ operation error EC2: AuthorizeSecurityGroupIngress, https response error
│ StatusCode: 400, RequestID: <UUID>, api error
│ InvalidPermission.Duplicate: the specified rule "peer: 0.0.0.0/0, TCP, from
│ port: 6666, to port: 6666, ALLOW" already exists
無事、コンフリクトエラーでました...
あせってretryを流すも、再実行に3分くらいかかり、その間6666のポートがブロックされた状態になってしまいました...
結果、アプリのダウンタイムにつながってしまいました...
Devだったのでまだ良かったですが、Prodだと大惨事です。
じゃあどうすれば良かったのかというと...
普通に下記のように、for_each用のkeyを用意しておけば良かったかなと思います。
そうすれば、Planも Plan: 1 to add, 0 to change, 0 to destroy. のようにただのaddになって失敗することもなかったでしょう。
locals {
sg_ingress_rules = {
"tcm_cidr_443": {
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 443
to_port = 443
},
"tcm_cidr_6666": {
ip_protocol = "tcp"
cidr_ipv4 = cidr
from_port = 6666
to_port = 6666
},
]
}
resource "aws_security_group" "this" {
name = "${local.prefix}-sg"
vpc_id = var.vpc_id
tags = {
Name = "${local.prefix}-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "this" {
for_each = local.sg_ingress_rules
security_group_id = aws_security_group.this.id
cidr_ipv4 = each.value.cidr_ipv4
from_port = each.value.from_port
to_port = each.value.to_port
ip_protocol = each.value.ip_protocol
}
ということで、みなさん、for_eachでarrayを使いたい場合、適当にindexを使ったりせずに、arrayをmapにして、keyをしっかり定義した方がよさそうです!
ではでは、良いOpenTofu/Terraformライフを〜!
