mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
feat(prod): add seahaven-prod DynamoDB CMK and site-alerts alarm-topic stacks
Provisions the two shared dependencies procurement-ingest imports by name, ahead of its migration from mgmt to seahaven-prod: - dynamodb-cmk-prod: second DynamoDbCmkStack instance (same stack name, prod account) creating alias/seahaven-dynamodb + the /seahaven/dynamodb/cmk-arn SSM param. Adds a cross-account key-policy statement so the mgmt seahaven-slack-bot roles can keep reading the CMK-encrypted purchase-orders table after it moves (ViaService + PrincipalArn-wildcard scoped; identity-policy half lands in the slack-bot repo's cutover PR). - alarm-topic-prod: codified site-alerts SNS topic + seahaven-alarm-topics CMK with the cloudwatch.amazonaws.com publish grant (mirrors the working mgmt pattern; mgmt's topic remains CLI-managed debt). - deploy.yaml: both appended to the deploy-prod job's explicit stack list (SH-ORG-005 rule: unlisted stacks silently never deploy).
This commit is contained in:
parent
88fa5777d1
commit
5dab14ce68
3 changed files with 110 additions and 1 deletions
2
.github/workflows/deploy.yaml
vendored
2
.github/workflows/deploy.yaml
vendored
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main
|
||||
with:
|
||||
node-version: "24"
|
||||
stacks: "prod-baseline"
|
||||
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod"
|
||||
stack-name: "seahaven-prod-baseline"
|
||||
secrets:
|
||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }}
|
||||
|
|
|
|||
41
bin/app.ts
41
bin/app.ts
|
|
@ -1,7 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
import "source-map-support/register";
|
||||
import * as cdk from "aws-cdk-lib";
|
||||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { AccountBaselineStack } from "../lib/account-baseline-stack";
|
||||
import { AlarmTopicStack } from "../lib/alarm-topic-stack";
|
||||
import { BackupOffsiteStack } from "../lib/backup-offsite-stack";
|
||||
import { BackupStack } from "../lib/backup-stack";
|
||||
import { RegionalBaselineStack } from "../lib/regional-baseline-stack";
|
||||
|
|
@ -165,6 +167,45 @@ new DynamoDbCmkStack(app, "dynamodb-cmk", {
|
|||
env: { account: ACCOUNT, region: "us-east-1" },
|
||||
});
|
||||
|
||||
// ── seahaven-prod copies for the procurement-ingest migration ────────────────
|
||||
// procurement-ingest is moving from mgmt to seahaven-prod; its stacks resolve
|
||||
// the DynamoDB CMK via SSM /seahaven/dynamodb/cmk-arn and import the SNS topic
|
||||
// `site-alerts` by constructed in-account ARN, so both must exist in prod
|
||||
// BEFORE that app's first prod deploy. Same stack names as mgmt (unique
|
||||
// per-account); distinct CDK ids.
|
||||
const prodDynamoCmk = new DynamoDbCmkStack(app, "dynamodb-cmk-prod", {
|
||||
stackName: "seahaven-dynamodb-cmk",
|
||||
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
||||
});
|
||||
|
||||
// seahaven-slack-bot stays in mgmt but reads the CMK-encrypted purchase-orders
|
||||
// table cross-account after the migration. The base statement's
|
||||
// kms:CallerAccount pin (correctly) excludes foreign callers, so the mgmt bot
|
||||
// roles need their own statement. Delegation-to-IAM per this stack's header:
|
||||
// PrincipalArn is wildcarded by stack prefix because CFN role suffixes rotate;
|
||||
// each bot role must ALSO carry an identity-policy grant on this key ARN
|
||||
// (added in the slack-bot repo's cutover PR).
|
||||
prodDynamoCmk.key.addToResourcePolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "AllowMgmtSlackBotReadViaDynamoDb",
|
||||
effect: iam.Effect.ALLOW,
|
||||
principals: [new iam.AccountPrincipal(ACCOUNT)],
|
||||
actions: ["kms:Decrypt", "kms:DescribeKey"],
|
||||
resources: ["*"],
|
||||
conditions: {
|
||||
StringEquals: { "kms:ViaService": "dynamodb.us-east-1.amazonaws.com" },
|
||||
ArnLike: {
|
||||
"aws:PrincipalArn": `arn:aws:iam::${ACCOUNT}:role/seahaven-slack-bot-*`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
new AlarmTopicStack(app, "alarm-topic-prod", {
|
||||
stackName: "seahaven-alarm-topic",
|
||||
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
||||
});
|
||||
|
||||
// ── Secondary-region baselines (INFRA-16, INFRA-91) ──────────────────────────
|
||||
// The us-east-1 baseline above is region-pinned by design. These stacks extend
|
||||
// a minimal detective/logging footprint into the secondary regions, codifying
|
||||
|
|
|
|||
68
lib/alarm-topic-stack.ts
Normal file
68
lib/alarm-topic-stack.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue