Skip to content

Commit

Permalink
feat(cloudwatch): EC2 actions (aws#13281)
Browse files Browse the repository at this point in the history
Fixes aws#13228
----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
cyuste authored and cornerwings committed Mar 8, 2021
1 parent 1736d5b commit 6dc2a2d
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch-actions/README.md
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
@@ -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}` };
}
}

1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts
@@ -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
@@ -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',
],
],
},
],
});
});
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts
@@ -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,33 @@ 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 =>
this.validateActionArn(a.bind(this, this).alarmActionArn),
));
}

private validateActionArn(actionArn: string): string {
const ec2ActionsRegexp: RegExp = /arn:aws:automate:[a-z|\d|-]+:ec2:[a-z]+/;
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 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
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 requires an EC2 Per-Instance Metric. \(.+ does not have an 'InstanceId' dimension\)/);
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 };
}
}

0 comments on commit 6dc2a2d

Please sign in to comment.