TerraformでEC2を作った後に、security_groupsの指定方法でインスタンスの再作成になってしまった話

# What's？

Terraformを使ってEC2を立てる時に、条件によってデフォルトのセキュリティグループを作る・作らないみたいな制御を入れていたら、妙な挙動をされたので困ったという話。

結論としては、EC2の定義に指定していたセキュリティグループの指定に使う属性が誤っていたのですが。


# やりたかったこと

こんな感じに、変数でセキュリティグループを与えたら指定のものを使い、そうでなければデフォルトのセキュリティグループを作成する、みたいなことをやろうとしました。

```go
variable "my_security_groups" {
  type    = list(string)
  default = []
}

resource "aws_security_group" "this" {
  count = length(var.my_security_groups) > 0 ? 0 : 1

  ...
}

resource "aws_instance" "this" {
  ami           = "..."
  instance_type = "..."
  subnet_id     = "..."

  security_groups = length(var.my_security_groups) > 0 ? var.my_security_groups : [aws_security_group.this[0].id]
}
```

まあ、ハマりましたと。


# 環境

今回の環境は、こちらです。

```shell
$ terraform version
Terraform v0.12.26
+ provider.aws v2.66.0
```

# AWS Providerの指定とVPCの作成

VPCの作成までは、AWS VPC Terraform moduleを使ってさっくりと。

[AWS VPC Terraform module](https://github.com/terraform-aws-modules/terraform-aws-vpc)

```go
terraform {
  required_version = "0.12.26"
}

provider "aws" {
  version = "2.66.0"
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.39.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs            = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]

  map_public_ip_on_launch = true

  enable_nat_gateway = false
}
```


# EC2＋セキュリティグループ

こんな感じで、セキュリティグループをInput Variableとして定義し、指定されていればEC2のセキュリティグループに設定し、そうでなければデフォルトのセキュリティグループを作って指定するように定義してみました。

```go
variable "my_security_groups" {
  type    = list(string)
  default = []
}

resource "aws_security_group" "this" {
  count = length(var.my_security_groups) > 0 ? 0 : 1

  name   = "my-ec2-instance-sg"
  vpc_id = module.vpc.vpc_id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "this" {
  ami           = "ami-0a1c2ec61571737db"
  instance_type = "t2.micro"
  subnet_id     = module.vpc.public_subnets[0]

  security_groups = length(var.my_security_groups) > 0 ? var.my_security_groups : [aws_security_group.this[0].id]
}
```

※ アウトバウンドしか許可していませんが、今回は簡単に済ませています

で、apply。

```shell
$ terraform apply
```

リソースの作成は、とりあえずうまくいきます。今回はInput Variableに値を指定していないので、デフォルトのセキュリティグループ、つまりこちらが作成されてEC2に割り当てられます。

```go
resource "aws_security_group" "this" {
  count = length(var.my_security_groups) > 0 ? 0 : 1

  name   = "my-ec2-instance-sg"
  vpc_id = module.vpc.vpc_id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

ここで、そのままplanを実行してみます。

```shell
$ terraform plan
```

なんと、EC2の再作成が提示されます。

```shell
------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # aws_instance.this must be replaced
-/+ resource "aws_instance" "this" {
```

セキュリティグループが再作成の理由みたいです。

```shell
      ~ security_groups              = [ # forces replacement
          + "sg-0f3490bf5526991db",
        ]
```

実際、このままapplyするとEC2が再作成されてしまいます。

ちなみに、**セキュリティグループ自体はなにも変わりませんし、再作成もされません**。

この指定方法がいけないのかな？と思い

```go
  security_groups = length(var.my_security_groups) > 0 ? var.my_security_groups : [aws_security_group.this[0].id]
```

指定方法を変えたり

```go
  security_groups = length(var.my_security_groups) > 0 ? var.my_security_groups : aws_security_group.this.*.id
```

セキュリティグループだけ別モジュールに切り出したり、変数の定義方法を変えたり、ループを`count`ではなく`for_each`に変えてみたりしたのですが、やっぱり再作成になってしまいます。

で、いろいろ見ていたらこんな記事を見つけたので

[Terraformで立てたEC2に後からSGを追加しようとするとEC2が再作成される](https://cross-black777.hatenablog.com/entry/2016/02/25/090000)

なるほど、`security_groups`を使っているのがダメなんだということで、`vpc_security_group_ids`にすると差分が出なくなりました。

```go
  vpc_security_group_ids = length(var.my_security_groups) > 0 ? var.my_security_groups : [aws_security_group.this[0].id]
  #vpc_security_group_ids = length(var.my_security_groups) > 0 ? var.my_security_groups : aws_security_group.this.*.id  # こちらも可
```

デフォルトのVPCを使っているわけではないので、これで大丈夫ですね。

めでたし、めでたし。


# 本当に？

EC2でセキュリティグループを指定する際に、`security_groups`から`vpc_security_group_ids`へ変更することで、この挙動を回避できたわけですが。

`security_groups`と`vpc_security_group_ids`の定義の差を見比べてみると、`ForceNew`しか違いがありません。

https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_instance.go#L170-L185

差分があったら、リソースの再作成を強制するみたいですね。

https://github.com/terraform-providers/terraform-provider-aws/blob/v2.66.0/vendor/github.com/hashicorp/terraform-plugin-sdk/helper/schema/resource_diff.go#L324-L339

ところで、今回のケースだと、問題になっているセキュリティグループ自体には差分がありません。

にも関わらず、**EC2の再作成を強制させられます**。

実際、`security_groups`を使っている時に、`terraform apply`でEC2を再作成してもセキュリティグループの定義や、EC2に割り当てているセキュリティグループの情報は`terraform.tfstate`を見ても差がないんですよね。

`ForceNew`で定義されている引数に対して、こういう条件指定したりすると、危ないかも？という話なんでしょうかね（リストで指定するのも微妙なのかなぁという気もしますが…）。

どっちにしろ、リソース作成後もちゃんと`terraform plan`をして、妙な動きをしないか確認しようという気になりました…。