Skip to content

Commit

Permalink
[DBInstance] Add support for AutomatedBackupReplication (#446)
Browse files Browse the repository at this point in the history
* [DBInstance] Add support for AutomatedBackupReplication

* [DBInstance] Address comments and nits

* [DBInstance] Address comments and nits in tests

* Address more comments

* Add missing exception

---------

Co-authored-by: Valentin Shirshov <[email protected]>
  • Loading branch information
khebul and Valentin Shirshov authored Sep 28, 2023
1 parent 4966a17 commit 65307fd
Show file tree
Hide file tree
Showing 15 changed files with 583 additions and 2 deletions.
7 changes: 7 additions & 0 deletions aws-rds-dbinstance/aws-rds-dbinstance.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@
"type": "boolean",
"description": "A value that indicates whether minor engine upgrades are applied automatically to the DB instance during the maintenance window. By default, minor engine upgrades are applied automatically."
},
"AutomaticBackupReplicationRegion": {
"type": "string",
"description": "Enables replication of automated backups to a different Amazon Web Services Region."
},
"AvailabilityZone": {
"type": "string",
"description": "The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones."
Expand Down Expand Up @@ -578,6 +582,7 @@
"rds:RebootDBInstance",
"rds:RestoreDBInstanceFromDBSnapshot",
"rds:RestoreDBInstanceToPointInTime",
"rds:StartDBInstanceAutomatedBackupsReplication",
"secretsmanager:CreateSecret",
"secretsmanager:TagResource"
],
Expand Down Expand Up @@ -622,6 +627,8 @@
"rds:RebootDBInstance",
"rds:RemoveRoleFromDBInstance",
"rds:RemoveTagsFromResource",
"rds:StartDBInstanceAutomatedBackupsReplication",
"rds:StopDBInstanceAutomatedBackupsReplication",
"secretsmanager:CreateSecret",
"secretsmanager:TagResource"
],
Expand Down
12 changes: 12 additions & 0 deletions aws-rds-dbinstance/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy
"<a href="#allowmajorversionupgrade" title="AllowMajorVersionUpgrade">AllowMajorVersionUpgrade</a>" : <i>Boolean</i>,
"<a href="#associatedroles" title="AssociatedRoles">AssociatedRoles</a>" : <i>[ <a href="dbinstancerole.md">DBInstanceRole</a>, ... ]</i>,
"<a href="#autominorversionupgrade" title="AutoMinorVersionUpgrade">AutoMinorVersionUpgrade</a>" : <i>Boolean</i>,
"<a href="#automaticbackupreplicationregion" title="AutomaticBackupReplicationRegion">AutomaticBackupReplicationRegion</a>" : <i>String</i>,
"<a href="#availabilityzone" title="AvailabilityZone">AvailabilityZone</a>" : <i>String</i>,
"<a href="#backupretentionperiod" title="BackupRetentionPeriod">BackupRetentionPeriod</a>" : <i>Integer</i>,
"<a href="#cacertificateidentifier" title="CACertificateIdentifier">CACertificateIdentifier</a>" : <i>String</i>,
Expand Down Expand Up @@ -100,6 +101,7 @@ Properties:
<a href="#associatedroles" title="AssociatedRoles">AssociatedRoles</a>: <i>
- <a href="dbinstancerole.md">DBInstanceRole</a></i>
<a href="#autominorversionupgrade" title="AutoMinorVersionUpgrade">AutoMinorVersionUpgrade</a>: <i>Boolean</i>
<a href="#automaticbackupreplicationregion" title="AutomaticBackupReplicationRegion">AutomaticBackupReplicationRegion</a>: <i>String</i>
<a href="#availabilityzone" title="AvailabilityZone">AvailabilityZone</a>: <i>String</i>
<a href="#backupretentionperiod" title="BackupRetentionPeriod">BackupRetentionPeriod</a>: <i>Integer</i>
<a href="#cacertificateidentifier" title="CACertificateIdentifier">CACertificateIdentifier</a>: <i>String</i>
Expand Down Expand Up @@ -222,6 +224,16 @@ _Type_: Boolean

_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt)

#### AutomaticBackupReplicationRegion

Enables replication of automated backups to a different Amazon Web Services Region.

_Required_: No

_Type_: String

_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)

#### AvailabilityZone

The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones.
Expand Down
2 changes: 2 additions & 0 deletions aws-rds-dbinstance/resource-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Resources:
- "rds:RemoveTagsFromResource"
- "rds:RestoreDBInstanceFromDBSnapshot"
- "rds:RestoreDBInstanceToPointInTime"
- "rds:StartDBInstanceAutomatedBackupsReplication"
- "rds:StopDBInstanceAutomatedBackupsReplication"
- "secretsmanager:CreateSecret"
- "secretsmanager:TagResource"
Resource: "*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.amazonaws.arn.Arn;
import org.apache.commons.lang3.BooleanUtils;

import com.amazonaws.util.CollectionUtils;
Expand Down Expand Up @@ -56,6 +57,7 @@
import software.amazon.awssdk.services.rds.model.InstanceQuotaExceededException;
import software.amazon.awssdk.services.rds.model.InsufficientDbInstanceCapacityException;
import software.amazon.awssdk.services.rds.model.InvalidDbClusterStateException;
import software.amazon.awssdk.services.rds.model.InvalidDbInstanceAutomatedBackupStateException;
import software.amazon.awssdk.services.rds.model.InvalidDbInstanceStateException;
import software.amazon.awssdk.services.rds.model.InvalidDbSecurityGroupStateException;
import software.amazon.awssdk.services.rds.model.InvalidDbSnapshotStateException;
Expand Down Expand Up @@ -90,6 +92,7 @@
import software.amazon.rds.common.handler.Events;
import software.amazon.rds.common.handler.HandlerConfig;
import software.amazon.rds.common.handler.HandlerMethod;
import software.amazon.rds.common.handler.Probing;
import software.amazon.rds.common.handler.Tagging;
import software.amazon.rds.common.logging.LoggingProxyClient;
import software.amazon.rds.common.logging.RequestLogger;
Expand Down Expand Up @@ -295,6 +298,14 @@ public abstract class BaseHandlerStd extends BaseHandler<CallbackContext> {
InvalidDbInstanceStateException.class)
.build();

protected static final ErrorRuleSet MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET = ErrorRuleSet
.extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET)
.withErrorClasses(ErrorStatus.ignore(OperationStatus.IN_PROGRESS),
InvalidDbInstanceAutomatedBackupStateException.class)
.withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ServiceLimitExceeded),
DbInstanceAutomatedBackupQuotaExceededException.class)
.build();

protected static final ErrorRuleSet MODIFY_DB_INSTANCE_ERROR_RULE_SET = ErrorRuleSet
.extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET)
.withErrorCodes(ErrorStatus.failWith(HandlerErrorCode.ResourceConflict),
Expand Down Expand Up @@ -684,6 +695,28 @@ protected boolean isDBInstanceStabilizedAfterMutate(
return isDBInstanceStabilizedAfterMutateResult;
}

protected boolean isInstanceStabilizedAfterReplicationStop(final ProxyClient<RdsClient> rdsProxyClient,
final ResourceModel model) {
final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model);

assertNoTerminalStatus(dbInstance);
return isDBInstanceAvailable(dbInstance)
&& !dbInstance.hasDbInstanceAutomatedBackupsReplications();
}

protected boolean isInstanceStabilizedAfterReplicationStart(final ProxyClient<RdsClient> rdsProxyClient,
final ResourceModel model) {
final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model);

assertNoTerminalStatus(dbInstance);
return isDBInstanceAvailable(dbInstance)
&& dbInstance.hasDbInstanceAutomatedBackupsReplications() &&
!dbInstance.dbInstanceAutomatedBackupsReplications().isEmpty() &&
model.getAutomaticBackupReplicationRegion()
.equalsIgnoreCase(
Arn.fromString(dbInstance.dbInstanceAutomatedBackupsReplications().get(0).dbInstanceAutomatedBackupsArn()).getRegion());
}

protected boolean isDBInstanceStabilizedAfterReboot(
final ProxyClient<RdsClient> rdsProxyClient,
final ResourceModel model
Expand Down Expand Up @@ -1053,4 +1086,56 @@ protected ProgressEvent<ResourceModel, CallbackContext> versioned(
}
return methodVersions.get(apiVersion).invoke(proxy, rdsProxyClient.forVersion(apiVersion), progress, allTags);
}

protected ProgressEvent<ResourceModel, CallbackContext> stopAutomaticBackupReplicationInRegion(
final String dbInstanceArn,
final AmazonWebServicesClientProxy proxy,
final ProgressEvent<ResourceModel, CallbackContext> progress,
final ProxyClient<RdsClient> sourceRegionClient,
final String region
) {
final ProxyClient<RdsClient> rdsClient = proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(region));

return proxy.initiate("rds::stop-db-instance-automatic-backup-replication", rdsClient, progress.getResourceModel(), progress.getCallbackContext())
.translateToServiceRequest(resourceModel -> Translator.stopDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn))
.backoffDelay(config.getBackoff())
.makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2(
request,
rdsClient.client()::stopDBInstanceAutomatedBackupsReplication
))
.stabilize((request, response, client, model, context) ->
isInstanceStabilizedAfterReplicationStop(sourceRegionClient, model))
.handleError((request, exception, client, model, context) -> Commons.handleException(
ProgressEvent.progress(model, context),
exception,
MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET
))
.progress();
}

protected ProgressEvent<ResourceModel, CallbackContext> startAutomaticBackupReplicationInRegion(
final String dbInstanceArn,
final AmazonWebServicesClientProxy proxy,
final ProgressEvent<ResourceModel, CallbackContext> progress,
final ProxyClient<RdsClient> sourceRegionClient,
final String region
) {
final ProxyClient<RdsClient> rdsClient = proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(region));

return proxy.initiate("rds::start-db-instance-automatic-backup-replication", rdsClient, progress.getResourceModel(), progress.getCallbackContext())
.translateToServiceRequest(resourceModel -> Translator.startDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn))
.backoffDelay(config.getBackoff())
.makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2(
request,
rdsClient.client()::startDBInstanceAutomatedBackupsReplication
))
.stabilize((request, response, proxyInvocation, model, context) ->
isInstanceStabilizedAfterReplicationStart(sourceRegionClient, model))
.handleError((request, exception, client, model, context) -> Commons.handleException(
ProgressEvent.progress(model, context),
exception,
MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET
))
.progress();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ public class CallbackContext extends StdCallbackContext implements TaggingContex
private boolean storageAllocated;
private boolean allocatingStorage;
private boolean readReplicaPromoted;
private boolean automaticBackupReplicationStopped;
private boolean automaticBackupReplicationStarted;
private String dbInstanceArn;

private TaggingContext taggingContext;

private Map<String, Long> timestamps;

public CallbackContext() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import org.apache.commons.lang3.BooleanUtils;

import com.amazonaws.util.StringUtils;
import software.amazon.awssdk.services.ec2.Ec2Client;
import software.amazon.awssdk.services.rds.RdsClient;
import software.amazon.awssdk.services.rds.model.DBInstance;
import software.amazon.awssdk.services.rds.model.DBSnapshot;
import software.amazon.awssdk.services.rds.model.SourceType;
import software.amazon.awssdk.utils.ImmutableMap;
Expand All @@ -27,6 +32,7 @@
import software.amazon.rds.common.request.Validations;
import software.amazon.rds.common.util.IdentifierFactory;
import software.amazon.rds.dbinstance.client.ApiVersion;
import software.amazon.rds.dbinstance.client.RdsClientProvider;
import software.amazon.rds.dbinstance.client.VersionedProxyClient;
import software.amazon.rds.dbinstance.util.ResourceModelHelper;

Expand Down Expand Up @@ -169,6 +175,22 @@ ApiVersion.DEFAULT, safeAddTags(this::createDbInstance)
.then(progress -> Commons.execOnce(progress, () ->
updateAssociatedRoles(proxy, rdsProxyClient.defaultClient(), progress, Collections.emptyList(), desiredRoles),
CallbackContext::isUpdatedRoles, CallbackContext::setUpdatedRoles))
.then(progress -> Commons.execOnce(progress, () -> {
if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())
&& StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn())) {
final DBInstance dbInstance = fetchDBInstance(rdsProxyClient.defaultClient(), progress.getResourceModel());
callbackContext.setDbInstanceArn(dbInstance.dbInstanceArn());
}
return progress;
}, (m) -> !StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn()), (v, c) -> {}))
.then(progress -> Commons.execOnce(progress, () -> {
if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) {
return startAutomaticBackupReplicationInRegion(callbackContext.getDbInstanceArn(), proxy, progress, rdsProxyClient.defaultClient(),
ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState()));
}
return progress;
},
CallbackContext::isAutomaticBackupReplicationStarted, CallbackContext::setAutomaticBackupReplicationStarted))
.then(progress -> {
model.setTags(Translator.translateTagsFromSdk(Tagging.translateTagsToSdk(allTags)));
return Commons.reportResourceDrift(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.amazonaws.arn.Arn;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
Expand All @@ -42,6 +43,8 @@
import software.amazon.awssdk.services.rds.model.RemoveRoleFromDbInstanceRequest;
import software.amazon.awssdk.services.rds.model.RestoreDbInstanceFromDbSnapshotRequest;
import software.amazon.awssdk.services.rds.model.RestoreDbInstanceToPointInTimeRequest;
import software.amazon.awssdk.services.rds.model.StartDbInstanceAutomatedBackupsReplicationRequest;
import software.amazon.awssdk.services.rds.model.StopDbInstanceAutomatedBackupsReplicationRequest;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.rds.common.handler.Commons;
import software.amazon.rds.common.handler.Tagging;
Expand Down Expand Up @@ -705,6 +708,22 @@ public static DescribeDbEngineVersionsRequest describeDbEngineVersionsRequest(
.build();
}

public static StartDbInstanceAutomatedBackupsReplicationRequest startDbInstanceAutomatedBackupsReplicationRequest(
final String dbInstanceArn
) {
return StartDbInstanceAutomatedBackupsReplicationRequest.builder()
.sourceDBInstanceArn(dbInstanceArn)
.build();
}

public static StopDbInstanceAutomatedBackupsReplicationRequest stopDbInstanceAutomatedBackupsReplicationRequest(
final String dbInstanceArn
) {
return StopDbInstanceAutomatedBackupsReplicationRequest.builder()
.sourceDBInstanceArn(dbInstanceArn)
.build();
}

public static List<ResourceModel> translateDbInstancesFromSdk(
final List<software.amazon.awssdk.services.rds.model.DBInstance> dbInstances
) {
Expand Down Expand Up @@ -764,9 +783,16 @@ public static ResourceModel.ResourceModelBuilder translateDbInstanceFromSdkBuild
optionGroupName = dbInstance.optionGroupMemberships().get(0).optionGroupName();
}

String automatedReplicationRegion = null;
if (dbInstance.hasDbInstanceAutomatedBackupsReplications() && !dbInstance.dbInstanceAutomatedBackupsReplications().isEmpty()) {
automatedReplicationRegion = Arn.fromString(dbInstance.dbInstanceAutomatedBackupsReplications()
.get(0).dbInstanceAutomatedBackupsArn()).getRegion();
}

return ResourceModel.builder()
.allocatedStorage(allocatedStorage)
.associatedRoles(translateAssociatedRolesFromSdk(dbInstance.associatedRoles()))
.automaticBackupReplicationRegion(automatedReplicationRegion)
.autoMinorVersionUpgrade(dbInstance.autoMinorVersionUpgrade())
.availabilityZone(dbInstance.availabilityZone())
.backupRetentionPeriod(dbInstance.backupRetentionPeriod())
Expand Down
Loading

0 comments on commit 65307fd

Please sign in to comment.