やろうとしたこと
CDKでTableV2 クラスを使用して DynamoDB Global Table を作成していました。テーブルは複数リージョンにレプリケーションすると同時に各テーブルにはリソースポリシーを設定します。
発生した事象
ここで以下のようなエラーが発生してテーブル作成に失敗しました。
A replica cannot be created in the same stack update as putting a resource-based policy on that new replica
原因
AWSの公式ドキュメントDynamoDB リソースベースのポリシーの考慮事項
に以下のような記載があります。どうやらレプリカ作成とそのレプリカへのリソースポリシーを同じスタックでデプロイしようとしていたのがダメだったようです。
AWS::DynamoDB::GlobalTable リソースでは、スタックの更新を行うリージョン以外のリージョンでは、スタックの更新時にレプリカの作成と、そのレプリカへのリソースベースのポリシーの追加を同時に行うことはできません。
解決策
要するにDynamoDBのレプリカ作成とそのレプリカへのリソースポリシー設定を別のスタックでデプロイすれば良いのですが、CDKでは外部スタックで定義したリソースの参照は出来ても更新は出来ないため単純にCDKのみでリソースポリシーの設定まで実装することが出来ません。(ここの認識違っていたらご指摘いただきたいです)
そこで今回はAwsSdkCallというCDKのモジュールを使用してリソースポリシーを後付けすることで対応しました。
サンプルコード
import * as cr from "aws-cdk-lib/custom-resources";
import { Construct } from "constructs";
import { Stack, StackProps } from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
export interface TenantExtensionStackProps extends StackProps {
readonly abacRoleArn: string;
readonly enableDevMode?: boolean;
readonly allowedAccount?: string[];
}
export class TenantExtensionStack extends Stack {
constructor(scope: Construct, id: string, props: TenantExtensionStackProps) {
super(scope, id, props);
tableArn = "SAMPLE_REPLICA_TABLE"
const policyStatements: iam.PolicyStatement[] = [
new iam.PolicyStatement({
actions: [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:ConditionCheckItem",
],
principals: [new iam.ArnPrincipal(SAMPLE_ROLE)],
resources: [tableArn],
}),
];
function addDevelopmentDynamoDBPermissionsTmp(
statements: iam.PolicyStatement[],
tableArn: string,
accounts: string[]
) {
accounts.forEach((account) => {
statements.push(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [
new iam.ArnPrincipal(`arn:aws:iam::${account}:root`),
],
actions: ["dynamodb:*"],
resources: [tableArn],
})
);
});
}
if (props.enableDevMode && props.allowedAccount) {
addDevelopmentDynamoDBPermissionsTmp(
policyStatements,
tableArn,
props.allowedAccount
);
}
const resourcePolicy = JSON.stringify({
Version: "2012-10-17",
Statement: policyStatements.map((statement) =>
statement.toStatementJson()
),
});
const putResourcePolicy = new cr.AwsCustomResource(
this,
`PutDynamoDBPolicy-${tableConfig.name}`,
{
onUpdate: {
service: "DynamoDB",
action: "putResourcePolicy",
parameters: {
ResourceArn: tableArn,
Policy: resourcePolicy,
},
physicalResourceId: cr.PhysicalResourceId.of(
`put-dynamodb-policy-${tableConfig.name}`
),
},
onDelete: {
service: "DynamoDB",
action: "deleteResourcePolicy",
parameters: {
ResourceArn: tableArn,
Policy: resourcePolicy,
},
physicalResourceId: cr.PhysicalResourceId.of(
`put-dynamodb-policy-${tableConfig.name}`
),
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: [tableArn],
}),
}
);
});
}
}