meal-order-manager/template.yaml
Adam Moussa c6b16678ce
Some checks failed
Deploy / deploy (push) Has been cancelled
Add CloudWatch alarm coverage (meal-order-manager) (#32)
* Add CloudWatch alarm coverage for the meal-order-manager stack

Add CloudWatch alarms (all notifying the shared site-alerts SNS topic,
no OKActions, TreatMissingData notBreaching) across the stack:

- Lambda Errors + Throttles alarms for all 7 functions (Sum, 5min,
  threshold 0).
- Lambda Duration p99 alarms at ~80% of each function's timeout;
  API-fronted functions eval 3/3, cron/async functions eval 1/1.
  Thresholds pending sign-off.
- DynamoDB orders-table Read/WriteThrottleEvents alarms (TableName dim).
  ThrottledRequests/SystemErrors are not published at the table-only
  dimension, so they are intentionally omitted.
- API Gateway (OrderApi v2) 5xx, 4xx (threshold 20, 3/2 to absorb
  routine authorizer 401s), and p99 Latency alarms.

Update README with a Monitoring section and correct the Lambda count
to 7 (admin-authorizer was missing).

* Drop pending-sign-off wording from alarm docs

Duration/Latency thresholds are owner-approved; remove PENDING ADAM
SIGN-OFF / pending-sign-off notes from template.yaml comments and README.
2026-06-17 14:45:54 -04:00

1188 lines
40 KiB
YAML

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<String>
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 — they're explicitly set to NONE on their events.
Auth:
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
# 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
- x-api-key
- 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:
FORM_APIKEY_SM_NAME: meal-order-manager/form-api-key
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: secretsmanager:GetSecretValue
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
- 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
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-<fn>-<signal> (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 "<YOUR_GOOGLE_CLIENT_ID>"
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