Automating Database Cloning on AWS and Kubernetes Safely
Posted on July 9, 2020
Updated
Database-cloning automation needs more than a successful restore. Isolate the destination, track exact resource identifiers, sanitize sensitive data before granting access, and test cleanup. The original 2020 commands included obsolete APIs and a MySQL data-directory deletion; those commands are removed from this refreshed guide.
Choose the clone method
For Aurora, compare a snapshot restore with Aurora cloning for the same engine and supported configuration. A snapshot restore creates a new cluster; provision the required instances for the selected capacity model. Logical export/import is a different operation and needs compatible server/client versions, sufficient storage and explicit application database selection.
Select the snapshot explicitly
This read-only AWS CLI v2 query sorts available snapshots by creation time. Set the intended account/profile, Region and source identifier first. An empty result means there is no matching candidate, not permission to choose another database.
aws sts get-caller-identity
aws rds describe-db-cluster-snapshots \
--db-cluster-identifier "$SOURCE_CLUSTER" \
--snapshot-type automated \
--query 'sort_by(DBClusterSnapshots[?Status==`available`], &SnapshotCreateTime)[-1].{Arn:DBClusterSnapshotArn,Engine:Engine,Created:SnapshotCreateTime}' \
--output jsonMake the restore an isolated workflow
- Create a unique job identifier and a durable inventory of owned resources.
- Restore using the snapshot’s supported engine, such as
aurora-mysql, explicit private subnet/security groups and required KMS permissions. - Use bounded RDS waiters and fail the job on timeout or any unsuccessful API call.
- Apply a restricted destination credential strategy and deny application access until sanitization succeeds.
- Run schema-aware masking, then verify sensitive fields, external integrations and outbound messages cannot affect real users.
- Grant development access only after approval and record the retention deadline.
Never copy production records into an unrestricted test environment merely to validate these commands. Start with a disposable source containing synthetic data.
Schedule only a verified implementation
A Kubernetes CronJob uses batch/v1. concurrencyPolicy: Forbid helps avoid overlapping Jobs from one CronJob, but external resource creation still needs idempotency and failure cleanup. Use workload IAM roles and mounted secrets; do not put database passwords in a manifest or command-line argument.
Verify failure recovery and deletion
Test an unavailable snapshot, denied permissions, restore timeout and failed masking. The workflow must keep the destination inaccessible on those failures. Delete only tracked resources, account for retained snapshots and secrets, and confirm their absence. Never use a broad tag match or a filesystem wildcard to decide what to remove.
Live verification
On 30 October 2026, we restored a synthetic Aurora MySQL 8.4 snapshot into a private destination, changed its credential, and tested a failed masking step before applying the successful mask. The release gate stayed blocked on failure and opened only after the checks passed. Source records were unchanged. The temporary source, clone, snapshots and credentials were then deleted. This does not validate an arbitrary production masking policy or a scheduled cloning implementation. Cost depends on engine, capacity, storage and runtime. Use the External Secrets guide for a tested workload-secret pattern.
Sources and further reading
Tested snapshot and restore commands
Use an approved synthetic source and an isolated DB subnet/security group. These values are placeholders; the commands create billable resources. Confirm the AWS identity, Region, permissions and cleanup inventory first. The subnet group and security group must belong to the intended VPC; the destination must have no development access until sanitization succeeds.
export AWS_REGION=eu-central-1
export SOURCE_CLUSTER='your-approved-synthetic-source'
export CLONE_CLUSTER="article-clone-$(date -u +%Y%m%d%H%M%S)"
export CLONE_INSTANCE="${CLONE_CLUSTER}-writer"
export SNAPSHOT_NAME="${CLONE_CLUSTER}-seed"
export DB_SUBNET_GROUP='your-isolated-db-subnet-group'
export DB_SECURITY_GROUP='sg-your-isolated-db-security-group'
aws sts get-caller-identity
aws rds create-db-cluster-snapshot --db-cluster-identifier "$SOURCE_CLUSTER" --db-cluster-snapshot-identifier "$SNAPSHOT_NAME"
aws rds wait db-cluster-snapshot-available --db-cluster-snapshot-identifier "$SNAPSHOT_NAME"
SNAPSHOT_ARN=$(aws rds describe-db-cluster-snapshots --db-cluster-snapshot-identifier "$SNAPSHOT_NAME" --query 'DBClusterSnapshots[0].DBClusterSnapshotArn' --output text)
aws rds restore-db-cluster-from-snapshot --db-cluster-identifier "$CLONE_CLUSTER" --snapshot-identifier "$SNAPSHOT_ARN" --engine aurora-mysql --db-subnet-group-name "$DB_SUBNET_GROUP" --vpc-security-group-ids "$DB_SECURITY_GROUP"
aws rds create-db-instance --db-instance-identifier "$CLONE_INSTANCE" --db-cluster-identifier "$CLONE_CLUSTER" --db-instance-class db.t4g.medium --engine aurora-mysql --no-publicly-accessible
aws rds wait db-instance-available --db-instance-identifier "$CLONE_INSTANCE"The snapshot’s engine and version must support the chosen instance class. Our run used Aurora MySQL 8.4.mysql_aurora.8.4.10 and db.t4g.medium. The waiters are bounded, but their timeout is still a failure to investigate. Do not grant access just because an API request was accepted.
Make the sanitization gate observable
Rotate the destination credential through a protected secret workflow. Avoid passwords in shell history, process arguments or manifests. Connect using a protected client option file and the verified RDS CA. Set the destination gate to blocked before attempting the mask, and make the SQL client stop on error; do not use a continue-on-error mode. In our synthetic schema, a missing-table masking error kept the gate blocked. The successful transaction masked every example email and disabled the example outbound integration before setting the gate to ready.
UPDATE article_lab.release_gate SET status='blocked';
START TRANSACTION;
UPDATE article_lab.customers
SET email=CONCAT('masked-',id,'@example.invalid'),
display_name=CONCAT('Person ',id);
UPDATE article_lab.integrations SET enabled=0;
UPDATE article_lab.release_gate SET status='ready'
WHERE NOT EXISTS (
SELECT 1 FROM article_lab.customers
WHERE email NOT LIKE 'masked-%@example.invalid'
) AND NOT EXISTS (
SELECT 1 FROM article_lab.integrations WHERE enabled<>0
);
COMMIT;
SELECT status FROM article_lab.release_gate;This SQL demonstrates a gate on three synthetic tables. A production dataset needs a schema-specific policy, verification of indirect identifiers and a decision about external integrations. Do not treat the example as a complete anonymization policy.
Delete the exact resources you created
aws rds delete-db-instance --db-instance-identifier "$CLONE_INSTANCE" --skip-final-snapshot
aws rds wait db-instance-deleted --db-instance-identifier "$CLONE_INSTANCE"
aws rds delete-db-cluster --db-cluster-identifier "$CLONE_CLUSTER" --skip-final-snapshot --delete-automated-backups
aws rds wait db-cluster-deleted --db-cluster-identifier "$CLONE_CLUSTER"
aws rds delete-db-cluster-snapshot --db-cluster-snapshot-identifier "$SNAPSHOT_NAME"Verify absence, then check the rest of the inventory: any synthetic source, secrets, buckets, instances, disks and network resources created for the lab. The commands above deliberately name only the clone and its manual snapshot.