BackupFactory.java
package com.cloudforgeci.api.storage;
import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.enums.AwsRegion;
import com.cloudforge.core.enums.SecurityProfile;
import io.github.cdklabs.cdknag.NagPackSuppression;
import io.github.cdklabs.cdknag.NagSuppressions;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.RemovalPolicy;
import software.amazon.awscdk.services.backup.BackupPlan;
import software.amazon.awscdk.services.backup.BackupPlanRule;
import software.amazon.awscdk.services.backup.BackupResource;
import software.amazon.awscdk.services.backup.BackupSelectionOptions;
import software.amazon.awscdk.services.backup.BackupVault;
import software.amazon.awscdk.services.events.CronOptions;
import software.amazon.awscdk.services.events.Schedule;
import software.constructs.Construct;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* Factory for creating AWS Backup resources for EFS and RDS.
*
* <p>This factory creates backup infrastructure based on security profile settings:</p>
* <ul>
* <li><strong>Backup Vault:</strong> Encrypted vault for storing backups</li>
* <li><strong>Backup Plan:</strong> Daily backups with configurable retention</li>
* <li><strong>Backup Selection:</strong> Targets EFS and RDS resources in the stack</li>
* <li><strong>Cross-Region Copy:</strong> Optional geographic redundancy (PRODUCTION)</li>
* </ul>
*
* <p><strong>Compliance Coverage:</strong></p>
* <ul>
* <li>SOC2-A1.3: Automated backups for disaster recovery</li>
* <li>PCI-DSS: efs-resources-protected-by-backup-plan</li>
* <li>HIPAA: Data backup and recovery requirements</li>
* </ul>
*
* <p><strong>Security Profile Behavior:</strong></p>
* <ul>
* <li><strong>DEV:</strong> Backups disabled by default</li>
* <li><strong>STAGING:</strong> Daily backups, 14-day retention</li>
* <li><strong>PRODUCTION:</strong> Daily backups, 90-day retention, cross-region copy</li>
* </ul>
*
* @see com.cloudforgeci.api.interfaces.SecurityProfileConfiguration#isAutomatedBackupEnabled()
* @see com.cloudforgeci.api.interfaces.SecurityProfileConfiguration#getBackupRetentionDays()
*/
public class BackupFactory extends BaseFactory {
private static final Logger LOG = Logger.getLogger(BackupFactory.class.getName());
@SystemContext("security")
private SecurityProfile security;
@SystemContext("stackName")
private String stackName;
public BackupFactory(Construct scope, String id) {
super(scope, id);
}
@Override
public void create() {
// Check if automated backups are enabled for this security profile
if (!config.isAutomatedBackupEnabled()) {
LOG.info("Automated backups disabled for security profile: " + security);
return;
}
LOG.info("Creating AWS Backup infrastructure for security profile: " + security);
// Create backup vault
BackupVault vault = createBackupVault();
// Create backup plan with rules
BackupPlan plan = createBackupPlan(vault);
// Add resources to backup
addBackupResources(plan);
LOG.info("AWS Backup infrastructure created successfully");
LOG.info(" Retention: " + config.getBackupRetentionDays() + " days");
LOG.info(" Cross-region copy: " + config.isCrossRegionBackupEnabled());
}
/**
* Creates an encrypted backup vault.
* Vault name must be 2-50 characters, alphanumeric with hyphens and underscores only.
*/
private BackupVault createBackupVault() {
String vaultName = sanitizeResourceName(stackName, "-vault");
// Determine removal policy based on security profile configuration
RemovalPolicy removalPolicy = config.isBackupVaultRetentionEnabled()
? RemovalPolicy.RETAIN
: RemovalPolicy.DESTROY;
BackupVault.Builder builder = BackupVault.Builder.create(this, "BackupVault")
.backupVaultName(vaultName)
// AWS Backup encrypts with AWS-managed key by default
// For customer-managed KMS key, add .encryptionKey(key)
.removalPolicy(removalPolicy);
// Add vault lock based on security profile configuration
if (config.isBackupVaultLockEnabled()) {
int retentionDays = config.getBackupRetentionDays();
builder.lockConfiguration(software.amazon.awscdk.services.backup.LockConfiguration.builder()
.minRetention(Duration.days(retentionDays))
// AWS Backup requires minimum 72-hour cooling-off period before lock becomes immutable
.changeableFor(Duration.days(3))
.build());
LOG.info("Vault lock enabled for compliance: min retention = " + retentionDays + " days, changeable for 3 days");
}
BackupVault vault = builder.build();
LOG.info("Created backup vault: " + vaultName + " (RemovalPolicy: " + removalPolicy + ")");
return vault;
}
/**
* Creates a backup plan with daily backup rules.
* Plan name must follow AWS naming conventions.
*/
private BackupPlan createBackupPlan(BackupVault vault) {
String planName = sanitizeResourceName(stackName, "-plan");
int retentionDays = config.getBackupRetentionDays();
List<BackupPlanRule> rules = new ArrayList<>();
// Daily backup rule
BackupPlanRule.Builder dailyRuleBuilder = BackupPlanRule.Builder.create()
.ruleName("DailyBackup")
.backupVault(vault)
.scheduleExpression(Schedule.cron(CronOptions.builder()
.hour("3") // 3 AM UTC
.minute("0")
.build()))
.startWindow(Duration.hours(1)) // Must start within 1 hour
.completionWindow(Duration.hours(8)) // Must complete within 8 hours
.deleteAfter(Duration.days(retentionDays));
// Add cross-region copy for PRODUCTION if enabled
if (config.isCrossRegionBackupEnabled() && security == SecurityProfile.PRODUCTION) {
AwsRegion.getSecondaryRegion(cfc.region()).ifPresent(destinationRegion -> {
LOG.info("Cross-region backup copy enabled to: " + destinationRegion);
// Note: Cross-region copy requires a backup vault in the destination region
// This is handled at the AWS Backup level, not CDK
// The copy is configured via BackupPlanRule.copyActions()
});
}
rules.add(dailyRuleBuilder.build());
// Weekly backup with longer retention for PRODUCTION
if (security == SecurityProfile.PRODUCTION) {
BackupPlanRule weeklyRule = BackupPlanRule.Builder.create()
.ruleName("WeeklyBackup")
.backupVault(vault)
.scheduleExpression(Schedule.cron(CronOptions.builder()
.weekDay("SUN")
.hour("2") // 2 AM UTC on Sundays
.minute("0")
.build()))
.startWindow(Duration.hours(1))
.completionWindow(Duration.hours(12))
.deleteAfter(Duration.days(retentionDays * 4)) // 4x retention for weekly
.build();
rules.add(weeklyRule);
LOG.info("Weekly backup rule added for PRODUCTION profile");
}
BackupPlan plan = BackupPlan.Builder.create(this, "BackupPlan")
.backupPlanName(planName)
.backupPlanRules(rules)
.build();
// Suppress IAM4 for AWS Backup service role - CDK automatically creates a role with
// AWSBackupServiceRolePolicyForBackup managed policy which is AWS-recommended
NagSuppressions.addResourceSuppressions(
plan,
List.of(
NagPackSuppression.builder()
.id("AwsSolutions-IAM4")
.reason("AWS Backup service role uses AWSBackupServiceRolePolicyForBackup " +
"managed policy which is AWS-recommended for backup operations. " +
"CDK automatically creates this role with appropriate permissions.")
.build(),
NagPackSuppression.builder()
.id("AwsSolutions-IAM5")
.reason("AWS Backup service role requires wildcard permissions for backup " +
"operations across multiple resource types. This is AWS-recommended.")
.build()
),
Boolean.TRUE
);
LOG.info("Created backup plan: " + planName);
return plan;
}
/**
* Adds EFS and RDS resources to the backup plan.
*/
private void addBackupResources(BackupPlan plan) {
List<BackupResource> resources = new ArrayList<>();
// Add EFS if present
ctx.efs.get().ifPresent(efs -> {
resources.add(BackupResource.fromEfsFileSystem(efs));
LOG.info("Added EFS to backup plan: " + efs.getFileSystemId());
});
// Add RDS if present
ctx.rdsDatabase.get().ifPresent(rds -> {
resources.add(BackupResource.fromRdsDatabaseInstance(rds));
LOG.info("Added RDS to backup plan: " + rds.getInstanceIdentifier());
});
if (resources.isEmpty()) {
LOG.warning("No resources found to backup (EFS or RDS not yet created)");
LOG.warning("BackupFactory should run AFTER EfsFactory and RdsFactory");
return;
}
// Create backup selection
plan.addSelection("BackupSelection", BackupSelectionOptions.builder()
.resources(resources)
.build());
LOG.info("Added " + resources.size() + " resource(s) to backup plan");
}
/**
* Sanitizes a resource name to comply with AWS naming conventions.
* AWS Backup vault/plan names must be 2-50 characters, alphanumeric with hyphens and underscores.
*
* @param baseName The base name (typically stack name)
* @param suffix The suffix to append (e.g., "-vault", "-plan")
* @return A sanitized name that complies with AWS naming rules
*/
private String sanitizeResourceName(String baseName, String suffix) {
// Replace invalid characters with hyphens
String sanitized = baseName.replaceAll("[^a-zA-Z0-9-_]", "-");
// Remove consecutive hyphens
sanitized = sanitized.replaceAll("-+", "-");
// Remove leading/trailing hyphens
sanitized = sanitized.replaceAll("^-|-$", "");
// Truncate to fit within 50 char limit (accounting for suffix)
int maxBaseLength = 50 - suffix.length();
if (sanitized.length() > maxBaseLength) {
sanitized = sanitized.substring(0, maxBaseLength);
}
return sanitized + suffix;
}
}