私が歌川です

@utgwkk が書いている

terraform-provider-aws v6でDynamoDBのGSIを追加・削除すると、既存の関係ないGSIが作り直しになる現象

tl;dr

  • terraform-provider-aws v6.32.0 以降でDynamoDBのテーブルにkey_schemaブロックを使ってGSIを追加すると、関係ないGSIが作り直しになってしまう
  • 記事執筆時点の最新版であるterraform-provider-aws v6.38.0で未解決

前提

TerraformでDynamoDBのテーブルにGSIを追加するとき、terraform-provider-aws v6.31.0までは↓のように書いていたと思います*1

resource "aws_dynamodb_table" "basic-dynamodb-table" {
  // 中略

  global_secondary_index {
    name               = "GameTitleIndex"
    hash_key           = "GameTitle"
    range_key          = "TopScore"
    write_capacity     = 10
    read_capacity      = 10
    projection_type    = "INCLUDE"
    non_key_attributes = ["UserId"]
  }
}

terraform-provider-aws v6.32.0 からは global_secondary_index ブロック中の hash_key range_key 属性がdeprecatedになり、代わりに key_schema ブロックを使って書くように案内されています*2。先述したGSIであれば以下のように書くことになるでしょう。

resource "aws_dynamodb_table" "basic-dynamodb-table" {
  // 中略

  global_secondary_index {
    name = "GameTitleIndex"

    key_schema {
      name      = "GameTitle"
      key_type = "HASH"
    }
    key_schema {
      name      = "TopScore"
      key_type = "RANGE"
    }

    write_capacity     = 10
    read_capacity      = 10
    projection_type    = "INCLUDE"
    non_key_attributes = ["UserId"]
  }
}

起こったこと

以下のようなテーブル定義があったとして:

resource "aws_dynamodb_table" "test" {
  name         = "gsi-test"
  billing_mode = "PAY_PER_REQUEST"

  attribute {
    name = "pk"
    type = "S"
  }
  attribute {
    name = "field1"
    type = "S"
  }

  hash_key = "pk"

  global_secondary_index {
    name = "field1"

    key_schema {
      attribute_name = "field1"
      key_type       = "HASH"
    }

    projection_type = "ALL"
  }
}

次のようにGSI field2 を追加する変更を加えました。

diff --git a/dynamodb-test.tf b/dynamodb-test.tf
index 39d22c1..cec86ac 100644
--- a/dynamodb-test.tf
+++ b/dynamodb-test.tf
@@ -10,6 +10,10 @@ resource "aws_dynamodb_table" "test" {
     name = "field1"
     type = "S"
   }
+  attribute {
+    name = "field2"
+    type = "S"
+  }

   hash_key = "pk"

@@ -23,4 +27,15 @@ resource "aws_dynamodb_table" "test" {

     projection_type = "ALL"
   }
+
+  global_secondary_index {
+    name = "field2"
+
+    key_schema {
+      attribute_name = "field2"
+      key_type       = "HASH"
+    }
+
+    projection_type = "ALL"
+  }
 }

この状態で terraform plan を実行すると、既存のGSI field1 が削除されてから作成されるような差分が出ます。そして、terraform apply を実行すると、plan結果通りに既存のGSIが作り直しになりました。アプリケーションでGSI field1 をに対するクエリを発行している場合はエラーが発生します。

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_dynamodb_table.test will be updated in-place
  ~ resource "aws_dynamodb_table" "test" {
        id                          = "gsi-test"
        name                        = "gsi-test"
        tags                        = {}
        # (13 unchanged attributes hidden)

      + attribute {
          + name = "field2"
          + type = "S"
        }

      - global_secondary_index {
          - hash_key           = "field1" -> null
          - name               = "field1" -> null
          - non_key_attributes = [] -> null
          - projection_type    = "ALL" -> null
          - read_capacity      = 0 -> null
          - write_capacity     = 0 -> null
            # (1 unchanged attribute hidden)

          - key_schema {
              - attribute_name = "field1" -> null
              - key_type       = "HASH" -> null
            }
        }
      + global_secondary_index {
          + hash_key           = (known after apply)
          + name               = "field1"
          + non_key_attributes = []
          + projection_type    = "ALL"
          + read_capacity      = (known after apply)
          + write_capacity     = (known after apply)
            # (1 unchanged attribute hidden)

          + key_schema {
              + attribute_name = "field1"
              + key_type       = "HASH"
            }

          + warm_throughput (known after apply)
        }
      + global_secondary_index {
          + hash_key           = (known after apply)
          + name               = "field2"
          + non_key_attributes = []
          + projection_type    = "ALL"
          + read_capacity      = (known after apply)
          + write_capacity     = (known after apply)
            # (1 unchanged attribute hidden)

          + key_schema {
              + attribute_name = "field2"
              + key_type       = "HASH"
            }

          + warm_throughput (known after apply)
        }

        # (4 unchanged blocks hidden)
    }

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

考察・対策

おそらく以下のissueの事象が発生しているのではないか、と考えられます。

github.com

当面は手でGSIを追加したあとにtfstateを同期させる、(deprecatedだけど) hash_key range_key を使ってGSIを管理する、などで回避する必要がありそうです。マルチキーのGSIを追加したい場合は必然的に前者でしのぐしかない?

おわりに

Terraformの差分、たまに data resourceなどを経由することで実際には実害のない (applyしても何も起こらない) 差分が出ることもあるので油断しがちだけど、差分通りに作り直しが発生してしまう場合もあるのだ、と思い知りました。気をつけてください。本番環境に対して反映する前に気づけたので助かった……。

時間のないサイト運営者リング