Skip to content

Commit dc2164f

Browse files
feat(core): Creating Metrics and Alarms (#363)
* Creating CloudWatch LogGroup MetricFilters and Alarms (#363)
1 parent 49fff99 commit dc2164f

File tree

20 files changed

+597
-9
lines changed

20 files changed

+597
-9
lines changed

reference-artifacts/master-config-sample-snippets/sample_snippets.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,48 @@ Config File Examples: {Not in sample config file}
1616
"exclusions": ["def/*"]
1717
}
1818
],
19+
************************************CloudWatch MetricFilters and Alarms
20+
"central-log-services": {
21+
"cloudwatch": {
22+
"metrics": [{
23+
"filter-name": "SecurityGroupChangeMetricTest",
24+
"accounts": [
25+
"master"
26+
],
27+
"regions": [
28+
"ca-central-1"
29+
],
30+
"loggroup-name": "/PBMMAccel/CloudTrailTest",
31+
"filter-pattern": "{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }",
32+
"metric-namespace": "CloudTrailMetrics",
33+
"metric-name": "SecurityGroupEventCountTest",
34+
"metric-value": "1",
35+
"default-value": 0
36+
}],
37+
"alarms": {
38+
"default-accounts": [
39+
"master"
40+
],
41+
"default-regions": [
42+
"ca-central-1"
43+
],
44+
"default-namespace": "CloudTrailMetrics",
45+
"default-statistic": "Sum",
46+
"default-period": 300,
47+
"default-threshold-type": "Static",
48+
"default-comparison-operator": "GreaterThanOrEqualToThreshold",
49+
"default-threshold": 1,
50+
"default-evaluation-periods": 1,
51+
"default-treat-missing-data": "notBreaching",
52+
"definitions": [{
53+
"alarm-name": "AWS-Security-Group-Changed",
54+
"metric-name": "SecurityGroupEventCount",
55+
"sns-alert-level": "Low",
56+
"alarm-description": "Alarms when one or more API calls are made to create, update or delete a Security Group (in any account, any region of your AWS Organization)."
57+
}]
58+
}
59+
}
60+
}
1961
************************************Additional regions for Amazon CloudWatch Central Logging to S3
2062
"additional-cwl-regions": {
2163
"us-east-1": {

src/deployments/cdk/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@aws-cdk/aws-securityhub": "1.46.0",
5858
"@aws-cdk/aws-sns": "1.46.0",
5959
"@aws-cdk/aws-ssm": "1.46.0",
60+
"@aws-cdk/aws-cloudwatch": "1.46.0",
6061
"@aws-cdk/core": "1.46.0",
6162
"@aws-cdk/custom-resources": "1.46.0",
6263
"@aws-cdk/aws-events": "1.46.0",
@@ -106,6 +107,7 @@
106107
"@aws-accelerator/custom-resource-guardduty-admin-setup": "workspace:^0.0.1",
107108
"@aws-accelerator/custom-resource-vpc-default-security-group": "workspace:^0.0.1",
108109
"@aws-accelerator/custom-resource-ssm-session-manager-document": "workspace:^0.0.1",
110+
"@aws-accelerator/custom-resource-logs-metric-filter": "workspace:^0.0.1",
109111
"@types/cfn-response": "^1.0.3",
110112
"colors": "1.4.0",
111113
"constructs": "2.0.1",

src/deployments/cdk/src/apps/phase--1.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import * as globalRoles from '../deployments/iam';
1212
* - Creating required roles for createSSMDocument custom resource
1313
* - Creating required roles for createLogGroup custom resource
1414
* - Creating required roles for CWLCentralLoggingSubscriptionFilterRole custom resource
15+
* - Creating required roles for createLogsMetricFilter custom resource
16+
* - Creating required roles for SnsSubscriberLambda custom resource
1517
*/
1618
export async function deploy({ acceleratorConfig, accountStacks, accounts }: PhaseInput) {
1719
// creates roles for macie custom resources
@@ -64,4 +66,10 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts }: Pha
6466
accounts,
6567
config: acceleratorConfig,
6668
});
69+
70+
// Creates role for createLogsMetricFilter custom resource
71+
await globalRoles.createLogsMetricFilterRole({
72+
accountStacks,
73+
accounts,
74+
});
6775
}

src/deployments/cdk/src/apps/phase-2.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -289,17 +289,17 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte
289289
accounts,
290290
outputs,
291291
});
292-
293-
/**
294-
* Creating required SNS Topics in Log accont if baseline is Organizations
295-
*/
296-
await snsDeployment.step1({
297-
accountStacks,
298-
config: acceleratorConfig,
299-
outputs,
300-
});
301292
}
302293

294+
/**
295+
* Creating required SNS Topics in Log accont
296+
*/
297+
await snsDeployment.step1({
298+
accountStacks,
299+
config: acceleratorConfig,
300+
outputs,
301+
});
302+
303303
const logBucket = accountBuckets[acceleratorConfig['global-options']['central-log-services'].account];
304304
await guardDutyDeployment.step3({
305305
accountStacks,

src/deployments/cdk/src/apps/phase-4.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getStackJsonOutput, ResolversOutput, MadRuleOutput } from '@aws-acceler
55
import { Route53ResolverRuleSharing } from '../common/r53-resolver-rule-sharing';
66
import { PhaseInput } from './shared';
77
import * as securityHub from '../deployments/security-hub';
8+
import * as cloudWatchDeployment from '../deployments/cloud-watch';
89

910
type ResolversOutputs = ResolversOutput[];
1011

@@ -96,4 +97,14 @@ export async function deploy({ acceleratorConfig, accounts, accountStacks, outpu
9697
config: acceleratorConfig,
9798
outputs,
9899
});
100+
101+
/**
102+
* CloudWatch Deployment step-1
103+
* Creates CloudWatch Metrics on LogGroups
104+
*/
105+
await cloudWatchDeployment.step1({
106+
accountStacks,
107+
config: acceleratorConfig,
108+
outputs,
109+
});
99110
}

src/deployments/cdk/src/apps/phase-5.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { RdgwArtifactsOutput } from './phase-4';
1313
import * as cwlCentralLoggingToS3 from '../deployments/central-services/central-logging-s3';
1414
import { ArtifactOutputFinder } from '../deployments/artifacts/outputs';
1515
import { ImageIdOutputFinder } from '@aws-accelerator/common-outputs/src/ami-output';
16+
import * as cloudWatchDeployment from '../deployments/cloud-watch';
1617

1718
export async function deploy({ acceleratorConfig, accountStacks, accounts, context, outputs }: PhaseInput) {
1819
const accountNames = acceleratorConfig
@@ -170,4 +171,14 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte
170171
});
171172
}
172173
}
174+
175+
/**
176+
* CloudWatch Deployment step-2
177+
* Creates CloudWatch Alarms
178+
*/
179+
await cloudWatchDeployment.step2({
180+
accountStacks,
181+
config: acceleratorConfig,
182+
accounts,
183+
});
173184
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './step-1';
2+
export * from './step-2';
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as c from '@aws-accelerator/common-config';
2+
import { AccountStacks } from '../../common/account-stacks';
3+
import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role';
4+
import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output';
5+
import { LogsMetricFilter } from '@aws-accelerator/custom-resource-logs-metric-filter';
6+
import { createName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator';
7+
export interface CloudWatchStep1Props {
8+
accountStacks: AccountStacks;
9+
config: c.AcceleratorConfig;
10+
outputs: StackOutput[];
11+
}
12+
13+
export async function step1(props: CloudWatchStep1Props) {
14+
const { accountStacks, config, outputs } = props;
15+
const globalOptions = config['global-options'];
16+
if (!globalOptions.cloudwatch) {
17+
console.log(`No Configuration defined for CloudWatch Deployment`);
18+
return;
19+
}
20+
const metricsConfig = globalOptions.cloudwatch.metrics;
21+
metricsConfig.forEach((metricConfig, _) => {
22+
const accountKeys: string[] = [];
23+
const regions: string[] = [];
24+
if (metricConfig.accounts && metricConfig.accounts.includes('ALL')) {
25+
// TODO Ignore for now implementation will come in phase 2
26+
} else {
27+
accountKeys.push(...metricConfig.accounts);
28+
}
29+
30+
if (metricConfig.regions && metricConfig.regions.includes('ALL')) {
31+
// TODO Ignore for now implementation will come in phase 2
32+
} else {
33+
regions.push(...metricConfig.regions);
34+
}
35+
for (const accountKey of accountKeys) {
36+
const metricFilterRole = IamRoleOutputFinder.tryFindOneByName({
37+
outputs,
38+
accountKey,
39+
roleKey: 'LogsMetricFilterRole',
40+
});
41+
if (!metricFilterRole) {
42+
console.error(`Role is not created for LogsMetricFilter CustomResource in account: ${accountKey}`);
43+
continue;
44+
}
45+
for (const region of regions) {
46+
const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, region);
47+
if (!accountStack) {
48+
console.error(`Cannot find account stack ${accountKey}: ${region}, while deploying CloudWatch Metric Filter`);
49+
continue;
50+
}
51+
new LogsMetricFilter(accountStack, `LogGroupMetricFilter${metricConfig['metric-name']}`, {
52+
roleArn: metricFilterRole.roleArn,
53+
defaultValue: metricConfig['default-value'],
54+
metricValue: metricConfig['metric-value'],
55+
filterPattern: metricConfig['filter-pattern'].trim(),
56+
metricName: metricConfig['metric-name'],
57+
metricNamespace: metricConfig['metric-namespace'],
58+
filterName: createName({
59+
name: metricConfig['filter-name'],
60+
suffixLength: 0,
61+
}),
62+
logGroupName: metricConfig['loggroup-name'],
63+
});
64+
}
65+
}
66+
});
67+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as c from '@aws-accelerator/common-config';
2+
import { AccountStacks } from '../../common/account-stacks';
3+
import * as cdk from '@aws-cdk/core';
4+
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
5+
import { Account, getAccountId } from '@aws-accelerator/common-outputs/src/accounts';
6+
import { createName, createSnsTopicName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator';
7+
8+
export interface CloudWatchStep2Props {
9+
accountStacks: AccountStacks;
10+
config: c.AcceleratorConfig;
11+
accounts: Account[];
12+
}
13+
14+
export async function step2(props: CloudWatchStep2Props) {
15+
const { accountStacks, config, accounts } = props;
16+
const globalOptions = config['global-options'];
17+
const centralLogServices = globalOptions['central-log-services'];
18+
if (!globalOptions.cloudwatch) {
19+
console.log(`No Configuration defined for CloudWatch Deployment`);
20+
return;
21+
}
22+
const alarmsConfig = globalOptions.cloudwatch.alarms;
23+
const alarmDefaultDefinition: c.CloudWatchDefaultAlarmDefinition = alarmsConfig;
24+
for (const alarmconfig of alarmsConfig.definitions) {
25+
const accountKeys: string[] = [];
26+
const regions: string[] = [];
27+
if (alarmconfig.accounts && alarmconfig.accounts.includes('ALL')) {
28+
// TODO Ignore for now implementation will come in phase 2
29+
} else {
30+
accountKeys.push(...(alarmconfig.accounts || alarmDefaultDefinition['default-accounts']));
31+
}
32+
33+
if (alarmconfig.regions && alarmconfig.regions.includes('ALL')) {
34+
// TODO Ignore for now implementation will come in phase 2
35+
} else {
36+
regions.push(...(alarmconfig.regions || alarmDefaultDefinition['default-regions']));
37+
}
38+
for (const accountKey of accountKeys) {
39+
for (const region of regions) {
40+
const accountStack = accountStacks.tryGetOrCreateAccountStack(accountKey, region);
41+
if (!accountStack) {
42+
console.error(`Cannot find account stack ${accountKey}: ${region}, while deploying CloudWatch Alarm`);
43+
continue;
44+
}
45+
new cloudwatch.CfnAlarm(accountStack, `CloudAlarm${alarmconfig['alarm-name']}`, {
46+
alarmDescription: alarmconfig['alarm-description'],
47+
alarmName: createName({
48+
name: alarmconfig['alarm-name'],
49+
suffixLength: 0,
50+
}),
51+
metricName: alarmconfig['metric-name'],
52+
evaluationPeriods: alarmconfig['evaluation-periods'] || alarmDefaultDefinition['default-evaluation-periods'],
53+
comparisonOperator:
54+
alarmconfig['comparison-operator'] || alarmDefaultDefinition['default-comparison-operator'],
55+
namespace: alarmconfig.namespace || alarmDefaultDefinition['default-namespace'],
56+
statistic: alarmconfig.statistic || alarmDefaultDefinition['default-statistic'],
57+
period: alarmconfig.period || alarmDefaultDefinition['default-period'],
58+
treatMissingData: alarmconfig['treat-missing-data'] || alarmDefaultDefinition['default-treat-missing-data'],
59+
threshold: alarmconfig.threshold || alarmDefaultDefinition['default-threshold'],
60+
alarmActions: [
61+
`arn:aws:sns:${cdk.Aws.REGION}:${getAccountId(accounts, centralLogServices.account)}:${createSnsTopicName(
62+
alarmconfig['sns-alert-level'],
63+
)}`,
64+
],
65+
});
66+
}
67+
}
68+
}
69+
}

src/deployments/cdk/src/deployments/iam/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export * from './ssm-document-roles';
99
export * from './log-group-role';
1010
export * from './cwl-central-logging-roles';
1111
export * from './cwl-add-subscription-filter-role';
12+
export * from './logs-metric-filter-role';
1213
export * from './sns-subscriber-lambda-role';

0 commit comments

Comments
 (0)