Skip to content

Commit cdf8bd0

Browse files
authored
feat(SecretsManager): Add support for secrets count metric (#333)
Added new Secrets Manager Monitor to support Account-Level Secrets Count. This includes a new SecretsManagerMonitor, a new SecretsManagerMetricsFactory, and a new SecretsManagerAlarmFactory. There is alarm support for Min/Max Secrets count and change in secrets count. Closes #137 --- _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_
1 parent 81f0c6b commit cdf8bd0

File tree

10 files changed

+2271
-3
lines changed

10 files changed

+2271
-3
lines changed

API.md

Lines changed: 1566 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ You can browse the documentation at https://constructs.dev/packages/cdk-monitori
8989
| AWS RDS (`.monitorRdsCluster()`) | Query duration, connections, latency, disk/CPU usage | Connections, disk and CPU usage | |
9090
| AWS Redshift (`.monitorRedshiftCluster()`) | Query duration, connections, latency, disk/CPU usage | Query duration, connections, disk and CPU usage | |
9191
| AWS S3 Bucket (`.monitorS3Bucket()`) | Bucket size and number of objects | | |
92-
| AWS SecretsManager (`.monitorSecretsManagerSecret()`) | Days since last rotation | Days since last change or rotation | |
92+
| AWS SecretsManager (`.monitorSecretsManager()`) | Max secret count, min secret sount, secret count change | Min/max secret count or change in secret count | |
93+
| AWS SecretsManager Secret (`.monitorSecretsManagerSecret()`) | Days since last rotation | Days since last change or rotation | |
9394
| AWS SNS Topic (`.monitorSnsTopic()`) | Message count, size, failed notifications | Failed notifications, min/max published messages | |
9495
| AWS SQS Queue (`.monitorSqsQueue()`, `.monitorSqsQueueWithDlq()`) | Message count, age, size | Message count, age, DLQ incoming messages | |
9596
| AWS Step Functions (`.monitorStepFunction()`, `.monitorStepFunctionActivity()`, `monitorStepFunctionLambdaIntegration()`, `.monitorStepFunctionServiceIntegration()`) | Execution count and breakdown per state | Duration, failed, failed rate, aborted, throttled, timed out executions | |
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
ComparisonOperator,
3+
TreatMissingData,
4+
} from "aws-cdk-lib/aws-cloudwatch";
5+
6+
import { AlarmFactory, CustomAlarmThreshold } from "../../alarm";
7+
import { MetricWithAlarmSupport } from "../../metric";
8+
9+
const NUMBER_OF_DATAPOINTS = 1;
10+
11+
export interface MinSecretCountThreshold extends CustomAlarmThreshold {
12+
readonly minSecretCount: number;
13+
}
14+
15+
export interface MaxSecretCountThreshold extends CustomAlarmThreshold {
16+
readonly maxSecretCount: number;
17+
}
18+
19+
export interface ChangeInSecretCountThreshold extends CustomAlarmThreshold {
20+
readonly requiredSecretCount: number;
21+
readonly alarmWhenIncreased: boolean;
22+
readonly alarmWhenDecreased: boolean;
23+
readonly additionalDescription?: string;
24+
}
25+
26+
export class SecretsManagerAlarmFactory {
27+
protected readonly alarmFactory: AlarmFactory;
28+
29+
constructor(alarmFactory: AlarmFactory) {
30+
this.alarmFactory = alarmFactory;
31+
}
32+
33+
addMinSecretCountAlarm(
34+
metric: MetricWithAlarmSupport,
35+
props: MinSecretCountThreshold,
36+
disambiguator?: string
37+
) {
38+
return this.alarmFactory.addAlarm(metric, {
39+
treatMissingData:
40+
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
41+
datapointsToAlarm: props.datapointsToAlarm ?? NUMBER_OF_DATAPOINTS,
42+
comparisonOperator:
43+
props.comparisonOperatorOverride ??
44+
ComparisonOperator.LESS_THAN_THRESHOLD,
45+
...props,
46+
disambiguator,
47+
threshold: props.minSecretCount,
48+
alarmNameSuffix: "Secrets-Count-Min",
49+
alarmDescription: "Number of secrets is too low.",
50+
});
51+
}
52+
53+
addMaxSecretCountAlarm(
54+
metric: MetricWithAlarmSupport,
55+
props: MaxSecretCountThreshold,
56+
disambiguator?: string
57+
) {
58+
return this.alarmFactory.addAlarm(metric, {
59+
treatMissingData:
60+
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
61+
comparisonOperator:
62+
props.comparisonOperatorOverride ??
63+
ComparisonOperator.GREATER_THAN_THRESHOLD,
64+
datapointsToAlarm: props.datapointsToAlarm ?? NUMBER_OF_DATAPOINTS,
65+
...props,
66+
disambiguator,
67+
threshold: props.maxSecretCount,
68+
alarmNameSuffix: "Secrets-Count-Max",
69+
alarmDescription: "Number of secrets is too high.",
70+
});
71+
}
72+
73+
addChangeInSecretCountAlarm(
74+
metric: MetricWithAlarmSupport,
75+
props: ChangeInSecretCountThreshold,
76+
disambiguator?: string
77+
) {
78+
return this.alarmFactory.addAlarm(metric, {
79+
...props,
80+
disambiguator,
81+
treatMissingData:
82+
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
83+
threshold: props.requiredSecretCount,
84+
comparisonOperator: this.getComparisonOperator(props),
85+
datapointsToAlarm: props.datapointsToAlarm ?? NUMBER_OF_DATAPOINTS,
86+
alarmNameSuffix: "Secrets-Count-Change",
87+
alarmDescription: this.getDefaultDescription(props),
88+
});
89+
}
90+
91+
private getDefaultDescription(props: ChangeInSecretCountThreshold) {
92+
if (props.alarmWhenIncreased && props.alarmWhenDecreased) {
93+
return "Secret count: Secret count has changed.";
94+
} else if (props.alarmWhenIncreased) {
95+
return "Secret count: Secret count has increased.";
96+
} else if (props.alarmWhenDecreased) {
97+
return "Secret count: Secret count has decreased.";
98+
} else {
99+
throw new Error(
100+
"You need to alarm when the value has increased, decreased, or both."
101+
);
102+
}
103+
}
104+
105+
private getComparisonOperator(props: ChangeInSecretCountThreshold) {
106+
if (props.alarmWhenIncreased && props.alarmWhenDecreased) {
107+
return ComparisonOperator.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD;
108+
} else if (props.alarmWhenDecreased) {
109+
return ComparisonOperator.LESS_THAN_THRESHOLD;
110+
} else if (props.alarmWhenIncreased) {
111+
return ComparisonOperator.GREATER_THAN_THRESHOLD;
112+
} else {
113+
throw new Error(
114+
"You need to alarm when the value has increased, decreased, or both."
115+
);
116+
}
117+
}
118+
}

lib/common/monitoring/alarms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "./LatencyAlarmFactory";
1111
export * from "./LogLevelAlarmFactory";
1212
export * from "./OpenSearchClusterAlarmFactory";
1313
export * from "./QueueAlarmFactory";
14+
export * from "./SecretsManagerAlarmFactory";
1415
export * from "./TaskHealthAlarmFactory";
1516
export * from "./ThroughputAlarmFactory";
1617
export * from "./TopicAlarmFactory";

lib/facade/MonitoringFacade.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ import {
9494
RedshiftClusterMonitoringProps,
9595
S3BucketMonitoring,
9696
S3BucketMonitoringProps,
97+
SecretsManagerMonitoring,
98+
SecretsManagerMonitoringProps,
9799
SecretsManagerSecretMonitoring,
98100
SecretsManagerSecretMonitoringProps,
99101
SimpleEc2ServiceMonitoringProps,
@@ -637,6 +639,12 @@ export class MonitoringFacade extends MonitoringScope {
637639
return this;
638640
}
639641

642+
monitorSecretsManager(props: SecretsManagerMonitoringProps) {
643+
const segment = new SecretsManagerMonitoring(this, props);
644+
this.addSegment(segment, props);
645+
return this;
646+
}
647+
640648
monitorSecretsManagerSecret(props: SecretsManagerSecretMonitoringProps) {
641649
const segment = new SecretsManagerSecretMonitoring(this, props);
642650
this.addSegment(segment, props);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Duration } from "aws-cdk-lib";
2+
import { MetricFactory, MetricStatistic } from "../../common";
3+
4+
const CLASS = "None";
5+
const DEFAULT_METRIC_PERIOD = Duration.hours(1);
6+
const METRICNAMESECRETCOUNT = "ResourceCount";
7+
const NAMESPACE = "AWS/SecretsManager";
8+
const RESOURCE = "SecretCount";
9+
const SERVICE = "Secrets Manager";
10+
const TYPE = "Resource";
11+
12+
export class SecretsManagerMetricFactory {
13+
protected readonly metricFactory: MetricFactory;
14+
15+
constructor(metricFactory: MetricFactory) {
16+
this.metricFactory = metricFactory;
17+
}
18+
19+
metricSecretCount() {
20+
const dimensionsMap = {
21+
Class: CLASS,
22+
Resource: RESOURCE,
23+
Service: SERVICE,
24+
Type: TYPE,
25+
};
26+
27+
return this.metricFactory.createMetric(
28+
METRICNAMESECRETCOUNT,
29+
MetricStatistic.AVERAGE,
30+
"Count",
31+
dimensionsMap,
32+
undefined,
33+
NAMESPACE,
34+
DEFAULT_METRIC_PERIOD
35+
);
36+
}
37+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {
2+
GraphWidget,
3+
HorizontalAnnotation,
4+
IWidget,
5+
} from "aws-cdk-lib/aws-cloudwatch";
6+
import { SecretsManagerMetricFactory } from "./SecretsManagerMetricFactory";
7+
import {
8+
BaseMonitoringProps,
9+
ChangeInSecretCountThreshold,
10+
CountAxisFromZero,
11+
DefaultGraphWidgetHeight,
12+
DefaultSummaryWidgetHeight,
13+
HalfWidth,
14+
MaxSecretCountThreshold,
15+
MetricWithAlarmSupport,
16+
MinSecretCountThreshold,
17+
Monitoring,
18+
MonitoringScope,
19+
SecretsManagerAlarmFactory,
20+
ThirdWidth,
21+
} from "../../common";
22+
import {
23+
MonitoringHeaderWidget,
24+
MonitoringNamingStrategy,
25+
} from "../../dashboard";
26+
27+
export interface SecretsManagerMonitoringOptions extends BaseMonitoringProps {
28+
readonly addMinNumberSecretsAlarm?: Record<string, MinSecretCountThreshold>;
29+
readonly addMaxNumberSecretsAlarm?: Record<string, MaxSecretCountThreshold>;
30+
readonly addChangeInSecretsAlarm?: Record<
31+
string,
32+
ChangeInSecretCountThreshold
33+
>;
34+
}
35+
36+
export interface SecretsManagerMonitoringProps
37+
extends SecretsManagerMonitoringOptions {}
38+
39+
export class SecretsManagerMonitoring extends Monitoring {
40+
readonly title: string;
41+
42+
readonly secretsManagerAlarmFactory: SecretsManagerAlarmFactory;
43+
readonly secretsCountAnnotation: HorizontalAnnotation[];
44+
45+
readonly secretsCountMetric: MetricWithAlarmSupport;
46+
47+
constructor(scope: MonitoringScope, props: SecretsManagerMonitoringProps) {
48+
super(scope);
49+
50+
const namingStrategy = new MonitoringNamingStrategy({
51+
...props,
52+
fallbackConstructName: "SecretsManager",
53+
});
54+
55+
this.title = namingStrategy.resolveHumanReadableName();
56+
57+
const alarmFactory = this.createAlarmFactory(
58+
namingStrategy.resolveAlarmFriendlyName()
59+
);
60+
this.secretsManagerAlarmFactory = new SecretsManagerAlarmFactory(
61+
alarmFactory
62+
);
63+
this.secretsCountAnnotation = [];
64+
65+
const metricFactory = new SecretsManagerMetricFactory(
66+
scope.createMetricFactory()
67+
);
68+
this.secretsCountMetric = metricFactory.metricSecretCount();
69+
70+
for (const disambiguator in props.addMaxNumberSecretsAlarm) {
71+
const alarmProps = props.addMaxNumberSecretsAlarm[disambiguator];
72+
const createdAlarm =
73+
this.secretsManagerAlarmFactory.addMaxSecretCountAlarm(
74+
this.secretsCountMetric,
75+
alarmProps,
76+
disambiguator
77+
);
78+
this.secretsCountAnnotation.push(createdAlarm.annotation);
79+
this.addAlarm(createdAlarm);
80+
}
81+
82+
for (const disambiguator in props.addMinNumberSecretsAlarm) {
83+
const alarmProps = props.addMinNumberSecretsAlarm[disambiguator];
84+
const createdAlarm =
85+
this.secretsManagerAlarmFactory.addMinSecretCountAlarm(
86+
this.secretsCountMetric,
87+
alarmProps,
88+
disambiguator
89+
);
90+
this.secretsCountAnnotation.push(createdAlarm.annotation);
91+
this.addAlarm(createdAlarm);
92+
}
93+
94+
for (const disambiguator in props.addChangeInSecretsAlarm) {
95+
const alarmProps = props.addChangeInSecretsAlarm[disambiguator];
96+
const createdAlarm =
97+
this.secretsManagerAlarmFactory.addChangeInSecretCountAlarm(
98+
this.secretsCountMetric,
99+
alarmProps,
100+
disambiguator
101+
);
102+
this.secretsCountAnnotation.push(createdAlarm.annotation);
103+
this.addAlarm(createdAlarm);
104+
}
105+
props.useCreatedAlarms?.consume(this.createdAlarms());
106+
}
107+
108+
summaryWidgets(): IWidget[] {
109+
return [
110+
this.createTitleWidget(),
111+
this.createSecretsCountWidget(HalfWidth, DefaultSummaryWidgetHeight),
112+
];
113+
}
114+
115+
widgets(): IWidget[] {
116+
return [
117+
this.createTitleWidget(),
118+
this.createSecretsCountWidget(ThirdWidth, DefaultGraphWidgetHeight),
119+
];
120+
}
121+
122+
createTitleWidget() {
123+
return new MonitoringHeaderWidget({
124+
family: "Secrets Manager Secrets",
125+
title: this.title,
126+
});
127+
}
128+
129+
createSecretsCountWidget(width: number, height: number) {
130+
return new GraphWidget({
131+
width,
132+
height,
133+
title: "Secret Count",
134+
left: [this.secretsCountMetric],
135+
leftYAxis: CountAxisFromZero,
136+
leftAnnotations: this.secretsCountAnnotation,
137+
});
138+
}
139+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from "./SecretsManagerMetricFactory";
12
export * from "./SecretsManagerMetricsPublisher";
23
export * from "./SecretsManagerSecretMetricFactory";
4+
export * from "./SecretsManagerMonitoring";
35
export * from "./SecretsManagerSecretMonitoring";

0 commit comments

Comments
 (0)