zaki work log

作業ログやら生活ログやらなんやら

[Terraform] EC2のセキュリティグループ指定はsecurity_groupsでなくvpc_security_group_idsを使えば強制再作成されない

結論

VPCEC2インスタンス作成する定義の場合は、security_groupsでなくvpc_security_group_idsでセキュリティグループを指定しましょう。

該当ドキュメントはこちら

実装例は以下。

github.com

事象

お題は以下のコード

zaki-hmkc.hatenablog.com

コード

resource "aws_instance" "bastion" {
  count = var.host_count

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_public.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = true

  tags = {
    Name = "bastion-${count.index}"
  }
}

以前Terraformで作ってたこの内容のEC作成の定義ファイル部分、作成は問題ないけど、EC2の再作成が必要ないはずの変更が生じても強制再作成(作成済みEC2は削除され、同じ定義で別のEC2が新しく作成)されてしまう。

なんなら、定義ファイルを一切変更しなくても、terraform planすると、"no changes"とならずに以下のように「replaced」となってしまう。

動作

前述の定義内容でterraform planすると、EC2部分は以下の通り。

$ terraform plan

[...]

Terraform will perform the following actions:

  # aws_instance.bastion[0] must be replaced
-/+ resource "aws_instance" "bastion" {
      ~ arn                          = "arn:aws:ec2:***:****:instance/i-****" -> (known after apply)
      ~ availability_zone            = "****" -> (known after apply)
      ~ cpu_core_count               = 1 -> (known after apply)
      ~ cpu_threads_per_core         = 2 -> (known after apply)
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
      - hibernation                  = false -> null
      + host_id                      = (known after apply)
      - iam_instance_profile         = "" -> null
      ~ id                           = "i-********" -> (known after apply)
      ~ instance_state               = "running" -> (known after apply)
      ~ ipv6_address_count           = 0 -> (known after apply)
      ~ ipv6_addresses               = [] -> (known after apply)
      - monitoring                   = false -> null
      ~ outpost_arn                  = "" -> (known after apply)
      ~ password_data                = "" -> (known after apply)
      ~ placement_group              = "" -> (known after apply)
      ~ primary_network_interface_id = "eni-****" -> (known after apply)
      ~ private_dns                  = "ip-172-25-10-169.***.compute.internal" -> (known after apply)
      ~ private_ip                   = "172.25.10.169" -> (known after apply)
      ~ public_dns                   = "ec2-********.compute.amazonaws.com" -> (known after apply)
      ~ public_ip                    = "********" -> (known after apply)
      ~ secondary_private_ips        = [] -> (known after apply)
      ~ security_groups              = [ # forces replacement
          + "sg-01281f2eb5db9a2b9",
        ]
        tags                         = {
            "Name" = "bastion-0"
        }
      ~ tenancy                      = "default" -> (known after apply)
      ~ vpc_security_group_ids       = [
          - "sg-01281f2eb5db9a2b9",
        ] -> (known after apply)
        # (7 unchanged attributes hidden)

      - credit_specification {
          - cpu_credits = "unlimited" -> null
        }

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + tags                  = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      ~ enclave_options {
          ~ enabled = false -> (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      ~ metadata_options {
          ~ http_endpoint               = "enabled" -> (known after apply)
          ~ http_put_response_hop_limit = 1 -> (known after apply)
          ~ http_tokens                 = "optional" -> (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      ~ root_block_device {
          ~ delete_on_termination = true -> (known after apply)
          ~ device_name           = "/dev/xvda" -> (known after apply)
          ~ encrypted             = false -> (known after apply)
          ~ iops                  = 100 -> (known after apply)
          + kms_key_id            = (known after apply)
          ~ tags                  = {} -> (known after apply)
          ~ throughput            = 0 -> (known after apply)
          ~ volume_id             = "vol-0ca9e88e004237231" -> (known after apply)
          ~ volume_size           = 8 -> (known after apply)
          ~ volume_type           = "gp2" -> (known after apply)
        }
    }

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

最初は明示的に定義してないストレージ周りの定義でも影響してるんかなと思ったりしたけど、ターミナルで見ると赤字でわざわざ「# forces replacement」とコメント出力されている以下のセキュリティグループの設定が原因。

f:id:zaki-hmkc:20210522065033p:plain

対応

aws_instanceリソースのsecurity_groupsオプションの項のドキュメントを確認すると以下の通り、vpc_security_group_idsを使うように書いてある。

NOTE:

If you are creating Instances in a VPC, use vpc_security_group_ids instead.

ということでコード変更。

--- a/aws/practice/ec2.tf
+++ b/aws/practice/ec2.tf
@@ -14,7 +14,7 @@ resource "aws_instance" "bastion" {
   instance_type               = "t3.nano"
   key_name                    = aws_key_pair.my_key.id
   subnet_id                   = aws_subnet.prac_public.id
-  security_groups             = [aws_security_group.allow_ssh_icmp.id]
+  vpc_security_group_ids      = [aws_security_group.allow_ssh_icmp.id]
   associate_public_ip_address = true
 
   tags = {

この内容でterraform planを実行すると、期待通り"no changes"になった。

$ terraform plan
aws_key_pair.my_key: Refreshing state... [id=***]
aws_vpc.practice: Refreshing state... [id=***]
aws_internet_gateway.gw: Refreshing state... [id=***]
aws_security_group.allow_ssh_icmp: Refreshing state... [id=***]
aws_subnet.prac_priv2: Refreshing state... [id=***]
aws_subnet.prac_public: Refreshing state... [id=***]
aws_subnet.prac_priv1: Refreshing state... [id=***]
aws_route_table.public_route: Refreshing state... [id=***]
aws_security_group_rule.egress: Refreshing state... [id=***]
aws_security_group_rule.ssh: Refreshing state... [id=***]
aws_security_group_rule.icmp: Refreshing state... [id=***]
aws_instance.bastion[0]: Refreshing state... [id=***]
aws_route_table_association.public_subnet: Refreshing state... [id=***]

No changes. Infrastructure is up-to-date.

これで作成済みEC2に影響しないコード変更を行ったときに、余計なEC2再作成が発生しなくなった。

参考

qiita.com

教訓

勘と雰囲気と勢いも良いけど、ドキュメントちゃんと読もう。