Amazon Web Services ブログ
Terraform の新機能: Amazon DynamoDB のグローバルセカンダリインデックスのドリフトを管理する
Amazon DynamoDB のグローバルセカンダリインデックス (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:00 に実行され、50 リードキャパシティユニット (RCU) が必要ですが、通常の時間帯は 5 RCU で十分です。運用チームは、負荷に対応するためにレポート実行前に手動でキャパシティを増やします。
午前 1:50 に、運用チームは AWS Command Line Interface (AWS CLI) を使用してキャパシティを 5 から 50 に増やします。レポートは午前 2:00 から 3:00 まで高いキャパシティで実行されます。その日の後半、無関係な変更をデプロイするために 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 に対してどの属性を無視するかをきめ細かく制御できると同時に、削除やスキーマ変更などの重要な変更を検出できます。
前提条件
始める前に、以下があることを確認してください。
- DynamoDB テーブルを作成および管理する権限を持つ AWS アカウント(例で使用するテストリソースを作成するために必要)
- DynamoDB 権限を持つ AWS Identity and Access Management (IAM) ロールを持つ Amazon Linux を実行している Amazon Elastic Compute Cloud (Amazon EC2) インスタンス
- インストールおよび設定された AWS Command Line Interface (AWS CLI)
- AWS Terraform Provider バージョン 6.28.0 以降(
aws_dynamodb_global_secondary_indexリソースは v6.28.0 で導入されました)
aws_dynamodb_global_secondary_index リソースは、現在 Terraform AWS provider で 実験的 としてマークされています。これは、スキーマまたは動作が予告なく変更される可能性があり、プロバイダーの後方互換性保証の対象ではないことを意味します。
この実験的リソースを有効にするには、環境変数 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 のキー:
| リソース | ハッシュキー | レンジキー | キャパシティ |
|---|---|---|---|
| テーブル | 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
StatusUserIndex(ignore_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 を実行すると、AWS で StatusUserIndex のキャパシティが 10/10 に変更されたにもかかわらず、No changes と表示されます。これは ignore_changes = [provisioned_throughput] のために発生します。
TimestampIndex(ignore_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 の重要な変更を検出する
従来の方法との主な違いは、テーブルがテーブル自体で使用される属性(id、timestamp)を定義するのに対し、GSI 固有の属性(status、user_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 plan で移行を確認し、No changes と表示され、新しいリソースモデルへの移行に成功しました。
重要なポイント:
- 移行には
terraform importを使用 - AWS リソースは変更または再作成されない
- GSI は移行全体を通じてゼロダウンタイムで存在し続ける
- 移行後、
ignore_changesで無視するものをきめ細かく制御できる - 移行プロセスは安全で元に戻すことができる
移行に関する考慮事項
aws_dynamodb_global_secondary_index リソースを aws_dynamodb_table の global_secondary_index ブロックと組み合わせないでください。そうすると、競合、永続的な差異、GSI の上書きが発生する可能性があります。
移行する場合は、次の手順に従ってください。
- 状態をバックアップ:
terraform state pull > backup.tfstate - 環境変数を設定:
export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1 - 設定を更新: テーブルから GSI ブロックを削除し、新しい GSI リソースを作成
- 既存の GSI をインポート:
terraform import <resource> 'table_name,index_name' - 確認:
terraform planを実行し、No changesと表示されるはず - テスト: キャパシティを手動で変更し、Terraform が変更を無視することを確認
terraform import を使用して正しく行えば、移行中にダウンタイムは発生しません。GSI は移行全体を通じて AWS に存在し続けます。terraform import コマンドは Terraform の状態ファイルのみを更新し、AWS リソースは変更しません。
テーブルに複数の GSI がある場合は、一度に 1 つずつ移行します。
- 最初の GSI をインポートし、
terraform planで確認 - 2 番目の GSI をインポートし、
terraform planで確認 - すべての GSI が移行されるまで続ける
これによりリスクが軽減され、トラブルシューティングが簡素化されます。
比較: 従来の方法と新しい方法
次の表は、従来のネストされたブロックアプローチと新しい個別リソースメソッドの主な違いをまとめたものです。
| 側面 | 従来の方法(ネストされたブロック) | 新しい方法(個別リソース) |
|---|---|---|
| リソースの有効化 | 環境変数は不要 | TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1 が必要 |
きめ細かい ignore_changes |
サポートされていない | サポートされている |
| 独立した GSI 管理 | すべての GSI が一緒に管理される | 各 GSI が独立して管理される |
| ドリフト検出 | オールオアナッシング | GSI ごとに選択的 |
| ライフサイクルルール | すべての GSI に適用 | GSI ごとのライフサイクルルール |
| 状態管理 | 複雑なネストされた状態 | わかりやすいフラットな状態 |
| キャパシティ設定 | トップレベル属性(read_capacity、write_capacity) |
ブロック構文(provisioned_throughput ブロック) |
| プロジェクション設定 | トップレベル属性(projection_type) |
ブロック構文(projection ブロック) |
| ウォームスループットのサポート | 限定的 | 完全サポート(属性構文: warm_throughput = { }) |
| 移行の複雑さ | 該当なし | インポートプロセスが必要 |
| 後方互換性 | 既存の方法 | 従来の方法と混在できない |
| 安定性 | 安定 | 実験的(スキーマが変更される可能性あり) |
クリーンアップ
今後の料金の発生を避けるために、このウォークスルーで作成したリソースを削除します。
# Terraform で管理されているすべてのリソースを破棄
terraform destroy
# プロンプトが表示されたら削除を確認
# 続行するには 'yes' と入力
テスト中に手動でリソースを作成した場合は、AWS Management Console または 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 年 2 月 9 日 に公開された “New in Terraform: Manage global secondary index drift in Amazon DynamoDB” を翻訳したものです。
著者について

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