import * as cdk from "aws-cdk-lib"; import * as kms from "aws-cdk-lib/aws-kms"; import * as iam from "aws-cdk-lib/aws-iam"; import * as sns from "aws-cdk-lib/aws-sns"; import { Construct } from "constructs"; /** * Shared CloudWatch-alarm SNS topic (`site-alerts`) + its CMK for a member * account. First tenant: seahaven-prod, for the procurement-ingest migration * (its stacks import the topic by constructed ARN `site-alerts` in-account). * * mgmt's equivalent topic is unmanaged (created via CLI, acknowledged debt in * cis-monitoring.ts) - this stack codifies the same working pattern instead of * replicating the debt: * - CMK `alias/seahaven-alarm-topics`, NOT alias/aws/sns: the AWS-managed SNS * key's policy cannot grant cloudwatch.amazonaws.com, so alarms silently * fail to publish through it. * - Key policy grants cloudwatch.amazonaws.com only (SourceAccount-scoped). * Subscribers (AWS Chatbot -> Slack) need no KMS grant: SNS decrypts at * delivery. Verified live by mgmt's site-alerts + Chatbot wiring. * - Chatbot workspace auth + channel config are console-only (per-account) * and deliberately out of scope here; verify delivery post-deploy with * `aws sns publish` + a Slack message check. */ export class AlarmTopicStack extends cdk.Stack { public readonly topic: sns.Topic; constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const alarmTopicKey = new kms.Key(this, "AlarmTopicKey", { alias: "seahaven-alarm-topics", description: "SSE for SNS alarm topics; grants CloudWatch alarms publish-side usage", enableKeyRotation: true, removalPolicy: cdk.RemovalPolicy.RETAIN, }); // GenerateDataKey* is the publish-side envelope-encryption call CloudWatch // makes when writing to an encrypted topic; Decrypt covers retried // deliveries re-reading its own envelope. Both are required for alarms to // publish at all. alarmTopicKey.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowCloudWatchAlarmsUse", principals: [new iam.ServicePrincipal("cloudwatch.amazonaws.com")], actions: ["kms:GenerateDataKey*", "kms:Decrypt", "kms:DescribeKey"], resources: ["*"], conditions: { StringEquals: { "aws:SourceAccount": this.account }, }, }), ); this.topic = new sns.Topic(this, "SiteAlertsTopic", { topicName: "site-alerts", displayName: "Sea Haven operational alarms", masterKey: alarmTopicKey, }); cdk.Tags.of(this).add("Project", "account-baseline"); cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); cdk.Tags.of(this).add("Environment", "prod"); cdk.Tags.of(this).add("ManagedBy", "cdk"); new cdk.CfnOutput(this, "SiteAlertsTopicArn", { value: this.topic.topicArn }); new cdk.CfnOutput(this, "AlarmTopicKeyArn", { value: alarmTopicKey.keyArn }); } }