This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
seahaven-slack-bot/lib/seahaven-slack-bot-stack.ts
Adam Moussa c28d5b1170 Add CloudWatch alarm coverage via MonitoringConstruct
Add a MonitoringConstruct (lib/constructs/monitoring.ts) wiring CloudWatch
alarms to the shared site-alerts SNS topic for the seahaven-slack-bot stack.
Every alarm uses an SNS alarm action only (no OK action) and treats missing
data as NOT_BREACHING; alarm names are repo-namespaced kebab-case.

Coverage:
- Lambda (9 fns): Errors, Throttles, Duration (p99, eval3/dp2, ~80% of timeout)
- DynamoDB (seahaven-conversations, seahaven-unanswered-questions):
  ThrottledRequests + SystemErrors via the per-operations metric-math helpers
  (the bare TableName-only helpers are deprecated/invalid); operations scoped
  to 6 CRUD ops to stay under the 10-metric alarm-math cap
- API Gateway v2 (seahaven-slack-webhook): 5xx, 4xx, Latency (ApiId dimension)
- ECS Fargate (seahaven-socket-mode): CPU + Memory utilization (AWS/ECS)

Expose qbo-oauth Lambda and the ECS cluster/service as public readonly handles
without changing logical IDs. RunningTaskCount alarm is gated off pending
Container Insights sign-off (separate commit).
2026-06-17 13:57:07 -04:00

135 lines
6.6 KiB
TypeScript

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
import { ConversationLogConstruct } from './constructs/conversation-log';
import { KnowledgeBaseConstruct } from './constructs/knowledge-base';
import { BedrockAgentConstruct } from './constructs/bedrock-agent';
import { SlackHandlerConstruct } from './constructs/slack-handler';
import { SocketModeConstruct } from './constructs/socket-mode';
import { NotionSyncConstruct } from './constructs/notion-sync';
import { PoSyncConstruct } from './constructs/po-sync';
import { WorkorderSyncConstruct } from './constructs/workorder-sync';
import { MonitoringConstruct } from './constructs/monitoring';
export class SeahavenSlackBotStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const wildcardCertArn = this.node.tryGetContext('wildcardCertArn') as string | undefined;
if (!wildcardCertArn || wildcardCertArn.includes('CHANGE-ME')) {
throw new Error(
'Set wildcardCertArn in cdk.json or pass --context wildcardCertArn=arn:aws:acm:...\n' +
'Run: aws acm list-certificates --region us-east-1 to find your cert ARN.',
);
}
// ── VPC (existing) — QBO Lambdas + Socket Mode run here ──────────────────
const vpc = ec2.Vpc.fromLookup(this, 'SeahavenVpc', { vpcId: 'vpc-0d3d4b67bd0cf8a68' });
const lambdaSecurityGroup = new ec2.SecurityGroup(this, 'QBOLambdaSG', {
vpc,
securityGroupName: 'seahaven-qbo-lambda',
description: 'QBO Lambdas - outbound HTTPS only',
allowAllOutbound: true,
});
// ── Conversation history + unanswered questions (DynamoDB) ────────────────
const conversationLog = new ConversationLogConstruct(this, 'ConversationLog');
// ── Bedrock Knowledge Base (OpenSearch Serverless + S3) ───────────────────
const knowledgeBase = new KnowledgeBaseConstruct(this, 'KnowledgeBase', {
accountId: this.account,
region: this.region,
});
// ── Bedrock Agent (Alex — Claude Sonnet + action groups) ─────────────────
const bedrockAgent = new BedrockAgentConstruct(this, 'BedrockAgent', {
accountId: this.account,
region: this.region,
knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId,
knowledgeBaseArn: knowledgeBase.knowledgeBase.knowledgeBaseArn,
vpc,
lambdaSecurityGroup,
});
// ── Notion → KB daily sync (EventBridge + Lambda) ────────────────────────
const notionSync = new NotionSyncConstruct(this, 'NotionSync', {
region: this.region,
kbDocsBucket: knowledgeBase.docsBucket,
knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId,
dataSourceId: knowledgeBase.dataSource.dataSourceId,
});
// ── Purchase Orders → KB daily sync (EventBridge + Lambda) ────────────────
const poSync = new PoSyncConstruct(this, 'PoSync', {
region: this.region,
kbDocsBucket: knowledgeBase.docsBucket,
knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId,
dataSourceId: knowledgeBase.dataSource.dataSourceId,
});
// ── Work Orders → KB daily sync (EventBridge + Lambda) ────────────────────
const workorderSync = new WorkorderSyncConstruct(this, 'WorkorderSync', {
region: this.region,
kbDocsBucket: knowledgeBase.docsBucket,
knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId,
dataSourceId: knowledgeBase.dataSource.dataSourceId,
});
// ── Slack handler (processor + app home + QBO OAuth + API Gateway) ────────
const slackHandler = new SlackHandlerConstruct(this, 'SlackHandler', {
accountId: this.account,
region: this.region,
agentId: bedrockAgent.agent.attrAgentId,
agentAliasId: bedrockAgent.agentAlias.attrAgentAliasId,
conversationTable: conversationLog.table,
unansweredTable: conversationLog.unansweredTable,
wildcardCertArn,
vpc,
lambdaSecurityGroup,
});
// ── Socket Mode (ECS Fargate — replaces webhook Lambda) ──────────────────
const socketMode = new SocketModeConstruct(this, 'SocketMode', {
vpc,
processorLambda: slackHandler.processorLambda,
appHomeLambda: slackHandler.appHomeLambda,
});
// ── CloudWatch alarm coverage → site-alerts SNS ───────────────────────────
new MonitoringConstruct(this, 'Monitoring', {
lambdas: [
{ name: 'slack-processor', fn: slackHandler.processorLambda, timeout: cdk.Duration.minutes(5) },
{ name: 'app-home', fn: slackHandler.appHomeLambda, timeout: cdk.Duration.seconds(10) },
{ name: 'qbo-oauth', fn: slackHandler.qboOAuthLambda, timeout: cdk.Duration.seconds(15) },
{ name: 'qbo-lookup', fn: bedrockAgent.qboLambda, timeout: cdk.Duration.seconds(30) },
{ name: 'maps-lookup', fn: bedrockAgent.mapsLambda, timeout: cdk.Duration.seconds(30) },
{ name: 'wo-po-lookup', fn: bedrockAgent.woPoLambda, timeout: cdk.Duration.seconds(30) },
{ name: 'po-sync', fn: poSync.syncLambda, timeout: cdk.Duration.minutes(15) },
{ name: 'workorder-sync', fn: workorderSync.syncLambda, timeout: cdk.Duration.minutes(5) },
{ name: 'notion-sync', fn: notionSync.syncLambda, timeout: cdk.Duration.minutes(5) },
],
tables: [
{ name: 'ddb-conversations', table: conversationLog.table },
{ name: 'ddb-unanswered-questions', table: conversationLog.unansweredTable },
],
httpApi: slackHandler.api,
ecsService: socketMode.service,
ecsCluster: socketMode.cluster,
// RunningTaskCount alarm requires Container Insights (cost/config change) —
// gated off by default; enabled in a separate, sign-off-gated commit.
enableRunningTaskAlarm: false,
});
// ── Stack outputs ─────────────────────────────────────────────────────────
new cdk.CfnOutput(this, 'KBDocsBucketName', {
value: knowledgeBase.docsBucket.bucketName,
description: 'Upload SA8000 docs, SOPs, and employee handbook here to populate the KB',
});
new cdk.CfnOutput(this, 'AgentId', {
value: bedrockAgent.agent.attrAgentId,
description: 'Bedrock Agent ID (Alex)',
});
}
}