diff --git a/lib/door-unlock-stack.ts b/lib/door-unlock-stack.ts index 3278165..4a9ca4e 100644 --- a/lib/door-unlock-stack.ts +++ b/lib/door-unlock-stack.ts @@ -12,6 +12,9 @@ import * as route53 from "aws-cdk-lib/aws-route53"; import * as route53Targets from "aws-cdk-lib/aws-route53-targets"; import * as acm from "aws-cdk-lib/aws-certificatemanager"; import * as logs from "aws-cdk-lib/aws-logs"; +import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch"; +import * as cwactions from "aws-cdk-lib/aws-cloudwatch-actions"; +import * as sns from "aws-cdk-lib/aws-sns"; import { Construct } from "constructs"; import * as path from "path"; @@ -329,5 +332,68 @@ export class DoorUnlockStack extends cdk.Stack { new cdk.CfnOutput(this, "LockdownApiUrl", { value: `https://doorunlock.seahaven.com/lockdown`, }); + + // ── CloudWatch error alarms (INFRA-101) ────────────────────────────────── + // Import the cross-stack site-alerts SNS topic. This topic is managed by + // seahaven-account-baseline and encrypted with alias/seahaven-alarm-topics + // (CMK). Never use alias/aws/sns here — CloudWatch cannot publish to topics + // using the AWS-managed SNS key (silent publish failure). + // ALARM state only; no OK/recovery actions per Sea Haven preference. + const siteAlerts = sns.Topic.fromTopicArn( + this, + "SiteAlerts", + "arn:aws:sns:us-east-1:328440206208:site-alerts" + ); + + interface AlarmSpec { + readonly id: string; + readonly fn: lambda.Function; + readonly alarmName: string; + readonly description: string; + } + + const alarmSpecs: AlarmSpec[] = [ + { + id: "UnlockErrors", + fn: unlockHandler, + alarmName: "door-unlock-api-unlock-errors", + description: "door-unlock-api-unlock Lambda is erroring — physical door unlock calls may be failing", + }, + { + id: "LockdownErrors", + fn: lockdownHandler, + alarmName: "door-unlock-api-lockdown-errors", + description: "door-unlock-api-lockdown Lambda is erroring — lockdown commands may not be executing", + }, + { + id: "AuthorizerErrors", + fn: authorizerHandler, + alarmName: "door-unlock-api-authorizer-errors", + description: "door-unlock-api-authorizer Lambda is erroring — all API requests will be rejected", + }, + { + id: "PollerErrors", + fn: pollerHandler, + alarmName: "door-unlock-api-lockdown-poller-errors", + description: "door-unlock-api-lockdown-poller Lambda is erroring — lockdown state polling may be broken", + }, + ]; + + for (const spec of alarmSpecs) { + const alarm = new cloudwatch.Alarm(this, spec.id, { + alarmName: spec.alarmName, + alarmDescription: spec.description, + metric: spec.fn.metricErrors({ + period: cdk.Duration.minutes(5), + statistic: "Sum", + }), + threshold: 0, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + evaluationPeriods: 1, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, + }); + // ALARM-only: addAlarmAction only (no addOkAction / addInsufficientDataAction) + alarm.addAlarmAction(new cwactions.SnsAction(siteAlerts)); + } } }