Skip to content

Commit 36b8fdb

Browse files
authoredDec 6, 2021
fix(dynamodb): changing waitForReplicationToFinish fails deployment (#17842)
Our CFN Custom Resource for global DynamoDB Tables did not correctly handle changing the `waitForReplicationToFinish` parameter introduced in #16983 - adding it to an existing replica would error out by attempting to re-create the existing replica. Fix this by adding a check in the Custom Resource whether a replica already exists. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent e232ab7 commit 36b8fdb

File tree

4 files changed

+82
-32
lines changed

4 files changed

+82
-32
lines changed
 

‎packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts

+34-20
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,45 @@ export async function onEventHandler(event: OnEventRequest): Promise<OnEventResp
77

88
const dynamodb = new DynamoDB();
99

10-
let updateTableAction: 'Create' | 'Update' | 'Delete';
10+
const tableName = event.ResourceProperties.TableName;
11+
const region = event.ResourceProperties.Region;
12+
13+
let updateTableAction: 'Create' | 'Update' | 'Delete' | undefined;
1114
if (event.RequestType === 'Create' || event.RequestType === 'Delete') {
1215
updateTableAction = event.RequestType;
1316
} else { // Update
14-
// This can only be a table replacement so we create a replica
15-
// in the new table. The replica for the "old" table will be
16-
// deleted when CF issues a Delete event on the old physical
17-
// resource id.
18-
updateTableAction = 'Create';
17+
// There are two cases where an Update can happen:
18+
// 1. A table replacement. In that case, we need to create the replica in the new Table
19+
// (the replica for the "old" Table will be deleted when CFN issues a Delete event on the old physical resource id).
20+
// 2. A customer has changed one of the properties of the Custom Resource,
21+
// like 'waitForReplicationToFinish'. In that case, we don't have to do anything.
22+
// To differentiate the two cases, we make an API call to DynamoDB to check whether a replica already exists.
23+
const describeTableResult = await dynamodb.describeTable({
24+
TableName: tableName,
25+
}).promise();
26+
console.log('Describe table: %j', describeTableResult);
27+
const replicaExists = describeTableResult.Table?.Replicas?.some(replica => replica.RegionName === region);
28+
updateTableAction = replicaExists ? undefined : 'Create';
1929
}
2030

21-
const data = await dynamodb.updateTable({
22-
TableName: event.ResourceProperties.TableName,
23-
ReplicaUpdates: [
24-
{
25-
[updateTableAction]: {
26-
RegionName: event.ResourceProperties.Region,
31+
if (updateTableAction) {
32+
const data = await dynamodb.updateTable({
33+
TableName: tableName,
34+
ReplicaUpdates: [
35+
{
36+
[updateTableAction]: {
37+
RegionName: region,
38+
},
2739
},
28-
},
29-
],
30-
}).promise();
31-
console.log('Update table: %j', data);
40+
],
41+
}).promise();
42+
console.log('Update table: %j', data);
43+
} else {
44+
console.log("Skipping updating Table, as a replica in '%s' already exists", region);
45+
}
3246

3347
return event.RequestType === 'Create' || event.RequestType === 'Update'
34-
? { PhysicalResourceId: `${event.ResourceProperties.TableName}-${event.ResourceProperties.Region}` }
48+
? { PhysicalResourceId: `${tableName}-${region}` }
3549
: {};
3650
}
3751

@@ -45,11 +59,11 @@ export async function isCompleteHandler(event: IsCompleteRequest): Promise<IsCom
4559
}).promise();
4660
console.log('Describe table: %j', data);
4761

48-
const tableActive = !!(data.Table?.TableStatus === 'ACTIVE');
62+
const tableActive = data.Table?.TableStatus === 'ACTIVE';
4963
const replicas = data.Table?.Replicas ?? [];
5064
const regionReplica = replicas.find(r => r.RegionName === event.ResourceProperties.Region);
51-
const replicaActive = !!(regionReplica?.ReplicaStatus === 'ACTIVE');
52-
const skipReplicationCompletedWait = event.ResourceProperties.SkipReplicationCompletedWait ?? false;
65+
const replicaActive = regionReplica?.ReplicaStatus === 'ACTIVE';
66+
const skipReplicationCompletedWait = event.ResourceProperties.SkipReplicationCompletedWait === 'true';
5367

5468
switch (event.RequestType) {
5569
case 'Create':

‎packages/@aws-cdk/aws-dynamodb/lib/table.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,11 @@ export class Table extends TableBase {
15571557
properties: {
15581558
TableName: this.tableName,
15591559
Region: region,
1560-
SkipReplicationCompletedWait: waitForReplicationToFinish === undefined ? undefined : !waitForReplicationToFinish,
1560+
SkipReplicationCompletedWait: waitForReplicationToFinish == null
1561+
? undefined
1562+
// CFN changes Custom Resource properties to strings anyways,
1563+
// so let's do that ourselves to make it clear in the handler this is a string, not a boolean
1564+
: (!waitForReplicationToFinish).toString(),
15611565
},
15621566
});
15631567
currentRegion.node.addDependency(

‎packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2468,7 +2468,7 @@ describe('global', () => {
24682468
Ref: 'TableCD117FA1',
24692469
},
24702470
Region: 'eu-west-2',
2471-
SkipReplicationCompletedWait: true,
2471+
SkipReplicationCompletedWait: 'true',
24722472
},
24732473
Condition: 'TableStackRegionNotEqualseuwest2A03859E7',
24742474
}, ResourcePart.CompleteDefinition);
@@ -2479,7 +2479,7 @@ describe('global', () => {
24792479
Ref: 'TableCD117FA1',
24802480
},
24812481
Region: 'eu-central-1',
2482-
SkipReplicationCompletedWait: true,
2482+
SkipReplicationCompletedWait: 'true',
24832483
},
24842484
Condition: 'TableStackRegionNotEqualseucentral199D46FC0',
24852485
}, ResourcePart.CompleteDefinition);

‎packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ afterAll(() => {
1616

1717
AWS.setSDK(require.resolve('aws-sdk'));
1818

19+
const REGION = 'eu-west-2';
20+
1921
const createEvent: OnEventRequest = {
2022
RequestType: 'Create',
2123
ResourceProperties: {
2224
TableName: 'my-table',
23-
Region: 'eu-west-2',
25+
Region: REGION,
2426
ServiceToken: 'token',
2527
},
2628
ServiceToken: 'token',
@@ -47,7 +49,7 @@ test('on event', async () => {
4749
ReplicaUpdates: [
4850
{
4951
Create: {
50-
RegionName: 'eu-west-2',
52+
RegionName: REGION,
5153
},
5254
},
5355
],
@@ -58,9 +60,16 @@ test('on event', async () => {
5860
});
5961
});
6062

61-
test('on event calls updateTable with Create for Update requests with table replacement', async () => {
62-
const updateTableMock = sinon.fake.resolves({});
63+
test("on Update event from CFN calls updateTable with Create if a replica in the region doesn't exist", async () => {
64+
AWS.mock('DynamoDB', 'describeTable', sinon.fake.resolves({
65+
Table: {
66+
Replicas: [
67+
// no replicas exist yet
68+
],
69+
},
70+
}));
6371

72+
const updateTableMock = sinon.fake.resolves({});
6473
AWS.mock('DynamoDB', 'updateTable', updateTableMock);
6574

6675
const data = await onEventHandler({
@@ -76,7 +85,7 @@ test('on event calls updateTable with Create for Update requests with table repl
7685
ReplicaUpdates: [
7786
{
7887
Create: {
79-
RegionName: 'eu-west-2',
88+
RegionName: REGION,
8089
},
8190
},
8291
],
@@ -87,6 +96,29 @@ test('on event calls updateTable with Create for Update requests with table repl
8796
});
8897
});
8998

99+
test("on Update event from CFN calls doesn't call updateTable if a replica in the region does exist", async () => {
100+
AWS.mock('DynamoDB', 'describeTable', sinon.fake.resolves({
101+
Table: {
102+
Replicas: [
103+
{ RegionName: REGION },
104+
],
105+
},
106+
}));
107+
108+
const updateTableMock = sinon.fake.resolves({});
109+
AWS.mock('DynamoDB', 'updateTable', updateTableMock);
110+
111+
await onEventHandler({
112+
...createEvent,
113+
OldResourceProperties: {
114+
TableName: 'my-old-table',
115+
},
116+
RequestType: 'Update',
117+
});
118+
119+
sinon.assert.notCalled(updateTableMock);
120+
});
121+
90122
test('on event calls updateTable with Delete', async () => {
91123
const updateTableMock = sinon.fake.resolves({});
92124

@@ -102,7 +134,7 @@ test('on event calls updateTable with Delete', async () => {
102134
ReplicaUpdates: [
103135
{
104136
Delete: {
105-
RegionName: 'eu-west-2',
137+
RegionName: REGION,
106138
},
107139
},
108140
],
@@ -129,7 +161,7 @@ test('is complete for create returns false when replica is not active', async ()
129161
Table: {
130162
Replicas: [
131163
{
132-
RegionName: 'eu-west-2',
164+
RegionName: REGION,
133165
ReplicaStatus: 'CREATING',
134166
},
135167
],
@@ -148,7 +180,7 @@ test('is complete for create returns false when table is not active', async () =
148180
Table: {
149181
Replicas: [
150182
{
151-
RegionName: 'eu-west-2',
183+
RegionName: REGION,
152184
ReplicaStatus: 'ACTIVE',
153185
},
154186
],
@@ -168,7 +200,7 @@ test('is complete for create returns true when replica is active', async () => {
168200
Table: {
169201
Replicas: [
170202
{
171-
RegionName: 'eu-west-2',
203+
RegionName: REGION,
172204
ReplicaStatus: 'ACTIVE',
173205
},
174206
],

0 commit comments

Comments
 (0)
Please sign in to comment.