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

cloudwatch: Use of EC2 action with Multiple dimension set in metric results into error #29331

Closed
rtejwani1309 opened this issue Mar 1, 2024 · 6 comments · Fixed by #29364
Closed
Assignees
Labels
@aws-cdk/aws-cloudwatch Related to Amazon CloudWatch blocked Work is blocked on this issue for this codebase. Other labels or comments may indicate why. bug This issue is a bug. effort/medium Medium work item – several days of effort p1

Comments

@rtejwani1309
Copy link

rtejwani1309 commented Mar 1, 2024

Describe the bug

While trying to create a Custom Metric with multiple dimension, and adding EC2 action, the CDK synth fails with the error below.

/Users/Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1
"use strict";var _a;Object.defineProperty(exports,"__esModule",{value:!0}),exports.Alarm=exports.TreatMissingData=exports.ComparisonOperator=void 0;var jsiiDeprecationWarnings=()=>{var tmp=require("../../.warnings.jsii.js");return jsiiDeprecationWarnings=()=>tmp,tmp};const JSII_RTTI_SYMBOL_1=Symbol.for("jsii.rtti");var alarm_base_1=()=>{var tmp=require("./alarm-base");return alarm_base_1=()=>tmp,tmp},cloudwatch_generated_1=()=>{var tmp=require("./cloudwatch.generated");return cloudwatch_generated_1=()=>tmp,tmp},metric_util_1=()=>{var tmp=require("./private/metric-util");return metric_util_1=()=>tmp,tmp},object_1=()=>{var tmp=require("./private/object");return object_1=()=>tmp,tmp},rendering_1=()=>{var tmp=require("./private/rendering");return rendering_1=()=>tmp,tmp},statistic_1=()=>{var tmp=require("./private/statistic");return statistic_1=()=>tmp,tmp},core_1=()=>{var tmp=require("../../core");return core_1=()=>tmp,tmp},ComparisonOperator;(function(ComparisonOperator2){ComparisonOperator2.GREATER_THAN_OR_EQUAL_TO_THRESHOLD="GreaterThanOrEqualToThreshold",ComparisonOperator2.GREATER_THAN_THRESHOLD="GreaterThanThreshold",ComparisonOperator2.LESS_THAN_THRESHOLD="LessThanThreshold",ComparisonOperator2.LESS_THAN_OR_EQUAL_TO_THRESHOLD="LessThanOrEqualToThreshold",ComparisonOperator2.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD="LessThanLowerOrGreaterThanUpperThreshold",ComparisonOperator2.GREATER_THAN_UPPER_THRESHOLD="GreaterThanUpperThreshold",ComparisonOperator2.LESS_THAN_LOWER_THRESHOLD="LessThanLowerThreshold"})(ComparisonOperator||(exports.ComparisonOperator=ComparisonOperator={}));const OPERATOR_SYMBOLS={GreaterThanOrEqualToThreshold:">=",GreaterThanThreshold:">",LessThanThreshold:"<",LessThanOrEqualToThreshold:"<="};var TreatMissingData;(function(TreatMissingData2){TreatMissingData2.BREACHING="breaching",TreatMissingData2.NOT_BREACHING="notBreaching",TreatMissingData2.IGNORE="ignore",TreatMissingData2.MISSING="missing"})(TreatMissingData||(exports.TreatMissingData=TreatMissingData={}));class Alarm extends alarm_base_1().AlarmBase{static fromAlarmName(scope,id,alarmName){const stack=core_1().Stack.of(scope);return this.fromAlarmArn(scope,id,stack.formatArn({service:"cloudwatch",resource:"alarm",resourceName:alarmName,arnFormat:core_1().ArnFormat.COLON_RESOURCE_NAME}))}static fromAlarmArn(scope,id,alarmArn){class Import extends alarm_base_1().AlarmBase{constructor(){super(...arguments),this.alarmArn=alarmArn,this.alarmName=core_1().Stack.of(scope).splitArn(alarmArn,core_1().ArnFormat.COLON_RESOURCE_NAME).resourceName}}return new Import(scope,id)}constructor(scope,id,props){super(scope,id,{physicalName:props.alarmName});try{jsiiDeprecationWarnings().aws_cdk_lib_aws_cloudwatch_AlarmProps(props)}catch(error){throw process.env.JSII_DEBUG!=="1"&&error.name==="DeprecationError"&&Error.captureStackTrace(error,Alarm),error}const comparisonOperator=props.comparisonOperator||ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,metricProps=this.renderMetric(props.metric);props.period&&(metricProps.period=props.period.toSeconds()),props.statistic&&Object.assign(metricProps,{statistic:renderIfSimpleStatistic(props.statistic),extendedStatistic:renderIfExtendedStatistic(props.statistic)});const alarm=new(cloudwatch_generated_1()).CfnAlarm(this,"Resource",{alarmDescription:props.alarmDescription,alarmName:this.physicalName,comparisonOperator,threshold:props.threshold,datapointsToAlarm:props.datapointsToAlarm,evaluateLowSampleCountPercentile:props.evaluateLowSampleCountPercentile,evaluationPeriods:props.evaluationPeriods,treatMissingData:props.treatMissingData,actionsEnabled:props.actionsEnabled,alarmActions:core_1().Lazy.list({produce:()=>this.alarmActionArns}),insufficientDataActions:core_1().Lazy.list({produce:()=>this.insufficientDataActionArns}),okActions:core_1().Lazy.list({produce:()=>this.okActionArns}),...metricProps});this.alarmArn=this.getResourceArnAttribute(alarm.attrArn,{service:"cloudwatch",resource:"alarm",resourceName:this.physicalName,arnFormat:core_1().ArnFormat.COLON_RESOURCE_NAME}),this.alarmName=this.getResourceNameAttribute(alarm.ref),this.metric=props.metric;const datapoints=props.datapointsToAlarm||props.evaluationPeriods;this.annotation={label:`${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${datapoints} datapoints within ${describePeriod(props.evaluationPeriods*(0,metric_util_1().metricPeriod)(props.metric).toSeconds())}`,value:props.threshold};for(const[i,message]of Object.entries(this.metric.warningsV2??{}))core_1().Annotations.of(this).addWarningV2(i,message)}toAnnotation(){return this.annotation}addAlarmAction(...actions){try{jsiiDeprecationWarnings().aws_cdk_lib_aws_cloudwatch_IAlarmAction(actions)}catch(error){throw process.env.JSII_DEBUG!=="1"&&error.name==="DeprecationError"&&Error.captureStackTrace(error,this.addAlarmAction),error}this.alarmActionArns===void 0&&(this.alarmActionArns=[]),this.alarmActionArns.push(...actions.map(a=>this.validateActionArn(a.bind(this,this).alarmActionArn)))}validateActionArn(actionArn){if(/arn:aws[a-z0-9-]*:automate:[a-z|\d|-]+:ec2:[a-z]+/.test(actionArn)){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}renderMetric(metric){const self=this;return(0,metric_util_1().dispatchMetric)(metric,{withStat(stat,conf){return self.validateMetricStat(stat,metric),conf.renderingProperties?.label==null&&!self.requiresAccountId(stat)?(0,object_1().dropUndefined)({dimensions:stat.dimensions,namespace:stat.namespace,metricName:stat.metricName,period:stat.period?.toSeconds(),statistic:renderIfSimpleStatistic(stat.statistic),extendedStatistic:renderIfExtendedStatistic(stat.statistic),unit:stat.unitFilter}):{metrics:[{metricStat:{metric:{metricName:stat.metricName,namespace:stat.namespace,dimensions:stat.dimensions},period:stat.period.toSeconds(),stat:stat.statistic,unit:stat.unitFilter},id:"m1",accountId:self.requiresAccountId(stat)?stat.account:void 0,label:conf.renderingProperties?.label,returnData:!0}]}},withExpression(){const mset=new(rendering_1()).MetricSet;mset.addTopLevel(!0,metric);let eid=0;function uniqueMetricId(){return`expr_${++eid}`}return{metrics:mset.entries.map(entry=>(0,metric_util_1().dispatchMetric)(entry.metric,{withStat(stat,conf){return self.validateMetricStat(stat,entry.metric),{metricStat:{metric:{metricName:stat.metricName,namespace:stat.namespace,dimensions:stat.dimensions},period:stat.period.toSeconds(),stat:stat.statistic,unit:stat.unitFilter},id:entry.id||uniqueMetricId(),accountId:self.requiresAccountId(stat)?stat.account:void 0,label:conf.renderingProperties?.label,returnData:entry.tag?void 0:!1}},withExpression(expr,conf){const hasSubmetrics=mathExprHasSubmetrics(expr);return hasSubmetrics&&assertSubmetricsCount(expr),self.validateMetricExpression(expr),{expression:expr.expression,id:entry.id||uniqueMetricId(),label:conf.renderingProperties?.label,period:hasSubmetrics?void 0:expr.period,returnData:entry.tag?void 0:!1}}}))}}})}validateMetricStat(stat,metric){const stack=core_1().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}'`)}validateMetricExpression(expr){if(expr.searchAccount!==void 0||expr.searchRegion!==void 0)throw new Error("Cannot create an Alarm based on a MathExpression which specifies a searchAccount or searchRegion")}requiresAccountId(stat){const stackAccount=core_1().Stack.of(this).account;return stat.account===void 0?!1:stackAccount!==stat.account}}exports.Alarm=Alarm,_a=JSII_RTTI_SYMBOL_1,Alarm[_a]={fqn:"aws-cdk-lib.aws_cloudwatch.Alarm",version:"2.128.0"};function definitelyDifferent(x,y){return x&&!core_1().Token.isUnresolved(y)&&x!==y}function describePeriod(seconds){return seconds===60?"1 minute":seconds===1?"1 second":seconds>60?seconds/60+" minutes":seconds+" seconds"}function renderIfSimpleStatistic(statistic){if(statistic===void 0)return;const parsed=(0,statistic_1().parseStatistic)(statistic);if(parsed.type==="simple")return(0,statistic_1().normalizeStatistic)(parsed)}function renderIfExtendedStatistic(statistic){if(statistic===void 0)return;const parsed=(0,statistic_1().parseStatistic)(statistic);if(parsed.type!=="simple")return parsed.type==="single"||parsed.type==="pair"?(0,statistic_1().normalizeStatistic)(parsed):parsed.statistic}function mathExprHasSubmetrics(expr){return Object.keys(expr.usingMetrics).length>0}function assertSubmetricsCount(expr){if(Object.keys(expr.usingMetrics).length>10)throw new Error("Alarms on math expressions cannot contain more than 10 individual metrics")}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   ^
Error: EC2 alarm actions requires an EC2 Per-Instance Metric. ({"metricStat":{"dimensions":[{"name":"ImageId","value":"ami-0e670eb768a5fc3d4"},{"name":"InstanceId","value":"i-080e122e0fe45205e"},{"name":"InstanceType","value":"t2.micro"},{"name":"device","value":"xvda1"},{"name":"fstype","value":"xfs"},{"name":"path","value":"/"}],"namespace":"CWAgent","metricName":"disk_used_percent","period":{"amount":5,"unit":{"label":"minutes","isoLabel":"M","inMillis":60000}},"statistic":"Average"},"renderingProperties":{}} does not have an 'InstanceId' dimension)
    at Alarm.validateActionArn (/Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:5300)
    at /Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:4982
    at Array.map (<anonymous>)
    at Alarm.addAlarmAction (/Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:4970)
    at new TscdkStack (/Users//Desktop/tscdk/lib/tscdk-stack.ts:66:15)
    at Object.<anonymous> (/Users//Desktop/tscdk/bin/tscdk.ts:7:1)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module.m._compile (/Users//Desktop/tscdk/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Object.require.extensions.<computed> [as .ts] (/Users//Desktop/tscdk/node_modules/ts-node/src/index.ts:1621:12)

Subprocess exited with error 1

The code for generating the error is as below

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as cw from 'aws-cdk-lib/aws-cloudwatch'
import { Ec2Action, Ec2InstanceAction } from 'aws-cdk-lib/aws-cloudwatch-actions';

export class TscdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const metric = new cw.Metric({
      namespace: 'CWAgent',
      metricName: 'disk_used_percent',
      dimensionsMap: {
        
        InstanceId: "<instance-id>",
        ImageId: "ami-id",
        InstanceType: "t2.micro",
        path: "/",
        device: "xvda1",
        fstype: "xfs"
      },
      period: cdk.Duration.minutes(5),
      statistic: "Average"
    })
    
    const sev3Alarm = new cw.Alarm(this, `DISK_USED_PERCENT_SEV3`, {
      alarmName: `DISK_USED_PERCENT_SEV3`,
      actionsEnabled: true,
      metric: metric,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      threshold: 75,
      comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cw.TreatMissingData.BREACHING,
    });
    
 
    sev3Alarm.addAlarmAction(new Ec2Action(Ec2InstanceAction.REBOOT));
  }
}

To resolve the issue, there are 2 ways of doing it, the first one is we comment out the other dimensions apart from instance id in the dimensions map as below.

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as cw from 'aws-cdk-lib/aws-cloudwatch'
import { Ec2Action, Ec2InstanceAction } from 'aws-cdk-lib/aws-cloudwatch-actions';

export class TscdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const metric = new cw.Metric({
      namespace: 'CWAgent',
      metricName: 'disk_used_percent',
      dimensionsMap: {
        InstanceId: "instance-id"
      },
      period: cdk.Duration.minutes(5),
      statistic: "Average"
    })
    
    const sev3Alarm = new cw.Alarm(this, `DISK_USED_PERCENT_SEV3`, {
      alarmName: `DISK_USED_PERCENT_SEV3`,
      actionsEnabled: true,
      metric: metric,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      threshold: 75,
      comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cw.TreatMissingData.BREACHING,
    });
    
 
    sev3Alarm.addAlarmAction(new Ec2Action(Ec2InstanceAction.REBOOT));
  }
}

Or we use escape hatch as below.

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as cw from 'aws-cdk-lib/aws-cloudwatch'
import { Ec2Action, Ec2InstanceAction } from 'aws-cdk-lib/aws-cloudwatch-actions';

export class TscdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const metric = new cw.Metric({
      namespace: 'CWAgent',
      metricName: 'disk_used_percent',
      dimensionsMap: {
        
        InstanceId: "instance-id"
      },
      period: cdk.Duration.minutes(5),
      statistic: "Average"
    })
    
    const sev3Alarm = new cw.Alarm(this, `DISK_USED_PERCENT_SEV3`, {
      alarmName: `DISK_USED_PERCENT_SEV3`,
      actionsEnabled: true,
      metric: metric,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      threshold: 75,
      comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cw.TreatMissingData.BREACHING,
    });
    var sev3AlarmCfn = sev3Alarm.node.defaultChild as cw.CfnAlarm
    sev3AlarmCfn.dimensions = [{
        name: "InstanceId",
        value: "instance-id"
      },
       {
        name: "ImageId",
        value: "imageId"
      },                                                          
      {
        name: "device",
        value: "xvda1"
      },
      {
        name: "InstanceType",
        value: "t2.micro"
      },
      {
        name: "path",
        value: "/"
      },
      {
        name: "device",
        value: "xvda1"
      },
      {
        name: "fstype",
        value: "xfs"
      }

    ]
 
    sev3Alarm.addAlarmAction(new Ec2Action(Ec2InstanceAction.REBOOT));
  }
}

Expected Behavior

The expected behaviour is that it should allow to add EC2 action for the custom metrics too, as one can do using CloudWatch Console, and CloudFormation.

Current Behavior

The current behaviour is that it results into the error stated below.

/Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1
"use strict";var _a;Object.defineProperty(exports,"__esModule",{value:!0}),exports.Alarm=exports.TreatMissingData=exports.ComparisonOperator=void 0;var jsiiDeprecationWarnings=()=>{var tmp=require("../../.warnings.jsii.js");return jsiiDeprecationWarnings=()=>tmp,tmp};const JSII_RTTI_SYMBOL_1=Symbol.for("jsii.rtti");var alarm_base_1=()=>{var tmp=require("./alarm-base");return alarm_base_1=()=>tmp,tmp},cloudwatch_generated_1=()=>{var tmp=require("./cloudwatch.generated");return cloudwatch_generated_1=()=>tmp,tmp},metric_util_1=()=>{var tmp=require("./private/metric-util");return metric_util_1=()=>tmp,tmp},object_1=()=>{var tmp=require("./private/object");return object_1=()=>tmp,tmp},rendering_1=()=>{var tmp=require("./private/rendering");return rendering_1=()=>tmp,tmp},statistic_1=()=>{var tmp=require("./private/statistic");return statistic_1=()=>tmp,tmp},core_1=()=>{var tmp=require("../../core");return core_1=()=>tmp,tmp},ComparisonOperator;(function(ComparisonOperator2){ComparisonOperator2.GREATER_THAN_OR_EQUAL_TO_THRESHOLD="GreaterThanOrEqualToThreshold",ComparisonOperator2.GREATER_THAN_THRESHOLD="GreaterThanThreshold",ComparisonOperator2.LESS_THAN_THRESHOLD="LessThanThreshold",ComparisonOperator2.LESS_THAN_OR_EQUAL_TO_THRESHOLD="LessThanOrEqualToThreshold",ComparisonOperator2.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD="LessThanLowerOrGreaterThanUpperThreshold",ComparisonOperator2.GREATER_THAN_UPPER_THRESHOLD="GreaterThanUpperThreshold",ComparisonOperator2.LESS_THAN_LOWER_THRESHOLD="LessThanLowerThreshold"})(ComparisonOperator||(exports.ComparisonOperator=ComparisonOperator={}));const OPERATOR_SYMBOLS={GreaterThanOrEqualToThreshold:">=",GreaterThanThreshold:">",LessThanThreshold:"<",LessThanOrEqualToThreshold:"<="};var TreatMissingData;(function(TreatMissingData2){TreatMissingData2.BREACHING="breaching",TreatMissingData2.NOT_BREACHING="notBreaching",TreatMissingData2.IGNORE="ignore",TreatMissingData2.MISSING="missing"})(TreatMissingData||(exports.TreatMissingData=TreatMissingData={}));class Alarm extends alarm_base_1().AlarmBase{static fromAlarmName(scope,id,alarmName){const stack=core_1().Stack.of(scope);return this.fromAlarmArn(scope,id,stack.formatArn({service:"cloudwatch",resource:"alarm",resourceName:alarmName,arnFormat:core_1().ArnFormat.COLON_RESOURCE_NAME}))}static fromAlarmArn(scope,id,alarmArn){class Import extends alarm_base_1().AlarmBase{constructor(){super(...arguments),this.alarmArn=alarmArn,this.alarmName=core_1().Stack.of(scope).splitArn(alarmArn,core_1().ArnFormat.COLON_RESOURCE_NAME).resourceName}}return new Import(scope,id)}constructor(scope,id,props){super(scope,id,{physicalName:props.alarmName});try{jsiiDeprecationWarnings().aws_cdk_lib_aws_cloudwatch_AlarmProps(props)}catch(error){throw process.env.JSII_DEBUG!=="1"&&error.name==="DeprecationError"&&Error.captureStackTrace(error,Alarm),error}const comparisonOperator=props.comparisonOperator||ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,metricProps=this.renderMetric(props.metric);props.period&&(metricProps.period=props.period.toSeconds()),props.statistic&&Object.assign(metricProps,{statistic:renderIfSimpleStatistic(props.statistic),extendedStatistic:renderIfExtendedStatistic(props.statistic)});const alarm=new(cloudwatch_generated_1()).CfnAlarm(this,"Resource",{alarmDescription:props.alarmDescription,alarmName:this.physicalName,comparisonOperator,threshold:props.threshold,datapointsToAlarm:props.datapointsToAlarm,evaluateLowSampleCountPercentile:props.evaluateLowSampleCountPercentile,evaluationPeriods:props.evaluationPeriods,treatMissingData:props.treatMissingData,actionsEnabled:props.actionsEnabled,alarmActions:core_1().Lazy.list({produce:()=>this.alarmActionArns}),insufficientDataActions:core_1().Lazy.list({produce:()=>this.insufficientDataActionArns}),okActions:core_1().Lazy.list({produce:()=>this.okActionArns}),...metricProps});this.alarmArn=this.getResourceArnAttribute(alarm.attrArn,{service:"cloudwatch",resource:"alarm",resourceName:this.physicalName,arnFormat:core_1().ArnFormat.COLON_RESOURCE_NAME}),this.alarmName=this.getResourceNameAttribute(alarm.ref),this.metric=props.metric;const datapoints=props.datapointsToAlarm||props.evaluationPeriods;this.annotation={label:`${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${datapoints} datapoints within ${describePeriod(props.evaluationPeriods*(0,metric_util_1().metricPeriod)(props.metric).toSeconds())}`,value:props.threshold};for(const[i,message]of Object.entries(this.metric.warningsV2??{}))core_1().Annotations.of(this).addWarningV2(i,message)}toAnnotation(){return this.annotation}addAlarmAction(...actions){try{jsiiDeprecationWarnings().aws_cdk_lib_aws_cloudwatch_IAlarmAction(actions)}catch(error){throw process.env.JSII_DEBUG!=="1"&&error.name==="DeprecationError"&&Error.captureStackTrace(error,this.addAlarmAction),error}this.alarmActionArns===void 0&&(this.alarmActionArns=[]),this.alarmActionArns.push(...actions.map(a=>this.validateActionArn(a.bind(this,this).alarmActionArn)))}validateActionArn(actionArn){if(/arn:aws[a-z0-9-]*:automate:[a-z|\d|-]+:ec2:[a-z]+/.test(actionArn)){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}renderMetric(metric){const self=this;return(0,metric_util_1().dispatchMetric)(metric,{withStat(stat,conf){return self.validateMetricStat(stat,metric),conf.renderingProperties?.label==null&&!self.requiresAccountId(stat)?(0,object_1().dropUndefined)({dimensions:stat.dimensions,namespace:stat.namespace,metricName:stat.metricName,period:stat.period?.toSeconds(),statistic:renderIfSimpleStatistic(stat.statistic),extendedStatistic:renderIfExtendedStatistic(stat.statistic),unit:stat.unitFilter}):{metrics:[{metricStat:{metric:{metricName:stat.metricName,namespace:stat.namespace,dimensions:stat.dimensions},period:stat.period.toSeconds(),stat:stat.statistic,unit:stat.unitFilter},id:"m1",accountId:self.requiresAccountId(stat)?stat.account:void 0,label:conf.renderingProperties?.label,returnData:!0}]}},withExpression(){const mset=new(rendering_1()).MetricSet;mset.addTopLevel(!0,metric);let eid=0;function uniqueMetricId(){return`expr_${++eid}`}return{metrics:mset.entries.map(entry=>(0,metric_util_1().dispatchMetric)(entry.metric,{withStat(stat,conf){return self.validateMetricStat(stat,entry.metric),{metricStat:{metric:{metricName:stat.metricName,namespace:stat.namespace,dimensions:stat.dimensions},period:stat.period.toSeconds(),stat:stat.statistic,unit:stat.unitFilter},id:entry.id||uniqueMetricId(),accountId:self.requiresAccountId(stat)?stat.account:void 0,label:conf.renderingProperties?.label,returnData:entry.tag?void 0:!1}},withExpression(expr,conf){const hasSubmetrics=mathExprHasSubmetrics(expr);return hasSubmetrics&&assertSubmetricsCount(expr),self.validateMetricExpression(expr),{expression:expr.expression,id:entry.id||uniqueMetricId(),label:conf.renderingProperties?.label,period:hasSubmetrics?void 0:expr.period,returnData:entry.tag?void 0:!1}}}))}}})}validateMetricStat(stat,metric){const stack=core_1().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}'`)}validateMetricExpression(expr){if(expr.searchAccount!==void 0||expr.searchRegion!==void 0)throw new Error("Cannot create an Alarm based on a MathExpression which specifies a searchAccount or searchRegion")}requiresAccountId(stat){const stackAccount=core_1().Stack.of(this).account;return stat.account===void 0?!1:stackAccount!==stat.account}}exports.Alarm=Alarm,_a=JSII_RTTI_SYMBOL_1,Alarm[_a]={fqn:"aws-cdk-lib.aws_cloudwatch.Alarm",version:"2.128.0"};function definitelyDifferent(x,y){return x&&!core_1().Token.isUnresolved(y)&&x!==y}function describePeriod(seconds){return seconds===60?"1 minute":seconds===1?"1 second":seconds>60?seconds/60+" minutes":seconds+" seconds"}function renderIfSimpleStatistic(statistic){if(statistic===void 0)return;const parsed=(0,statistic_1().parseStatistic)(statistic);if(parsed.type==="simple")return(0,statistic_1().normalizeStatistic)(parsed)}function renderIfExtendedStatistic(statistic){if(statistic===void 0)return;const parsed=(0,statistic_1().parseStatistic)(statistic);if(parsed.type!=="simple")return parsed.type==="single"||parsed.type==="pair"?(0,statistic_1().normalizeStatistic)(parsed):parsed.statistic}function mathExprHasSubmetrics(expr){return Object.keys(expr.usingMetrics).length>0}function assertSubmetricsCount(expr){if(Object.keys(expr.usingMetrics).length>10)throw new Error("Alarms on math expressions cannot contain more than 10 individual metrics")}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   ^
Error: EC2 alarm actions requires an EC2 Per-Instance Metric. ({"metricStat":{"dimensions":[{"name":"ImageId","value":"ami-0e670eb768a5fc3d4"},{"name":"InstanceId","value":"i-080e122e0fe45205e"},{"name":"InstanceType","value":"t2.micro"},{"name":"device","value":"xvda1"},{"name":"fstype","value":"xfs"},{"name":"path","value":"/"}],"namespace":"CWAgent","metricName":"disk_used_percent","period":{"amount":5,"unit":{"label":"minutes","isoLabel":"M","inMillis":60000}},"statistic":"Average"},"renderingProperties":{}} does not have an 'InstanceId' dimension)
    at Alarm.validateActionArn (/Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:5300)
    at /Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:4982
    at Array.map (<anonymous>)
    at Alarm.addAlarmAction (/Users//Desktop/tscdk/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:4970)
    at new TscdkStack (/Users//Desktop/tscdk/lib/tscdk-stack.ts:66:15)
    at Object.<anonymous> (/Users//Desktop/tscdk/bin/tscdk.ts:7:1)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module.m._compile (/Users//Desktop/tscdk/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Object.require.extensions.<computed> [as .ts] (/Users//Desktop/tscdk/node_modules/ts-node/src/index.ts:1621:12)

Subprocess exited with error 1

Reproduction Steps

Use the below code, and run cdk synth

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as cw from 'aws-cdk-lib/aws-cloudwatch'
import { Ec2Action, Ec2InstanceAction } from 'aws-cdk-lib/aws-cloudwatch-actions';

export class TscdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const metric = new cw.Metric({
      namespace: 'CWAgent',
      metricName: 'disk_used_percent',
      dimensionsMap: {
        
        InstanceId: "instance-id",
        ImageId: "image-id",
        InstanceType: "t2.micro",
        path: "/",
        device: "xvda1",
        fstype: "xfs"
      },
      period: cdk.Duration.minutes(5),
      statistic: "Average"
    })
    
    const sev3Alarm = new cw.Alarm(this, `DISK_USED_PERCENT_SEV3`, {
      alarmName: `DISK_USED_PERCENT_SEV3`,
      actionsEnabled: true,
      metric: metric,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      threshold: 75,
      comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cw.TreatMissingData.BREACHING,
    });
    
 
    sev3Alarm.addAlarmAction(new Ec2Action(Ec2InstanceAction.REBOOT));
  }
}


### Possible Solution

Possible bug, it is causing the issue here at this piece of code.

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


### Additional Information/Context

_No response_

### CDK CLI Version

"aws-cdk-lib": "2.128.0",

### Framework Version

"constructs": "^10.0.0",

### Node.js Version

v18.18.0

### OS

MacOs

### Language

TypeScript

### Language Version

"typescript": "~5.3.3"

### Other information

Please note, I see a workflow issue over here, as if adding the dimensions directly to the metric is not working, then it should also not work while adding it through escape hatch.
@rtejwani1309 rtejwani1309 added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Mar 1, 2024
@github-actions github-actions bot added the @aws-cdk/aws-cloudwatch Related to Amazon CloudWatch label Mar 1, 2024
@pahud pahud changed the title (aws-cloudwatch/): Use of EC2 action with Multiple dimension set in metric results into error cloudwatch: Use of EC2 action with Multiple dimension set in metric results into error Mar 4, 2024
@pahud
Copy link
Contributor

pahud commented Mar 4, 2024

Looks like this error is from:

private validateActionArn(actionArn: string): string {
const ec2ActionsRegexp: RegExp = /arn:aws[a-z0-9-]*: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;
}

I don't have the immediate fix off the top of my head. Making it a p1 bug.

@pahud pahud added p1 effort/medium Medium work item – several days of effort and removed needs-triage This issue or PR still needs to be triaged. labels Mar 4, 2024
@GavinZZ
Copy link
Contributor

GavinZZ commented Mar 4, 2024

Maybe I'm missing some step but I cannot reproduce this issue. I ran cdk synth using the following example as you provided

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cw from 'aws-cdk-lib/aws-cloudwatch'
import { Ec2Action, Ec2InstanceAction } from 'aws-cdk-lib/aws-cloudwatch-actions';

export class UnlinkedCdkAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const metric = new cw.Metric({
      namespace: 'CWAgent',
      metricName: 'disk_used_percent',
      dimensionsMap: {
        
        InstanceId: "instance-id",
        ImageId: "ami-id",
        InstanceType: "t2.micro",
        path: "/",
        device: "xvda1",
        fstype: "xfs"
      },
      period: cdk.Duration.minutes(5),
      statistic: "Average"
    })
    
    const sev3Alarm = new cw.Alarm(this, `DISK_USED_PERCENT_SEV3`, {
      alarmName: `DISK_USED_PERCENT_SEV3`,
      actionsEnabled: true,
      metric: metric,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      threshold: 75,
      comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cw.TreatMissingData.BREACHING,
    });
    

    sev3Alarm.addAlarmAction(new Ec2Action(Ec2InstanceAction.REBOOT));
  }
}

cdk synth correct synth the code into the following template

Resources:
  DISKUSEDPERCENTSEV36CE6B660:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ActionsEnabled: true
      AlarmActions:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - ":automate:"
              - Ref: AWS::Region
              - :ec2:reboot
      AlarmName: DISK_USED_PERCENT_SEV3
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 1
      Dimensions:
        - Name: ImageId
          Value: ami-id
        - Name: InstanceId
          Value: instance-id
        - Name: InstanceType
          Value: t2.micro
        - Name: device
          Value: xvda1
        - Name: fstype
          Value: xfs
        - Name: path
          Value: /
      EvaluationPeriods: 1
      MetricName: disk_used_percent
      Namespace: CWAgent
      Period: 300
      Statistic: Average
      Threshold: 75
      TreatMissingData: breaching
    Metadata:
      aws:cdk:path: UnlinkedCdkAppStack/DISK_USED_PERCENT_SEV3/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/yXIPQ6DMAxA4bOwJ4ZkYEfcAA5QpU6qGoIt5acMiLu30Ol9ehaM7aFr3J41+lVHesIxF4er+q0HRql+dwXfcAzRpU2NL75xXppClpowXB6FPRUSPhWLD7Dk9mN6sB2YZslEOlUutAWY/v0C8ssPDnoAAAA=
    Metadata:
      aws:cdk:path: UnlinkedCdkAppStack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - af-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - il-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - me-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
Rules:
  CheckBootstrapVersion:
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - "1"
                  - "2"
                  - "3"
                  - "4"
                  - "5"
                - Ref: BootstrapVersion
        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.

@GavinZZ GavinZZ added the blocked Work is blocked on this issue for this codebase. Other labels or comments may indicate why. label Mar 4, 2024
@pahud
Copy link
Contributor

pahud commented Mar 5, 2024

Hi @GavinZZ

I can reproduce it. Check out my full cdk app.

#!/usr/bin/env node
import 'source-map-support/register';
import { App, StackProps, Stack, CfnOutput, Duration,
  aws_cloudwatch as cw,
  aws_cloudwatch_actions as cwactions,
} from 'aws-cdk-lib';

import { Construct } from 'constructs';

export class UnlinkedCdkAppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    
    const metric = new cw.Metric({
      namespace: 'CWAgent',
      metricName: 'disk_used_percent',
      dimensionsMap: {
        
        InstanceId: "instance-id",
        ImageId: "ami-id",
        InstanceType: "t2.micro",
        path: "/",
        device: "xvda1",
        fstype: "xfs"
      },
      period: Duration.minutes(5),
      statistic: "Average"
    })
    
    const sev3Alarm = new cw.Alarm(this, `DISK_USED_PERCENT_SEV3`, {
      alarmName: `DISK_USED_PERCENT_SEV3`,
      actionsEnabled: true,
      metric: metric,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      threshold: 75,
      comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cw.TreatMissingData.BREACHING,
    });
    
    sev3Alarm.addAlarmAction(new cwactions.Ec2Action(cwactions.Ec2InstanceAction.REBOOT));
  }
}

const app = new App();
const env = { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT };
new UnlinkedCdkAppStack(app, 'UnlinkedCdkAppStack', { env });

cdk synth returns this error:

Error: EC2 alarm actions requires an EC2 Per-Instance Metric. ({"metricStat":{"dimensions":[{"name":"ImageId","value":"ami-id"},{"name":"InstanceId","value":"instance-id"},{"name":"InstanceType","value":"t2.micro"},{"name":"device","value":"xvda1"},{"name":"fstype","value":"xfs"},{"name":"path","value":"/"}],"namespace":"CWAgent","metricName":"disk_used_percent","period":{"amount":5,"unit":{"label":"minutes","isoLabel":"M","inMillis":60000}},"statistic":"Average"},"renderingProperties":{}} does not have an 'InstanceId' dimension)
    at Alarm.validateActionArn (/Users/hunhsieh/repos/xxx/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:5300)
    at /Users/hunhsieh/repos/xxx/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:4982
    at Array.map (<anonymous>)
    at Alarm.addAlarmAction (/Users/hunhsieh/repos/xxx/node_modules/aws-cdk-lib/aws-cloudwatch/lib/alarm.js:1:4970)
    at new UnlinkedCdkAppStack (/Users/hunhsieh/repos/xxx/bin/xxx.ts:41:15)
    at Object.<anonymous> (/Users/hunhsieh/repos/xxx/bin/xxx.ts:47:1)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module.m._compile (/Users/hunhsieh/repos/xxx/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/hunhsieh/repos/xxx/node_modules/ts-node/src/index.ts:1621:12)

Subprocess exited with error 1

% npx cdk --version
2.131.0 (build 92b912d)

@GavinZZ
Copy link
Contributor

GavinZZ commented Mar 5, 2024

Thanks Pahud, I can confirm that this is reproducible. Will investigate this issue.

Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

1 similar comment
Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-cloudwatch Related to Amazon CloudWatch blocked Work is blocked on this issue for this codebase. Other labels or comments may indicate why. bug This issue is a bug. effort/medium Medium work item – several days of effort p1
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants