aws_dynamodb_tableリソースは、変更内容によって「インプレース更新(テーブルはそのまま)」と「リソース置換(テーブル再作成=データ消失)」の2パターンに分かれる。どちらになるかをterraform planで読み取れないまま apply すると、本番データを失うリスクがある。
この記事では、TerraformでDynamoDBテーブルを宣言的に定義する方法を解説する。GSI追加・TTL有効化・Streams設定変更がterraform planでどう差分に出るか、そしてリソース置換を招かない変更順序を実践例で示す。
動作確認環境: Terraform v1.9 / AWS Provider v5.x
この記事のポイント
・name/hash_key/range_key変更はforces replacement(テーブル再作成・データ消失)
・GSI追加・TTL有効化・Streams変更はインプレース更新で通る
・GSIのhash_key/range_keyを変え直すときは2ステップapplyで置換を回避する
・terraform planの「forces replacement」を必ず確認してからapplyする
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
TerraformでDynamoDBを管理するときに最初に知るべきこと
TerraformのDynamoDB管理で最初につまずくのが「どの属性変更がテーブル再作成を引き起こすか」だ。aws_dynamodb_tableリソースの属性は変更時の挙動で大きく3種類に分類できる。| 属性の種類 | 変更時の挙動 | 代表的な属性 |
|---|---|---|
| キースキーマ | forces replacement(テーブル再作成) | name、hash_key、range_key |
| インデックス・機能設定 | インプレース更新 | global_secondary_index、ttl、stream_enabled |
| キャパシティ設定 | インプレース更新 | billing_mode、read_capacity、write_capacity |
一方、GSI・TTL・Streamsはテーブルを消さずに変更できる。ただし変更の順序や組み合わせを間違えるとエラーになるため、planの差分を正しく読む習慣が必要だ。
aws_dynamodb_tableリソースの基本定義
1. テーブルの基本属性とキースキーマを書く
基本的なaws_dynamodb_tableリソースの定義は以下のようになる。resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PAY_PER_REQUEST" hash_key = "order_id" range_key = "created_at" attribute { name = "order_id" type = "S" } attribute { name = "created_at" type = "S" } tags = { Environment = "production" } }
2. プロビジョニングモードとオンデマンドを切り替える
`billing_mode` は `PAY_PER_REQUEST`(オンデマンド)と `PROVISIONED`(プロビジョニング)の2択だ。プロビジョニングを使う場合は `read_capacity` と `write_capacity` を追加する。resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PROVISIONED" read_capacity = 5 write_capacity = 5 hash_key = "order_id" attribute { name = "order_id" type = "S" } }
GSI(グローバルセカンダリインデックス)のplan差分と安全な追加手順
1. GSIをHCLで定義する
GSIは `global_secondary_index` ブロックで定義する。GSIのキーとして使う属性は、テーブルの `attribute` ブロックにも追加が必要だ。resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PAY_PER_REQUEST" hash_key = "order_id" range_key = "created_at" attribute { name = "order_id" type = "S" } attribute { name = "created_at" type = "S" } # GSIで使う属性を追加 attribute { name = "status" type = "S" } global_secondary_index { name = "GSI-ByStatus" hash_key = "status" range_key = "created_at" projection_type = "ALL" } }
2. terraform planでGSI追加差分を読む
既存テーブルにGSIを追加するときのplan出力例は以下のようになる。# aws_dynamodb_table.orders will be updated in-place ~ resource "aws_dynamodb_table" "orders" { id = "orders" name = "orders" + global_secondary_index { + hash_key = "status" + name = "GSI-ByStatus" + non_key_attributes = [] + projection_type = "ALL" + range_key = "created_at" + read_capacity = 0 + write_capacity = 0 } } Plan: 0 to add, 1 to change, 0 to destroy.
3. GSI変更でリソース置換が起きるケース
DynamoDBはGSIを作成した後にそのキースキーマを変更できない。そのため、既存GSIの `hash_key` や `range_key` を変更しようとすると、Terraformは既存GSIを削除してから新しいGSIを作成する。テーブル自体は再作成されないが、1回のapplyで「古いGSI削除+新しいGSI追加」を同時に行おうとすると問題が起きやすい。DynamoDBはテーブルあたりのGSIをデフォルトで最大20個まで制限している。Terraformが新しいGSIの追加を古いGSIの削除より先に試みると、一時的に上限に達してエラーになることがある。また、同名のGSIを一度削除してから同名で再作成する場合も、削除完了を待たずに作成しようとするとResourceInUseExceptionが発生する。
安全な変更手順は2ステップに分けることだ。
・ステップ1: 古いGSI定義をHCLから削除してapply(GSIがAWS上から削除される)
・ステップ2: 新しいGSI定義をHCLに追加してapply(新しいGSIが作成される)
dynamodb ttl terraformでTTLを有効化するときのplan差分
1. TTLをHCLで定義する
TTL(Time to Live)は `ttl` ブロックで定義する。`attribute_name` にTTL期限を格納する属性名を指定し、`enabled = true` にする。resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PAY_PER_REQUEST" hash_key = "order_id" attribute { name = "order_id" type = "S" } ttl { enabled = true attribute_name = "expires_at" } }
2. TTL有効化のplan差分
TTLが無効の既存テーブルに対してTTLを有効化するplan出力は以下のようになる。# aws_dynamodb_table.orders will be updated in-place ~ resource "aws_dynamodb_table" "orders" { id = "orders" ~ ttl { ~ enabled = false -> true ~ attribute_name = "" -> "expires_at" } } Plan: 0 to add, 1 to change, 0 to destroy.
3. TTLを後から無効化するときの注意
`enabled = false` でTTLを無効化することもインプレース更新で通る。ただし、一度TTLで削除されたレコードは復元できない。TTL無効化より前にアプリ側で `expires_at` のセットを止めるほうが安全だ。「TTLの設定はアプリチームが直接AWSコンソールで変更する」という運用ルールがある場合は、`lifecycle` ブロックで差分を無視する選択肢もある。
lifecycle { ignore_changes = [ttl] }
DynamoDB Streamsの設定変更とplan差分
1. Streamsをterraformで定義する
DynamoDB Streamsは `stream_enabled` と `stream_view_type` で定義する。resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PAY_PER_REQUEST" hash_key = "order_id" attribute { name = "order_id" type = "S" } stream_enabled = true stream_view_type = "NEW_AND_OLD_IMAGES" }
・`KEYS_ONLY`: 変更されたアイテムのキー属性のみ
・`NEW_IMAGE`: 変更後のアイテム全体
・`OLD_IMAGE`: 変更前のアイテム全体
・`NEW_AND_OLD_IMAGES`: 変更前後の両方(最も情報量が多い)
Lambdaのイベントソースマッピングでストリームを使う場合は、`aws_lambda_event_source_mapping` も合わせて定義し、`event_source_arn` に `aws_dynamodb_table.orders.stream_arn` を参照させる。
2. stream_view_typeを変更したときのplan差分
ストリームを無効から有効に変更するplan差分は以下のようになる。# aws_dynamodb_table.orders will be updated in-place ~ resource "aws_dynamodb_table" "orders" { id = "orders" ~ stream_enabled = false -> true ~ stream_view_type = "" -> "NEW_AND_OLD_IMAGES" + stream_arn = (known after apply) } Plan: 0 to add, 1 to change, 0 to destroy.
リソース置換(forces replacement)を招かない変更順序の設計
1. forces replacementのサインをplanで見分ける
terraform planの出力で `must be replaced` や `forces replacement` の文字列が出たとき、そのリソースは一度削除されてから再作成される。DynamoDBテーブルの場合、これはデータ消失を意味する。# aws_dynamodb_table.orders must be replaced -/+ resource "aws_dynamodb_table" "orders" { ~ hash_key = "order_id" -> "user_id" # forces replacement ~ name = "orders" -> "orders_v2" # forces replacement } Plan: 1 to add, 0 to change, 1 to destroy.
2. prevent_destroyで誤削除を防止する
本番テーブルには `lifecycle` の `prevent_destroy = true` を設定しておくと、万が一 `terraform destroy` や誤った変更でテーブルが削除されようとしたときにエラーで止まる。resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PAY_PER_REQUEST" hash_key = "order_id" attribute { name = "order_id" type = "S" } lifecycle { prevent_destroy = true } }
3. 2ステップapplyで安全に変更する
本番テーブルで名前の変更など、やむを得ずリソース置換が必要な場合は以下の手順で進める。・ステップ1: 新しいテーブルを別名でHCLに追加してapply(既存テーブルはそのまま残る)
・ステップ2: アプリ側の書き込み先を新テーブルに切り替える
・ステップ3: 旧テーブルのデータをDynamoDB StreamsかAWS Data Migration Serviceで移行する
・ステップ4: 移行確認後、旧テーブルのHCL定義を削除してapply
Terraformのリソースラベルを変えるだけ(HCL上の名前を `aws_dynamodb_table.orders` から `aws_dynamodb_table.orders_prod` に変更する)なら `moved` ブロックが使える。`moved` はstateの参照名を変えるだけなので、DynamoDBのテーブル名(`name` 属性)は変わらず、テーブル再作成も起きない。
# HCL上のリソースラベルを変更する場合 moved { from = aws_dynamodb_table.orders to = aws_dynamodb_table.orders_prod }
よくあるエラーと対処
「ValidationException: Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitions」GSIのキーとして使っている属性を `attribute` ブロックに定義していないときに発生する。GSIで使う `hash_key` と `range_key` はすべて `attribute` ブロックに追加すること。
「Error: creating DynamoDB Table: ResourceInUseException: Table already exists」
terraform import せずに既存テーブルと同名のリソースをapplyしようとしたときに発生する。`terraform import aws_dynamodb_table.orders orders` でstateに取り込んでからapplyする。
「LimitExceededException: Only 20 GSIs per table are allowed」
1回のapplyで古いGSIを削除しながら新しいGSIを追加しようとしたとき、削除より追加が先に試みられてGSIの上限に引っかかる。前述の2ステップapply(削除→追加)で対応する。
「Error: deleting DynamoDB Table: ResourceNotFoundException」
stateファイルにはリソースが残っているが、AWSコンソール側でテーブルが手動削除された場合に発生する。`terraform state rm aws_dynamodb_table.orders` でstateから除外するか、`terraform import` でstateを再同期する。
本記事のまとめ
TerraformでDynamoDBテーブルを安全に変更するポイントをまとめる。| やりたいこと | 挙動 |
|---|---|
| name/hash_key/range_keyを変更する | forces replacement(テーブル再作成・データ消失) |
| GSIを追加する | インプレース更新(テーブルはそのまま) |
| 既存GSIのhash_keyを変更する | GSI削除→再作成(2ステップapply推奨) |
| dynamodb ttl terraformでTTLを有効化する | インプレース更新 |
| stream_enabled = trueにする | インプレース更新 |
| billing_modeを切り替える | インプレース更新 |
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:Terraformで「Backend configuration changed」が出たときの復旧手順|-migrate-stateと-reconfigureの判断基準
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます