| name | aws-database-backup |
| description | Expert guide for backing up AWS databases including Amazon Relational Database Service (Amazon RDS), Aurora, MS-SQL, MySQL, and PostgreSQL. Use when planning or executing database backup operations, retention policies, or restore procedures. Covers Amazon Simple Storage Service (Amazon S3) upload security. |
AWS Database Backup Skill
Security: Always ensure backup configurations meet or exceed the security configuration of the source resources. Refer to SECURITY.md for security requirements.
Overview
This skill covers database backup strategies using AWS-native services and advanced CLI techniques for Amazon Relational Database Service (Amazon RDS), Amazon Aurora, MS-SQL, MySQL, PostgreSQL, and more.
Amazon RDS Automated Backups (Recommended)
Enable/Modify Backup Retention
aws rds modify-db-instance --db-instance-identifier <db-id> --backup-retention-period 30 --preferred-backup-window "03:00-04:00" --apply-immediately --region <region>
aws rds describe-db-instances --db-instance-identifier <db-id> --query 'DBInstances[0].{Retention:BackupRetentionPeriod,Window:PreferredBackupWindow,Status:DBInstanceStatus}' --region <region>
Manual Snapshots
aws rds create-db-snapshot --db-instance-identifier <db-id> --db-snapshot-identifier "manual-$(date +%Y%m%d-%H%M)" --region <region>
aws rds wait db-snapshot-available --db-snapshot-identifier "manual-$(date +%Y%m%d-%H%M)" --region <region>
aws rds describe-db-snapshots --db-instance-identifier <db-id> --query 'DBSnapshots[*].{ID:DBSnapshotIdentifier,Status:Status,Created:SnapshotCreateTime,Size:AllocatedStorage}' --output table --region <region>
Cross-Region Backup Copy
aws rds copy-db-snapshot --source-db-snapshot-identifier arn:aws:rds:<source-region>:<account>:snapshot:<snap-id> --target-db-snapshot-identifier "dr-copy-$(date +%Y%m%d)" --region <target-region>
Export Snapshot to S3
aws rds start-export-task --export-task-identifier "export-$(date +%Y%m%d)" --source-arn arn:aws:rds:<region>:<account>:snapshot:<snap-id> --s3-bucket-name <bucket> --iam-role-arn <role-arn> --kms-key-id <kms-key> --region <region>
Aurora Backups
Cluster Snapshots
aws rds create-db-cluster-snapshot --db-cluster-identifier <cluster-id> --db-cluster-snapshot-identifier "aurora-backup-$(date +%Y%m%d)" --region <region>
aws rds copy-db-cluster-snapshot --source-db-cluster-snapshot-identifier arn:aws:rds:<source-region>:<account>:cluster-snapshot:<snap-id> --target-db-cluster-snapshot-identifier "aurora-dr-$(date +%Y%m%d)" --region <target-region>
Aurora Backtrack (MySQL-compatible)
aws rds modify-db-cluster --db-cluster-identifier <cluster-id> --backtrack-window 86400 --apply-immediately --region <region>
aws rds backtrack-db-cluster --db-cluster-identifier <cluster-id> --backtrack-to "2026-03-11T10:00:00Z" --region <region>
Aurora Cloning (Fast Copy)
aws rds restore-db-cluster-to-point-in-time --source-db-cluster-identifier <cluster-id> --db-cluster-identifier "clone-$(date +%Y%m%d)" --restore-type copy-on-write --use-latest-restorable-time --region <region>
MS-SQL Backups
RDS SQL Server Native Backup to S3
Security: The examples below use -P to demonstrate syntax. In production environments, use Windows Authentication (-E flag), SQL Server configuration files, or retrieve credentials from AWS Secrets Manager: PASS=$(aws secretsmanager get-secret-value --secret-id <secret-id> --query SecretString --output text); sqlcmd -S <endpoint> -U <user> -P "$PASS". Command-line passwords are visible in process lists and shell history. See SECURITY.md.
aws rds add-option-to-option-group --option-group-name <og-name> --options "OptionName=SQLSERVER_BACKUP_RESTORE,OptionSettings=[{Name=IAM_ROLE_ARN,Value=<role-arn>}]" --region <region>
Advanced: sqlcmd CLI backup (self-managed SQL Server on EC2)
Pre-Backup Diagnostics (run via sqlcmd before any backup)
Pre-backup diagnostics (read-only queries to verify disk space and backup history before starting new backups).
When implementing similar queries that accept user input, use parameterized queries or stored procedures to prevent SQL injection.
SELECT [File Name] = name,
[File Location] = physical_name,
[Total Size (MB)] = size/128.0,
[Free Space (MB)] = size/128.0
- CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)/128.0,
[Type] = type_desc
FROM sys.database_files;
SELECT DISTINCT dovs.logical_volume_name AS LogicalName,
dovs.volume_mount_point AS Drive,
CONVERT(INT,dovs.available_bytes/1048576.0) AS FreeSpaceMB
FROM sys.master_files mf
CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.FILE_ID) dovs
ORDER BY FreeSpaceMB ASC;
SELECT
CONVERT(CHAR(100), SERVERPROPERTY('Servername')) AS Server,
msdb.dbo.backupset.database_name,
msdb.dbo.backupset.backup_start_date,
msdb.dbo.backupset.backup_finish_date,
CASE msdb..backupset.type
WHEN 'D' THEN 'Database'
WHEN 'L' THEN 'Log'
END AS backup_type,
msdb.dbo.backupset.backup_size,
msdb.dbo.backupmediafamily.physical_device_name,
msdb.dbo.backupset.name AS backupset_name
FROM msdb.dbo.backupmediafamily
INNER JOIN msdb.dbo.backupset ON msdb.dbo.backupmediafamily.media_set_id = msdb.dbo.backupset.media_set_id
WHERE (CONVERT(datetime, msdb.dbo.backupset.backup_start_date, 102) >= GETDATE() - 7)
ORDER BY msdb.dbo.backupset.database_name, msdb.dbo.backupset.backup_finish_date DESC;
sqlcmd -S <host> -U <user> -P "$SQLCMD_PASSWORD" -Q "BACKUP DATABASE [<dbname>] TO DISK='D:\backups\<dbname>_$(date +%Y%m%d).bak' WITH COMPRESSION, CHECKSUM, INIT"
sqlcmd -S <host> -U <user> -P "$SQLCMD_PASSWORD" -Q "BACKUP LOG [<dbname>] TO DISK='D:\backups\<dbname>_log_$(date +%Y%m%d%H%M).trn' WITH COMPRESSION"
aws s3 cp "D:\backups\<dbname>_$(date +%Y%m%d).bak" s3://<bucket>/mssql-backups/ --sse aws:kms --region <region>
Advanced: PowerShell backup for Windows EC2
# Full backup with compression and verification
Backup-SqlDatabase -ServerInstance "<host>" -Database "<dbname>" -BackupFile "D:\backups\<dbname>_$(Get-Date -Format yyyyMMdd).bak" -CompressionOption On -Checksum
Restore-SqlDatabase -ServerInstance "<host>" -Database "<dbname>" -BackupFile "D:\backups\<dbname>.bak" -VerifyOnly
MS-SQL BCP (Bulk Copy Program)
Customer responsibility: The examples below use $SQLCMD_PASSWORD environment variable. In production, use Windows Authentication (-T flag), SQL Server credential files, or retrieve credentials from AWS Secrets Manager: export SQLCMD_PASSWORD=$(aws secretsmanager get-secret-value --secret-id <secret-id> --query SecretString --output text). Hardcoded passwords in scripts are visible in version control history and process listings. Use Windows Authentication (-T flag), SQL Server credential files, or retrieve credentials from AWS Secrets Manager as shown above.
BCP exports/imports data between SQL Server and flat files. bcp out exports (source unchanged), bcp in imports (appends by default). Use native format (-n) for speed or character format (-c) for portability. Large operations consume network bandwidth and disk I/O — plan for off-peak hours on production systems.
Export Table to File
bcp "<dbname>.dbo.<table>" out /tmp/<table>.bcp -S <host> -U <user> -P "$SQLCMD_PASSWORD" -n
bcp "<dbname>.dbo.<table>" out /tmp/<table>.csv -S <host> -U <user> -P "$SQLCMD_PASSWORD" -c -t, -r\\n
bcp "<dbname>.dbo.<table>" out /tmp/<table>.tsv -S <host> -U <user> -P "$SQLCMD_PASSWORD" -c -t\\t
DATE="2026-01-01"
if [[ ! "$DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then echo "Invalid date format"; exit 1; fi
bcp "SELECT * FROM <dbname>.dbo.<table> WHERE created_at > '$DATE'" queryout /tmp/<table>_recent.csv -S <host> -U <user> -P "$SQLCMD_PASSWORD" -c -t,
Import File to Table
bcp "<dbname>.dbo.<table>" in /tmp/<table>.bcp -S <host> -U <user> -P "$SQLCMD_PASSWORD" -n
bcp "<dbname>.dbo.<table>" in /tmp/<table>.csv -S <host> -U <user> -P "$SQLCMD_PASSWORD" -c -t,
bcp "<dbname>.dbo.<table>" in /tmp/<table>.csv -S <host> -U <user> -P "$SQLCMD_PASSWORD" -c -t, -b 10000
bcp "<dbname>.dbo.<table>" in /tmp/<table>.csv -S <host> -U <user> -P "$SQLCMD_PASSWORD" -c -t, -e /tmp/bcp_errors.log -m 100
Cross-Server Migration with BCP
bcp "<dbname>.dbo.<table>" out /tmp/<table>.bcp -S <source-host> -U <user> -P "$SQLCMD_PASSWORD" -n
aws s3 cp /tmp/<table>.bcp s3://<bucket>/mssql-bcp/ --sse aws:kms --region <region>
aws s3 cp s3://<bucket>/mssql-bcp/<table>.bcp /tmp/<table>.bcp --region <region>
bcp "<dbname>.dbo.<table>" in /tmp/<table>.bcp -S <target-host> -U <user> -P "$SQLCMD_PASSWORD" -n
BCP with Format Files (Complex Schemas)
bcp "<dbname>.dbo.<table>" format nul -S <host> -U <user> -P "$SQLCMD_PASSWORD" -n -f /tmp/<table>.fmt
bcp "<dbname>.dbo.<table>" in /tmp/<table>.bcp -S <host> -U <user> -P "$SQLCMD_PASSWORD" -f /tmp/<table>.fmt
BCP Performance Tips
for i in $(seq 0 9); do
bcp "SELECT * FROM <dbname>.dbo.<table> WHERE id % 10 = $i" queryout /tmp/<table>_part${i}.bcp -S <host> -U <user> -P "$SQLCMD_PASSWORD" -n &
done
wait
MySQL Backups
mysqldump (Recommended for logical backups)
mysqldump -h <host> -u <user> -p --single-transaction --routines --triggers --events --set-gtid-purged=OFF <dbname> | gzip > <dbname>_$(date +%Y%m%d).sql.gz
mysqldump -h <host> -u <user> -p --single-transaction --routines --triggers --events --all-databases | gzip > all_dbs_$(date +%Y%m%d).sql.gz
aws s3 cp <dbname>_$(date +%Y%m%d).sql.gz s3://<bucket>/mysql-backups/ --sse aws:kms --region <region>
mysqlpump (Parallel dump)
mysqlpump -h <host> -u <user> -p --default-parallelism=4 --databases <dbname> | gzip > <dbname>_pump_$(date +%Y%m%d).sql.gz
Advanced: xtrabackup (Physical backup, near-zero downtime)
Customer responsibility: Protect database credentials by using --defaults-file with restricted permissions (chmod 600) instead of --password on the command line. Before streaming backups to Amazon S3, verify the target bucket has Block Public Access enabled and default encryption configured.
xtrabackup --backup --target-dir=/backups/full_$(date +%Y%m%d) --user=<user> --defaults-file=~/.my.cnf
xtrabackup --prepare --target-dir=/backups/full_$(date +%Y%m%d)
xtrabackup --backup --target-dir=/backups/inc_$(date +%Y%m%d) --incremental-basedir=/backups/full_$(date +%Y%m%d) --user=<user> --defaults-file=~/.my.cnf
xtrabackup --backup --stream=xbstream --user=<user> --defaults-file=~/.my.cnf | gzip | aws s3 cp - s3://<bucket>/xtrabackup/full_$(date +%Y%m%d).xbstream.gz --sse aws:kms --region <region>
Advanced: mydumper (Multi-threaded logical backup)
mydumper -h <host> -u <user> -p "$DB_PASSWORD" -B <dbname> -t 8 -c -o /backups/mydumper_$(date +%Y%m%d)
myloader -h <target-host> -u <user> -p "$DB_PASSWORD" -B <dbname> -t 8 -d /backups/mydumper_$(date +%Y%m%d)
PostgreSQL Backups
pg_dump (Recommended)
pg_dump -h <host> -U <user> -Fc -Z 6 <dbname> > <dbname>_$(date +%Y%m%d).dump
pg_dump -h <host> -U <user> -Fd -j 4 <dbname> -f /backups/<dbname>_$(date +%Y%m%d)
pg_dump -h <host> -U <user> -Fp <dbname> | gzip > <dbname>_$(date +%Y%m%d).sql.gz
aws s3 cp <dbname>_$(date +%Y%m%d).dump s3://<bucket>/pg-backups/ --sse aws:kms --region <region>
pg_basebackup (Physical backup)
pg_basebackup -h <host> -U <repl-user> -D /backups/base_$(date +%Y%m%d) -Ft -z -Xs -P
pg_basebackup -h <host> -U <repl-user> -Ft -Xs -D - | aws s3 cp - s3://<bucket>/pg-base/base_$(date +%Y%m%d).tar --sse aws:kms --region <region>
Advanced: pgBackRest (Enterprise-grade)
pgbackrest --stanza=<stanza> --type=full backup
pgbackrest --stanza=<stanza> --type=diff backup
pgbackrest --stanza=<stanza> --type=incr backup
pgbackrest --stanza=<stanza> check
pgbackrest --stanza=<stanza> info
AWS Backup Service (Unified)
Create Backup Plan
aws backup create-backup-vault --backup-vault-name "db-backups" --region <region>
aws backup create-backup-plan --backup-plan '{
"BackupPlanName": "daily-db-backup",
"Rules": [{
"RuleName": "daily-rule",
"TargetBackupVaultName": "db-backups",
"ScheduleExpression": "cron(0 3 * * ? *)",
"StartWindowMinutes": 60,
"CompletionWindowMinutes": 180,
"Lifecycle": {
"MoveToColdStorageAfterDays": 30,
"DeleteAfterDays": 365
},
"CopyActions": [{
"DestinationBackupVaultArn": "arn:aws:backup:<target-region>:<account>:backup-vault:dr-vault",
"Lifecycle": {"DeleteAfterDays": 365}
}]
}]
}' --region <region>
aws backup create-backup-selection --backup-plan-id <plan-id> --backup-selection '{
"SelectionName": "all-rds",
"IamRoleArn": "<backup-role-arn>",
"Resources": [
"arn:aws:rds:<region>:<account>:db:*"
]
}' --region <region>
On-Demand Backup
aws backup start-backup-job --backup-vault-name "db-backups" --resource-arn arn:aws:rds:<region>:<account>:db:<db-id> --iam-role-arn <role-arn> --region <region>
List and Monitor
aws backup list-backup-jobs --by-state COMPLETED --by-resource-type RDS --region <region> --query 'BackupJobs[*].{Resource:ResourceArn,Status:State,Created:CreationDate,Size:BackupSizeInBytes}' --output table
aws backup list-recovery-points-by-backup-vault --backup-vault-name "db-backups" --region <region>
Automated Backup Scripts
Cron-based backup rotation (Linux)
#!/bin/bash
set -euo pipefail
DB_HOST="<host>"
DB_USER="<user>"
DB_NAME="<dbname>"
S3_BUCKET="<bucket>"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d-%H%M)
if ! mysqldump -h "$DB_HOST" -u "$DB_USER" --single-transaction "$DB_NAME" | gzip > "/tmp/${DB_NAME}_${DATE}.sql.gz"; then
echo "ERROR: Backup failed for ${DB_NAME}"
exit 1
fi
aws s3 cp "/tmp/${DB_NAME}_${DATE}.sql.gz" "s3://${S3_BUCKET}/mysql/${DB_NAME}/" --sse aws:kms --region <region>
rm -f "/tmp/${DB_NAME}_${DATE}.sql.gz"
aws s3 ls "s3://${S3_BUCKET}/mysql/${DB_NAME}/" | while read -r line; do
file_date=$(echo "$line" | awk '{print $1}')
file_name=$(echo "$line" | awk '{print $4}')
if [[ $(date -d "$file_date" +%s) -lt $(date -d "-${RETENTION_DAYS} days" +%s) ]]; then
aws s3 rm "s3://${S3_BUCKET}/mysql/${DB_NAME}/${file_name}"
fi
done
Snapshot cleanup (delete old manual snapshots)
aws rds describe-db-snapshots --snapshot-type manual --region <region> --query 'DBSnapshots[?SnapshotCreateTime<=`'$(date -d '-90 days' -Iseconds)'`].DBSnapshotIdentifier' --output text | tr '\t' '\n' | while read snap; do
echo "Deleting $snap"
aws rds delete-db-snapshot --db-snapshot-identifier "$snap" --region <region>
done
Restore Procedures
RDS Restore from Snapshot
aws rds restore-db-instance-from-db-snapshot --db-instance-identifier "<restored-db>" --db-snapshot-identifier "<snap-id>" --db-subnet-group-name <subnet-group> --region <region>
Point-in-Time Restore
aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier <db-id> --target-db-instance-identifier "pit-restore-$(date +%Y%m%d)" --restore-time "2026-03-11T14:00:00Z" --region <region>
MySQL Restore from dump
gunzip < <dbname>_20260311.sql.gz | mysql -h <host> -u <user> -p <dbname>
PostgreSQL Restore
pg_restore -h <host> -U <user> -d <dbname> -j 4 --no-owner <dbname>.dump
Backup Verification Checklist
- Test restores regularly (at least monthly)
- Verify backup integrity with checksums
- Monitor backup job completion via CloudWatch or AWS Backup
- Verify cross-region copies for DR
- Validate encryption at rest (KMS keys accessible in target region)
- Document RTO/RPO for each database
- Set up SNS alerts for backup failures
Data Security for Backups
- Customer responsibility — Encryption at rest: Configure
--sse aws:kms for all Amazon S3 uploads. Enable encryption for Amazon RDS snapshots. Use --kms-key-id when copying snapshots cross-region.
- Customer responsibility — Encryption in transit: AWS CLI is designed to use HTTPS by default; verify your configuration. For native database tools, customers must explicitly configure encryption using
--ssl-mode=REQUIRED (MySQL) or sslmode=require (PostgreSQL). For SQL Server, use Encrypt=yes in connection strings.
- Customer responsibility — S3 bucket security: Before uploading backups, verify the target Amazon S3 bucket has Block Public Access enabled, default encryption configured, versioning enabled, and a bucket policy enforcing TLS-only access.
- Credential handling: Use IAM roles with temporary credentials. For database credentials, use AWS Secrets Manager or
mysql_config_editor / --defaults-file. See SECURITY.md.
- Backup integrity: Verify backup checksums after creation and after cross-region transfer.
S3 Bucket Security Setup
aws s3api put-public-access-block --bucket <bucket> \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" --region <region>
aws s3api put-bucket-encryption --bucket <bucket> \
--server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"<kms-key-id>"}}]}' --region <region>
aws s3api put-bucket-versioning --bucket <bucket> \
--versioning-configuration Status=Enabled --region <region>
aws s3api put-bucket-policy --bucket <bucket> --region <region> --policy '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::<bucket>", "arn:aws:s3:::<bucket>/*"],
"Condition": {"Bool": {"aws:SecureTransport": "false"}}
}]
}'
IAM Policies for Backup Roles
Customer responsibility: Create and configure IAM roles with least-privilege permissions for backup operations. The <backup-role-arn> and <iam-role-arn> placeholders in this guide require IAM roles with specific permissions. Replace wildcard resource ARNs (*) with specific resource ARNs for your environment. At minimum:
AWS Backup service role needs: backup:StartBackupJob, backup:StartCopyJob, rds:CreateDBSnapshot, rds:CopyDBSnapshot, s3:GetObject, s3:PutObject, kms:Decrypt, kms:GenerateDataKey — scoped to the specific backup vault, S3 bucket, and AWS KMS key ARNs.
SQL Server native backup role needs: s3:PutObject, s3:GetObject, s3:ListBucket, s3:GetBucketLocation — scoped to the specific S3 bucket ARN used for backups.
Use IAM Access Analyzer to generate least-privilege policies based on actual usage. Review IAM policies quarterly and remove unused permissions.