【Terraform】countで作成したリソースを削除する時の注意点

2019.10.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは!AWS事業本部、岩本町オフィスの島川です。
皆さん、インフラのコード化は進んでおりますでしょうか?

Terraformでリソースを複製したいときに便利なcount
作るだけなら問題ないのですが、後々ここ削除したい!となったときに意図しない動きをすることがあります。

今回はその意図しない動きを避ける下記2つの方法をサンプルコードと一緒にご紹介します。

  • コードを変えずに乗り切る方法
  • countをやめてfor_eachに乗り換える方法
    • for_eachはTerraform v0.12.6でリリースされた機能。それ以上でなければ使えないので注意してください。

環境について

  • Ubuntu 18.04
  • Terraform v0.12.10
    • 2019/10/11時点の最新バージョン

サンプルコード

  • main.tf(VPCとサブネット2つ作るコード)
### VPCを作成する
resource aws_vpc this {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "test-vpc"
  }
}

//----------------------

### サブネット情報を記載
variable subnets {
  default = {
  "test-subnet-1a" = {
    cidr = "10.0.1.0/24"
    az = "ap-northeast-1a"
  }
  "test-subnet-1c" = {
    cidr = "10.0.2.0/24"
    az = "ap-northeast-1c"
    }
  }
}

### サブネットを作成する
resource aws_subnet this {
  count = length(var.subnets)
  vpc_id = aws_vpc.this.id
  cidr_block = values(var.subnets)[count.index].cidr
  availability_zone = values(var.subnets)[count.index].az
}

現在の状況を確認する

サンプルコードを実行した後、Terraformで管理しているリソースを確認します。

$ terraform state list
aws_subnet.this[0]
aws_subnet.this[1]
aws_vpc.this

VPCとサブネットが2つ配列で管理されています。

意図しない削除が起こるケース

aws_subnet.this[0]を削除する時

該当部分をコメントアウトします。

### Subnet情報を記載
variable subnets {
  default = {
  /*
    "test-subnet-1a" = {
      cidr = "10.0.1.0/24"
      az = "ap-northeast-1a"
    }
  */
    "test-subnet-1c" = {
      cidr = "10.0.2.0/24"
      az = "ap-northeast-1c"
    }
  }
}

この状態で実行すると

aws_subnet.this[0]だけを消したいが、aws_subnet.this[1]を消して、aws_subnet.this[0]で作り直す。

といった動きをします。

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

Terraform will perform the following actions:

  # aws_subnet.this[0] must be replaced
-/+ resource "aws_subnet" "this" {
      ~ arn                             = "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-************" -> (known after apply)
        assign_ipv6_address_on_creation = false
      ~ availability_zone               = "ap-northeast-1a" -> "ap-northeast-1c" # forces replacement
      ~ availability_zone_id            = "apne1-az4" -> (known after apply)
      ~ cidr_block                      = "10.0.1.0/24" -> "10.0.2.0/24" # forces replacement
      ~ id                              = "subnet-************" -> (known after apply)
      + ipv6_cidr_block                 = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
        map_public_ip_on_launch         = false
      ~ owner_id                        = "************" -> (known after apply)
      - tags                            = {} -> null
        vpc_id                          = "vpc-************"
    }

  # aws_subnet.this[1] will be destroyed
  - resource "aws_subnet" "this" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-************" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1c" -> null
      - availability_zone_id            = "apne1-az1" -> null
      - cidr_block                      = "10.0.2.0/24" -> null
      - id                              = "subnet-************" -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "************" -> null
      - tags                            = {} -> null
      - vpc_id                          = "vpc-************" -> null
    }

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

これは

  • Terraform側で配列を詰める動き
  • サブネットはリソースの変更ができない仕様

がぶつかって発生しています。 このまま実行すると意図しない削除が起きてしまいます。

もちろん、aws_subnet.this[1]の削除であれば問題なく実施できます。

対策

コードを変えずに乗り切る方法

※公式推奨のやり方ではなくちょっとした裏技になります。

terraform state mvコマンドを活用して、Terraform側で配列を詰める動きを先にやっておけばOKです。

$ terraform state list
aws_subnet.this[0] <--- 削除したい。
aws_subnet.this[1] <--- 0番目に移動したい。 
aws_vpc.this

aws_subnet.this[0]を適当な配列に移動します。

$ terraform state mv aws_subnet.this[0] aws_subnet.this[1234]
Move "aws_subnet.this[0]" to "aws_subnet.this[1234]"
Successfully moved 1 object(s).

移動されました。

$ terraform state list
aws_subnet.this[1]
aws_subnet.this[1234]
aws_vpc.this

次にaws_subnet.this[1]をaws_subnet.this[0]に移動します。

$ terraform state mv aws_subnet.this[1] aws_subnet.this[0]
Move "aws_subnet.this[1]" to "aws_subnet.this[0]"
Successfully moved 1 object(s).

移動されました。

$ terraform state list
aws_subnet.this[0]
aws_subnet.this[1234]
aws_vpc.this

この状態で実行すると、期待する動きになりました!

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

Terraform will perform the following actions:

  # aws_subnet.this[1234] will be destroyed
  - resource "aws_subnet" "this" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-************" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1a" -> null
      - availability_zone_id            = "apne1-az4" -> null
      - cidr_block                      = "10.0.1.0/24" -> null
      - id                              = "subnet-************" -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "************" -> null
      - tags                            = {} -> null
      - vpc_id                          = "vpc-************" -> null
    }

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

countをやめてfor_eachに乗り換える方法

リソースを複製する場合のやり方としてcountの他にfor_eachがあります。

コードを変更します。

### Subnet情報を記載
variable subnets {
  default = {
    "test-subnet-1a" = {
      cidr = "10.0.1.0/24"
      az = "ap-northeast-1a"
    }
    "test-subnet-1c" = {
      cidr = "10.0.2.0/24"
      az = "ap-northeast-1c"
    }
  }
}

### Subnetを作成する
resource aws_subnet this {
  for_each = var.subnets // <---count = length(var.subnets)から変更
  vpc_id = aws_vpc.this.id
  cidr_block = each.value.cidr // <--- for_each用の記載に変更
  availability_zone = each.value.az // <--- for_each用の記載に変更
}

for_eachでmap型の変数を呼び出すと、定義されている要素分ループしてくれます。

今回であれば、var.subnetsの要素はtest-subnet-1aとtest-subnet-1cの2つ分ループします。

これで実行して、リソースの状態を確認します。

$ terraform state list
aws_subnet.this["test-subnet-1a"]
aws_subnet.this["test-subnet-1c"]
aws_vpc.this

連想配列で管理されています。この状態であれば配列を気にすることなく削除することができます。

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

Terraform will perform the following actions:

  # aws_subnet.this["test-subnet-1a"] will be destroyed
  - resource "aws_subnet" "this" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-************" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1a" -> null
      - availability_zone_id            = "apne1-az4" -> null
      - cidr_block                      = "10.0.1.0/24" -> null
      - id                              = "subnet-************" -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "************" -> null
      - tags                            = {} -> null
      - vpc_id                          = "vpc-************" -> null
    }

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

補足 変数の呼び出しについて

例:

variable subnets {
  "test-subnet-1a" = {
    cidr = "10.0.1.0/24"
    az = "ap-northeast-1a"
  }
}

for_eachで呼び出す場合

呼び出し方 呼び出される値
each.key test-subnet-1a
each.value 複数valueがあるため無効になります
each.value.cidr 10.0.1.0/24
each.value.az ap-northeast-1a

さいごに

今回はcountのくせとfor_eachについて紹介しました。
Terraformのアップデートは頻繁に行われています。
便利な機能がどんどん追加されているので要注目です!