Skip to content

Commit

Permalink
feat(cloudwatch): add support for cross-account alarms (aws#16007)
Browse files Browse the repository at this point in the history
Allows `accountId` to be specified in a CloudWatch Alarm. This opens up the ability to create cross-account alarms, which was just [announced](https://aws.amazon.com/about-aws/whats-new/2021/08/announcing-amazon-cloudwatch-cross-account-alarms/).

closes aws#15959.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
kaizencc authored and david-doyle-as24 committed Sep 7, 2021
1 parent 154dbf4 commit 29f49ae
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 6 deletions.
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ represents the amount of errors reported by that Lambda function:
const errors = fn.metricErrors();
```

`Metric` objects can be account and region aware. You can specify `account` and `region` as properties of the metric, or use the `metric.attachTo(Construct)` method. `metric.attachTo()` will automatically copy the `region` and `account` fields of the `Construct`, which can come from anywhere in the Construct tree.

You can also instantiate `Metric` objects to reference any
[published metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html)
that's not exposed using a convenience method on the CDK construct.
Expand Down Expand Up @@ -160,6 +162,8 @@ The most important properties to set while creating an Alarms are:
- `evaluationPeriods`: how many consecutive periods the metric has to be
breaching the the threshold for the alarm to trigger.

To create a cross-account alarm, make sure you have enabled [cross-account functionality](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Cross-Account-Cross-Region.html) in CloudWatch. Then, set the `account` property in the `Metric` object either manually or via the `metric.attachTo()` method.

### Alarm Actions

To add actions to an alarm, use the integration classes from the
Expand Down
11 changes: 6 additions & 5 deletions packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ export class Alarm extends AlarmBase {
return dispatchMetric(metric, {
withStat(stat, conf) {
self.validateMetricStat(stat, metric);
if (conf.renderingProperties?.label == undefined) {
const canRenderAsLegacyMetric = conf.renderingProperties?.label == undefined &&
(stat.account == undefined || Stack.of(self).account == stat.account);
// Do this to disturb existing templates as little as possible
if (canRenderAsLegacyMetric) {
return dropUndefined({
dimensions: stat.dimensions,
namespace: stat.namespace,
Expand All @@ -283,6 +286,7 @@ export class Alarm extends AlarmBase {
unit: stat.unitFilter,
},
id: 'm1',
accountId: stat.account,
label: conf.renderingProperties?.label,
returnData: true,
} as CfnAlarm.MetricDataQueryProperty,
Expand Down Expand Up @@ -344,17 +348,14 @@ export class Alarm extends AlarmBase {
}

/**
* Validate that if a region and account are in the given stat config, they match the Alarm
* Validate that if a region is in the given stat config, they match the Alarm
*/
private validateMetricStat(stat: MetricStatConfig, metric: IMetric) {
const stack = Stack.of(this);

if (definitelyDifferent(stat.region, stack.region)) {
throw new Error(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`);
}
if (definitelyDifferent(stat.account, stack.account)) {
throw new Error(`Cannot create an Alarm in account '${stack.account}' based on metric '${metric}' in '${stat.account}'`);
}
}
}

Expand Down
67 changes: 66 additions & 1 deletion packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ const a = new Metric({ namespace: 'Test', metricName: 'ACount' });

let stack1: Stack;
let stack2: Stack;
let stack3: Stack;
describe('cross environment', () => {
beforeEach(() => {
stack1 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '1234' } });
stack2 = new Stack(undefined, undefined, { env: { region: 'mars', account: '5678' } });

stack3 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '0000' } });
});

describe('in graphs', () => {
Expand Down Expand Up @@ -112,6 +113,70 @@ describe('cross environment', () => {


});

test('metric attached to stack3 will render in stack1', () => {
//Cross-account metrics are supported in Alarms

// GIVEN
new Alarm(stack1, 'Alarm', {
threshold: 1,
evaluationPeriods: 1,
metric: a.attachTo(stack3),
});

// THEN
Template.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', {
Metrics: [
{
AccountId: '0000',
Id: 'm1',
MetricStat: {
Metric: {
MetricName: 'ACount',
Namespace: 'Test',
},
Period: 300,
Stat: 'Average',
},
ReturnData: true,
},
],
});
});

test('metric can render in a different account', () => {
// GIVEN
const b = new Metric({
namespace: 'Test',
metricName: 'ACount',
account: '0000',
});

new Alarm(stack1, 'Alarm', {
threshold: 1,
evaluationPeriods: 1,
metric: b,
});

// THEN
Template.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', {
Metrics: [
{
AccountId: '0000',
Id: 'm1',
MetricStat: {
Metric: {
MetricName: 'ACount',
Namespace: 'Test',
},
Period: 300,
Stat: 'Average',
},
ReturnData: true,
},
],
});
});
});
});

Expand Down

0 comments on commit 29f49ae

Please sign in to comment.