From ace6611c0ea6dcf9dbf3cf6ae66293a5394eafa2 Mon Sep 17 00:00:00 2001 From: Victor San Kho Lin Date: Tue, 10 Dec 2024 12:14:45 +1100 Subject: [PATCH] Implemented OrcaBus database tier-2 backup retention * Leveraged AWS Backup service to set up long term tier-2 backup retention in production. Assert with test harness to make sure compliance rule is enforced in production configuration. Resolves #178 --- config/stacks/shared.ts | 3 ++ .../shared/constructs/database/index.ts | 49 ++++++++++++++++++ test/stateful/pipeline/deployment.test.ts | 1 + .../stateful/shared/databaseConstruct.test.ts | 51 +++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/config/stacks/shared.ts b/config/stacks/shared.ts index 9ee30617d..a7de0c4a2 100644 --- a/config/stacks/shared.ts +++ b/config/stacks/shared.ts @@ -156,6 +156,7 @@ const getDatabaseConstructProps = (stage: AppStage): ConfigurableDatabaseProps = enablePerformanceInsights: true, removalPolicy: RemovalPolicy.DESTROY, backupRetention: Duration.days(1), + createT2BackupRetention: false, }; case AppStage.GAMMA: return { @@ -167,6 +168,7 @@ const getDatabaseConstructProps = (stage: AppStage): ConfigurableDatabaseProps = enablePerformanceInsights: true, removalPolicy: RemovalPolicy.DESTROY, backupRetention: Duration.days(1), + createT2BackupRetention: false, }; case AppStage.PROD: return { @@ -178,6 +180,7 @@ const getDatabaseConstructProps = (stage: AppStage): ConfigurableDatabaseProps = enablePerformanceInsights: true, removalPolicy: RemovalPolicy.RETAIN, backupRetention: Duration.days(7), + createT2BackupRetention: true, }; } }; diff --git a/lib/workload/stateful/stacks/shared/constructs/database/index.ts b/lib/workload/stateful/stacks/shared/constructs/database/index.ts index be7ed23f9..436e0e45c 100644 --- a/lib/workload/stateful/stacks/shared/constructs/database/index.ts +++ b/lib/workload/stateful/stacks/shared/constructs/database/index.ts @@ -4,6 +4,8 @@ import * as rds from 'aws-cdk-lib/aws-rds'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as sm from 'aws-cdk-lib/aws-secretsmanager'; +import * as backup from 'aws-cdk-lib/aws-backup'; +import * as events from 'aws-cdk-lib/aws-events'; import { SecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { DatabaseCluster } from 'aws-cdk-lib/aws-rds'; @@ -90,9 +92,17 @@ export type ConfigurableDatabaseProps = MonitoringProps & { */ secretRotationSchedule: Duration; /** + * Tier 1 backup - using built-in RDS system capability + * * RDS aurora automated backup retention (in Duration) */ backupRetention: Duration; + /** + * Tier 2 backup - leveraging another AWS Backup service is intentional redundancy + * + * Create long term tier-2 RDS aurora backup using AWS Backup service (in boolean) + */ + createT2BackupRetention?: boolean; }; /** @@ -199,5 +209,44 @@ export class DatabaseConstruct extends Construct { description: 'orcabus rds writer cluster endpoint host', parameterName: props.clusterEndpointHostParameterName, }); + + /** + * See compliance rule + * https://trello.com/c/RFnECxRa + * https://github.com/umccr/orcabus/issues/178 + * + * Backup weekly and keep it for 6 weeks + * Cron At 17:00 on every Sunday UTC = AEST/AEDT 3AM/4AM on every Monday + * cron(0 17 ? * SUN *) + */ + if (props.createT2BackupRetention) { + const t2BackupVault = new backup.BackupVault(this, 'OrcaBusDatabaseTier2BackupVault', { + backupVaultName: 'OrcaBusDatabaseTier2BackupVault', + removalPolicy: RemovalPolicy.RETAIN, + }); + + const t2BackupPlan = new backup.BackupPlan(this, 'OrcaBusDatabaseTier2BackupPlan', { + backupPlanName: 'OrcaBusDatabaseTier2BackupPlan', + backupVault: t2BackupVault, + }); + t2BackupPlan.applyRemovalPolicy(RemovalPolicy.RETAIN); + + // https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-backup/lib/rule.ts + t2BackupPlan.addRule( + new backup.BackupPlanRule({ + ruleName: 'Weekly', + scheduleExpression: events.Schedule.cron({ + hour: '17', + minute: '0', + weekDay: 'SUN', + }), + deleteAfter: Duration.days(42), + }) + ); + + t2BackupPlan.addSelection('OrcaBusDatabaseTier2BackupSelection', { + resources: [backup.BackupResource.fromRdsServerlessCluster(this.cluster)], + }); + } } } diff --git a/test/stateful/pipeline/deployment.test.ts b/test/stateful/pipeline/deployment.test.ts index 54149025a..03e074b1e 100644 --- a/test/stateful/pipeline/deployment.test.ts +++ b/test/stateful/pipeline/deployment.test.ts @@ -111,6 +111,7 @@ function applyNagSuppression(stackId: string, stack: Stack) { '/SharedStack/EventBusConstruct/UniversalEventArchiver/UniversalEventArchiver/ServiceRole/Resource', '/SharedStack/EventBusConstruct/UniversalEventArchiver/UniversalEventArchiver/ServiceRole/DefaultPolicy/Resource', '/SharedStack/DatabaseConstruct/Cluster/MonitoringRole/Resource', + '/SharedStack/DatabaseConstruct/OrcaBusDatabaseTier2BackupPlan/OrcaBusDatabaseTier2BackupSelection/Role/Resource', ], [ { diff --git a/test/stateful/shared/databaseConstruct.test.ts b/test/stateful/shared/databaseConstruct.test.ts index 4f7f9cd96..60683265f 100644 --- a/test/stateful/shared/databaseConstruct.test.ts +++ b/test/stateful/shared/databaseConstruct.test.ts @@ -65,3 +65,54 @@ test('Test other SG Allow Ingress to DB SG', () => { }, }); }); + +test('Test tier-2 backup created for DBCluster and it is compliance in production configuration', () => { + const prodConfig = getEnvironmentConfig(AppStage.PROD); + if (!prodConfig) throw new Error('No construct config for the test'); + + expect(prodConfig).toBeTruthy(); + const dbProps = prodConfig.stackProps.statefulConfig.sharedStackProps.databaseProps; + + new DatabaseConstruct(stack, 'TestDatabaseConstruct', { + vpc, + ...dbProps, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::RDS::DBCluster', { + DBClusterIdentifier: 'orcabus-db', + DatabaseName: 'orcabus', + DBClusterParameterGroupName: dbProps.parameterGroupName, + ServerlessV2ScalingConfiguration: { + MaxCapacity: dbProps.maxACU, + MinCapacity: dbProps.minACU, + }, + }); + + // Assert that we are compliance with long term tier-2 backup plan + + template.hasResource('AWS::Backup::BackupVault', { + DeletionPolicy: 'Retain', + }); + template.hasResourceProperties('AWS::Backup::BackupVault', { + BackupVaultName: 'OrcaBusDatabaseTier2BackupVault', + }); + + template.hasResource('AWS::Backup::BackupPlan', { + DeletionPolicy: 'Retain', + }); + template.hasResourceProperties('AWS::Backup::BackupPlan', { + BackupPlan: { + BackupPlanName: 'OrcaBusDatabaseTier2BackupPlan', + BackupPlanRule: [ + { + Lifecycle: { + DeleteAfterDays: 42, + }, + RuleName: 'Weekly', + ScheduleExpression: 'cron(0 17 ? * SUN *)', + }, + ], + }, + }); +});