Terraformのfor_eachとnullで、効率的にAWSのセキュリティグループを定義する

nullを初めて使った
2021.12.16

ちゃだいん(@chazuke4649)です。

普段、IaCでAWSリソースを管理している人は、セキュリティグループについて効率よく書けないか、一度は頭をひねった経験があるのではないでしょうか?

自分もその一人でして、今回Terraformでセキュリティグループを定義するにあたり、いろんな方の記事をありがたく参考にしつつ、改善することができたので紹介します。

改善前

改善前は以下のようにセキュリティグループを定義していました。

resource "aws_security_group" "test" {
  vpc_id      = aws_vpc.main.id
  name        = "test-sg"
  description = "For test"
  tags        = { Name = "test-sg" }
}

## Inbound
resource "aws_security_group_rule" "test_inbound_01" {
  security_group_id = aws_security_group.test.id
  type              = "ingress"
  description       = "HTTP from Internet"
  protocol          = "tcp"
  from_port         = "80"
  to_port           = "80"
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_security_group_rule" "test_inbound_02" {
  security_group_id        = aws_security_group.test.id
  type                     = "ingress"
  description              = "SSH From Bastion"
  protocol                 = "tcp"
  from_port                = "22"
  to_port                  = "22"
  source_security_group_id = ["sg-1234567890"]
}

## Outbound
resource "aws_security_group_rule" "test_outbound_01" {
  security_group_id = aws_security_group.test.id
  type              = "egress"
  description       = "Allow any outbound traffic"
  protocol          = "-1"
  from_port         = "0"
  to_port           = "0"
  cidr_blocks       = ["0.0.0.0/0"]
}

これを修正し、結果的に以下のようになりました。

改善後

locals {
  test_sg = {
    ## [ type, from_port, to_port, protocol, sg-id, cidr_blocks, description ]
    "rule_1" = ["ingress", 80, 80, "tcp", null, ["0.0.0.0/0"], "HTTP from Internet"],
    "rule_2" = ["ingress", 22, 22, "tcp", "sg-1234567890", null, "SSH from Bastion"],
    "rule_3" = ["egress", 0, 0, "-1", null, ["0.0.0.0/0"], "Allow any outbound traffic"]
  }
}

resource "aws_security_group" "test_sg" {
  vpc_id      = aws_vpc.main.id
  description = "For test"
  name        = "test-sg"
  tags        = { Name = "test-sg" }
}

resource "aws_security_group_rule" "test" {
  security_group_id        = aws_security_group.test_sg.id
  for_each                 = local.test_sg
  type                     = each.value[0]
  from_port                = each.value[1]
  to_port                  = each.value[2]
  protocol                 = each.value[3]
  source_security_group_id = each.value[4]
  cidr_blocks              = each.value[5]
  description              = each.value[6]
}

解説

改善するにあたり、冗長的な記述になっているセキュリティグループルールをコンパクトにできないか考えます。

for_eachを使うと、例えばresourceブロックを繰り返し処理で複数作ることができます。こちらは比較的人気かと思いますので説明は割愛します。詳しくは参考ブログをご覧ください。

for_eachによって複数作れそうですが、ルールの内容によって一部引数に差異があります。ここをどう解決するかが肝になりそうです。条件分岐を入れると複雑化しそうなので、できるだけシンプルな方法を検討しました。

今回のポイントは null 型です。

null: 不在または省略を表す値。リソースの引数をnullにすると、Terraformは完全に省略したかのように振る舞います - 引数にデフォルト値がある場合はそれを使用し、引数が必須の場合はエラーを発生させます。nullは条件式で最も有用なので、条件が満たされない場合に動的に引数を省略することができます。(意訳)

Types and Values - Configuration Language | Terraform by HashiCorp

今回の場合、aws_security_group_ruleブロックの引数source_sercurity_group_idcidr_blocksは、両方入力することができず、いずれか一方のみしか入力できません。

例えばインバウンドの80番ポートでcidr_blocks0.0.0.0/0で開放する場合、source_security_group_id""とブランクにしてplanするとエラーになります。

│ Error: Conflicting configuration arguments
│
│   with aws_security_group_rule.test["rule_1"],
│   on security_group.tf line 217, in resource "aws_security_group_rule" "test":
│  217:   source_security_group_id = each.value[4]
│
│ "source_security_group_id": conflicts with cidr_blocks

そこでブランクの代わりにnullを入力すると、対象の引数は省略されたものとみなし、競合しなくなるというわけです。

nullの活用により、単一のセキュリティグループルールをガワだけ作り、実際の値はローカル変数にてMAP型の行を追加するだけで、ルールを追加することができます。

これによって、修正前よりコード量を減らし同等の構成を再現することができました。

紹介としては以上となります。もっといい方法があったらぜひTwitterなどでこそっと教えてください。

参考URL

aws_security_group | Resources | hashicorp/aws | Terraform Registry

aws_security_group_rule | Resources | hashicorp/aws | Terraform Registry

以下の記事を参考にしました。ありがとうございます!

Terraformerとしてコードを書いて思うこと | フューチャー技術ブログ

TerraformでAWSのセキュリティグループをListで定義してみた - Kumanote corporate website

Terraformでのloop処理の書き方(for, for_each, count)