Amazon Web Services ブログ

Terraform の新機能: Amazon DynamoDB のグローバルセカンダリインデックスのドリフトを管理する

Amazon DynamoDB のグローバルセカンダリインデックス (Global Secondary Index、GSI) のキャパシティを Terraform の外部で調整したことがある方なら、Terraform がドリフトを検出して望ましくない復元を強制する様子をご存知でしょう。Terraform の新しい aws_dynamodb_global_secondary_index リソースを使用すれば、この問題に対処できます。

新しい aws_dynamodb_global_secondary_index リソースは、各 GSI を独自のライフサイクル管理を持つ独立したリソースとして扱います。この機能を使用して、Terraform の外部で GSI とテーブルのキャパシティ調整を行うことができます。

この記事では、Terraform の新しい aws_dynamodb_global_secondary_index リソースを使用して、GSI のドリフトを選択的に管理する方法を実演します。現在のアプローチの制限事項を説明し、ソリューションの実装方法をガイドします。

問題: Terraform のドリフトと GSI 管理

ソリューションに入る前に、インフラストラクチャ管理における ドリフト の意味を確認しましょう。Infrastructure as Code (IaC) において、ドリフトは、インフラストラクチャの実際の状態が Terraform 設定で定義されている内容と異なる場合に発生します。Terraform は、望ましい状態(.tf 設定ファイル)、最後に既知の状態(terraform.tfstate に保存)、実際の状態(AWS からクエリ)を比較することでドリフトを検出します。これらが一致しない場合、Terraform はドリフトを報告し、差異を調整するための変更を提案します。

DynamoDB の GSI は、さまざまな運用上の理由でキャパシティ調整が必要になることがよくあります。負荷テスト、キャパシティプランニング、緊急のパフォーマンス要件、またはウォームスループットの管理などです。DynamoDB のキャパシティは、オートスケーリングイベントによっても変更される可能性があります。Terraform の外部でこれらの変更を行うたびに、Terraform の設定と AWS の実態との間にドリフトが発生します。

例えば、分析チームが GSI に対して大量のクエリを実行する日次レポートを実行しているとします。レポートは午前 2 時に実行され、50 リードキャパシティユニット (Read Capacity Units、RCU) が必要ですが、通常時間帯は 5 RCU で十分です。運用チームは、負荷に対応するためにレポート実行前に手動でキャパシティを増やします。

午前 1 時 50 分に、運用チームは AWS コマンドラインインターフェイス (AWS CLI) を使用してキャパシティを 5 から 50 に増やします。レポートは午前 2 時から 3 時まで高いキャパシティで実行されます。その日の後半、無関係な変更をデプロイするために terraform plan を実行すると、実際のキャパシティ (50) が設定 (5) と一致しないため、Terraform はドリフトを検出します。Terraform はキャパシティを 5 に戻そうとしますが、これは運用上のキャパシティ管理に干渉することになります。

一般的な回避策とその制限事項

一般的な回避策は、テーブルのライフサイクルブロックで ignore_changes = [global_secondary_index] を使用することです。これにより、Terraform がキャパシティのドリフトを検出しなくなります。ただし、このアプローチは範囲が広すぎます。キャパシティだけでなく、すべての GSI の変更を無視します。global_secondary_index は複雑なネストされた型であるため、ignore_changes はトップレベルでのみ機能し、個々の属性では機能しません。誰かが誤って GSI を削除したり、キースキーマを変更したりしても、Terraform は検出しません。意図的なキャパシティチューニングと偶発的な GSI の削除を区別できません。

ソリューション: 個別の GSI リソース

新しい aws_dynamodb_global_secondary_index リソースは、各 GSI を独自のライフサイクル管理を持つ独立したリソースとして扱います。これにより、各 GSI に対してどの属性を無視するかをきめ細かく制御できると同時に、削除やスキーマ変更などの重要な変更を検出できます。

前提条件

開始する前に、以下があることを確認してください。

aws_dynamodb_global_secondary_index リソースは、現在 Terraform AWS プロバイダー実験的 としてマークされています。これは、スキーマや動作が予告なく変更される可能性があり、プロバイダーの下位互換性保証の対象ではないことを意味します。

この実験的リソースを有効にするには、環境変数 TF_AWS_EXPERIMENT_dynamodb_global_secondary_index を設定する必要があります。この環境変数がないと、aws_dynamodb_global_secondary_index を使用しようとしたときに Terraform がエラーを返します。Terraform コマンドを実行する前に設定してください。

export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1

本番環境で使用する前に、非本番環境で十分にテストしてください。GitHub Issue #45640 でフィードバックを提供することを歓迎します。

AWS Provider v5.x から v6.x にアップグレードする場合は、続行する前に v6.0.0 アップグレードガイドで破壊的変更を確認してください。

Amazon Linux に Terraform をインストール:

# システムを更新
sudo yum update -y

# yum-config-manager をインストール
sudo yum install -y yum-utils

# HashiCorp リポジトリを追加
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo

# Terraform をインストール
sudo yum -y install terraform

# インストールを確認
terraform --version

新しいリソースの使用

新しい個別リソース方式を使用して、2 つの GSI を持つプロビジョニングされたキャパシティテーブルを作成します。テーブルと GSI が独立したリソースとして定義される main.tf を作成します。

テーブルと GSI のキー:

リソース ハッシュキー レンジキー キャパシティ
Table id timestamp 5/5
StatusUserIndex status user_id 5/5
TimestampIndex timestamp 3/3

設定:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.28"
}
}
}

provider "aws" {
region = "us-east-1"
}

# GSI ブロックのない DynamoDB テーブル(GSI は個別に管理)
# テーブルキー(hash_key/range_key)として使用される属性のみを定義
# GSI 属性は個別の aws_dynamodb_global_secondary_index リソースで定義
resource "aws_dynamodb_table" "test_table" {
name = "GSITestTable"
billing_mode = "PROVISIONED"
read_capacity = 5
write_capacity = 5
hash_key = "id"
range_key = "timestamp"

attribute {
name = "id"
type = "S"
}

attribute {
name = "timestamp"
type = "N"
}

tags = {
Name = "GSITestTable"
Environment = "test"
Purpose = "Testing new GSI resource"
}
}

# 個別リソースとしての GSI
resource "aws_dynamodb_global_secondary_index" "status_index" {
table_name = aws_dynamodb_table.test_table.name
index_name = "StatusUserIndex"

# プロビジョニングされたスループット設定
provisioned_throughput {
read_capacity_units = 5
write_capacity_units = 5
}

# key_schema に attribute_type が含まれるようになりました(新しいリソースで必須)
key_schema {
attribute_name = "status"
attribute_type = "S"
key_type = "HASH"
}

key_schema {
attribute_name = "user_id"
attribute_type = "S"
key_type = "RANGE"
}

# プロジェクション設定
projection {
projection_type = "ALL"
}

# 新しい個別リソースでは、GSI ごとに特定の属性を無視できるようになりました
lifecycle {
ignore_changes = [provisioned_throughput]
}
}

# 複数の独立した GSI をテストするための 2 番目の GSI
resource "aws_dynamodb_global_secondary_index" "timestamp_index" {
table_name = aws_dynamodb_table.test_table.name
index_name = "TimestampIndex"

# プロビジョニングされたスループット設定
provisioned_throughput {
read_capacity_units = 3
write_capacity_units = 3
}

key_schema {
attribute_name = "timestamp"
attribute_type = "N"
key_type = "HASH"
}

# プロジェクション設定
projection {
projection_type = "KEYS_ONLY"
}

# この GSI は Terraform によって完全に管理されます(ignore_changes なし)
}

リソースをデプロイします:

# 必要な環境変数を設定
export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1

terraform init
terraform plan
terraform apply

StatusUserIndexignore_changes を持つもの)のキャパシティを手動で変更して、選択的な ignore_changes をテストします:

aws dynamodb update-table \
--table-name GSITestTable \
--region us-east-1 \
--global-secondary-index-updates '[{
"Update": {
"IndexName": "StatusUserIndex",
"ProvisionedThroughput": {
"ReadCapacityUnits": 10,
"WriteCapacityUnits": 10
}
}
}]'

# 更新が完了するまで待機
sleep 30

terraform plan を実行すると、StatusUserIndex のキャパシティが AWS で 10/10 に変更されたにもかかわらず、No changes と表示されます。これは ignore_changes = [provisioned_throughput] のために発生します。

TimestampIndexignore_changes のないもの)を手動で変更して、ドリフト検出が機能することを確認します:

aws dynamodb update-table \
--table-name GSITestTable \
--region us-east-1 \
--global-secondary-index-updates '[{
"Update": {
"IndexName": "TimestampIndex",
"ProvisionedThroughput": {
"ReadCapacityUnits": 8,
"WriteCapacityUnits": 8
}
}
}]'

# 更新が完了するまで待機
sleep 30

terraform plan を実行すると、ドリフトが検出され、TimestampIndex のキャパシティを 8 から 3 に戻すことが提案されます。これにより、以下が実証されます:

  • StatusUserIndex は変更なし(意図したとおりキャパシティが無視される)
  • TimestampIndex はドリフト検出(キャパシティの変更が検出される)
  • 各 GSI は独立したライフサイクル管理を持つ
  • GSI ごとに特定の属性を選択的に無視できる
  • Terraform は ignore_changes のない GSI の重要な変更を検出する

従来の方法との主な違いは、テーブルがテーブル自体で使用される属性(idtimestamp)を定義するのに対し、GSI 固有の属性(statususer_id)は個別の GSI リソースの key_schema ブロックで attribute_type(新しいリソースで必須)とともに定義されることです。GSI がテーブル属性を再利用する場合、その属性はテーブルの attribute ブロックに残ります。GSI は独自のライフサイクルを持つ個別のリソースです。

新しいリソースの利点

新しいリソースモデルにはいくつかの利点があります。他の GSI に影響を与えることなく、GSI の特定の属性を無視できるようになりました。自動化されたスクリプトは、Terraform のドリフトを作成することなく、トラフィックパターンに基づいてキャパシティを調整できます。キースキーマの変更などの重要な変更を追跡し、偶発的な GSI の削除や再設定がないことを確認できます。Terraform の状態は GSI 構造の信頼できる情報源のままであり、DynamoDB API は実際のランタイムキャパシティを示します。

各 GSI は独自のライフサイクルルールを持つことができ、独立した管理が可能です。新しいリソースモデルは、各リソースが 1 つの論理インフラストラクチャコンポーネントを管理し、依存関係がリソース参照を通じて明示的であり、状態管理がより簡単になるという Terraform のベストプラクティスに従っています。

新しいリソースは、オンデマンドテーブルのウォームスループット設定を完全にサポートしています。ウォームスループットは、オンデマンドテーブルのベースラインキャパシティを指定するために使用できる DynamoDB の機能で、パフォーマンスとコストをより予測可能に管理するのに役立ちます。以下はテスト方法です。

ondemand.tf を作成します:

resource "aws_dynamodb_table" "ondemand_test" {
name = "OnDemandGSITest"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"

attribute {
name = "id"
type = "S"
}
}

resource "aws_dynamodb_global_secondary_index" "category_index" {
table_name = aws_dynamodb_table.ondemand_test.name
index_name = "CategoryIndex"

key_schema {
attribute_name = "category"
attribute_type = "S"
key_type = "HASH"
}

# プロジェクション設定
projection {
projection_type = "ALL"
}

# ウォームスループット設定(ブロックではなく属性)
warm_throughput = {
read_units_per_second = 13000
write_units_per_second = 5000
}

lifecycle {
# 手動でのウォームスループットチューニングを許可
ignore_changes = [warm_throughput]
}
}

デプロイしてテストします:

# まだ設定されていない場合は、必要な環境変数を設定
export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1

terraform apply

# ウォームスループットを手動で変更
aws dynamodb update-table \
--table-name OnDemandGSITest \
--region us-east-1 \
--global-secondary-index-updates '[{
"Update": {
"IndexName": "CategoryIndex",
"WarmThroughput": {
"ReadUnitsPerSecond": 14000,
"WriteUnitsPerSecond": 5100
}
}
}]'

# 更新を待機
sleep 30

# terraform plan を実行
terraform plan

ウォームスループットの変更は期待どおり無視されるため、Terraform は No changes を表示します。

次のセクションに進む前に、オンデマンドテストリソースを破棄します: terraform destroy

移行の例

新しいリソースの動作を確認したので、既存のインフラストラクチャの完全な実践的な移行を見ていきましょう。従来のネストされた GSI アプローチを使用するテーブルから始めて、ダウンタイムなしで新しい個別リソース方式に移行します。

ステップ 1: 従来の方法でインフラストラクチャを作成

従来のネストされたブロックアプローチを使用して、GSI を持つ DynamoDB テーブルを作成します。

migration-old.tf というファイルを作成します:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.28"
}
}
}

provider "aws" {
region = "us-east-1"
}

# 従来のアプローチ: GSI がネストされたブロックとして定義
resource "aws_dynamodb_table" "products" {
name = "ProductsTable"
billing_mode = "PROVISIONED"
read_capacity = 5
write_capacity = 5
hash_key = "ProductId"

attribute {
name = "ProductId"
type = "S"
}

attribute {
name = "Category"
type = "S"
}

# GSI がネストされたブロックとして定義(従来の方法)
global_secondary_index {
name = "CategoryIndex"
hash_key = "Category"
projection_type = "ALL"
read_capacity = 3
write_capacity = 3
}

tags = {
Name = "ProductsTable"
Environment = "migration-demo"
}
}

このインフラストラクチャをデプロイします:

terraform init
terraform plan
terraform apply

テーブルと GSI が作成されたことを確認します:

aws dynamodb describe-table --table-name ProductsTable --region us-east-1 \
--query 'Table.GlobalSecondaryIndexes[0].IndexName'

出力:

CategoryIndex

ステップ 2: 移行の準備

移行する前に、Terraform の状態をバックアップします:

terraform state pull > backup-before-migration.tfstate

必要な環境変数を設定します:

export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1

ステップ 3: Terraform 設定を更新

更新された設定で migration-new.tf という新しいファイルを作成します。今のところ両方のファイルを保持します。インポート後に古いファイルを削除します。

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.28"
}
}
}

provider "aws" {
region = "us-east-1"
}

# 更新されたテーブル: GSI ブロックを削除
resource "aws_dynamodb_table" "products" {
name = "ProductsTable"
billing_mode = "PROVISIONED"
read_capacity = 5
write_capacity = 5
hash_key = "ProductId"

# テーブル自身のキーで使用される属性のみを定義
attribute {
name = "ProductId"
type = "S"
}

tags = {
Name = "ProductsTable"
Environment = "migration-demo"
}
}

# 新規: 個別リソースとしての GSI
resource "aws_dynamodb_global_secondary_index" "category_index" {
table_name = aws_dynamodb_table.products.name
index_name = "CategoryIndex"

# プロビジョニングされたスループット設定
provisioned_throughput {
read_capacity_units = 3
write_capacity_units = 3
}

key_schema {
attribute_name = "Category"
attribute_type = "S"
key_type = "HASH"
}

# プロジェクション設定
projection {
projection_type = "ALL"
}

# 運用チームが Terraform による復元なしでキャパシティを調整できるようにする
lifecycle {
ignore_changes = [provisioned_throughput]
}
}

ステップ 4: 古い設定を削除

古いファイルを削除または名前変更します:

mv migration-old.tf migration-old.tf.backup

この時点で terraform plan を実行すると、Terraform がテーブルから GSI を削除し(ネストされたブロックがなくなったため)、新しい個別の GSI リソースを作成しようとすることがわかります。

まだ適用しないでください。 これによりダウンタイムが発生します。代わりに、既存の GSI をインポートします。

ステップ 5: 既存の GSI をインポート

既存の GSI を新しいリソースの状態にインポートします:

# インポート形式: 'table_name,index_name'
terraform import aws_dynamodb_global_secondary_index.category_index \
'ProductsTable,CategoryIndex'

出力:

aws_dynamodb_global_secondary_index.category_index: Importing from ID "ProductsTable,CategoryIndex"...
aws_dynamodb_global_secondary_index.category_index: Import prepared!
Prepared aws_dynamodb_global_secondary_index for import
aws_dynamodb_global_secondary_index.category_index: Refreshing state... [id=ProductsTable,CategoryIndex]

Import successful!

ステップ 6: 移行を確認

terraform plan を実行して確認します:

terraform plan

期待される出力:

aws_dynamodb_table.products: Refreshing state... [id=ProductsTable]
aws_dynamodb_global_secondary_index.category_index: Refreshing state... [id=ProductsTable,CategoryIndex]

No changes. Your infrastructure matches the configuration.

No changes と表示された場合、移行は成功しています。GSI は個別のリソースとして管理されるようになりました。

移行の概要

移行を完了するには、従来のネストされた GSI 設定から始め、terraform import を使用してダウンタイムなしで個別の GSI リソースに移行しました。その後、terraform planNo changes が表示されることで移行を確認し、新しいリソースモデルへの移行に成功しました。

重要なポイント:

  • 移行には terraform import を使用
  • AWS リソースは変更または再作成されない
  • GSI は移行中も存在し続け、ダウンタイムはゼロ
  • 移行後、ignore_changes で無視する内容をきめ細かく制御できる
  • 移行プロセスは安全で元に戻すことができる

移行に関する考慮事項

aws_dynamodb_global_secondary_index リソースを aws_dynamodb_tableglobal_secondary_index ブロックと組み合わせないでください。そうすると、競合、永続的な差異、GSI の上書きが発生する可能性があります。

移行する際は、以下の手順に従ってください:

  1. 状態をバックアップ: terraform state pull > backup.tfstate
  2. 環境変数を設定: export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1
  3. 設定を更新: テーブルから GSI ブロックを削除し、新しい GSI リソースを作成
  4. 既存の GSI をインポート: terraform import <resource> 'table_name,index_name'
  5. 確認: terraform plan を実行し、No changes と表示されることを確認
  6. テスト: キャパシティを手動で変更し、Terraform が変更を無視することを確認

terraform import を使用して正しく行えば、移行中にダウンタイムは発生しません。GSI は移行中も AWS に存在し続けます。terraform import コマンドは Terraform の状態ファイルのみを更新し、AWS リソースは変更しません。

テーブルに複数の GSI がある場合は、一度に 1 つずつ移行します:

  1. 最初の GSI をインポートし、terraform plan で確認
  2. 2 番目の GSI をインポートし、terraform plan で確認
  3. すべての GSI が移行されるまで続ける

これによりリスクが軽減され、トラブルシューティングが簡素化されます。

比較: 従来の方法と新しい方法

以下の表は、従来のネストされたブロックアプローチと新しい個別リソース方式の主な違いをまとめたものです:

側面 従来の方法(ネストされたブロック) 新しい方法(個別リソース)
リソースの有効化 環境変数は不要 TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1 が必要
きめ細かい ignore_changes サポートされていない サポートされている
独立した GSI 管理 すべての GSI が一緒に管理される 各 GSI が独立して管理される
ドリフト検出 全部か無か GSI ごとに選択的
ライフサイクルルール すべての GSI に適用 GSI ごとのライフサイクルルール
状態管理 複雑なネストされた状態 簡単なフラットな状態
キャパシティ設定 トップレベル属性(read_capacitywrite_capacity ブロック構文(provisioned_throughput ブロック)
プロジェクション設定 トップレベル属性(projection_type ブロック構文(projection ブロック)
ウォームスループットのサポート 限定的 完全サポート(属性構文: warm_throughput = { }
移行の複雑さ N/A インポートプロセスが必要
下位互換性 既存の方法 従来の方法と混在できない
安定性 安定 実験的(スキーマが変更される可能性あり)

クリーンアップ

今後の料金の発生を避けるために、このウォークスルーで作成したリソースを削除します:

# Terraform で管理されているすべてのリソースを破棄
terraform destroy

# プロンプトが表示されたら削除を確認
# 続行するには 'yes' と入力

テスト中に手動で作成したリソースがある場合は、今後のコストの発生を避けるために、AWS マネジメントコンソールまたは AWS CLI を通じてそれらも削除してください。

まとめ

この記事では、新しい aws_dynamodb_global_secondary_index リソースが、Terraform での DynamoDB GSI ドリフト管理という長年の課題をどのように解決するかを示しました。ネストされた global_secondary_index ブロックを無視する全部か無かの性質は、運用の柔軟性とインフラストラクチャガバナンスの間にギャップを生み出していました。

GSI をファーストクラスのリソースとして扱うことで、特定の GSI 属性に対する選択的な ignore_changes によるきめ細かい制御、各 GSI が独自のライフサイクルルールを持つ独立した管理、運用上の調整を許可しながら重要な変更を追跡するより良いドリフト検出、テーブルとインデックス設定の関心の分離によるより簡単なアーキテクチャを獲得できます。

aws_dynamodb_global_secondary_index リソースは現在 実験的 としてマークされていることを覚えておいてください。GSI ドリフトを管理するための強力な機能を提供しますが、以下の点に注意してください:

  • スキーマや動作は将来のプロバイダーバージョンで変更される可能性がある
  • このリソースを有効にするには、環境変数 TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1 を設定する必要がある
  • プロバイダーの下位互換性保証の対象ではない
  • このリソースを同じテーブルの従来の global_secondary_index ブロックと混在させることはできない

常に非本番環境で十分にテストし、プロバイダーのリリースノートで更新を監視してください。フィードバックがある場合は、GitHub Issue #45640 で提供して、この機能の将来を形作るのに役立ててください。

本記事は 2026 年 02 月 09 日 に公開された “New in Terraform: Manage global secondary index drift in Amazon DynamoDB” を翻訳したものです。

原文: https://aws.amazon.com/blogs/database/new-in-terraform-manage-global-secondary-index-drift-in-amazon-dynamodb/


著者について

Vaibhav Bhardwaj

Vaibhav Bhardwaj

Vaibhav は、AWS シンガポールを拠点とするシニア DynamoDB スペシャリストソリューションアーキテクトです。19 年の経験を持つサーバーレス愛好家で、DynamoDB を使用した高性能、スケーラビリティ、信頼性を要求するアプリケーションのアーキテクチャを設計するために顧客と協力することを好みます。