Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudwatch): EC2 actions #13281

Merged
merged 9 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,19 @@

This library contains a set of classes which can be used as CloudWatch Alarm actions.

The currently implemented actions are: EC2 Actions, SNS Actions, Autoscaling Actions and Aplication Autoscaling Actions


## EC2 Action Example

```ts
import * as cw from "@aws-cdk/aws-cloudwatch";
// Alarm must be configured with an EC2 per-instance metric
let alarm: cw.Alarm;
// Attach a reboot when alarm triggers
alarm.addAlarmAction(
new Ec2Action(Ec2InstanceActions.REBOOT)
);
```

See `@aws-cdk/aws-cloudwatch` for more information.
47 changes: 47 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import { Stack } from '@aws-cdk/core';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

/**
* Types of EC2 actions available
*/
export enum Ec2InstanceAction {
/**
* Stop the instance
*/
STOP = 'stop',
/**
* Terminatethe instance
*/
TERMINATE = 'terminate',
/**
* Recover the instance
*/
RECOVER = 'recover',
/**
* Reboot the instance
*/
REBOOT = 'reboot'
}

/**
* Use an EC2 action as an Alarm action
*/
export class Ec2Action implements cloudwatch.IAlarmAction {
private ec2Action: Ec2InstanceAction;

constructor(instanceAction: Ec2InstanceAction) {
this.ec2Action = instanceAction;
}

/**
* Returns an alarm action configuration to use an EC2 action as an alarm action
*/
bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig {
return { alarmActionArn: `arn:aws:automate:${Stack.of(_scope).region}:ec2:${this.ec2Action}` };
cyuste marked this conversation as resolved.
Show resolved Hide resolved
}
}

1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './appscaling';
export * from './autoscaling';
export * from './sns';
export * from './ec2';
41 changes: 41 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import '@aws-cdk/assert/jest';
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import { Stack } from '@aws-cdk/core';
import * as actions from '../lib';

test('can use instance reboot as alarm action', () => {
// GIVEN
const stack = new Stack();
const alarm = new cloudwatch.Alarm(stack, 'Alarm', {
metric: new cloudwatch.Metric({
namespace: 'AWS/EC2',
metricName: 'StatusCheckFailed',
dimensions: {
InstanceId: 'i-03cb889aaaafffeee',
},
}),
evaluationPeriods: 3,
threshold: 100,
});

// WHEN
alarm.addAlarmAction(new actions.Ec2Action(actions.Ec2InstanceAction.REBOOT));

// THEN
expect(stack).toHaveResource('AWS::CloudWatch::Alarm', {
AlarmActions: [
{
'Fn::Join': [
'',
[
'arn:aws:automate:',
{
Ref: 'AWS::Region',
},
':ec2:reboot',
],
],
},
],
});
});
26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Lazy, Stack, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { IAlarmAction } from './alarm-action';
import { AlarmBase, IAlarm } from './alarm-base';
import { CfnAlarm, CfnAlarmProps } from './cloudwatch.generated';
import { HorizontalAnnotation } from './graph';
Expand Down Expand Up @@ -224,6 +225,31 @@ export class Alarm extends AlarmBase {
return this.annotation;
}

/**
* Trigger this action if the alarm fires
*
* Typically the ARN of an SNS topic or ARN of an AutoScaling policy.
*/
public addAlarmAction(...actions: IAlarmAction[]) {
if (this.alarmActionArns === undefined) {
this.alarmActionArns = [];
}

this.alarmActionArns.push(...actions.map(a => {
const actionArn = a.bind(this, this).alarmActionArn;

const ec2ActionsRegexp: RegExp = /arn:aws:automate:[a-z|\d|-]+:ec2:[a-z]+/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind this functionality living here, but could you factor it out into a separate (private) function?

if (ec2ActionsRegexp.test(actionArn)) {
// Check per-instance metric
const metricConfig = this.metric.toMetricConfig();
if (metricConfig.metricStat?.dimensions?.length != 1 || metricConfig.metricStat?.dimensions![0].name != 'InstanceId') {
throw new Error('EC2 alarm actions must use an EC2 Per-Instance Metric');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should show the incorrect configuration.

How about:

`EC2 alarm actions requires an EC2 Per-Instance Metric (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`

}
}
return actionArn;
}));
}

private renderMetric(metric: IMetric) {
const self = this;
return dispatchMetric(metric, {
Expand Down
23 changes: 23 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ export = {

test.done();
},
'non ec2 instance related alarm does not accept EC2 action'(test: Test) {

const stack = new Stack();
const alarm = new Alarm(stack, 'Alarm', {
metric: testMetric,
threshold: 1000,
evaluationPeriods: 2,
});

test.throws(() => {
alarm.addAlarmAction(new Ec2TestAlarmAction('arn:aws:automate:us-east-1:ec2:reboot'));
}, /EC2 alarm actions must use an EC2 Per-Instance Metric/);
test.done();
},
'can make simple alarm'(test: Test) {
// GIVEN
const stack = new Stack();
Expand Down Expand Up @@ -253,3 +267,12 @@ class TestAlarmAction implements IAlarmAction {
return { alarmActionArn: this.arn };
}
}

class Ec2TestAlarmAction implements IAlarmAction {
constructor(private readonly arn: string) {
}

public bind(_scope: Construct, _alarm: IAlarm) {
return { alarmActionArn: this.arn };
}
}