
AWS Backup Restore Testing for Aurora provisioned: Zero Lambda, Full Automation
How to validate Aurora provisioned backups automatically using only Step Functions SDK integrations and the RDS Data API
Creating backups is simple, but ensuring they work is a different challenge. Regular restore testing is the only effective way to confirm preparedness for data loss or ransomware and to ensure recovery times meet the organization's RTO. AWS Backup Restore Testing automates this process; however, the validation, which is the most critical part, must be implemented manually.
AWS introduced Restore Testing at re:Invent 2023 as an enhancement to AWS Backup. This feature enables periodic restoration and validation of recovery points.
The testing process has three phases:
1- Restore
2- Validation (the most important phase, which you must define and implement)
3- Deletion
According to AWS documentation, AWS Lambda is recommended for running validation logic and sending restore test validation results to AWS Backup. Although AWS Lambda is widely used for automation, it is not always required. AWS provides integrated SDK services that can perform specific tasks without the need for custom code.
Why choose Step Functions over Lambda?
- No code maintenance required. The state machine calls AWS APIs directly using the SDK, eliminating the need for a Lambda controller, deployment package, or runtime environment.
- Built-in retries and error handling. Each state has configurable retry policies with exponential backoff and catch blocks, ensuring cleanup always runs.
- Step Functions logs all transitions, entries, exits, and errors to CloudWatch, providing comprehensive tracking without additional instrumentation.
In this article, I'll show you how to validate the restoration of a provisioned Aurora database using AWS Step Functions, the SDK, and the RDS data API, without Lambda or any code, and with the entire infrastructure managed by Terraform.
This approach serves as a pattern rather than a prescriptive recipe.
This implementation is provided as a reference pattern, not a universal solution. In practice, you should adapt this workflow to your infrastructure and recovery requirements, including database engine, schemas, tables, and application-specific data checks. The core principle is to automate restore validation as a workflow, with specifics tailored to your system’s definition of successful recovery.
Architecture Overview

EventBridge initiates the workflow when AWS Backup completes an Aurora restore. The state machine then manages the validation cycle.
A key architectural detail is that AWS Backup restores an Aurora cluster without any instances. A writer instance must be created before connecting to the cluster, which is why CreateWriterInstance is included in the workflow.
The RDS Data API is essential for enabling this solution without Lambda or network connectivity. Once enabled on the restored cluster, Step Functions can execute SQL directly using the
rdsdata:ExecuteStatement SDK, without requiring VPCs, security groups, or bastion hosts.The RDS data API does not work with burst instances (db.t3 and db.t4g). You must use compatible instances, such as db.r6g.large or db.m6g.large, for the restore, even if you use small or burst instances in production.
Furthermore, the RDS data API is only compatible with Amazon Aurora (Provisioned or Serverless), not with standard RDS instances. This makes Aurora a critical component of this architecture, as it enables you to run SQL over HTTPS without managing network connectivity.
Prerequisites
Before implementing this solution, ensure you have the following:
- An AWS Backup snapshot of an Aurora PostgreSQL Provisioned cluster.
- The cluster master user secret is stored in AWS Secrets Manager (this is done automatically when using RDS-managed credentials).
Implementation
The solution consists of three Terraform files and an ASL (Amazon States Language) definition:
These are provided in the following GitHub repository, along with other files, if you wish to perform a Proof of Concept (PoC): https://github.com/kloudpepper/IaC/tree/main/Terraform/aws_backup_restore_testing
Step 1: Configure the Restore Testing Plan
The
aws_backup_restore_testing_plan and aws_backup_restore_testing_selection resources define when restores are triggered and how recovery points are selected. Metadata overrides are essential, as they specify the subnet group, parameter group, security groups, KMS key, and engine version for the restored cluster.Terraform
# AWS Backup - Restore Testing for Aurora# Planresource "aws_backup_restore_testing_plan" "aurora" {name = "aurora_restore_test"schedule_expression = "cron(30 18 ? * * *)"schedule_expression_timezone = "Europe/Madrid"start_window_hours = 1recovery_point_selection {algorithm = "LATEST_WITHIN_WINDOW"include_vaults = ["*"] # Consider scoping this down to specific vaults if you have multiple vaults in the accountrecovery_point_types = ["SNAPSHOT"]selection_window_days = 1}tags = var.tags}# Selection for Aurora resources to restore, with metadata overrides to ensure the restored instance can be created in the same VPC and subnets as the original, and use the same KMS key for encryptionresource "aws_backup_restore_testing_selection" "aurora" {name = "aurora_selection"restore_testing_plan_name = aws_backup_restore_testing_plan.aurora.nameprotected_resource_type = "Aurora"iam_role_arn = aws_iam_role.backup_restore_testing_aurora.arnprotected_resource_arns = [module.cluster.cluster_arn]restore_metadata_overrides = {dbSubnetGroupName = module.cluster.db_subnet_group_namedbClusterParameterGroupName = module.cluster.db_cluster_parameter_group_idvpcSecurityGroupIds = jsonencode([module.sg_aurora.security_group_id])kmsKeyId = module.kms_aurora.key_arnport = module.cluster.cluster_portengineVersion = module.cluster.cluster_engine_version_actual}validation_window_hours = 1}# Role for Backup to assume when performing the restoredata "aws_iam_policy_document" "backup_restore_testing_assume_role" {statement {effect = "Allow"principals {type = "Service"identifiers = ["backup.amazonaws.com"]}actions = ["sts:AssumeRole"]}}resource "aws_iam_role" "backup_restore_testing_aurora" {name = "aws-backup-role"assume_role_policy = data.aws_iam_policy_document.backup_restore_testing_assume_role.jsontags = var.tags}resource "aws_iam_role_policy_attachment" "backup_restore_testing_backup_policy" {role = aws_iam_role.backup_restore_testing_aurora.namepolicy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"}resource "aws_iam_role_policy_attachment" "backup_restore_testing_restore_policy" {role = aws_iam_role.backup_restore_testing_aurora.namepolicy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores"}
The IAM role for the restore test plan includes two AWS-managed policies that provide the required permissions for backup and restore operations.
Step 2: Deploy the Step Functions State Machine
The state machine is deployed using the terraform-aws-modules/step-functions module. The ASL definition is based on a Terraform templatefile, so the validation parameters (database name, schema, table, secret ARN, instance class) are injected during deployment.
Terraform
# Step Functions state machine for backup restore validationlocals {restore_validation_db_instance_class = "db.r6g.large"restore_validation_engine = "aurora-postgresql"restore_validation_database = "PETS"restore_validation_schema = "testing"restore_validation_table = "pets"restore_validation_secret_arn = module.cluster.cluster_master_user_secret[0].secret_arn}module "backup_restore_validation_sfn" {source = "terraform-aws-modules/step-functions/aws"name = "sfn-backup-restore-validation"type = "STANDARD"definition = templatefile("${path.module}/backup_restore_validation_asl.json", {db_instance_class = local.restore_validation_db_instance_classengine = local.restore_validation_enginedatabase = local.restore_validation_databaseschema = local.restore_validation_schematable = local.restore_validation_tablesecret_arn = local.restore_validation_secret_arn})attach_policy_json = truepolicy_json = data.aws_iam_policy_document.backup_restore_validation_sfn.jsonlogging_configuration = {include_execution_data = truelevel = "ALL"}cloudwatch_log_group_name = "/aws/vendedlogs/states/sfn-backup-restore-validation"cloudwatch_log_group_retention_in_days = 14tags = var.tags}# Policy for Step Functions to allow it to call Backup and RDS APIs, and read the secret value for validationdata "aws_iam_policy_document" "backup_restore_validation_sfn" {statement {sid = "BackupRestoreValidation"actions = ["backup:DescribeRestoreJob","backup:PutRestoreValidationResult"]resources = ["*"]}statement {sid = "RdsInstanceLifecycle"actions = ["rds:CreateDBInstance","rds:DescribeDBInstances","rds:DeleteDBInstance","rds:EnableHttpEndpoint"]resources = ["*"]}statement {sid = "RdsDataApi"actions = ["rds-data:ExecuteStatement"]resources = ["arn:aws:rds:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:cluster:*"]}statement {sid = "ReadValidationSecret"actions = ["secretsmanager:GetSecretValue"]resources = [local.restore_validation_secret_arn]}}
The IAM policy for the state machine grants the minimum necessary permissions.
Step 3: Configure the EventBridge Trigger
The EventBridge rule filters for restore job status change events that are COMPLETED, have an Aurora resource type, and a restoreTestingPlanArn matching the test plan. This ensures the state machine is triggered only for restores included in the test plan, not for manual restores.
Terraform
# EventBridge rules for backup restore testingmodule "eventbridge_default_rules" {source = "terraform-aws-modules/eventbridge/aws"create = true# Use default bus configurationcreate_bus = falsebus_name = "default"append_rule_postfix = falsecreate_log_delivery_source = false# Rules configurationcreate_rules = truerules = {"backup_restore_completed" = {name = "backup_restore_completed"description = "Trigger Aurora restore validation on backup restore completion"event_pattern = jsonencode({source = ["aws.backup"]detail-type = ["Restore Job State Change"]detail = {status = ["COMPLETED"]resourceType = ["Aurora"]restoreTestingPlanArn = [{prefix = aws_backup_restore_testing_plan.aurora.arn}]}})enabled = true}}# Targets configurationcreate_targets = truetargets = {"backup_restore_completed" = [{name = "StartRestoreValidationStateMachine"arn = module.backup_restore_validation_sfn.state_machine_arnattach_role_arn = true}]}sfn_target_arns = [module.backup_restore_validation_sfn.state_machine_arn]attach_sfn_policy = true}
State Machine Walkthrough
Initialize and BuildIdentifiers
The
Initialize state uses a Pass state to restructure the incoming EventBridge event into a controlled Context object, which is passed through all subsequent states. This approach is recommended in Step Functions to avoid direct dependency on the raw event structure.BuildIdentifiers then uses Step Functions' intrinsic functions to derive the values needed for the rest of the execution:JSON
"DBClusterIdentifier.$": "States.ArrayGetItem(States.StringSplit($.Restore.CreatedResourceArn, ':'), 6)","DBInstanceIdentifier.$": "States.Format('rt-{}', $.Restore.RestoreJobId)","Sql.$": "States.Format('SELECT 1 FROM \"{}\".\"{}\" LIMIT 1;', $.Context.Schema, $.Context.Table)"
The cluster identifier is extracted from the ARN returned by AWS Backup. The temporary instance name is prefixed with
rt- (restore test) and incorporates the RestoreJobId for uniqueness. The SQL query is generated dynamically from the configured schema and tableEnableDataApi
Before creating the writer instance, the Data API is enabled on the restored cluster. This is a non-blocking call; the API becomes available once the cluster has a running instance.
JSON
"EnableDataApi": {"Type": "Task","Resource": "arn:aws:states:::aws-sdk:rds:enableHttpEndpoint","Parameters": {"ResourceArn.$": "$.Context.CreatedResourceArn"}}
CreateWriterInstance and polling loop
AWS Backup restores Aurora clusters without any instances.
CreateWriterInstance adds a writer using the instance class, engine, and cluster identifier determined in the previous step.
The polling loop includes three states: WaitForInstance (a 30-second wait), DescribeWriterInstance , and CheckInstanceState . The Choice state evaluates DbInstanceStatus:-
available→ proceed to SQL validation
-
failed,incompatible-restore, orincompatible-network→ report failure and delete the instance
- Any other status → increment the poll counter and wait again.
The loop is limited to
MaxPolls: 40 , resulting in a maximum wait time of 20 minutes. The state machine’s global TimeoutSeconds: 5400 provides an additional safety limit.RunValidationSql
Once the instance is available, the validation query runs via the RDS Data API:
JSON
"RunValidationSql": {"Type": "Task","Resource": "arn:aws:states:::aws-sdk:rdsdata:executeStatement","Parameters": {"ResourceArn.$": "$.Context.CreatedResourceArn","SecretArn.$": "$.Context.SecretArn","Database.$": "$.Context.Database","Sql.$": "$.Context.Sql"}}
The query
SELECT 1 FROM "schema"."table" LIMIT 1 is intentionally simple. It validates that the schema and table exist and are readable, which is sufficient to confirm a successful restore. You can make the query more specific for your use case.Reporting and Cleanup
There are three reporting states, each calling
backup:PutRestoreValidationResult with a different ValidationStatus: SUCCESSFUL , FAILED , or TIMED_OUT . All reporting states leading to DeleteInstance include "ResultPath": "$.ReportResult" to prevent the API response from overwriting the execution context. Without this, DeleteInstance would receive an empty object and fail to locate $.Context.DBInstanceIdentifier .DeleteInstance always runs after reporting, except in the ReportFailedNoDelete path, which occurs before the instance is created. It includes retry logic for InvalidDBInstanceStateFault if the instance is still transitioning when deletion is attempted.
Backup validation is often overlooked, yet it is a critical component of any resilience strategy. Restoring data alone is insufficient; you must verify that recovery is successful.
I hope this article is of great help to you. Best regards, and I look forward to seeing you next time. Thank you!
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article