Pulumiで極めるAWS DynamoDB:マルチリージョン・グローバルテーブルとPITRの完全自動化設計
世の中の多くのインフラストラクチャ・コード(IaC)の記事は、「リソースを作って終わり」という薄っぺらいチュートリアルに終始している。しかし、本番環境でNoSQLを運用するSREにとって、真の地獄は「デプロイ後」にやってくる。リージョン障害、意図しないデータ破損、そして何より「IaCの適用順序ミスによるグローバルテーブルの構築失敗」だ。
今回は、宣言型IaCの限界を突破し、TypeScriptとPulumiを用いて、AWS DynamoDBのグローバルテーブル、ポイントインタイムリカバリ(PITR)、そしてミリ秒単位の耐障害性を持つインフラストラクチャを「完全に冪等かつ安全に」構築する極限の知見を授ける。
—
1. NoSQLデータベースのコード化における残酷な現実とPulumiの優位性
CloudFormationやTerraformでDynamoDBのグローバルテーブル(バージョン2019.11)を構築しようとしたことがある者なら、誰もが一度は「Circular Dependency(循環依存)」や「Replica Creation Timeout(レプリカ作成タイムアウト)」という悪夢に直面したはずだ。
Terraformの `aws_dynamodb_global_table` リソースは、単一リージョンのテーブルをベースにしつつ他リージョンを巻き込むため、プロバイダーのマルチリージョンエイリアス設定や、リソースの依存関係(`depends_on`)の迷宮にハマりやすい。特に、既存の単一リージョンテーブルをグローバルテーブルに昇格させる際のステート移行は、Terraformではしばしばリソースの再作成(ダウンタイム)を強要される。
なぜPulumi(TypeScript)なのか?
Pulumiは、実態が「プログラミング言語そのもの」である。つまり、単なる宣言型設定の記述に留まらず、制御構文、非同期処理、カスタムプロバイダによるAPIの直接制御を同一のコードベースにシームレスに統合できる。
- 動的なマルチリージョンループ: TypeScriptの `map` や `Promise.all` を駆使して、任意の数のリージョンへレプリカを動的に展開できる。
- 型安全性による設定ミスの撲滅: AWSの複雑なスケーリングポリシーやタイムアウト値を、強烈な静的型チェックの網にかける。
- プレビュー(`pulumi preview`)の精密さ: 変更がグローバルテーブルの可用性に与える影響を、適用前に完全に可視化する。
—
2. 実践:TypeScriptによるマルチリージョン・グローバルテーブルの構築
以下に示すのは、プライマリリージョン(`us-east-1`)とセカンダリリージョン(`ap-northeast-1`)にまたがり、PITRと自動スケーリングを完備したDynamoDBグローバルテーブルを構築するプロダクションコードだ。
プロジェクト構成
.
├── Pulumi.yaml
├── Pulumi.prod.yaml
├── package.json
└── index.ts
`index.ts` – 究極のインフラストラクチャ定義
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// —————————————————————————–
// Config & Constants
// —————————————————————————–
const config = new pulumi.Config();
const tableName = config.require(“tableName”);
// プライマリリージョンとレプリカリージョンの定義
// 拡張性を考慮し、設定や配列から動的にプロビジョニングできるようにする
const primaryRegion = aws.config.region || “us-east-1”;
const replicaRegions = [“ap-northeast-1”, “eu-west-1”];
// —————————————————————————–
// Providers Setup
// マルチリージョンリソースを制御するため、明示的にプロバイダを定義する
// —————————————————————————–
const primaryProvider = new aws.Provider(“primaryProvider”, {
region: primaryRegion as aws.Region,
});
const replicaProviders: { [key: string]: aws.Provider } = {};
for (const region of replicaRegions) {
replicaProviders[region] = new aws.Provider(`provider-${region}`, {
region: region as aws.Region,
});
}
// —————————————————————————–
// DynamoDB Table (Primary Region)
// —————————————————————————–
const globalTable = new aws.dynamodb.Table(tableName, {
name: tableName,
billingMode: “PAY_PER_REQUEST”, // 予測不可能なトラフィックに対応するオンデマンド
hashKey: “PK”,
rangeKey: “SK”,
attributes: [
{ name: “PK”, type: “S” },
{ name: “SK”, type: “S” },
],
// 災害対策の要:ポイントインタイムリカバリ (PITR) の強制有効化
pointInTimeRecovery: {
enabled: true,
},
// SSE(Server-Side Encryption)の明示的指定(AWSマネージドキー)
serverSideEncryption: {
enabled: true,
},
// グローバルテーブルとしてレプリカを指定
replicas: replicaRegions.map(region => ({
regionName: region,
// グローバルセカンダリインデックスのレプリケーション設定もここで行う
// デプロイ時の競合を防ぐため、各リージョンのプロバイダとの依存を意識する
})),
streamEnabled: true,
streamViewType: “NEW_AND_OLD_IMAGES”, // CDC (Change Data Capture) 用
}, {
provider: primaryProvider,
// AWS側のレプリカ作成処理は時間がかかるため、タイムアウトを延長
customTimeouts: {
create: “60m”,
update: “60m”,
delete: “30m”,
}
});
// —————————————————————————–
نيهون – Auto Scaling Configuration (Application Auto Scaling)
// プロビジョンドモードを使用する場合や、GSIのキャパシティ制御が必要な場合のベストプラクティス
// 今回はPAY_PER_REQUESTだが、万が一のプロビジョンド移行を見据えたスケーリングターゲットの骨組み
// —————————————————————————–
const readCapacityScalableTarget = new aws.appautoscaling.Target(`${tableName}-read-target`, {
maxCapacity: 10000,
minCapacity: 100,
resourceId: pulumi.interpolate`table/${globalTable.name}`,
scalableDimension: “dynamodb:table:ReadCapacityUnits”,
serviceNamespace: “dynamodb”,
}, { provider: primaryProvider });
new aws.appautoscaling.Policy(`${tableName}-read-scaling-policy`, {
policyType: “TargetTrackingScaling”,
resourceId: readCapacityScalableTarget.resourceId,
scalableDimension: readCapacityScalableTarget.scalableDimension,
serviceNamespace: readCapacityScalableTarget.serviceNamespace,
targetTrackingScalingPolicyConfiguration: {
predefinedMetricSpecification: {
predefinedMetricType: “DynamoDBReadCapacityUtilization”,
},
targetValue: 70.0, // 70%使用率を維持
},
}, { provider: primaryProvider });
// —————————————————————————–
// Exports
// —————————————————————————–
export const tableNameOutput = globalTable.name;
export const globalTableArn = globalTable.arn;
export const activeRegions = [primaryRegion, …replicaRegions];
—
3. PITRと自動スケーリングを安全に適用するベストプラクティス
本番運用の現場において、データベースの変更は常に「データの消失」や「スロットリング(429 Too Many Requests)」という致命的なリスクと隣り合わせだ。以下のベストプラクティスをコードと運用の両面で強制する。
① PITR(Point-In-Time Recovery)のイミュータブル化
開発者がうっかり `pointInTimeRecovery` を無効化する変更を加えた際、それをCI/CDパイプラインで弾く必要がある。Pulumiでは、`ResourceOptions` の `protect: true` を活用することで、データベース自体の誤削除や重要パラメータ(PITRや削除保護)の破壊的変更を防ぐことができる。
const globalTable = new aws.dynamodb.Table(tableName, {
// … 設定 …
pointInTimeRecovery: { enabled: true },
}, {
provider: primaryProvider,
protect: true, // リソースの保護を有効化。削除コマンドをブロックする
});
② グローバルテーブル構築時のタイムアウトハック
AWSのDynamoDBグローバルテーブル(2019.11版)は、裏側でKinesis Streamsと内部的な非同期レプリケーションを構築するため、CloudFormationやTerraformのデフォルトタイムアウト(通常30分)では平気でタイムアウトエラーを起こす。
Pulumiコード内で `customTimeouts` を明示的に `60m` などに設定し、AWS側のバックグラウンド処理が完了するのを待機させることが、デプロイパイプラインの破綻を防ぐ唯一の現実的なアプローチである。
③ キャパシティ設計:`PAY_PER_REQUEST` vs `PROVISIONED`
- 急激なスパイクが予測されるシステム: 迷わず `PAY_PER_REQUEST`(オンデマンド)を選択すべきだ。キャパシティプランニングの失敗によるスロットリングを完全に排除できる。
- 定常的高負荷かつコスト最適化が至上命題のシステム: `PROVISIONED` を選択し、上記コードで示した `Application Auto Scaling` を組み合わせる。ただし、ターゲット追跡スケーリングのターゲット値は `70%` を推奨する。AWSのAuto Scalingは急激なトラフィック増に対して追いつかない瞬間があるため、30%のバッファを必ず残すこと。
—
4. エキスパート向け:完全自動化パイプラインとAPIドリブンな検証ハック
IaCコードを書くだけで満足するジュニアエンジニアと違い、真のSREは「コードが適用された後、本当にデータが同期されているか」をプログラムで検証する自動化スクリプトまでをスコープに入れる。
以下のTypeScriptスニペットは、Pulumiのデプロイ完了後に、各リージョンのDynamoDBエンドポイントへ直接書き込み・読み込みを行い、グローバルテーブルのレプリケーション遅延(Lag)を測定・検証するカスタムヘルスチェックの概念実証(PoC)である。
import { DynamoDBClient, PutItemCommand, GetItemCommand } from “@aws-sdk/client-dynamodb”;
async function verifyGlobalTableReplication(tableName: string) {
const testKey = `test-sync-${Date.now()}`;
// 1. プライマリリージョンへ書き込み
const primaryClient = new DynamoDBClient({ region: “us-east-1” });
await primaryClient.send(new PutItemCommand({
TableName: tableName,
Item: {
PK: { S: testKey },
SK: { S: “METADATA” },
Payload: { S: “validation-payload” }
}
}));
console.log(`[Write] Successfully wrote item ${testKey} to us-east-1`);
// 2. レプリカリージョンからの読み込み確認(ポーリングによる遅延測定)
const replicaClient = new DynamoDBClient({ region: “ap-northeast-1” });
const startTime = Date.now();
const timeout = 15000; // 15秒タイムアウト
while (Date.now() – startTime < timeout) {
try {
const response = await replicaClient.send(new GetItemCommand({
TableName: tableName,
Key: {
PK: { S: testKey },
SK: { S: "METADATA" }
}
}));
if (response.Item) {
const lag = Date.now() - startTime;
console.log(`[Success] Replication verified in ap-northeast-1. Lag: ${lag}ms`);
return;
}
} catch (e) {
// まだレプリケートされていない場合のエラーは無視してリトライ
}
await new Promise(resolve => setTimeout(resolve, 500)); // 500msごとにポーリング
}
throw new Error(`[CRITICAL] Global table replication timed out for region ap-northeast-1`);
}
この検証ロジックをPulumiの `Command` リソース(`pulumi-command` パッケージ)やGitHub ActionsなどのCI/CDパイプラインの最終ステップとして組み込むことで、「インフラがデプロイされたが、実はデータが同期していなかった」というSREの悪夢を完全に根絶することができる。
—
結び:インフラストラクチャは「コード」ではなく「アプリケーション」である
Pulumiを用いたAWS DynamoDBの構築は、単なるJSONやYAMLの置き換えではない。TypeScriptという強力な表現力を持つ言語を使うことで、インフラはもはや単なる設定ファイルではなく、「堅牢な分散システムを構築するためのソフトウェア」へと昇華する。
マルチリージョン、PITRの強制、そして厳密な非同期処理の制御。これらを極めたとき、あなたのインフラストラクチャは、いかなる障害をも笑い飛ばす「不落の要塞」となる。さあ、今すぐコンソールを開く手を止め、コードで世界を支配せよ。