afterhours-shift-manager/template.yaml
Adam Moussa 424be7c2da
Some checks are pending
Deploy / deploy (push) Waiting to run
Attach permissions boundary to all Lambda execution roles (#105)
Applies seahaven-lambda-execution-boundary to all SAM auto-generated
function execution roles via Globals.Function.PermissionsBoundary.
Required so the github-cfn-execution-role scope-down (INFRA-97) can
safely constrain role creation without blocking Lambda deploys.

No explicit AWS::IAM::Role resources exist in this template.

Refs: INFRA-103
2026-06-10 14:14:46 -04:00

290 lines
9.2 KiB
YAML

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: After-Hours Shift Manager — Slack bot for managing on-call shifts with 3CX integration
Parameters:
Timezone:
Type: String
Default: "America/New_York"
ShiftChannel:
Type: String
Description: Slack channel ID for schedule posts and shift notifications
QueueNumber:
Type: String
Default: "801"
Description: 3CX queue extension number to update
Globals:
Function:
Runtime: python3.12
Timeout: 30
MemorySize: 1024
Architectures:
- arm64
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
# Access logging + default throttling on the implicit HTTP API (audit M-18).
HttpApi:
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
Resources:
ApiAccessLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/apigateway/afterhours-shift-manager
RetentionInDays: 90
# --- Shared Lambda Layer ---
SharedLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: afterhours-shared
ContentUri: src/shared/
CompatibleRuntimes:
- python3.12
CompatibleArchitectures:
- arm64
Metadata:
BuildMethod: python3.12
BuildArchitecture: arm64
# --- DynamoDB ---
ShiftTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: afterhours-shifts
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: expires_at
Enabled: true
# --- Slack Bot Lambda ---
SlackBotFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-shift-manager
Handler: handler.handler
CodeUri: src/slack-bot/
Layers:
- !Ref SharedLayer
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SLACK_SIGNING_SECRET: afterhours-shift-manager/slack-signing-secret
SHIFT_CHANNEL: !Ref ShiftChannel
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
QUEUE_NUMBER: !Ref QueueNumber
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
Events:
SlackEvents:
Type: HttpApi
Properties:
Path: /slack/events
Method: POST
# --- Weekly Schedule Post (Monday 7am ET) ---
WeeklyPostFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-weekly-post
Handler: app.handler
CodeUri: src/weekly-post/
Layers:
- !Ref SharedLayer
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SHIFT_CHANNEL: !Ref ShiftChannel
SES_SENDER: noreply@seahaven.com
PAYROLL_RECIPIENTS: payroll@seahaven.com
PAY_REPORT_USER: U0A3SC48T47
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
- Effect: Allow
Action:
- ses:SendEmail
Resource:
- !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/*"
Events:
# EST: 7am ET = 12:00 UTC (Nov-Mar)
WeeklyPostEST:
Type: Schedule
Properties:
Schedule: cron(0 12 ? * MON *)
Description: "Post weekly schedule Monday 7am EST"
Enabled: true
# EDT: 7am ET = 11:00 UTC (Mar-Nov)
WeeklyPostEDT:
Type: Schedule
Properties:
Schedule: cron(0 11 ? * MON *)
Description: "Post weekly schedule Monday 7am EDT"
Enabled: true
# --- Roster Sync Lambda (daily sync from 3CX) ---
RosterSyncFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-roster-sync
Handler: app.handler
CodeUri: src/roster-sync/
Layers:
- !Ref SharedLayer
Timeout: 60
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
SYNC_GROUP: DEFAULT
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
Events:
# Daily at 6am ET (before the 7am schedule post and 8am 3CX scheduler)
# EST: 6am ET = 11:00 UTC (Nov-Mar)
RosterSyncEST:
Type: Schedule
Properties:
Schedule: cron(0 11 ? * * *)
Description: "Sync roster from 3CX at 6am EST"
Enabled: true
# EDT: 6am ET = 10:00 UTC (Mar-Nov)
RosterSyncEDT:
Type: Schedule
Properties:
Schedule: cron(0 10 ? * * *)
Description: "Sync roster from 3CX at 6am EDT"
Enabled: true
# --- Ring Scheduler (daily 3CX queue routing updates) ---
RingSchedulerFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-ring-scheduler
Handler: app.handler
CodeUri: src/ring-scheduler/
Layers:
- !Ref SharedLayer
Timeout: 60
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
QUEUE_NUMBER: !Ref QueueNumber
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
Events:
# Daily at 8am ET — update after-hours routing
DailyScheduleEST:
Type: Schedule
Properties:
Schedule: cron(0 13 ? * * *)
Description: "Update 3CX queue at 8am EST"
Enabled: true
DailyScheduleEDT:
Type: Schedule
Properties:
Schedule: cron(0 12 ? * * *)
Description: "Update 3CX queue at 8am EDT"
Enabled: true
# Weekends at 5pm ET — switch to night shift person
WeekendEveningEST:
Type: Schedule
Properties:
Schedule: cron(0 22 ? * SAT,SUN *)
Description: "Update 3CX queue at 5pm EST weekends"
Enabled: true
WeekendEveningEDT:
Type: Schedule
Properties:
Schedule: cron(0 21 ? * SAT,SUN *)
Description: "Update 3CX queue at 5pm EDT weekends"
Enabled: true
# --- CloudWatch Log Groups (explicit 60-day retention) ---
SlackBotLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${SlackBotFunction}"
RetentionInDays: 60
WeeklyPostLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${WeeklyPostFunction}"
RetentionInDays: 60
RosterSyncLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${RosterSyncFunction}"
RetentionInDays: 60
RingSchedulerLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${RingSchedulerFunction}"
RetentionInDays: 60
Outputs:
SlackBotApiUrl:
Description: URL for Slack app Request URL configuration
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/events"
ShiftTableName:
Value: !Ref ShiftTable
SlackBotFunctionArn:
Value: !GetAtt SlackBotFunction.Arn
WeeklyPostFunctionArn:
Value: !GetAtt WeeklyPostFunction.Arn
RosterSyncFunctionArn:
Value: !GetAtt RosterSyncFunction.Arn
RingSchedulerFunctionArn:
Value: !GetAtt RingSchedulerFunction.Arn