Skip to content

Commit

Permalink
Merge pull request #87 from rererecursive/feature/copy-encrypted-backups
Browse files Browse the repository at this point in the history
Support cross-account copying of encrypted RDS cluster snapshots.
  • Loading branch information
Guslington authored May 10, 2019
2 parents 036a50e + deaa151 commit 7bfd273
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 16 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Shelvery *does not* cater for backup restore process.
- Create and clean Redshift manual backups
- Get notified about all operations performed by Shelvery via SNS Topic
- Share backups with other accounts automatically
- Copying backups shared by other AWS accounts automatically
- Copying backups shared by other AWS accounts automatically, enabling a *data bunker* setup
- Copy backups to disaster recovery AWS regions
- Multiple levels of configuration, with priorities: Resource tags, Lambda payload, Environment Variables, Config defaults

Expand Down Expand Up @@ -278,7 +278,7 @@ Available configuration keys are listed below:
- `shelvery_dr_regions` - List of disaster recovery regions, comma separated
- `shelvery_wait_snapshot_timeout` - Timeout in seconds to wait for snapshot to become available before copying it
to another region or sharing with other account. Defaults to 1200 (20 minutes)
- `shelvery_share_aws_account_ids` - AWS Account Ids to share backups with. Applies to both original and regional backups
- `shelvery_share_aws_account_ids` - AWS Account Ids to share backups with. Applies to both original and regional backups
- `shelvery_source_aws_account_ids` - List of AWS Account Ids, comma seperated, that are exposing/sharing their shelvery
backups with account where shelvery is running. This can be used for having DR aws account that aggregates backups
from other accounts.
Expand Down Expand Up @@ -391,6 +391,14 @@ $ export shelvery_source_aws_account_ids=222222222222,333333333333
$ shelvery ebs pull_shared_backups
```

## Cross-account copying of encrypted backups

Shelvery enables the sharing and copying of encrypted backups between accounts. In order to configure this, you must share the backup's KMS key and follow the instructions in step #2 above.
For instructions on how to share the KMS key, see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ShareSnapshot.html

This is currently only supported for:
- RDS clusters

## Waiting on backups to complete

By default shelvery will wait by sleeping and then querying the aws api for a complete status.
Expand Down
3 changes: 2 additions & 1 deletion shelvery/backup_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class BackupResource:
RETENTION_MONTHLY = 'monthly'
RETENTION_YEARLY = 'yearly'

def __init__(self, tag_prefix, entity_resource: EntityResource, construct=False, copy_resource_tags=True, exluded_resource_tag_keys=[]):
def __init__(self, tag_prefix, entity_resource: EntityResource, construct=False, copy_resource_tags=True, exluded_resource_tag_keys=[], resource_properties={}):
"""Construct new backup resource out of entity resource (e.g. ebs volume)."""
# if object manually created
if construct:
Expand Down Expand Up @@ -86,6 +86,7 @@ def __init__(self, tag_prefix, entity_resource: EntityResource, construct=False,
self.backup_id = None
self.expire_date = None
self.date_deleted = None
self.resource_properties = resource_properties

def cross_account_copy(self, new_backup_id):
backup = copy.deepcopy(self)
Expand Down
13 changes: 10 additions & 3 deletions shelvery/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def create_backups(self) -> List[BackupResource]:
backup_resource.tags[f"{RuntimeConfig.get_tag_prefix()}:dr_regions"] = ','.join(dr_regions)
self.logger.info(f"Processing {resource_type} with id {r.resource_id}")
self.logger.info(f"Creating backup {backup_resource.name}")

try:
self.backup_resource(backup_resource)
self.tag_backup_resource(backup_resource)
Expand Down Expand Up @@ -297,12 +298,18 @@ def clean_backups(self):
self.logger.exception(f"Error checking backup {backup.backup_id} for cleanup: {e}")

def pull_shared_backups(self):

account_id = self.account_id
s3_client = AwsHelper.boto3_client('s3')
for src_account_id in RuntimeConfig.get_source_backup_accounts(self):
accounts = RuntimeConfig.get_source_backup_accounts(self)

if not accounts:
self.logger.info("No shared backups will be pulled as no account IDs were specified to pull from.")
return

for src_account_id in accounts:
try:
bucket_name = self.get_remote_bucket_name(src_account_id)
self.logger.info(f"Pulling shared backup data from S3 bucket: {bucket_name}")
path = f"backups/shared/{account_id}/{self.get_engine_type()}/"
path_processed = f"backups/shared/{account_id}/{self.get_engine_type()}-processed"
path_failed = f"backups/shared/{account_id}/{self.get_engine_type()}-failed"
Expand Down Expand Up @@ -355,7 +362,7 @@ def pull_shared_backups(self):
})
except Exception as e:
backup_name = backup_object['Key'].split('/')[-1].replace('.yaml', '')
self.logger.exception(f"Failed to copy shared backup s3://{bucket_name}/{backup_object['Key']}")
self.logger.exception(f"Failed to copy shared backup '{backup_name}' specified in s3://{bucket_name}/{backup_object['Key']}")
self.snspublisher_error.notify({
'Operation': 'PullSharedBackup',
'Status': 'ERROR',
Expand Down
31 changes: 22 additions & 9 deletions shelvery/rds_cluster_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,22 @@ def backup_from_latest_automated(self, backup_resource: BackupResource):

# TODO handle case when there are no latest automated backups
automated_snapshot_id = auto_snapshots[0]['DBClusterSnapshotIdentifier']
rds_client.copy_db_cluster_snapshot(
response = rds_client.copy_db_cluster_snapshot(
SourceDBClusterSnapshotIdentifier=automated_snapshot_id,
TargetDBClusterSnapshotIdentifier=backup_resource.name,
CopyTags=False
)
backup_resource.resource_properties = response['DBClusterSnapshot']
backup_resource.backup_id = backup_resource.name
return backup_resource

def backup_from_cluster(self, backup_resource):
rds_client = AwsHelper.boto3_client('rds', arn=self.role_arn, external_id=self.role_external_id)
rds_client.create_db_cluster_snapshot(
response = rds_client.create_db_cluster_snapshot(
DBClusterSnapshotIdentifier=backup_resource.name,
DBClusterIdentifier=backup_resource.entity_id
)
backup_resource.resource_properties = response['DBClusterSnapshot']
backup_resource.backup_id = backup_resource.name
return backup_resource

Expand Down Expand Up @@ -119,12 +121,21 @@ def copy_shared_backup(self, source_account: str, source_backup: BackupResource)
rds_client = AwsHelper.boto3_client('rds', arn=self.role_arn, external_id=self.role_external_id)
# copying of tags happens outside this method
source_arn = f"arn:aws:rds:{source_backup.region}:{source_backup.account_id}:cluster-snapshot:{source_backup.backup_id}"
snap = rds_client.copy_db_cluster_snapshot(
SourceDBClusterSnapshotIdentifier=source_arn,
SourceRegion=source_backup.region,
CopyTags=False,
TargetDBClusterSnapshotIdentifier=source_backup.backup_id
)

params = {
'SourceDBClusterSnapshotIdentifier': source_arn,
'SourceRegion': source_backup.region,
'CopyTags': False,
'TargetDBClusterSnapshotIdentifier': source_backup.backup_id
}

# If the backup is encrypted, include the KMS key ID in the request.
if source_backup.resource_properties['StorageEncrypted']:
kms_key = source_backup.resource_properties['KmsKeyId']
self.logger.info(f"Snapshot {source_backup.backup_id} is encrypted. Copying backup with KMS key {kms_key} ...")
params['KmsKeyId'] = kms_key

snap = rds_client.copy_db_cluster_snapshot(**params)
return snap['DBClusterSnapshot']['DBClusterSnapshotIdentifier']

def get_backup_resource(self, backup_region: str, backup_id: str) -> BackupResource:
Expand All @@ -133,7 +144,9 @@ def get_backup_resource(self, backup_region: str, backup_id: str) -> BackupResou
snapshot = snapshots['DBClusterSnapshots'][0]
tags = rds_client.list_tags_for_resource(ResourceName=snapshot['DBClusterSnapshotArn'])['TagList']
d_tags = dict(map(lambda t: (t['Key'], t['Value']), tags))
return BackupResource.construct(d_tags['shelvery:tag_name'], backup_id, d_tags)
resource = BackupResource.construct(d_tags['shelvery:tag_name'], backup_id, d_tags)
resource.resource_properties = snapshot
return resource

def get_engine_type(self) -> str:
return 'rds_cluster'
Expand Down
2 changes: 1 addition & 1 deletion shelvery/runtime_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def get_source_backup_accounts(cls, shelvery):
shelvery.logger.warn(f"Account id {acc} is not 12-digit number, skipping for share")
else:
rval.append(acc)
shelvery.logger.info(f"Collected account {acc} to share backups with")
shelvery.logger.info(f"Collected account {acc} to collect backups from")

return rval

Expand Down

0 comments on commit 7bfd273

Please sign in to comment.