terraform で aws_security_group の更新を安全に行う

## ダメなパターン

これを

```
resource "aws_security_group" "test" {
    name        = "test"
    vpc_id      = "${aws_vpc.test.id}"
    ingress {
        from_port       = 22
        to_port         = 22
        protocol        = "tcp"
        cidr_blocks     = ["x.x.x.x/32"]
    }
}
```

こう変えると

```diff
 resource "aws_security_group" "test" {
     name        = "test"
     vpc_id      = "${aws_vpc.test.id}"
     ingress {
         from_port       = 22
         to_port         = 22
         protocol        = "tcp"
-        cidr_blocks     = ["x.x.x.x/32"]
+        cidr_blocks     = ["x.x.x.x/32","y.y.y.y/32"]
     }
 }
```

ログが次のように出て、なんか diff が怪しい。１回 ingress を全部消して、再度全部作ってるような。

```
aws_security_group.test: Modifying...
  ingress.2293169953.cidr_blocks.#:     "1" => "0"
2016/10/27 18:33:14 [DEBUG] apply: aws_security_group.test: executing Apply
  ingress.2293169953.cidr_blocks.0:     "x.x.x.x/32" => ""
  ingress.2293169953.from_port:         "22" => "0"
  ingress.2293169953.protocol:          "tcp" => ""
  ingress.2293169953.security_groups.#: "0" => "0"
  ingress.2293169953.self:              "0" => "0"
  ingress.2293169953.to_port:           "22" => "0"
  ingress.3838495408.cidr_blocks.#:     "0" => "2"
  ingress.3838495408.cidr_blocks.0:     "" => "x.x.x.x/32"
  ingress.3838495408.cidr_blocks.1:     "" => "y.y.y.y/32"
  ingress.3838495408.from_port:         "" => "22"
  ingress.3838495408.protocol:          "" => "tcp"
  ingress.3838495408.security_groups.#: "0" => "0"
  ingress.3838495408.self:              "" => "0"
  ingress.3838495408.to_port:           "" => "22"
```

TF_LOG=DEBUG をつけて実行すると以下のようにログが出て、Revoke & Authorize している感じ。

```
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws: 2016/10/27 18:33:15 [DEBUG] Revoking security group {
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   Description: "Managed by Terraform",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   GroupName: "test",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   IpPermissions: [{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       FromPort: 22,
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       IpProtocol: "tcp",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       IpRanges: [{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:           CidrIp: "x.x.x.x/32"
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:         }],
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       ToPort: 22
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:     }],
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws: } ingress rule: []*ec2.IpPermission{{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   FromPort: 22,
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   IpProtocol: "tcp",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   IpRanges: [{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       CidrIp: "x.x.x.x/32"
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:     }],
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   ToPort: 22
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws: }}
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws: 2016/10/27 18:33:15 [DEBUG] Authorizing security group {
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   Description: "Managed by Terraform",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   GroupName: "test",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   IpPermissions: [{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       FromPort: 22,
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       IpProtocol: "tcp",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       IpRanges: [{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:           CidrIp: "x.x.x.x/32"
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:         }],
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       ToPort: 22
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:     }],
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws: } ingress rule: []*ec2.IpPermission{{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   FromPort: 22,
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   IpProtocol: "tcp",
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   IpRanges: [{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       CidrIp: "x.x.x.x/32"
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:     },{
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:       CidrIp: "y.y.y.y/32"
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:     }],
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws:   ToPort: 22
2016/10/27 18:33:15 [DEBUG] terraform-provider-aws: }}
```

ソースコードを読むと https://github.com/hashicorp/terraform/blob/11b3b7cf29fa4120974d293458beac47e1eedd32/builtin/providers/aws/resource_aws_security_group.go#L552-L559

```
		// TODO: We need to handle partial state better in the in-between
		// in this update.

		// TODO: It'd be nicer to authorize before removing, but then we have
		// to deal with complicated unrolling to get individual CIDR blocks
		// to avoid authorizing already authorized sources. Removing before
		// adding is easier here, and Terraform should be fast enough to
		// not have service issues.
```

とあり、TODO になっている。Terraform は充分に早いからサービスISSUEは起こらないんじゃないかな(意訳)とのことだが ... 

## 安全安心なパターン

aws_security_group_rule を使う

```diff
 resource "aws_security_group" "test" {
     name        = "test"
     vpc_id      = "${aws_vpc.test.id}"
 }
 
 resource "aws_security_group_rule" "test_rule_1" {
     security_group_id = "${aws_security_group.test.id}"
     type = "ingress"
     from_port = 22
     to_port = 22
     protocol = "tcp"
     cidr_blocks = ["x.x.x.x/32"]
 }

+resource "aws_security_group_rule" "test_rule_2" {
+    security_group_id = "${aws_security_group.test.id}"
+    type = "ingress"
+    from_port = 22
+    to_port = 22
+    protocol = "tcp"
+    cidr_blocks = ["y.y.y.y/32"]
+}
```

これなら追加だけになる。ログは以下。

```
2016/10/27 18:40:29 [DEBUG] apply: aws_security_group_rule.test_rule2: executing Apply
aws_security_group_rule.test: Creating...
  cidr_blocks.#:            "" => "1"
  cidr_blocks.0:            "" => "y.y.y.y/32"
  from_port:                "" => "22"
  protocol:                 "" => "tcp"
  self:                     "" => "0"

2016/10/27 18:40:30 [DEBUG] terraform-provider-aws: 2016/10/27 18:40:30 [DEBUG] Authorizing security group sg-66d6231b Ingress rule: {
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   FromPort: 22,
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   IpProtocol: "tcp",
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   IpRanges: [{
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:       CidrIp: "y.y.y.y/32"
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:     }],
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   ToPort: 22
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws: }
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws: 2016/10/27 18:40:30 [DEBUG] Found rule for Security Group Rule (sgrule-3952629376): {
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   FromPort: 22,
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   IpProtocol: "tcp",
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   IpRanges: [{
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:       CidrIp: "x.x.x.x/32"
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:     },{
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:       CidrIp: "y.y.y.y/32"
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:     }],
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws:   ToPort: 22
2016/10/27 18:40:30 [DEBUG] terraform-provider-aws: }
```

注意点は、from_port, to_port が同じだからと cidr_blocks の配列要素に追加しようとすると、destory & add になってしまう点。つまり、以下はダメ。aws_security_group_rule を別途用意すべき。

```diff
 resource "aws_security_group" "test" {
     name        = "test"
     vpc_id      = "${aws_vpc.test.id}"
 }
 
 resource "aws_security_group_rule" "test_rule_1" {
     security_group_id = "${aws_security_group.test.id}"
     type = "ingress"
     from_port = 22
     to_port = 22
     protocol = "tcp"
-    cidr_blocks = ["x.x.x.x/32"]
+    cidr_blocks = ["x.x.x.x/32","y.y.y.y/32"]
 }
```

## セキュリティグループの瞬断について考察 (追記)

セキュリティグループの ingress または egress からエントリを削除すると新規接続が弾かれるようになるが、すでに開いている接続が切られるわけではないことを確認した。

なので、「ダメなパターン」でも　Revoke してから Authorize するまでの短い時間に新規接続が来たorをする場合にのみ問題となる。TCPであれば 少し待って(Linuxならデフォルト１秒)再接続するので、この少しを許容できるかという点と、UDPの送信失敗を許容できるかという点が論点になるだろう。

まぁ、`aws_security_group_rule` を使っておくほうが**安全**というのは間違いない。