Skip to content

Commit

Permalink
feat(SecretsManager): Add support for secrets count metric (cdklabs#137)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
0xDream committed Feb 27, 2023
1 parent 5b679b1 commit 6188049
Show file tree
Hide file tree
Showing 12 changed files with 25,539 additions and 1,031 deletions.
1,623 changes: 1,622 additions & 1 deletion API.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ You can browse the documentation at https://constructs.dev/packages/cdk-monitori
| AWS RDS (`.monitorRdsCluster()`) | Query duration, connections, latency, disk/CPU usage | Connections, disk and CPU usage | |
| AWS Redshift (`.monitorRedshiftCluster()`) | Query duration, connections, latency, disk/CPU usage | Query duration, connections, disk and CPU usage | |
| AWS S3 Bucket (`.monitorS3Bucket()`) | Bucket size and number of objects | | |
| AWS SecretsManager (`.monitorSecretsManagerSecret()`) | Days since last rotation | Days since last change or rotation | |
| AWS SecretsManager (`.monitorSecretsManager()`) | Max secret count, min secret sount, secret count change | Min/max secret count or change in secret count | |
| AWS SecretsManager Secret (`.monitorSecretsManagerSecret()`) | Days since last rotation | Days since last change or rotation | |
| AWS SNS Topic (`.monitorSnsTopic()`) | Message count, size, failed notifications | Failed notifications, min/max published messages | |
| AWS SQS Queue (`.monitorSqsQueue()`, `.monitorSqsQueueWithDlq()`) | Message count, age, size | Message count, age, DLQ incoming messages | |
| AWS Step Functions (`.monitorStepFunction()`, `.monitorStepFunctionActivity()`, `monitorStepFunctionLambdaIntegration()`, `.monitorStepFunctionServiceIntegration()`) | Execution count and breakdown per state | Duration, failed, failed rate, aborted, throttled, timed out executions | |
Expand Down
118 changes: 118 additions & 0 deletions lib/common/monitoring/alarms/SecretsManagerAlarmFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
ComparisonOperator,
TreatMissingData,
} from "aws-cdk-lib/aws-cloudwatch";

import { AlarmFactory, CustomAlarmThreshold } from "../../alarm";
import { MetricWithAlarmSupport } from "../../metric";

const NUMBER_OF_DATAPOINTS = 1;

export interface MinSecretCountThreshold extends CustomAlarmThreshold {
readonly minSecretCount: number;
}

export interface MaxSecretCountThreshold extends CustomAlarmThreshold {
readonly maxSecretCount: number;
}

export interface ChangeInSecretCountThreshold extends CustomAlarmThreshold {
readonly currentSecretCount: number;
readonly alarmWhenIncreased: boolean;
readonly alarmWhenDecreased: boolean;
readonly additionalDescription?: string;
}

export class SecretsManagerAlarmFactory {
protected readonly alarmFactory: AlarmFactory;

constructor(alarmFactory: AlarmFactory) {
this.alarmFactory = alarmFactory;
}

addMinSecretCountAlarm(
metric: MetricWithAlarmSupport,
props: MinSecretCountThreshold,
disambiguator?: string
) {
return this.alarmFactory.addAlarm(metric, {
treatMissingData:
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
datapointsToAlarm: props.datapointsToAlarm ?? NUMBER_OF_DATAPOINTS,
comparisonOperator:
props.comparisonOperatorOverride ??
ComparisonOperator.LESS_THAN_THRESHOLD,
...props,
disambiguator,
threshold: props.minSecretCount,
alarmNameSuffix: "Secrets-Count-Min",
alarmDescription: "Number of secrets in the queue is too low.",
});
}

addMaxSecretCountAlarm(
metric: MetricWithAlarmSupport,
props: MaxSecretCountThreshold,
disambiguator?: string
) {
return this.alarmFactory.addAlarm(metric, {
treatMissingData:
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
comparisonOperator:
props.comparisonOperatorOverride ??
ComparisonOperator.GREATER_THAN_THRESHOLD,
datapointsToAlarm: props.datapointsToAlarm ?? NUMBER_OF_DATAPOINTS,
...props,
disambiguator,
threshold: props.maxSecretCount,
alarmNameSuffix: "Secrets-Count-Max",
alarmDescription: "Number of secrets in the queue is too high.",
});
}

addChangeInSecretCountAlarm(
metric: MetricWithAlarmSupport,
props: ChangeInSecretCountThreshold,
disambiguator?: string
) {
return this.alarmFactory.addAlarm(metric, {
...props,
disambiguator,
treatMissingData:
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
threshold: props.currentSecretCount,
comparisonOperator: this.getComparisonOperator(props),
datapointsToAlarm: props.datapointsToAlarm ?? NUMBER_OF_DATAPOINTS,
alarmNameSuffix: "Secrets-Count-Change",
alarmDescription: this.getDefaultDescription(props),
});
}

private getDefaultDescription(props: ChangeInSecretCountThreshold) {
if (props.alarmWhenIncreased && props.alarmWhenDecreased) {
return "Secret count: Secret count has changed.";
} else if (props.alarmWhenIncreased) {
return "Secret count: Secret count has increased.";
} else if (props.alarmWhenDecreased) {
return "Secret count: Secret count has decreased.";
} else {
throw new Error(
"You need to alarm when the value has increased, decreased, or both."
);
}
}

private getComparisonOperator(props: ChangeInSecretCountThreshold) {
if (props.alarmWhenIncreased && props.alarmWhenDecreased) {
return ComparisonOperator.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD;
} else if (props.alarmWhenDecreased) {
return ComparisonOperator.LESS_THAN_THRESHOLD;
} else if (props.alarmWhenIncreased) {
return ComparisonOperator.GREATER_THAN_THRESHOLD;
} else {
throw new Error(
"You need to alarm when the value has increased, decreased, or both."
);
}
}
}
1 change: 1 addition & 0 deletions lib/common/monitoring/alarms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from "./LatencyAlarmFactory";
export * from "./LogLevelAlarmFactory";
export * from "./OpenSearchClusterAlarmFactory";
export * from "./QueueAlarmFactory";
export * from "./SecretsManagerAlarmFactory";
export * from "./TaskHealthAlarmFactory";
export * from "./ThroughputAlarmFactory";
export * from "./TopicAlarmFactory";
Expand Down
8 changes: 8 additions & 0 deletions lib/facade/MonitoringFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ import {
RedshiftClusterMonitoringProps,
S3BucketMonitoring,
S3BucketMonitoringProps,
SecretsManagerMonitoring,
SecretsManagerMonitoringProps,
SecretsManagerSecretMonitoring,
SecretsManagerSecretMonitoringProps,
SimpleEc2ServiceMonitoringProps,
Expand Down Expand Up @@ -598,6 +600,12 @@ export class MonitoringFacade extends MonitoringScope {
return this;
}

monitorSecretsManager(props: SecretsManagerMonitoringProps) {
const segment = new SecretsManagerMonitoring(this, props);
this.addSegment(segment, props);
return this;
}

monitorSecretsManagerSecret(props: SecretsManagerSecretMonitoringProps) {
const segment = new SecretsManagerSecretMonitoring(this, props);
this.addSegment(segment, props);
Expand Down
37 changes: 37 additions & 0 deletions lib/monitoring/aws-secretsmanager/SecretsManagerMetricFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Duration } from "aws-cdk-lib";
import { MetricFactory, MetricStatistic } from "../../common";

const DEFAULT_METRIC_PERIOD = Duration.hours(1);

export class SecretsManagerMetricFactory {
static readonly Namespace = "AWS/SecretsManager";
static readonly Class = "None";
static readonly Resource = "SecretCount";
static readonly Service = "Secrets Manager";
static readonly Type = "Resource";
static readonly MetricSecretCount = "ResourceCount";
protected readonly metricFactory: MetricFactory;

constructor(metricFactory: MetricFactory) {
this.metricFactory = metricFactory;
}

metricSecretCount() {
const dimensionsMap = {
Class: SecretsManagerMetricFactory.Class,
Resource: SecretsManagerMetricFactory.Resource,
Service: SecretsManagerMetricFactory.Service,
Type: SecretsManagerMetricFactory.Type,
};

return this.metricFactory.createMetric(
SecretsManagerMetricFactory.MetricSecretCount,
MetricStatistic.AVERAGE,
"Count",
dimensionsMap,
undefined,
SecretsManagerMetricFactory.Namespace,
DEFAULT_METRIC_PERIOD
);
}
}
139 changes: 139 additions & 0 deletions lib/monitoring/aws-secretsmanager/SecretsManagerMonitoring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
GraphWidget,
HorizontalAnnotation,
IWidget,
} from "aws-cdk-lib/aws-cloudwatch";
import { SecretsManagerMetricFactory } from "./SecretsManagerMetricFactory";
import {
BaseMonitoringProps,
ChangeInSecretCountThreshold,
CountAxisFromZero,
DefaultGraphWidgetHeight,
DefaultSummaryWidgetHeight,
HalfWidth,
MaxSecretCountThreshold,
MetricWithAlarmSupport,
MinSecretCountThreshold,
Monitoring,
MonitoringScope,
SecretsManagerAlarmFactory,
ThirdWidth,
} from "../../common";
import {
MonitoringHeaderWidget,
MonitoringNamingStrategy,
} from "../../dashboard";

export interface SecretsManagerMonitoringOptions extends BaseMonitoringProps {
readonly addMinNumberSecretsAlarm?: Record<string, MinSecretCountThreshold>;
readonly addMaxNumberSecretsAlarm?: Record<string, MaxSecretCountThreshold>;
readonly addChangeInSecretsAlarm?: Record<
string,
ChangeInSecretCountThreshold
>;
}

export interface SecretsManagerMonitoringProps
extends SecretsManagerMonitoringOptions {}

export class SecretsManagerMonitoring extends Monitoring {
readonly title: string;

readonly secretsManagerAlarmFactory: SecretsManagerAlarmFactory;
readonly secretsCountAnnotation: HorizontalAnnotation[];

readonly secretsCountMetric: MetricWithAlarmSupport;

constructor(scope: MonitoringScope, props: SecretsManagerMonitoringProps) {
super(scope);

const namingStrategy = new MonitoringNamingStrategy({
...props,
fallbackConstructName: "SecretsManager",
});

this.title = namingStrategy.resolveHumanReadableName();

const alarmFactory = this.createAlarmFactory(
namingStrategy.resolveAlarmFriendlyName()
);
this.secretsManagerAlarmFactory = new SecretsManagerAlarmFactory(
alarmFactory
);
this.secretsCountAnnotation = [];

const metricFactory = new SecretsManagerMetricFactory(
scope.createMetricFactory()
);
this.secretsCountMetric = metricFactory.metricSecretCount();

for (const disambiguator in props.addMaxNumberSecretsAlarm) {
const alarmProps = props.addMaxNumberSecretsAlarm[disambiguator];
const createdAlarm =
this.secretsManagerAlarmFactory.addMaxSecretCountAlarm(
this.secretsCountMetric,
alarmProps,
disambiguator
);
this.secretsCountAnnotation.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}

for (const disambiguator in props.addMinNumberSecretsAlarm) {
const alarmProps = props.addMinNumberSecretsAlarm[disambiguator];
const createdAlarm =
this.secretsManagerAlarmFactory.addMinSecretCountAlarm(
this.secretsCountMetric,
alarmProps,
disambiguator
);
this.secretsCountAnnotation.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}

for (const disambiguator in props.addChangeInSecretsAlarm) {
const alarmProps = props.addChangeInSecretsAlarm[disambiguator];
const createdAlarm =
this.secretsManagerAlarmFactory.addChangeInSecretCountAlarm(
this.secretsCountMetric,
alarmProps,
disambiguator
);
this.secretsCountAnnotation.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}
props.useCreatedAlarms?.consume(this.createdAlarms());
}

summaryWidgets(): IWidget[] {
return [
this.createTitleWidget(),
this.createSecretsCountWidget(HalfWidth, DefaultSummaryWidgetHeight),
];
}

widgets(): IWidget[] {
return [
this.createTitleWidget(),
this.createSecretsCountWidget(ThirdWidth, DefaultGraphWidgetHeight),
];
}

createTitleWidget() {
return new MonitoringHeaderWidget({
family: "Secrets Manager Secrets",
title: this.title,
});
}

createSecretsCountWidget(width: number, height: number) {
return new GraphWidget({
width,
height,
title: "Secret Count",
left: [this.secretsCountMetric],
leftYAxis: CountAxisFromZero,
leftAnnotations: this.secretsCountAnnotation,
});
}
}
2 changes: 2 additions & 0 deletions lib/monitoring/aws-secretsmanager/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./SecretsManagerMetricFactory";
export * from "./SecretsManagerMetricsPublisher";
export * from "./SecretsManagerSecretMetricFactory";
export * from "./SecretsManagerMonitoring";
export * from "./SecretsManagerSecretMonitoring";
Loading

0 comments on commit 6188049

Please sign in to comment.