From bd45633f64894ca39f6b2f436d8968a9f1a82e28 Mon Sep 17 00:00:00 2001 From: Nick Miles Date: Fri, 19 Sep 2025 13:12:43 +0100 Subject: [PATCH 1/2] ENG-893 Implement AWS Backup Restore Validation Module with Lambda and Step Functions ENG-893 Add restore validation sequence diagram and update documentation --- docs/diagrams/restore-validation-sequence.png | Bin 0 -> 103736 bytes .../diagrams/restore-validation-sequence.puml | 110 +++++++ docs/restore-testing-design.md | 310 ++++++++++++++++++ modules/aws-backup-validation/README.md | 41 +++ modules/aws-backup-validation/iam.tf | 112 +++++++ modules/aws-backup-validation/lambda.py | 75 +++++ modules/aws-backup-validation/lambda.tf | 34 ++ modules/aws-backup-validation/outputs.tf | 14 + .../statemachine.json.tpl | 49 +++ modules/aws-backup-validation/statemachine.tf | 76 +++++ modules/aws-backup-validation/variables.tf | 52 +++ modules/aws-backup-validation/versions.tf | 9 + 12 files changed, 882 insertions(+) create mode 100644 docs/diagrams/restore-validation-sequence.png create mode 100644 docs/diagrams/restore-validation-sequence.puml create mode 100644 docs/restore-testing-design.md create mode 100644 modules/aws-backup-validation/README.md create mode 100644 modules/aws-backup-validation/iam.tf create mode 100644 modules/aws-backup-validation/lambda.py create mode 100644 modules/aws-backup-validation/lambda.tf create mode 100644 modules/aws-backup-validation/outputs.tf create mode 100644 modules/aws-backup-validation/statemachine.json.tpl create mode 100644 modules/aws-backup-validation/statemachine.tf create mode 100644 modules/aws-backup-validation/variables.tf create mode 100644 modules/aws-backup-validation/versions.tf diff --git a/docs/diagrams/restore-validation-sequence.png b/docs/diagrams/restore-validation-sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..938d417d329c481dd3e97f16758ab57a582d9170 GIT binary patch literal 103736 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

}}MbgcU}H=u%vZ!m9eB1;eC5lVK|}PxXbkkeKp; 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 literal 0 HcmV?d00001 diff --git a/docs/diagrams/restore-validation-sequence.puml b/docs/diagrams/restore-validation-sequence.puml new file mode 100644 index 0000000..2ecdf38 --- /dev/null +++ b/docs/diagrams/restore-validation-sequence.puml @@ -0,0 +1,110 @@ +@startuml restore-validation-sequence +' Title & Legend +!theme plain +skinparam ParticipantPadding 8 +skinparam BoxPadding 6 +skinparam Shadowing false +skinparam ArrowThickness 1 +skinparam ArrowColor #2d5d86 +skinparam ActorStyle awesome +skinparam SequenceMessageAlign center +skinparam BackgroundColor #ffffff + +title AWS Backup Restore Testing & Validation Flow + +legend left + This diagram illustrates the post-restore validation workflow: + 1. Scheduled restore tests run via an AWS Backup Restore Testing Plan. + 2. When a restore job COMPLETES, an EventBridge rule targets a Step Functions + state machine that orchestrates validation. + 3. Lambda validator loads per-resource validation config from SSM Parameter Store + and executes resource‑type specific checks (e.g. RDS SQL assertions, + DynamoDB item sampling, S3 manifest / object probes). + 4. Validation result is published back to AWS Backup using PutRestoreValidationResult. +endlegend + +actor User as U +participant "AWS Backup Restore\nTesting Plan" as Plan +participant "AWS Backup\n(Service)" as Backup +participant "Restore Job" as Restore +participant "EventBridge Rule" as EB +participant "Step Functions\n(State Machine)" as SFN +participant "State: Enrich" as Enrich +participant "State: Route" as Route +participant "Lambda Validator" as Lambda +participant "SSM Parameter\n(Store Config)" as SSM +participant "Resource APIs\n(RDS | DynamoDB | S3 | etc.)" as APIs +participant "AWS Backup API\n(PutRestoreValidationResult)" as ResultAPI +participant "CloudWatch Logs" as Logs + +' 1. Scheduled restore initiated +U -> Plan : (Schedule configured) +Plan -> Backup : Initiate restore test jobs (per selection) +Backup -> Restore ++ : Create restore job(s) + +' 2. Restore completes +Restore -> Backup : Status = COMPLETED (success) +Backup -> EB : Event: Restore Job State Change\n(detail.status = COMPLETED) + +' 3. EventBridge triggers Step Functions +EB -> SFN : StartExecution (input = restore job event) +activate SFN +SFN -> Enrich : Pass original event / add metadata +activate Enrich +Enrich --> SFN : Enriched context +deactivate Enrich + +SFN -> Route : Determine resourceType +activate Route + +alt Supported resource type + Route -> Lambda : Invoke validator (payload: job + configRef) + activate Lambda + Lambda -> SSM : Get config parameter + SSM --> Lambda : JSON config + Lambda -> APIs : Perform type-specific checks + APIs --> Lambda : Check results / metrics + Lambda -> Logs : Structured validation logs + Lambda --> SFN : { status: SUCCESSFUL | FAILED, details } + deactivate Lambda +else Unsupported / disabled type + Route --> SFN : { status: SKIPPED, reason } +end + +deactivate Route + +' 4. Publish result back to AWS Backup +SFN -> ResultAPI : PutRestoreValidationResult\n(status, message, resourceType, metadata) +ResultAPI --> SFN : 200 OK + +SFN -> Logs : State machine execution log (success path) +SFN --> EB : (Implicit: EventBridge metrics / tracing) +SFN --> U : (Optional surfacing via reporting / notifications) + +SFN --> Backup : (Validation outcome associated to restore job) +deactivate SFN + +== Failure Handling == + +group Validator Error Path + Lambda -> Logs : Error + stack trace + Lambda --> SFN : { status: FAILED, errorMessage } + SFN -> ResultAPI : PutRestoreValidationResult (FAILED) + ResultAPI --> SFN : 200 OK +end + +== Notes == +note over Lambda,APIs + Validation logic pluggable per resource type. + Future extensions: metrics, alarms, custom plugins. +end note + +note over SFN + States (conceptual): + 1. EnrichRestoreJob + 2. RouteByResourceType + 3. InvokeValidator (task) OR SkipUnsupported (pass) + 4. PublishResult +end note + +@enduml diff --git a/docs/restore-testing-design.md b/docs/restore-testing-design.md new file mode 100644 index 0000000..cd560ba --- /dev/null +++ b/docs/restore-testing-design.md @@ -0,0 +1,310 @@ +# AWS Backup Restore Testing Validation & Integrity Design + +## 1. Objectives + +Provide a blueprint extension that not only provisions AWS Backup Restore Testing Plans (already partially implemented via `awscc_backup_restore_testing_plan` and selections) but also validates that restored resources are *functional* and *internally consistent*. Users (blueprint implementers) define integrity checks per resource type (e.g. SQL query for RDS/Aurora, manifest verification for S3, item checks for DynamoDB) executed automatically after AWS Backup restore tests complete. + +## 2. High-Level Architecture + +![end-to-end visual of the event-driven validation workflow](diagrams/restore-validation-sequence.png) + +```text +AWS Backup Restore Testing Plan (scheduled) + │ (runs restore jobs) + ▼ +Restore Test Jobs (Test restore of latest/random recovery points) + │ emit EventBridge events (Restore Job State Change: COMPLETED) + ▼ +EventBridge Rule (filters status=COMPLETED + restoreTestingPlanArn) + │ + ▼ +Step Functions State Machine (or direct Lambda) <── optional batching fan‑in + 1. Fetch restore job details + 2. Dispatch per resource-type validator (Lambda / Fargate / custom) + 3. Execute user-defined integrity logic (SQL / API / S3 diff etc.) + 4. Aggregate results + 5. Call PutRestoreValidationResult (per restore job) + 6. Emit metrics + SNS / EventBridge notifications + │ + ▼ +CloudWatch Metrics / Logs / Alarms + Backup Console Validation Status +``` + +### Why Step Functions? + +- Orchestrates retries, parallel fan-out per restored resource +- Standardises timeout + backoff policies +- Simplifies conditional branching for resource types +- Enables centralised audit trail for validation workflow + +A simpler single Lambda path remains possible for minimal setups; design supports either. + +## 3. Data & Control Flows + +| Flow | Source → Target | Notes | +|------|-----------------|-------| +| A | AWS Backup → EventBridge | "Restore Job State Change" event, includes `restoreJobId`, `resourceType`, `createdResourceArn`, `restoreTestingPlanArn` | +| B | EventBridge → Step Functions | Input filtered by plan ARN / resource types | +| C | Step Functions → AWS Backup API | `DescribeRestoreJob` for enrichment | +| D | Step Functions → Validator Lambdas | One per resource type OR generic dispatcher | +| E | Validators → Target resource | Run integrity checks (SQL, scan, HEAD, etc.) | +| F | Validators → AWS Backup | `PutRestoreValidationResult(ValidationStatus=SUCCESSFUL\|FAILED\|SKIPPED)` | +| G | Step Functions → CloudWatch / SNS | Emit metrics, structured JSON log, optional alert | + +## 4. State Machine Definition (Express or Standard) + +Recommended: **Standard** (because restores may take hours; we only start after COMPLETED but validation might be longer running for large datasets). Express acceptable if you guarantee short validations. + +Proposed states (Amazon States Language pseudo): + +```json +{ + "Comment": "Restore Test Validation Orchestrator", + "StartAt": "Init", + + "States": { + "Init": { "Type": "Pass", "ResultPath": "$.context", "Next": "EnrichRestoreJob" }, + "EnrichRestoreJob": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:backup:describeRestoreJob", "Parameters": { "RestoreJobId": "$.detail.restoreJobId" }, "ResultPath": "$.restoreJob", "Next": "RouteByResourceType" }, + "RouteByResourceType": { "Type": "Choice", "Choices": [ + { "Variable": "$.detail.resourceType", "StringEquals": "Aurora", "Next": "AuroraValidation" }, + { "Variable": "$.detail.resourceType", "StringEquals": "RDS", "Next": "RDSValidation" }, + { "Variable": "$.detail.resourceType", "StringEquals": "DynamoDB", "Next": "DynamoValidation" }, + { "Variable": "$.detail.resourceType", "StringEquals": "S3", "Next": "S3Validation" } + ], "Default": "GenericSkip" }, + "AuroraValidation": { "Type": "Task", "Resource": "${lambda_arn_aurora}" , "ResultPath": "$.validation", "Next": "PublishResult" }, + "RDSValidation": { "Type": "Task", "Resource": "${lambda_arn_rds}" , "ResultPath": "$.validation", "Next": "PublishResult" }, + "DynamoValidation": { "Type": "Task", "Resource": "${lambda_arn_dynamo}" , "ResultPath": "$.validation", "Next": "PublishResult" }, + "S3Validation": { "Type": "Task", "Resource": "${lambda_arn_s3}" , "ResultPath": "$.validation", "Next": "PublishResult" }, + "GenericSkip": { "Type": "Pass", "Result": { "status": "SKIPPED", "message": "No validator implemented for resourceType" }, "ResultPath": "$.validation", "Next": "PublishResult" }, + "PublishResult": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:backup:putRestoreValidationResult", "Parameters": { "RestoreJobId": "$.detail.restoreJobId", "ValidationStatus": "$.validation.status", "ValidationStatusMessage": "$.validation.message" }, "Next": "EmitMetrics" }, + "EmitMetrics": { "Type": "Task", "Resource": "${lambda_arn_metrics}", "End": true } + } +} +``` + +Notes: + +- `${lambda_arn_*}` produced conditionally via Terraform based on enabled validators. +- Timeout & retry policies applied per Task (e.g. RDS 5 min, S3 2 min, Dynamo 1 min) with `Retry` blocks. +- Could collapse validators into one generic Lambda with plugin pattern. + +## 5. Extensibility Interface + +Users supply validation definitions via Terraform variables consumed by validator Lambda(s). + +### 5.1 Terraform Variables (additions) + +```hcl +variable "restore_validation_config" { + description = "Map keyed by resource type containing validation directives." + type = object({ + rds = optional(object({ + enabled = bool + cluster_identifiers = optional(list(string)) + sql_checks = list(object({ + database = string + statement = string + expected_rows = optional(number) + expected_hash = optional(string) # SHA256 of concatenated row values + timeout_seconds = optional(number) + })) + secret_arn = string # AWS Secrets Manager ARN for master creds or read-only + })) + dynamodb = optional(object({ + enabled = bool + tables = list(string) + checks = list(object({ + table = string + expected_item_count = optional(number) + key_sample = optional(list(object({ + pk = string + sk = optional(string) + expected_item_hash = optional(string) + }))) + })) + })) + s3 = optional(object({ + enabled = bool + buckets = list(object({ + name = string + manifest_s3_uri = optional(string) # points to authoritative manifest + sample_prefixes = optional(list(string)) + compare_object_tags = optional(bool) + })) + })) + aurora = optional(object({ + enabled = bool + clusters = list(string) + sql_checks = list(object({ + cluster_endpoint = optional(string) + database = string + statement = string + expected_rows = optional(number) + })) + secret_arn = string + })) + }) + default = {} +} +``` + + +### 5.2 Lambda Validator Contract + +All validator handlers accept unified event schema: + +```json +{ + "restoreJobId": "string", + "resourceType": "RDS|Aurora|DynamoDB|S3|...", + "createdResourceArn": "arn:aws:...", + "config": { "...resource specific config subset..." } +} +``` +Return object: + + +```json +{ "status": "SUCCESSFUL|FAILED|SKIPPED", "message": "Human readable" } +``` + + +### 5.3 Packaging Strategy + +- Single Lambda with language (Python/Node) loads `config` JSON from SSM Parameter or encrypted file in S3 (to avoid large env variables) +- Pluggable validators registered in a dict keyed by resource type +- Optional user-provided Lambda ARN override per resource type for complete custom logic + +### 5.4 Validation Logic Patterns + +| Resource | Strategy | Failure Conditions | +|----------|----------|-------------------| +| RDS/Aurora | Execute SQL checks (each inside txn, read-only) | Query error, row count mismatch, hash mismatch, timeout | +| DynamoDB | DescribeTable + (optional) Scan limit or PartiQL key gets | Table missing, item count variance > threshold, sample hash mismatch | +| S3 | HEAD sample objects, optional compare against manifest (object key + size + etag) | Missing objects, size/etag mismatch, manifest not accessible | +| EBS (future) | (Optional) Attach test volume to temp instance and run FS metadata probe script | Attach failure, FS errors | + +## 6. Examples + +### 6.1 RDS Example Config + +```hcl +restore_validation_config = { + rds = { + enabled = true + secret_arn = aws_secretsmanager_secret.rds_ro.arn + sql_checks = [ + { database = "appdb", statement = "SELECT COUNT(*) c FROM customers", expected_rows = 1 }, + { database = "appdb", statement = "SELECT sha256(string_agg(id || ':' || status, ',' ORDER BY id)) h FROM orders", expected_hash = "abc123..." } + ] + } +} +``` + +### 6.2 DynamoDB Example Config + +```hcl +restore_validation_config = { + dynamodb = { + enabled = true + tables = ["orders", "customers"] + checks = [ + { table = "orders", expected_item_count = 15000 }, + { table = "customers", key_sample = [ { pk = "CUST#123", expected_item_hash = "d41d8cd98f" } ] } + ] + } +} +``` + +### 6.3 S3 Example Config + +```hcl +restore_validation_config = { + s3 = { + enabled = true + buckets = [{ + name = "images-bucket", + manifest_s3_uri = "s3://manifests-prod/images-bucket.manifest.json", + sample_prefixes = ["2025/09/", "2025/08/"] + }] + } +} +``` + +## 7. Security & Compliance + +- IAM: Validators assume dedicated role with least-privilege policies (RDS: `rds-data:ExecuteStatement` / `secretsmanager:GetSecretValue`; DynamoDB: `DescribeTable`, `GetItem`, limited `Scan` with `Limit`; S3: `HeadObject`, `GetObject` for manifest) +- Secrets: Use Secrets Manager for DB creds; do not log credentials or query data +- KMS: Encrypt Lambda environment variables, S3 manifest bucket, and Secrets Manager secret +- Network: For RDS/Aurora in private subnets, place Lambda in same VPC subnets with least required SG egress +- Auditing: Structured JSON logs (include `restoreJobId`, `resourceType`, check identifiers) +- PII Minimisation: Hash or count only; avoid selecting raw personal data rows +- Integrity of config: Optionally sign config file (S3 object with checksum validation before use) + +## 8. Operational Considerations & Cost + +- Throttle: Concurrency controls via Step Functions + reserved concurrency on validator Lambda to avoid storm after bulk restores +- Timeouts: Short per-check timeouts (e.g. 30s; fail fast pattern) +- Retention Window: If deeper validation requires longer retention, expose `retain_hours_before_cleanup` variable (aligns with AWS restore testing retention concept) +- Metrics: Emit CloudWatch custom metrics: `ValidationSuccess`, `ValidationFailure`, `ValidationDurationMs` with dimensions `ResourceType`, `PlanName` +- Alerting: SNS topic for failures >0 in last run, or error rate > threshold across rolling period +- Cost Levers: Limit number of SQL checks; use targeted `GetItem` vs full table scans; sample S3 objects (k=20 per prefix) unless manifest diff required + +## 9. Acceptance Criteria Mapping + +| Requirement | Design Element | +|------------|----------------| +| "Ability from the blueprint to run automated test to validate restoration" | EventBridge + Step Functions + validators triggered on restore completion | +| "Test integrity of restored resource, specific to blueprint implementer" | `restore_validation_config` + per-resource plugin architecture | +| "Define an SQL query for RDS to test integrity" | `sql_checks` array with expected rows/hash support | +| "Customer responsible for defining and validating check" | User supplies Terraform variable config and (optionally) custom Lambda override | +| "Step function would just allow this functionality" | State machine orchestrates and records results via `PutRestoreValidationResult` | + +## 10. Future Enhancements + +- Add cross-account validation (restore to isolated test account, assume role back) +- Support FSx / EFS mount probing using Fargate task +- Provide Terraform module subfolder `validation` generating Step Functions + default validator Lambda +- Add canned dashboards (CloudWatch) for validation pass rate & duration + +## 11. Terraform Module Additions (Summary) + +Minimal initial scope: + +1. New optional module `aws-backup-validation` OR integrated into `aws-backup-source` behind feature flag `enable_restore_validation` +2. Resources: + - EventBridge rule + - Step Functions state machine (JSON from templatefile) + - IAM roles/policies (state machine + lambda) + - Validator Lambda (zip from local build or external source) + - SSM Parameter / S3 object for config JSON +3. Variables: `enable_restore_validation`, `restore_validation_config`, `custom_validator_lambda_arns` (map) +4. Outputs: `restore_validation_state_machine_arn`, `restore_validation_config_parameter_arn` + +Current prototype implementation lives in `modules/aws-backup-validation` and provides a minimal Lambda + Step Functions + EventBridge rule path. Future iterations should harden IAM scoping and expand validator logic prior to production adoption. + +## 12. Example User Flow + +1. Enable restore testing (already done with existing plan resources) +2. Set `enable_restore_validation = true` +3. Provide `restore_validation_config` with at least one resource type +4. Apply Terraform – deploys validation infra +5. Wait for scheduled restore test; Step Functions records validation results +6. View status in AWS Backup Console / CloudWatch dashboard + +## 13. Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Long-running SQL leads to Lambda timeout | Enforce per-query timeout + limit operations (SELECT only) | +| Validator failure blocks result publishing | Wrap each validator in try/catch; on unhandled exception mark FAILED with reason | +| Sensitive data leakage in logs | Scrub query parameters and row data; log only counts + hashes | +| Drift between Terraform config and live validator config | Version config (include checksum) and log version per run | +| Excess costs from scanning large DynamoDB tables | Use item count from `DescribeTable` and targeted sample keys, avoid full scans | + +## 14. Open Questions + +- Provide managed library of validation query templates? (Out of initial scope) +- Should retention hours be explicitly configurable per selection via Terraform? (Potential future variable) +- Add option for concurrency-limited validation queue (SQS + Lambda) instead of Step Functions? (Future scale consideration) + diff --git a/modules/aws-backup-validation/README.md b/modules/aws-backup-validation/README.md new file mode 100644 index 0000000..4f4c835 --- /dev/null +++ b/modules/aws-backup-validation/README.md @@ -0,0 +1,41 @@ +# aws-backup-validation Module + +Prototype module that deploys infrastructure to validate AWS Backup Restore Testing jobs. + +## Components + +- Lambda validator (pluggable placeholder) reading config from SSM Parameter +- Step Functions state machine orchestrating describe + validator + publish result +- EventBridge rule triggering on restore job COMPLETED for a specific restore testing plan ARN +- IAM roles/policies (least-privilege baseline – refine for production) + +## Inputs + +Refer to `variables.tf` for full list. Key variables: + +- `restore_testing_plan_arn` (required) +- `validation_config_json` JSON document with resource-type validation definitions + +## Outputs + +- `state_machine_arn` +- `validator_lambda_arn` +- `config_parameter_name` + +## Example + +```hcl +module "backup_validation" { + source = "../modules/aws-backup-validation" + restore_testing_plan_arn = awscc_backup_restore_testing_plan.backup_restore_testing_plan.arn + validation_config_json = jsonencode({ + rds = { sql_checks = [{ database = "appdb", statement = "SELECT 1" }] } + }) +} +``` + +## Next Steps + +- Expand validator logic (RDS via rds-data, S3 manifest comparisons, DynamoDB samples) +- Add CloudWatch metrics & alarms +- Add optional custom Lambda override mapping per resource type diff --git a/modules/aws-backup-validation/iam.tf b/modules/aws-backup-validation/iam.tf new file mode 100644 index 0000000..3c11e66 --- /dev/null +++ b/modules/aws-backup-validation/iam.tf @@ -0,0 +1,112 @@ +locals { + validator_lambda_name = "${var.name_prefix}-validator" + state_machine_name = "${var.name_prefix}-state-machine" + ssm_param_name = "/${var.name_prefix}/config" +} + +resource "aws_iam_role" "validator_lambda" { + name = "${local.validator_lambda_name}-role" + assume_role_policy = data.aws_iam_policy_document.lambda_assume.json + tags = var.tags +} + +data "aws_iam_policy_document" "lambda_assume" { + statement { + effect = "Allow" + principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy" "validator_basic" { + name = "${local.validator_lambda_name}-basic" + role = aws_iam_role.validator_lambda.id + policy = data.aws_iam_policy_document.validator_policy.json +} + +data "aws_iam_policy_document" "validator_policy" { + statement { + sid = "Logs" + effect = "Allow" + actions = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] + resources = ["arn:aws:logs:*:*:*" ] + } + statement { + sid = "DescribeRestoreJob" + effect = "Allow" + actions = ["backup:DescribeRestoreJob", "backup:PutRestoreValidationResult"] + resources = ["*"] + } + statement { + sid = "GetConfig" + effect = "Allow" + actions = ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParameterHistory"] + resources = ["arn:aws:ssm:*:*:parameter${local.ssm_param_name}"] + } + # Add minimal read for services (extend if needed by resource type validators) + statement { + sid = "RDSData" + effect = "Allow" + actions = ["rds-data:ExecuteStatement"] + resources = ["*"] + } + statement { + sid = "DynamoRead" + effect = "Allow" + actions = ["dynamodb:DescribeTable", "dynamodb:GetItem"] + resources = ["*"] + } + statement { + sid = "S3Head" + effect = "Allow" + actions = ["s3:HeadObject", "s3:GetObject"] + resources = ["*"] + } + statement { + sid = "SecretsRead" + effect = "Allow" + actions = ["secretsmanager:GetSecretValue"] + resources = ["*"] + } +} + +resource "aws_iam_role" "state_machine" { + name = "${local.state_machine_name}-role" + assume_role_policy = data.aws_iam_policy_document.sfn_assume.json + tags = var.tags +} + +data "aws_iam_policy_document" "sfn_assume" { + statement { + effect = "Allow" + principals { type = "Service" identifiers = ["states.amazonaws.com"] } + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy" "state_machine_policy" { + name = "${local.state_machine_name}-policy" + role = aws_iam_role.state_machine.id + policy = data.aws_iam_policy_document.state_machine_policy.json +} + +data "aws_iam_policy_document" "state_machine_policy" { + statement { + sid = "InvokeValidator" + effect = "Allow" + actions = ["lambda:InvokeFunction"] + resources = [aws_lambda_function.validator.arn] + } + statement { + sid = "BackupCalls" + effect = "Allow" + actions = ["backup:DescribeRestoreJob", "backup:PutRestoreValidationResult"] + resources = ["*"] + } + statement { + sid = "Logs" + effect = "Allow" + actions = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] + resources = ["arn:aws:logs:*:*:*" ] + } +} diff --git a/modules/aws-backup-validation/lambda.py b/modules/aws-backup-validation/lambda.py new file mode 100644 index 0000000..aa4fe9e --- /dev/null +++ b/modules/aws-backup-validation/lambda.py @@ -0,0 +1,75 @@ +import json +import os +import boto3 +import hashlib + +ssm = boto3.client('ssm') +backup = boto3.client('backup') +secrets = boto3.client('secretsmanager') +rds_data = boto3.client('rds-data') +dynamodb = boto3.client('dynamodb') +s3 = boto3.client('s3') + +CONFIG_PARAM_NAME = os.environ.get('CONFIG_PARAM_NAME') + +_cached_config = None + +def load_config(): + global _cached_config + if _cached_config is not None: + return _cached_config + if not CONFIG_PARAM_NAME: + _cached_config = {} + return _cached_config + resp = ssm.get_parameter(Name=CONFIG_PARAM_NAME) + _cached_config = json.loads(resp['Parameter']['Value']) + return _cached_config + +def handler(event, context): + # Event expected from Step Functions state machine + restore_job_id = event.get('detail', {}).get('restoreJobId') or event.get('restoreJobId') + resource_type = event.get('detail', {}).get('resourceType') or event.get('resourceType') + created_arn = event.get('detail', {}).get('createdResourceArn') or event.get('createdResourceArn') + + config = load_config() + result = {"status": "SKIPPED", "message": f"No validator for {resource_type}"} + + try: + if resource_type in ("RDS", "Aurora"): + result = validate_rds_like(resource_type, created_arn, config.get('rds') or config.get('aurora')) + elif resource_type == "DynamoDB": + result = validate_dynamodb(created_arn, config.get('dynamodb')) + elif resource_type == "S3": + result = validate_s3(created_arn, config.get('s3')) + except Exception as exc: # noqa + result = {"status": "FAILED", "message": f"Unhandled validator error: {exc}"} + + return result + +def validate_rds_like(resource_type, arn, cfg): + if not cfg or not cfg.get('sql_checks'): + return {"status": "SKIPPED", "message": "No sql_checks configured"} + failures = [] + for chk in cfg['sql_checks']: + stmt = chk['statement'] + db = chk['database'] + # Placeholder: In real implementation we would look up secret and cluster endpoint + try: + # rds-data call would require secretArn + resourceArn for serverless Aurora or HTTP endpoint; omitted here + pass + except Exception as exc: # noqa + failures.append(f"{db}: {exc}") + if failures: + return {"status": "FAILED", "message": "; ".join(failures)[:1000]} + return {"status": "SUCCESSFUL", "message": "All RDS/Aurora checks passed (placeholder)"} + +def validate_dynamodb(arn, cfg): + if not cfg or not cfg.get('tables'): + return {"status": "SKIPPED", "message": "No dynamodb tables configured"} + # Placeholder logic only + return {"status": "SUCCESSFUL", "message": "DynamoDB validation placeholder"} + +def validate_s3(arn, cfg): + if not cfg or not cfg.get('buckets'): + return {"status": "SKIPPED", "message": "No s3 buckets configured"} + return {"status": "SUCCESSFUL", "message": "S3 validation placeholder"} diff --git a/modules/aws-backup-validation/lambda.tf b/modules/aws-backup-validation/lambda.tf new file mode 100644 index 0000000..de98e77 --- /dev/null +++ b/modules/aws-backup-validation/lambda.tf @@ -0,0 +1,34 @@ +data "archive_file" "validator_zip" { + type = "zip" + source_file = "${path.module}/lambda.py" + output_path = "${path.module}/lambda.zip" +} + +resource "aws_ssm_parameter" "config" { + name = local.ssm_param_name + type = "String" + value = var.validation_config_json + tags = var.tags +} + +resource "aws_lambda_function" "validator" { + function_name = local.validator_lambda_name + role = aws_iam_role.validator_lambda.arn + runtime = var.lambda_runtime + handler = "lambda.handler" + filename = data.archive_file.validator_zip.output_path + source_code_hash = data.archive_file.validator_zip.output_base64sha256 + timeout = var.lambda_timeout + environment { + variables = { + CONFIG_PARAM_NAME = aws_ssm_parameter.config.name + } + } + tags = var.tags +} + +resource "aws_cloudwatch_log_group" "validator" { + name = "/aws/lambda/${aws_lambda_function.validator.function_name}" + retention_in_days = var.log_retention_days + tags = var.tags +} diff --git a/modules/aws-backup-validation/outputs.tf b/modules/aws-backup-validation/outputs.tf new file mode 100644 index 0000000..ed5b501 --- /dev/null +++ b/modules/aws-backup-validation/outputs.tf @@ -0,0 +1,14 @@ +output "state_machine_arn" { + description = "ARN of the validation Step Functions state machine" + value = aws_sfn_state_machine.validation.arn +} + +output "validator_lambda_arn" { + description = "ARN of the validator lambda" + value = aws_lambda_function.validator.arn +} + +output "config_parameter_name" { + description = "Name of SSM parameter storing validation config" + value = aws_ssm_parameter.config.name +} diff --git a/modules/aws-backup-validation/statemachine.json.tpl b/modules/aws-backup-validation/statemachine.json.tpl new file mode 100644 index 0000000..c1b7949 --- /dev/null +++ b/modules/aws-backup-validation/statemachine.json.tpl @@ -0,0 +1,49 @@ +{ + "Comment": "Restore Test Validation Orchestrator", + "StartAt": "EnrichRestoreJob", + "States": { + "EnrichRestoreJob": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:backup:describeRestoreJob", + "Parameters": {"RestoreJobId.$": "$.detail.restoreJobId"}, + "ResultPath": "$.restoreJob", + "Next": "Route" + }, + "Route": { + "Type": "Choice", + "Choices": [ + {"Variable": "$.detail.resourceType", "StringEquals": "Aurora", "Next": "InvokeValidator"}, + {"Variable": "$.detail.resourceType", "StringEquals": "RDS", "Next": "InvokeValidator"}, + {"Variable": "$.detail.resourceType", "StringEquals": "DynamoDB", "Next": "InvokeValidator"}, + {"Variable": "$.detail.resourceType", "StringEquals": "S3", "Next": "InvokeValidator"} + ], + "Default": "Skip" + }, + "InvokeValidator": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "OutputPath": "$.Payload", + "Parameters": { + "FunctionName": "${lambda_arn}", + "Payload.$": "$" + }, + "Next": "PublishResult" + }, + "Skip": { + "Type": "Pass", + "Result": {"status": "SKIPPED", "message": "No validator implemented"}, + "ResultPath": "$.validation", + "Next": "PublishResult" + }, + "PublishResult": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:backup:putRestoreValidationResult", + "Parameters": { + "RestoreJobId.$": "$.detail.restoreJobId", + "ValidationStatus.$": "$.status", + "ValidationStatusMessage.$": "$.message" + }, + "End": true + } + } +} diff --git a/modules/aws-backup-validation/statemachine.tf b/modules/aws-backup-validation/statemachine.tf new file mode 100644 index 0000000..16b4188 --- /dev/null +++ b/modules/aws-backup-validation/statemachine.tf @@ -0,0 +1,76 @@ +locals { + statemachine_definition = templatefile("${path.module}/statemachine.json.tpl", { + lambda_arn = aws_lambda_function.validator.arn + }) +} + +resource "aws_sfn_state_machine" "validation" { + name = local.state_machine_name + role_arn = aws_iam_role.state_machine.arn + definition = local.statemachine_definition + tags = var.tags +} + +resource "aws_iam_role_policy" "allow_sfn_logs" { + name = "${local.state_machine_name}-logs" + role = aws_iam_role.state_machine.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] + Resource = "arn:aws:logs:*:*:*" + }] + }) +} + +# EventBridge Rule for restore job completion +resource "aws_cloudwatch_event_rule" "restore_completed" { + name = "${var.name_prefix}-restore-completed" + description = "Triggers validation on restore job completion" + event_pattern = jsonencode({ + source = ["aws.backup"], + "detail-type" = ["Restore Job State Change"], + detail = { + status = ["COMPLETED"] + restoreTestingPlanArn = [ var.restore_testing_plan_arn ] + } + }) + tags = var.tags +} + +resource "aws_cloudwatch_event_target" "sfn_target" { + rule = aws_cloudwatch_event_rule.restore_completed.name + target_id = "${var.name_prefix}-sfn" + arn = aws_sfn_state_machine.validation.arn + role_arn = aws_iam_role.eventbridge_invoke.arn +} + +resource "aws_iam_role" "eventbridge_invoke" { + name = "${var.name_prefix}-events-invoke-sfn-role" + assume_role_policy = data.aws_iam_policy_document.events_assume.json + tags = var.tags +} + +data "aws_iam_policy_document" "events_assume" { + statement { + effect = "Allow" + principals { type = "Service" identifiers = ["events.amazonaws.com"] } + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy" "events_invoke_sfn" { + name = "${var.name_prefix}-events-invoke-sfn" + role = aws_iam_role.eventbridge_invoke.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = ["states:StartExecution"], + Resource = aws_sfn_state_machine.validation.arn + } + ] + }) +} diff --git a/modules/aws-backup-validation/variables.tf b/modules/aws-backup-validation/variables.tf new file mode 100644 index 0000000..ee328db --- /dev/null +++ b/modules/aws-backup-validation/variables.tf @@ -0,0 +1,52 @@ +variable "enable" { + description = "Whether to deploy restore validation components." + type = bool + default = true +} + +variable "name_prefix" { + description = "Prefix for created resources (state machine, lambda, etc)." + type = string + default = "backup-restore-validation" +} + +variable "restore_testing_plan_arn" { + description = "ARN of the AWS Backup Restore Testing Plan to filter events." + type = string +} + +variable "resource_types" { + description = "List of resource types we will attempt to validate (e.g. [\"RDS\", \"Aurora\", \"DynamoDB\", \"S3\"])." + type = list(string) + default = [] +} + +variable "validation_config_json" { + description = "Raw JSON string of validation configuration to be stored in SSM Parameter for the Lambda validator." + type = string + default = "{}" +} + +variable "lambda_runtime" { + description = "Runtime for validator lambda." + type = string + default = "python3.11" +} + +variable "lambda_timeout" { + description = "Timeout in seconds for validator lambda." + type = number + default = 60 +} + +variable "log_retention_days" { + description = "CloudWatch log retention for validator lambda." + type = number + default = 14 +} + +variable "tags" { + description = "Tags to apply to created resources." + type = map(string) + default = {} +} diff --git a/modules/aws-backup-validation/versions.tf b/modules/aws-backup-validation/versions.tf new file mode 100644 index 0000000..7f163ea --- /dev/null +++ b/modules/aws-backup-validation/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} From 05df4955f74f4a5f264fd3eee5850edc7345b9fe Mon Sep 17 00:00:00 2001 From: Nick Miles Date: Fri, 19 Sep 2025 16:07:10 +0100 Subject: [PATCH 2/2] ENG-893 AWS Backup Validation Lambda to use Node.js and TypeScript --- modules/aws-backup-validation/lambda.tf | 14 ++-- modules/aws-backup-validation/package.json | 19 ++++++ modules/aws-backup-validation/src/index.ts | 71 +++++++++++++++++++++ modules/aws-backup-validation/tsconfig.json | 14 ++++ modules/aws-backup-validation/variables.tf | 2 +- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 modules/aws-backup-validation/package.json create mode 100644 modules/aws-backup-validation/src/index.ts create mode 100644 modules/aws-backup-validation/tsconfig.json diff --git a/modules/aws-backup-validation/lambda.tf b/modules/aws-backup-validation/lambda.tf index de98e77..b76410f 100644 --- a/modules/aws-backup-validation/lambda.tf +++ b/modules/aws-backup-validation/lambda.tf @@ -1,6 +1,6 @@ data "archive_file" "validator_zip" { type = "zip" - source_file = "${path.module}/lambda.py" + source_file = "${path.module}/dist/index.js" output_path = "${path.module}/lambda.zip" } @@ -12,13 +12,13 @@ resource "aws_ssm_parameter" "config" { } resource "aws_lambda_function" "validator" { - function_name = local.validator_lambda_name - role = aws_iam_role.validator_lambda.arn - runtime = var.lambda_runtime - handler = "lambda.handler" - filename = data.archive_file.validator_zip.output_path + function_name = local.validator_lambda_name + role = aws_iam_role.validator_lambda.arn + runtime = var.lambda_runtime + handler = "index.handler" + filename = data.archive_file.validator_zip.output_path source_code_hash = data.archive_file.validator_zip.output_base64sha256 - timeout = var.lambda_timeout + timeout = var.lambda_timeout environment { variables = { CONFIG_PARAM_NAME = aws_ssm_parameter.config.name diff --git a/modules/aws-backup-validation/package.json b/modules/aws-backup-validation/package.json new file mode 100644 index 0000000..d6ac2d7 --- /dev/null +++ b/modules/aws-backup-validation/package.json @@ -0,0 +1,19 @@ +{ + "name": "aws-backup-validation-lambda", + "version": "0.1.0", + "private": true, + "description": "Validator Lambda for AWS Backup restore testing (scaffold)", + "license": "UNLICENSED", + "type": "module", + "scripts": { + "build": "tsc", + "clean": "rimraf dist || rm -rf dist", + "package": "npm run build" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.129", + "@types/node": "^20.11.30", + "typescript": "^5.4.0", + "rimraf": "^5.0.5" + } +} diff --git a/modules/aws-backup-validation/src/index.ts b/modules/aws-backup-validation/src/index.ts new file mode 100644 index 0000000..359713d --- /dev/null +++ b/modules/aws-backup-validation/src/index.ts @@ -0,0 +1,71 @@ +import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm'; +import { BackupClient } from '@aws-sdk/client-backup'; +import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; +import { RDSDataClient } from '@aws-sdk/client-rds-data'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { S3Client } from '@aws-sdk/client-s3'; +import type { Context } from 'aws-lambda'; + +const ssm = new SSMClient({}); +const backup = new BackupClient({}); // reserved for future use +const secrets = new SecretsManagerClient({}); // future +const rdsData = new RDSDataClient({}); // future +const dynamodb = new DynamoDBClient({}); // future more detailed calls +const s3 = new S3Client({}); // future + +const CONFIG_PARAM_NAME = process.env.CONFIG_PARAM_NAME; +let cachedConfig: any | null = null; + +async function loadConfig(): Promise { + if (cachedConfig) return cachedConfig; + if (!CONFIG_PARAM_NAME) { + cachedConfig = {}; + return cachedConfig; + } + const resp = await ssm.send(new GetParameterCommand({ Name: CONFIG_PARAM_NAME })); + cachedConfig = resp.Parameter?.Value ? JSON.parse(resp.Parameter.Value) : {}; + return cachedConfig; +} + +interface ValidationResult { status: 'SUCCESSFUL' | 'FAILED' | 'SKIPPED'; message: string; } + +export const handler = async (event: any, _context: Context): Promise => { + const restoreJobId = event?.detail?.restoreJobId || event?.restoreJobId; + const resourceType = event?.detail?.resourceType || event?.resourceType; + const createdArn = event?.detail?.createdResourceArn || event?.createdResourceArn; + + const config = await loadConfig(); + let result: ValidationResult = { status: 'SKIPPED', message: `No validator for ${resourceType}` }; + + try { + if (resourceType === 'RDS' || resourceType === 'Aurora') { + result = await validateRdsLike(resourceType, createdArn, config.rds || config.aurora); + } else if (resourceType === 'DynamoDB') { + result = await validateDynamoDb(createdArn, config.dynamodb); + } else if (resourceType === 'S3') { + result = await validateS3(createdArn, config.s3); + } + } catch (err: any) { + result = { status: 'FAILED', message: `Unhandled validator error: ${err?.message || String(err)}` }; + } + + return result; +}; + +async function validateRdsLike(resourceType: string, arn: string, cfg: any): Promise { + if (!cfg || !cfg.sql_checks) { + return { status: 'SKIPPED', message: 'No sql_checks configured' }; + } + // Placeholder: iterate over cfg.sql_checks and (in future) execute statements via rds-data. + return { status: 'SUCCESSFUL', message: 'All RDS/Aurora checks passed (placeholder)' }; +} + +async function validateDynamoDb(arn: string, cfg: any): Promise { + if (!cfg || !cfg.tables) return { status: 'SKIPPED', message: 'No dynamodb tables configured' }; + return { status: 'SUCCESSFUL', message: 'DynamoDB validation placeholder' }; +} + +async function validateS3(arn: string, cfg: any): Promise { + if (!cfg || !cfg.buckets) return { status: 'SKIPPED', message: 'No s3 buckets configured' }; + return { status: 'SUCCESSFUL', message: 'S3 validation placeholder' }; +} diff --git a/modules/aws-backup-validation/tsconfig.json b/modules/aws-backup-validation/tsconfig.json new file mode 100644 index 0000000..ea190c3 --- /dev/null +++ b/modules/aws-backup-validation/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "Node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/modules/aws-backup-validation/variables.tf b/modules/aws-backup-validation/variables.tf index ee328db..6319bb2 100644 --- a/modules/aws-backup-validation/variables.tf +++ b/modules/aws-backup-validation/variables.tf @@ -30,7 +30,7 @@ variable "validation_config_json" { variable "lambda_runtime" { description = "Runtime for validator lambda." type = string - default = "python3.11" + default = "nodejs20.x" } variable "lambda_timeout" {