import * as cdk from "aws-cdk-lib"; import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch"; import * as events from "aws-cdk-lib/aws-events"; import * as events_targets from "aws-cdk-lib/aws-events-targets"; import * as iam from "aws-cdk-lib/aws-iam"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as logs from "aws-cdk-lib/aws-logs"; import * as s3 from "aws-cdk-lib/aws-s3"; import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha"; import { Construct } from "constructs"; interface BackupVerificationProps { sourceBucket: s3.IBucket; replicaBucketName: string; gcsBucket: string; gcsSaSecretName: string; slackWebhookSecretName: string; } export class BackupVerification extends Construct { public readonly functionArn: string; constructor(scope: Construct, id: string, props: BackupVerificationProps) { super(scope, id); const fn = new PythonFunction(this, "Function", { functionName: "forgejo-backup-verification", entry: "lambda/backup-verification", runtime: lambda.Runtime.PYTHON_3_12, architecture: lambda.Architecture.ARM_64, handler: "handler", index: "app.py", memorySize: 512, ephemeralStorageSize: cdk.Size.gibibytes(4), timeout: cdk.Duration.minutes(5), environment: { SOURCE_BUCKET: props.sourceBucket.bucketName, REPLICA_BUCKET: props.replicaBucketName, GCS_BUCKET: props.gcsBucket, GCS_SA_SECRET_NAME: props.gcsSaSecretName, SLACK_WEBHOOK_SECRET_NAME: props.slackWebhookSecretName, }, logRetention: logs.RetentionDays.TWO_MONTHS, }); props.sourceBucket.grantRead(fn); fn.addToRolePolicy( new iam.PolicyStatement({ actions: ["s3:ListBucket", "s3:GetObject"], resources: [ `arn:aws:s3:::${props.replicaBucketName}`, `arn:aws:s3:::${props.replicaBucketName}/*`, ], }) ); const account = cdk.Stack.of(this).account; const region = cdk.Stack.of(this).region; fn.addToRolePolicy( new iam.PolicyStatement({ actions: ["secretsmanager:GetSecretValue"], resources: [ `arn:aws:secretsmanager:${region}:${account}:secret:${props.gcsSaSecretName}-*`, `arn:aws:secretsmanager:${region}:${account}:secret:${props.slackWebhookSecretName}-*`, ], }) ); fn.addToRolePolicy( new iam.PolicyStatement({ actions: ["ec2:DescribeSnapshots"], resources: ["*"], }) ); new events.Rule(this, "DailyCheck", { ruleName: "forgejo-backup-daily-check", schedule: events.Schedule.cron({ hour: "8", minute: "0" }), targets: [ new events_targets.LambdaFunction(fn, { event: events.RuleTargetInput.fromObject({ mode: "daily" }), }), ], }); new events.Rule(this, "MonthlyRestoreTest", { ruleName: "forgejo-backup-monthly-restore-test", schedule: events.Schedule.cron({ hour: "9", minute: "0", day: "1", }), targets: [ new events_targets.LambdaFunction(fn, { event: events.RuleTargetInput.fromObject({ mode: "restore-test" }), }), ], }); new cloudwatch.Alarm(this, "ErrorAlarm", { alarmName: "forgejo-backup-verification-errors", alarmDescription: "Backup verification Lambda is failing — Slack notifications may not be firing", metric: fn.metricErrors({ period: cdk.Duration.hours(1) }), threshold: 1, evaluationPeriods: 1, treatMissingData: cloudwatch.TreatMissingData.BREACHING, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, }); this.functionArn = fn.functionArn; } }