AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > meal-order-manager — automated weekly meal ordering from Redefine Meals with employee order collection, Slack notifications, and payroll deduction reports. Parameters: CustomDomain: Type: String Default: orders.seahaven.com Description: Custom domain for the order form (requires ACM cert) CertificateArn: Type: String Default: '' Description: ACM certificate ARN for the custom domain (us-east-1) PayrollEmail: Type: String Default: payroll@seahavenind.com Description: Email address for payroll deduction reports SenderEmail: Type: String Default: adam@seahavenind.com Description: SES verified sender email for payroll reports # Shared CloudFront WAF WebACL ARN (audit M-17), published to SSM by # seahaven-account-baseline. Resolved at deploy time. WebAclArn: Type: AWS::SSM::Parameter::Value Default: /seahaven/waf/app-web-acl-arn Description: ARN of the shared seahaven-app-waf CloudFront WebACL Conditions: HasCustomDomain: !Not [!Equals [!Ref CertificateArn, '']] Globals: Function: Runtime: python3.12 Architectures: - arm64 Timeout: 30 MemorySize: 256 PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary Environment: Variables: TABLE_NAME: !Ref OrdersTable REPORTS_BUCKET: !Ref ReportsBucket SLACK_CHANNEL_PARAM: /meal-order-manager/slack-channel-id FORM_URL: !If - HasCustomDomain - !Sub 'https://${CustomDomain}' - !Sub 'https://${FormDistribution.DomainName}' SLACK_BOT_SM_NAME: meal-order-manager/slack-bot-token Layers: - !Ref SharedLayer Resources: # ─── Shared Layer ─────────────────────────────────────────────── SharedLayer: Type: AWS::Serverless::LayerVersion Properties: LayerName: meal-order-manager-shared ContentUri: src/shared/ CompatibleRuntimes: - python3.12 CompatibleArchitectures: - arm64 Metadata: BuildMethod: python3.12 BuildArchitecture: arm64 # ─── DynamoDB ─────────────────────────────────────────────────── OrdersTable: Type: AWS::DynamoDB::Table Properties: TableName: meal-order-manager-orders BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: PK AttributeType: S - AttributeName: SK AttributeType: S KeySchema: - AttributeName: PK KeyType: HASH - AttributeName: SK KeyType: RANGE TimeToLiveSpecification: AttributeName: ttl Enabled: true # ─── S3 Buckets ──────────────────────────────────────────────── FormBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'meal-order-manager-form-${AWS::AccountId}' PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true LifecycleConfiguration: Rules: - Id: delete-old-archives Prefix: archive/ Status: Enabled ExpirationInDays: 90 Tags: - Key: Purpose Value: meal-order-form-hosting - Key: ManagedBy Value: meal-order-manager FormBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref FormBucket PolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowCloudFrontOAC Effect: Allow Principal: Service: cloudfront.amazonaws.com Action: s3:GetObject Resource: !Sub '${FormBucket.Arn}/*' Condition: StringEquals: AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${FormDistribution}' ReportsBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'meal-order-manager-reports-${AWS::AccountId}' PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true LifecycleConfiguration: Rules: - Id: archive-old-reports Status: Enabled Transitions: - StorageClass: GLACIER_IR TransitionInDays: 90 Tags: - Key: Purpose Value: meal-order-reports - Key: ManagedBy Value: meal-order-manager # ─── CloudFront ──────────────────────────────────────────────── FormOAC: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Name: meal-order-manager-oac OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 FormDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Enabled: true DefaultRootObject: index.html Comment: meal-order-manager form hosting PriceClass: PriceClass_100 HttpVersion: http2and3 WebACLId: !Ref WebAclArn # shared CloudFront WAF (audit M-17) Aliases: !If - HasCustomDomain - [!Ref CustomDomain] - !Ref AWS::NoValue ViewerCertificate: !If - HasCustomDomain - AcmCertificateArn: !Ref CertificateArn SslSupportMethod: sni-only MinimumProtocolVersion: TLSv1.2_2021 - CloudFrontDefaultCertificate: true Origins: - Id: S3FormOrigin DomainName: !GetAtt FormBucket.RegionalDomainName OriginAccessControlId: !Ref FormOAC S3OriginConfig: OriginAccessIdentity: '' DefaultCacheBehavior: TargetOriginId: S3FormOrigin ViewerProtocolPolicy: redirect-to-https CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled Compress: true AllowedMethods: - GET - HEAD CachedMethods: - GET - HEAD CustomErrorResponses: - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: /index.html # ─── API Gateway ─────────────────────────────────────────────── ApiAccessLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/apigateway/meal-order-manager RetentionInDays: 90 OrderApi: Type: AWS::Serverless::HttpApi Properties: StageName: $default # Gateway-level auth for /api/admin/* routes (INFRA-100). A Lambda # authorizer validates the same Google ID token (Authorization: Bearer) # the admin panel already sends, so admin routes are no longer # AuthorizationType NONE. Public routes (submit-order, form-status, # roster) stay open, while weekly-menu publication routes opt into IAM. Auth: EnableIamAuthorizer: true Authorizers: AdminGoogleAuthorizer: FunctionArn: !GetAtt AdminAuthorizerFunction.Arn FunctionInvokeRole: !GetAtt AdminAuthorizerInvokeRole.Arn Identity: Headers: - Authorization AuthorizerPayloadFormatVersion: '2.0' EnableSimpleResponses: true # Disable result caching: with caching, an expired Google token or # an admin removed from admin_emails would stay authorized for the # cache TTL. The tokeninfo call is the dominant latency anyway. AuthorizerResultTtlInSeconds: 0 # No DefaultAuthorizer — routes opt in individually so the public # routes remain unauthenticated. # Access logging + default throttling (audit M-18). AccessLogSettings: DestinationArn: !GetAtt ApiAccessLogGroup.Arn Format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","method":"$context.httpMethod","routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","integrationError":"$context.integrationErrorMessage"}' DefaultRouteSettings: ThrottlingBurstLimit: 50 ThrottlingRateLimit: 100 RouteSettings: 'POST /api/submit-order': ThrottlingBurstLimit: 10 ThrottlingRateLimit: 5 'POST /api/publish/menu': ThrottlingBurstLimit: 2 ThrottlingRateLimit: 1 # CORS only allows the production domain. For local development, use the # Flask dev server (app.py) which proxies API requests and doesn't enforce CORS. CorsConfiguration: AllowOrigins: - !If - HasCustomDomain - !Sub 'https://${CustomDomain}' - !Sub 'https://${FormDistribution.DomainName}' AllowMethods: - GET - POST - PUT - DELETE - OPTIONS AllowHeaders: - Content-Type - Authorization MaxAge: 3600 # ─── Lambda Functions ────────────────────────────────────────── SubmitOrderFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-submit-order Handler: handler.lambda_handler CodeUri: functions/submit_order/ MemorySize: 128 Timeout: 10 Environment: Variables: SLACK_NOTIFIER_ARN: !GetAtt SlackNotifierFunction.Arn GOOGLE_CLIENT_ID_PARAM: /meal-order-manager/google-client-id REPORTS_BUCKET: !Ref ReportsBucket Policies: - DynamoDBCrudPolicy: TableName: !Ref OrdersTable - Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt SlackNotifierFunction.Arn - Effect: Allow Action: ssm:GetParameter Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*' # Read-only access to weekly summary PDFs (only — not the # payroll/order CSVs) for the admin summary-pdf presigned-URL # endpoint. - Effect: Allow Action: s3:GetObject Resource: !Sub '${ReportsBucket.Arn}/reports/*/weekly-summary-*.pdf' Events: SubmitOrder: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/submit-order Method: POST FormStatus: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/form-status/{week} Method: GET Roster: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/roster Method: GET PublishSettings: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/publish/settings Method: GET Auth: Authorizer: AWS_IAM PublishMenu: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/publish/menu Method: POST Auth: Authorizer: AWS_IAM AdminOrders: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/admin/orders Method: GET Auth: Authorizer: AdminGoogleAuthorizer AdminOrdersUpdate: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/admin/orders Method: PUT Auth: Authorizer: AdminGoogleAuthorizer AdminOrdersDelete: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/admin/orders Method: DELETE Auth: Authorizer: AdminGoogleAuthorizer AdminSummaryPdf: Type: HttpApi Properties: ApiId: !Ref OrderApi Path: /api/admin/summary-pdf Method: GET Auth: Authorizer: AdminGoogleAuthorizer # ─── Admin API Authorizer (INFRA-100) ───────────────────────── # Lambda authorizer validating the Google ID token + admin-email allow-list # for every /api/admin/* route. Mirrors submit_order's _verify_admin so the # existing admin panel works unchanged. AdminAuthorizerFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-admin-authorizer Handler: handler.lambda_handler CodeUri: functions/admin_authorizer/ MemorySize: 128 Timeout: 10 Environment: Variables: GOOGLE_CLIENT_ID_PARAM: /meal-order-manager/google-client-id Policies: - DynamoDBReadPolicy: TableName: !Ref OrdersTable - Statement: - Effect: Allow Action: ssm:GetParameter Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*' AdminAuthorizerLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${AdminAuthorizerFunction}' RetentionInDays: 60 # IAM role API Gateway assumes to invoke the authorizer Lambda. AdminAuthorizerInvokeRole: Type: AWS::IAM::Role Properties: Path: /cfn-managed/ PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: apigateway.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: invoke-admin-authorizer PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt AdminAuthorizerFunction.Arn CloseFormFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-close-form Handler: handler.lambda_handler CodeUri: functions/close_form/ MemorySize: 128 Timeout: 30 Policies: - DynamoDBCrudPolicy: TableName: !Ref OrdersTable - Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt AggregateOrdersFunction.Arn Environment: Variables: AGGREGATE_FUNCTION_ARN: !GetAtt AggregateOrdersFunction.Arn # Both EST and EDT schedules fire every week year-round. The handler is # idempotent, so the "wrong timezone" firing is a harmless no-op. Events: CloseEST: Type: Schedule Properties: Schedule: cron(59 4 ? * FRI *) Description: 'Close form Thursday 11:59pm EST (04:59 UTC Friday)' Enabled: true CloseEDT: Type: Schedule Properties: Schedule: cron(59 3 ? * FRI *) Description: 'Close form Thursday 11:59pm EDT (03:59 UTC Friday)' Enabled: true AggregateOrdersFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-aggregate-orders Handler: handler.lambda_handler CodeUri: functions/aggregate_orders/ MemorySize: 256 Timeout: 60 Policies: - DynamoDBCrudPolicy: TableName: !Ref OrdersTable - S3CrudPolicy: BucketName: !Ref ReportsBucket - Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt SlackNotifierFunction.Arn Environment: Variables: SLACK_NOTIFIER_ARN: !GetAtt SlackNotifierFunction.Arn SlackNotifierFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-slack-notifier Handler: handler.lambda_handler CodeUri: functions/slack_notifier/ MemorySize: 128 Timeout: 30 Policies: - DynamoDBReadPolicy: TableName: !Ref OrdersTable - Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*' - Effect: Allow Action: ssm:GetParameter Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*' Events: ReminderEST: Type: Schedule Properties: Schedule: cron(0 15 ? * THU *) Description: 'DM reminders Thursday 10am EST (15:00 UTC)' Enabled: true Input: '{"event": "reminder"}' ReminderEDT: Type: Schedule Properties: Schedule: cron(0 14 ? * THU *) Description: 'DM reminders Thursday 10am EDT (14:00 UTC)' Enabled: true Input: '{"event": "reminder"}' SyncRosterFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-sync-roster Handler: handler.lambda_handler CodeUri: functions/sync_roster/ MemorySize: 128 Timeout: 60 Policies: - DynamoDBCrudPolicy: TableName: !Ref OrdersTable - Statement: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*' - Effect: Allow Action: ssm:GetParameter Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*' Events: SyncEST: Type: Schedule Properties: Schedule: cron(55 11 ? * MON *) Description: 'Sync roster Monday 6:55am EST (11:55 UTC) — before menu publish' Enabled: true SyncEDT: Type: Schedule Properties: Schedule: cron(55 10 ? * MON *) Description: 'Sync roster Monday 6:55am EDT (10:55 UTC) — before menu publish' Enabled: true EmailReportFunction: Type: AWS::Serverless::Function Properties: FunctionName: meal-order-manager-email-report Handler: handler.lambda_handler CodeUri: functions/email_report/ MemorySize: 128 Timeout: 30 Environment: Variables: PAYROLL_EMAIL: !Ref PayrollEmail SENDER_EMAIL: !Ref SenderEmail Policies: - DynamoDBReadPolicy: TableName: !Ref OrdersTable - S3ReadPolicy: BucketName: !Ref ReportsBucket - Statement: - Effect: Allow Action: - ses:SendRawEmail Resource: '*' Events: PayrollEmailEST: Type: Schedule Properties: Schedule: cron(0 12 ? * MON *) Description: 'Email payroll deductions Monday 7am EST (12:00 UTC)' Enabled: true PayrollEmailEDT: Type: Schedule Properties: Schedule: cron(0 11 ? * MON *) Description: 'Email payroll deductions Monday 7am EDT (11:00 UTC)' Enabled: true # ─── CloudWatch Log Groups (60-day retention) ────────────────── SubmitOrderLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${SubmitOrderFunction}' RetentionInDays: 60 CloseFormLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${CloseFormFunction}' RetentionInDays: 60 AggregateOrdersLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${AggregateOrdersFunction}' RetentionInDays: 60 SlackNotifierLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${SlackNotifierFunction}' RetentionInDays: 60 EmailReportLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${EmailReportFunction}' RetentionInDays: 60 SyncRosterLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${SyncRosterFunction}' RetentionInDays: 60 # ─── CloudWatch Alarms ───────────────────────────────────────── # All alarms notify the shared site-alerts SNS topic. No OKActions # (no recovery spam); TreatMissingData notBreaching so idle / cron # functions don't sit in ALARM between invocations. # # Naming: meal-order-manager-- (repo-namespaced kebab-case). # # Duration alarms use ExtendedStatistic p99 at ~80% of each function's # configured timeout. Synchronous (API-fronted) functions evaluate over # 3 datapoints; cron / async-invoked functions evaluate a single datapoint # (they fire too rarely for a multi-datapoint window). # Lambda Errors (Sum, 5min, any error breaches) SubmitOrderErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-submit-order-errors AlarmDescription: submit-order Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref SubmitOrderFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts AdminAuthorizerErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-admin-authorizer-errors AlarmDescription: admin-authorizer Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref AdminAuthorizerFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts CloseFormErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-close-form-errors AlarmDescription: close-form Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref CloseFormFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts AggregateOrdersErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-aggregate-orders-errors AlarmDescription: aggregate-orders Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref AggregateOrdersFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts SlackNotifierErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-slack-notifier-errors AlarmDescription: slack-notifier Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref SlackNotifierFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts SyncRosterErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-sync-roster-errors AlarmDescription: sync-roster Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref SyncRosterFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts EmailReportErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-email-report-errors AlarmDescription: email-report Lambda reported one or more errors in 5 minutes. Namespace: AWS/Lambda MetricName: Errors Dimensions: - Name: FunctionName Value: !Ref EmailReportFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # Lambda Throttles (Sum, 5min, any throttle breaches) SubmitOrderThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-submit-order-throttles AlarmDescription: submit-order Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref SubmitOrderFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts AdminAuthorizerThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-admin-authorizer-throttles AlarmDescription: admin-authorizer Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref AdminAuthorizerFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts CloseFormThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-close-form-throttles AlarmDescription: close-form Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref CloseFormFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts AggregateOrdersThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-aggregate-orders-throttles AlarmDescription: aggregate-orders Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref AggregateOrdersFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts SlackNotifierThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-slack-notifier-throttles AlarmDescription: slack-notifier Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref SlackNotifierFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts SyncRosterThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-sync-roster-throttles AlarmDescription: sync-roster Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref SyncRosterFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts EmailReportThrottlesAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-email-report-throttles AlarmDescription: email-report Lambda was throttled in the last 5 minutes. Namespace: AWS/Lambda MetricName: Throttles Dimensions: - Name: FunctionName Value: !Ref EmailReportFunction Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # Lambda Duration p99 (~80% of timeout) # Synchronous (API-fronted) functions: eval 3 / datapoints 3. SubmitOrderDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-submit-order-duration AlarmDescription: submit-order p99 duration exceeded 8000ms (80% of 10s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref SubmitOrderFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 3 DatapointsToAlarm: 3 Threshold: 8000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts AdminAuthorizerDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-admin-authorizer-duration AlarmDescription: admin-authorizer p99 duration exceeded 8000ms (80% of 10s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref AdminAuthorizerFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 3 DatapointsToAlarm: 3 Threshold: 8000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # Cron / async-invoked functions: single datapoint (eval 1). CloseFormDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-close-form-duration AlarmDescription: close-form p99 duration exceeded 24000ms (80% of 30s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref CloseFormFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 1 DatapointsToAlarm: 1 Threshold: 24000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts AggregateOrdersDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-aggregate-orders-duration AlarmDescription: aggregate-orders p99 duration exceeded 48000ms (80% of 60s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref AggregateOrdersFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 1 DatapointsToAlarm: 1 Threshold: 48000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts SlackNotifierDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-slack-notifier-duration AlarmDescription: slack-notifier p99 duration exceeded 24000ms (80% of 30s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref SlackNotifierFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 1 DatapointsToAlarm: 1 Threshold: 24000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts SyncRosterDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-sync-roster-duration AlarmDescription: sync-roster p99 duration exceeded 48000ms (80% of 60s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref SyncRosterFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 1 DatapointsToAlarm: 1 Threshold: 48000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts EmailReportDurationAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-email-report-duration AlarmDescription: email-report p99 duration exceeded 24000ms (80% of 30s timeout). Namespace: AWS/Lambda MetricName: Duration Dimensions: - Name: FunctionName Value: !Ref EmailReportFunction ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 1 DatapointsToAlarm: 1 Threshold: 24000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # DynamoDB orders table. # NOTE: DynamoDB does NOT publish ThrottledRequests or SystemErrors at the # TableName-only dimension (verified via cloudwatch list-metrics on # 2026-06-17 — those metrics carry a TableName+Operation dimension pair and # only on-occurrence). A TableName-dim alarm on them would never evaluate. # The codifiable table-level throttle signals are ReadThrottleEvents and # WriteThrottleEvents, which DO carry a TableName-only dimension. Those are # used here for throttle coverage; a TableName-dim SystemErrors alarm is # omitted (no such metric is emitted). See PR body. OrdersTableReadThrottleAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-orders-read-throttle AlarmDescription: orders table read requests were throttled in the last 5 minutes. Namespace: AWS/DynamoDB MetricName: ReadThrottleEvents Dimensions: - Name: TableName Value: !Ref OrdersTable Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts OrdersTableWriteThrottleAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-orders-write-throttle AlarmDescription: orders table write requests were throttled in the last 5 minutes. Namespace: AWS/DynamoDB MetricName: WriteThrottleEvents Dimensions: - Name: TableName Value: !Ref OrdersTable Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # API Gateway (HTTP API v2) — OrderApi. Metrics carry the ApiId dimension. OrderApi5xxAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-order-api-5xx AlarmDescription: OrderApi returned one or more 5xx responses in 5 minutes. Namespace: AWS/ApiGateway MetricName: 5xx Dimensions: - Name: ApiId Value: !Ref OrderApi Statistic: Sum Period: 300 EvaluationPeriods: 1 Threshold: 0 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # 4xx threshold raised + eval 3 / dp 2 to absorb routine 401s from the # token-based admin authorizer without paging. OrderApi4xxAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-order-api-4xx AlarmDescription: OrderApi 4xx responses exceeded 20 in 5 minutes (beyond routine auth noise). Namespace: AWS/ApiGateway MetricName: 4xx Dimensions: - Name: ApiId Value: !Ref OrderApi Statistic: Sum Period: 300 EvaluationPeriods: 3 DatapointsToAlarm: 2 Threshold: 20 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # Latency p99 ~3000ms (see PR body). OrderApiLatencyAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: meal-order-manager-order-api-latency AlarmDescription: OrderApi p99 latency exceeded 3000ms. Namespace: AWS/ApiGateway MetricName: Latency Dimensions: - Name: ApiId Value: !Ref OrderApi ExtendedStatistic: p99 Period: 300 EvaluationPeriods: 3 DatapointsToAlarm: 3 Threshold: 3000 ComparisonOperator: GreaterThanThreshold TreatMissingData: notBreaching AlarmActions: - arn:aws:sns:us-east-1:328440206208:site-alerts # ─── SSM Parameters ──────────────────────────────────────────── SlackChannelParam: Type: AWS::SSM::Parameter Properties: Name: /meal-order-manager/slack-channel-id Type: String Value: CHANGE_ME Description: Slack channel ID for meal order notifications # GoogleClientIdParam (/meal-order-manager/google-client-id) is managed # manually via AWS CLI since it varies per environment. Create it with: # aws ssm put-parameter --name /meal-order-manager/google-client-id \ # --type String --value "" Outputs: ApiUrl: Description: API Gateway endpoint URL Value: !Sub 'https://${OrderApi}.execute-api.${AWS::Region}.amazonaws.com' FormUrl: Description: Order form URL Value: !If - HasCustomDomain - !Sub 'https://${CustomDomain}' - !Sub 'https://${FormDistribution.DomainName}' DistributionId: Description: CloudFront distribution ID (for cache invalidation) Value: !Ref FormDistribution FormBucketName: Description: S3 bucket for form HTML Value: !Ref FormBucket ReportsBucketName: Description: S3 bucket for CSV reports Value: !Ref ReportsBucket OrdersTableName: Description: DynamoDB table name Value: !Ref OrdersTable SubmitOrderFunctionArn: Description: Submit Order Lambda ARN Value: !GetAtt SubmitOrderFunction.Arn CloseFormFunctionArn: Description: Close Form Lambda ARN Value: !GetAtt CloseFormFunction.Arn AggregateOrdersFunctionArn: Description: Aggregate Orders Lambda ARN Value: !GetAtt AggregateOrdersFunction.Arn SlackNotifierFunctionArn: Description: Slack Notifier Lambda ARN Value: !GetAtt SlackNotifierFunction.Arn SyncRosterFunctionArn: Description: Sync Roster Lambda ARN Value: !GetAtt SyncRosterFunction.Arn EmailReportFunctionArn: Description: Email Report Lambda ARN Value: !GetAtt EmailReportFunction.Arn