Pulumiで挑むAWS DynamoDBグローバルテーブル&PITRの極限構築:マルチリージョン時代のIaC実践論
テックリードの私たちがIaC(Infrastructure as Code)に求めるのは、単に「リソースが作れること」ではない。「本番障害をゼロにし、マルチリージョン展開の複雑性をコードで完全に隠蔽し、誰がデプロイしても1ミリの狂いもなく再現できること」だ。
特に、グローバル展開するSaaSの根幹を支えるNoSQLデータベース、AWS DynamoDBの設計・構築において、Terraformの複雑なモジュール地獄や、CloudFormationの遅延と記述量の多さにフラストレーションを抱えていないか?
今回は、TypeScriptとPulumiを使用し、DynamoDBのグローバルテーブル(マルチリージョン同期)、ポイントインタイムリカバリ(PITR)、自動スケーリングを極限まで安全かつエレガントにコード化する手法を伝授する。
—
1. PulumiでNoSQLデータベースをコード化する際の課題と「真の解決策」
DynamoDBをIaCで管理する際、多くのエンジニアが以下の「死の罠」に直面する。
1. リージョン間依存関係の地獄: グローバルテーブルは複数リージョンにまたがる。単一のプロバイダー設定では複数リージョンを同時に安全にオーケストレーションできず、状態の不整合(State Drift)が起きやすい。
2. PITRと暗号化の初期値の罠: セキュリティ要件としてPITR(Point-in-Time Recovery)とKMS暗号化は必須だが、これらをリソース定義の「後から」追加すると、偶発的なダウンタイムや意図しないリソース再作成を引き起こす。
3. 自動スケーリングの競合: Application Auto ScalingとDynamoDB自体の設定が競合し、Pulumi(あるいはTerraform)のapplyループ(差分が永遠に消えない現象)が発生する。
匠の解決策:プロバイダーのエイリアス分割とライフサイクル管理
Pulumiでは、`pulumi.Provider`をリージョンごとに明示的にインスタンス化し、リソース側に渡すことで、単一のプログラムから美しくマルチリージョンを制御できる。さらに、スケーリング設定などは `ignoreChanges` を適切に用いて、AWS側の自動調整とIaCの衝突を防ぐのがプロの流儀だ。
—
2. チーム開発の生産性を極限まで高める環境構築
コードを書く前に、開発スピードを底上げする「プロの道具立て」を整えよう。これがないコードベースは、サスペンションのないスポーツカーのようなものだ。
開発スピードを劇的に高めるVSCode拡張機能(神プラグイン)
- Pulumi (Official): 型補完とインラインでのリソースステータス表示に必須。
- Error Lens: TypeScriptの型エラーやPulumiのプロパティ不足をコード行内に即座に表示させ、コンパイルエラーの発生率を激減させる。
- AWS Toolkit: IDEを離れることなく、構築されたDynamoDBのストリームやGSI(グローバルセカンダリインデックス)のメトリックを即座に確認できる。
チーム開発のルール:設定ファイルのベストプラクティス
Pulumiのスタック設定(`Pulumi.
Pulumi.production.yaml
config:
aws:region: ap-northeast-1
my-project:secondaryRegion: us-east-1
my-project:readCapacity: “100”
my-project:writeCapacity: “100”
> 💡 チーム共有化ルール: 機密情報は絶対にYAMLに直書きせず、`pulumi config set –secret` を使用してAWS KMSまたはPulumi Secret Backendで暗号化し、Gitには暗号化した状態でコミットすること。
—
3. 実践:TypeScriptによるグローバルテーブル&PITR完全コード
それでは、実務でそのまま使える堅牢なTypeScriptコードを提示する。東京リージョン(プライマリ)と米国東部リージョン(セカンダリ)にまたがるグローバルテーブルを構築し、PITR、KMS暗号化、自動スケーリングを完備する。
実装コード (`index.ts`)
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// 1. 設定値の読み込み
const config = new pulumi.Config();
const secondaryRegion = config.require(“secondaryRegion”);
const readCapacity = config.requireNumber(“readCapacity”);
const writeCapacity = config.requireNumber(“writeCapacity”);
// 2. マルチリージョン用プロバイダーの明示的定義
// プライマリプロバイダー (デフォルトのAWSプロバイダーを利用)
const primaryProvider = aws.config.region;
// セカンダリリージョン用プロバイダーの定義
const secondaryProvider = new aws.Provider(“secondary-provider”, {
region: secondaryRegion as aws.Region,
});
// 3. 共通のテーブルスキーマ定義 (両リージョンで同一である必要がある)
const tableName = “enterprise-users”;
const tableAttributes = [
{ name: “UserId”, type: “S” },
{ name: “Email”, type: “S” },
];
const tableKeySchema = [
{ attributeName: “UserId”, hashKey: true },
];
// 4. プライマリリージョン(東京)のDynamoDBテーブル構築
const primaryTable = new aws.dynamodb.Table(“primary-table”, {
name: tableName,
billingMode: “PROVISIONED”, // グローバルテーブルにはPROVISIONEDまたはPAY_PER_REQUESTを指定
readCapacity: readCapacity,
writeCapacity: writeCapacity,
hashKey: “UserId”,
attributes: tableAttributes,
// 【重要】ポイントインタイムリカバリ(PITR)の有効化
pointInTimeRecovery: {
enabled: true,
},
// 【重要】AWS管理キーまたは顧客管理KMSによる暗号化
serverSideEncryption: {
enabled: true,
},
// グローバルテーブルのレプリカ設定(ここでセカンダリリージョンを指定)
replicas: [
{
regionName: secondaryRegion,
},
],
tags: {
Environment: pulumi.getStack(),
ManagedBy: “Pulumi”,
},
}, {
// レプリカ作成時の競合を防ぐための保護設定
protect: true,
});
// 5. セカンダリリージョン側の自動スケーリング設定(例:読み取りキャパシティ)
// ※グローバルテーブルの書き込みキャパシティはプライマリ側で一元管理されることが多い点に注意
const secondaryReadScalingTarget = new aws.appautoscaling.Target(“secondary-read-target”, {
maxCapacity: 1000,
minCapacity: readCapacity,
resourceId: pulumi.interpolate`table/${tableName}`,
scalableDimension: “dynamodb:table:ReadCapacityUnits”,
serviceNamespace: “dynamodb”,
}, { provider: secondaryProvider });
const secondaryReadScalingPolicy = new aws.appautoscaling.Policy(“secondary-read-policy”, {
policyType: “TargetTrackingScaling”,
resourceId: secondaryReadScalingTarget.resourceId,
scalableDimension: secondaryReadScalingTarget.scalableDimension,
serviceNamespace: secondaryReadScalingTarget.serviceNamespace,
targetTrackingScalingPolicyConfiguration: {
predefinedMetricSpecification: {
predefinedMetricType: “DynamoDBReadCapacityUtilization”,
},
targetValue: 70.0, // 使用率70%を維持するように自動調整
},
}, { provider: secondaryProvider });
// 6. 出力値の定義
export const primaryTableArn = primaryTable.arn;
export const primaryTableName = primaryTable.name;
export const globalTableStreamArn = primaryTable.streamArn;
—
4. 本番運用で絶対に外せないベストプラクティス
上記のコードをデプロイし、運用する上で知っておくべき「現場の知見」を共有する。
A. 変更管理とデプロイの順序
グローバルテーブルのスキーマ変更(GSIの追加など)を行う際、Pulumiは裏側でAWSのAPIを叩いて順次レプリカに伝播させる。
大規模なテーブルの場合、インデックス作成に時間がかかるため、Pulumiのタイムアウト設定(`customTimeouts`)を長めに設定しておくことが、パイプラインの突然死を防ぐコツだ。
// タイムアウト設定の例(大規模テーブル用)
const primaryTable = new aws.dynamodb.Table(“primary-table”, {
// … 設定 …
}, {
customTimeouts: {
create: “60m”,
update: “60m”,
delete: “30m”,
}
});
B. 状態破壊を防ぐ `protect: true` の徹底
本番環境のデータベースが誤った `pulumi destroy` やリソースの再作成(Replace)によって吹き飛ぶ事故は、SREとしての最大のリスクである。
コード内でも示した通り、本番用のDynamoDBリソースには必ず `protect: true` を付与し、意図しない削除からインフラストラクチャを物理的に保護せよ。
C. データの整合性とバックアップの検証
PITR(ポイントインタイムリカバリ)は有効化しただけでは不十分だ。「本当に過去の特定時点にリストアできるか」をステージング環境で定期的に検証するスクリプトをCI/CDに組み込むこと。Pulumiを使えば、バックアップからの復元テスト用スタックをオンデマンドで立ち上げ、検証後に即座に破棄するといった高度な自動化が容易に行える。
—
5. おわりに:IaCの限界を超えるために
Pulumiを用いたTypeScriptによるインフラ構築は、単なる「コード化」の枠を超えている。厳密な型安全性、ループや条件分岐などのプログラミング言語のパワー、そしてマルチリージョンを統括するプロバイダー制御。これらを駆使することで、複雑怪奇になりがちなクラウドインフラを「手なずける」ことができる。
今回紹介したコードと知見をベースに、あなたのチームのデータベース基盤をより堅牢に、そしてアジャイルに進化させてほしい。妥協のないインフラストラクチャの構築こそが、プロダクトのスケールを支える唯一の道である。