mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 12:23:13 +00:00
Some checks failed
Deploy / deploy (push) Has been cancelled
Re-adds WebACLId (from SSM /seahaven/waf/app-web-acl-arn) now that the github-cfn-execution-role has wafv2 perms. Deployed + verified: orders.seahaven.com distribution now fronted by seahaven-app-waf.
560 lines
18 KiB
YAML
560 lines
18 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
|
|
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
|
|
# 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
|
|
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/*'
|
|
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
|
|
AdminOrdersUpdate:
|
|
Type: HttpApi
|
|
Properties:
|
|
ApiId: !Ref OrderApi
|
|
Path: /api/admin/orders
|
|
Method: PUT
|
|
AdminOrdersDelete:
|
|
Type: HttpApi
|
|
Properties:
|
|
ApiId: !Ref OrderApi
|
|
Path: /api/admin/orders
|
|
Method: DELETE
|
|
|
|
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
|
|
|
|
# ─── 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
|