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): add support for cross-account alarms #16007

Merged
merged 12 commits into from
Aug 25, 2021
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 &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain what you mean by the term "legacy" here? This doesn't quite make sense to me, since the accountId is only set if this is true, which would imply that a cross-account alarm only works if the metric is in a "legacy" format of some kind. But that doesn't make sense, since cross-account alarms are a new feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "legacy" term refers to the fact that cloudwatch has two ways to provision metrics on alarms, and only one way is being updated with new features like cross-account alarms. We want to avoid causing unnecessary diffs, so we shepherd metrics that do not require these "new" features to the "legacy" properties - the new way to provision metrics is via the CfnAlarm.MetricDataQueryProperty.

Here accountId is only set if canRenderAsLegacyMetric is false, not true. So it is correctly named here. (what seems to be the confusion is that github is hiding the unchanged lines and makes it seem like accountId is inside the if (canRenderAsLegacyMetric) scope when it is actually not.)

Copy link
Contributor

Choose a reason for hiding this comment

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

aha, thank you for the explanation! The github diff was confusing me here as well :)

(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