Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dynamodb): replicas not created on table replacement #13300

Merged
merged 9 commits into from
Mar 5, 2021
44 changes: 27 additions & 17 deletions packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,37 @@ import { DynamoDB } from 'aws-sdk'; // eslint-disable-line import/no-extraneous-
export async function onEventHandler(event: OnEventRequest): Promise<OnEventResponse> {
console.log('Event: %j', event);

/**
* Process only Create and Delete requests. We shouldn't receive any
* update request and in case we do there is nothing to update.
*/
const dynamodb = new DynamoDB();

let updateTableAction: 'Create' | 'Update' | 'Delete';

if (event.RequestType === 'Create' || event.RequestType === 'Delete') {
const dynamodb = new DynamoDB();

const data = await dynamodb.updateTable({
TableName: event.ResourceProperties.TableName,
ReplicaUpdates: [
{
[event.RequestType]: {
RegionName: event.ResourceProperties.Region,
},
updateTableAction = event.RequestType;
} else { // Update
// This can only be a table replacement so we create a replica
// in the new table. The replica for the "old" table will be
// deleted when CF issues a Delete event on the old physical
// resource id.
updateTableAction = 'Create';
}

const data = await dynamodb.updateTable({
TableName: event.ResourceProperties.TableName,
ReplicaUpdates: [
{
[updateTableAction]: {
RegionName: event.ResourceProperties.Region,
},
],
}).promise();
console.log('Update table: %j', data);
},
],
}).promise();
console.log('Update table: %j', data);

if (event.RequestType === 'Create' || event.RequestType === 'Update') {
return { PhysicalResourceId: `${event.ResourceProperties.TableName}-${event.ResourceProperties.Region}` };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually 2 functional changes, correct?

  1. Previously, we returned PhysicalResourceId also for deletes, now we don't.
  2. We used to return event.ResourceProperties.Region as the physical ID, now we return `${event.ResourceProperties.TableName}-${event.ResourceProperties.Region}`.

So, is 1. intentional, or accidental? And what about 2. - what will be the behavior for existing customers of global DB Tables?

Copy link
Contributor Author

@jogold jogold Mar 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For existing users of global DB tables if they update to the new the code the only thing that will happen is the replacement of the inline policies with managed policies. This is because nothing changes on the properties of the custom resource so it won't receive any event (Create/Update/Delete).

Now if later a user decides to remove one or more replica regions, the custom resource will receive a Delete event. If we don't change how the physical resource id is returned for a Delete event then CF will receive a new physical resource id (table-region instead of region). Changing the physical resource id in a Delete event is not allowed by CF and the Delete will fail (from a CF perspective and this also means that it will not wait for isComplete). So yes 1. is intentional. (this explanation is also valid for a table replacement when the old replica gets deleted).

Returning an empty object on delete ensures that the custom resource provider framework uses the physical resource id from the event (should have been done in the first place).

EDIT: it's the framework, not CF, that doesn't allow to change the physical resource id during a Delete event

throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${onEventResult.PhysicalResourceId}" during deletion`);

}

return { PhysicalResourceId: event.ResourceProperties.Region };
return {};
}

export async function isCompleteHandler(event: IsCompleteRequest): Promise<IsCompleteResponse> {
Expand Down
17 changes: 12 additions & 5 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1670,12 +1670,19 @@ interface ScalableAttributePair {
*/
class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable {
public readonly grantPrincipal: iam.IPrincipal;
public readonly policy: iam.IPolicy;
public readonly policy: iam.IManagedPolicy;

public constructor(sourceTable: Table, role: iam.IRole) {
super(sourceTable, `SourceTableAttachedPolicy-${Names.nodeUniqueId(role.node)}`);

const policy = new iam.Policy(this, 'Resource', { roles: [role] });
super(sourceTable, `SourceTableAttachedManagedPolicy-${Names.nodeUniqueId(role.node)}`);

const policy = new iam.ManagedPolicy(this, 'Resource', {
// A CF update of the description property of a managed policy requires
// a replacement. Use the table name in the description to force a managed
// policy replacement when the table name changes. This way we preserve permissions
// to delete old replicas in case of a table replacement.
description: `DynamoDB replication managed policy for table ${sourceTable.tableName}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an awesome trick! I'll have to remember this one to use in other Custom Resources.

I'm just a little worried about the delete order - since this depends on the Table, won't it be deleted first, before the replica?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine because it depends on the role used by onEventHandler and isCompleteHandler, see also #8224 for more context on this SourceTableAttachedPolicy

See also my test #13300 (comment).

roles: [role],
});
this.policy = policy;
this.grantPrincipal = new SourceTableAttachedPrincipal(role, policy);
}
Expand All @@ -1686,7 +1693,7 @@ class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable
* `SourceTableAttachedPolicy` class so it can act as an `IGrantable`.
*/
class SourceTableAttachedPrincipal extends iam.PrincipalBase {
public constructor(private readonly role: iam.IRole, private readonly policy: iam.Policy) {
public constructor(private readonly role: iam.IRole, private readonly policy: iam.ManagedPolicy) {
super();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF": {
"Type": "AWS::IAM::Policy",
"TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB": {
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
Expand Down Expand Up @@ -93,7 +93,18 @@
],
"Version": "2012-10-17"
},
"PolicyName": "leAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF",
"Description": {
"Fn::Join": [
"",
[
"DynamoDB replication managed policy for table ",
{
"Ref": "TableCD117FA1"
}
]
]
},
"Path": "/",
"Roles": [
{
"Fn::GetAtt": [
Expand All @@ -104,8 +115,8 @@
]
}
},
"TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D": {
"Type": "AWS::IAM::Policy",
"TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2": {
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
Expand All @@ -127,7 +138,18 @@
],
"Version": "2012-10-17"
},
"PolicyName": "ttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D",
"Description": {
"Fn::Join": [
"",
[
"DynamoDB replication managed policy for table ",
{
"Ref": "TableCD117FA1"
}
]
]
},
"Path": "/",
"Roles": [
{
"Fn::GetAtt": [
Expand All @@ -153,8 +175,8 @@
"Region": "us-east-2"
},
"DependsOn": [
"TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D",
"TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF",
"TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2",
"TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB",
"TableWriteScalingTargetE5669214",
"TableWriteScalingTargetTrackingD78DCCD8"
],
Expand All @@ -178,8 +200,8 @@
},
"DependsOn": [
"TableReplicauseast28A15C236",
"TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D",
"TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF",
"TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2",
"TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB",
"TableWriteScalingTargetE5669214",
"TableWriteScalingTargetTrackingD78DCCD8"
],
Expand Down Expand Up @@ -256,7 +278,7 @@
},
"/",
{
"Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7"
"Ref": "AssetParameters6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660dS3Bucket19B6A30C"
},
"/",
{
Expand All @@ -266,7 +288,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F"
"Ref": "AssetParameters6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660dS3VersionKey4D150293"
}
]
}
Expand All @@ -279,7 +301,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F"
"Ref": "AssetParameters6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660dS3VersionKey4D150293"
}
]
}
Expand All @@ -289,11 +311,11 @@
]
},
"Parameters": {
"referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket50997EC4Ref": {
"Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0"
"referencetoawscdkdynamodbglobalreplicasprovisionedAssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fS3Bucket99FDB11ARef": {
"Ref": "AssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fS3BucketA3328548"
},
"referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey0F47C425Ref": {
"Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275"
"referencetoawscdkdynamodbglobalreplicasprovisionedAssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fS3VersionKeyA9F26C9BRef": {
"Ref": "AssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fS3VersionKey30B00AEF"
},
"referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket6C51C355Ref": {
"Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1"
Expand Down Expand Up @@ -334,17 +356,17 @@
}
},
"Parameters": {
"AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0": {
"AssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fS3BucketA3328548": {
"Type": "String",
"Description": "S3 bucket for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\""
"Description": "S3 bucket for asset \"194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9f\""
},
"AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275": {
"AssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fS3VersionKey30B00AEF": {
"Type": "String",
"Description": "S3 key for asset version \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\""
"Description": "S3 key for asset version \"194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9f\""
},
"AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714ArtifactHash477AAEA7": {
"AssetParameters194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9fArtifactHashA6C4AE6F": {
"Type": "String",
"Description": "Artifact hash for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\""
"Description": "Artifact hash for asset \"194c9d621cd1021d904bc975179067fc56d895a7c14c6a55f8911906a68d7f9f\""
},
"AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": {
"Type": "String",
Expand All @@ -358,17 +380,17 @@
"Type": "String",
"Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\""
},
"AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7": {
"AssetParameters6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660dS3Bucket19B6A30C": {
"Type": "String",
"Description": "S3 bucket for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\""
"Description": "S3 bucket for asset \"6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660d\""
},
"AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F": {
"AssetParameters6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660dS3VersionKey4D150293": {
"Type": "String",
"Description": "S3 key for asset version \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\""
"Description": "S3 key for asset version \"6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660d\""
},
"AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdArtifactHash898696F1": {
"AssetParameters6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660dArtifactHash5E3F806E": {
"Type": "String",
"Description": "Artifact hash for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\""
"Description": "Artifact hash for asset \"6a97d2dd49c33ab70d22a730bbdd3ff9c020c0ae86916adb02b0183521e0660d\""
}
}
}
Loading