Repository: cdklabs/cdk-monitoring-constructs (language: python)

Issue:
Lambda Support as CloudWatch Alarm Action
### Feature scope

CloudWatch

### Describe your suggested feature

Since the end of December 2023, AWS now supports the triggering of a Lambda function as a CloudWatch Alarm action target (cf. https://aws.amazon.com/about-aws/whats-new/2023/12/amazon-cloudwatch-alarms-lambda-change-action/). This is useful, for example, when you want to modify the email body before sending it to another SNS topic where a list of email addresses is subscribed.

Currently, `cdk-monitoring-constructs` only supports SNS topics like CloudWatch Alarms action. Would it be possible to implement Lambda as an Alarm action as well?


Discussion / hints:
Seems reasonable to add, but we'd need to bump up to >= v2.119.0 first (we're currently at >= v2.112.0).

https://github.com/aws/aws-cdk/commit/b9f49234d2c317d1c7dc143994d3e3b3b7bc430a

In the meantime if you're already using a newer version of `aws-cdk-lib`, you can implement something like [how SNS is handled](https://github.com/cdklabs/cdk-monitoring-constructs/blob/main/lib/common/alarm/action/SnsAlarmActionStrategy.ts), but using `LambdaAction`.


Fix patch:
diff --git a/API.md b/API.md
index 42307b75..c2b6bc70 100644
--- a/API.md
+++ b/API.md
@@ -65597,6 +65597,55 @@ public readonly streamUrl: string;
 ---
 
 
+### LambdaAlarmActionStrategy <a name="LambdaAlarmActionStrategy" id="cdk-monitoring-constructs.LambdaAlarmActionStrategy"></a>
+
+- *Implements:* <a href="#cdk-monitoring-constructs.IAlarmActionStrategy">IAlarmActionStrategy</a>
+
+Alarm action strategy that triggers a Lambda function.
+
+#### Initializers <a name="Initializers" id="cdk-monitoring-constructs.LambdaAlarmActionStrategy.Initializer"></a>
+
+```typescript
+import { LambdaAlarmActionStrategy } from 'cdk-monitoring-constructs'
+
+new LambdaAlarmActionStrategy(lambdaFunction: IAlias | IFunction | IVersion)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#cdk-monitoring-constructs.LambdaAlarmActionStrategy.Initializer.parameter.lambdaFunction">lambdaFunction</a></code> | <code>aws-cdk-lib.aws_lambda.IAlias \| aws-cdk-lib.aws_lambda.IFunction \| aws-cdk-lib.aws_lambda.IVersion</code> | *No description.* |
+
+---
+
+##### `lambdaFunction`<sup>Required</sup> <a name="lambdaFunction" id="cdk-monitoring-constructs.LambdaAlarmActionStrategy.Initializer.parameter.lambdaFunction"></a>
+
+- *Type:* aws-cdk-lib.aws_lambda.IAlias | aws-cdk-lib.aws_lambda.IFunction | aws-cdk-lib.aws_lambda.IVersion
+
+---
+
+#### Methods <a name="Methods" id="Methods"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#cdk-monitoring-constructs.LambdaAlarmActionStrategy.addAlarmActions">addAlarmActions</a></code> | *No description.* |
+
+---
+
+##### `addAlarmActions` <a name="addAlarmActions" id="cdk-monitoring-constructs.LambdaAlarmActionStrategy.addAlarmActions"></a>
+
+```typescript
+public addAlarmActions(props: AlarmActionStrategyProps): void
+```
+
+###### `props`<sup>Required</sup> <a name="props" id="cdk-monitoring-constructs.LambdaAlarmActionStrategy.addAlarmActions.parameter.props"></a>
+
+- *Type:* <a href="#cdk-monitoring-constructs.AlarmActionStrategyProps">AlarmActionStrategyProps</a>
+
+---
+
+
+
+
 ### LambdaFunctionEnhancedMetricFactory <a name="LambdaFunctionEnhancedMetricFactory" id="cdk-monitoring-constructs.LambdaFunctionEnhancedMetricFactory"></a>
 
 #### Initializers <a name="Initializers" id="cdk-monitoring-constructs.LambdaFunctionEnhancedMetricFactory.Initializer"></a>
@@ -77818,7 +77867,7 @@ public readonly humanReadableName: string;
 
 ### IAlarmActionStrategy <a name="IAlarmActionStrategy" id="cdk-monitoring-constructs.IAlarmActionStrategy"></a>
 
-- *Implemented By:* <a href="#cdk-monitoring-constructs.MultipleAlarmActionStrategy">MultipleAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.NoopAlarmActionStrategy">NoopAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.OpsItemAlarmActionStrategy">OpsItemAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.SnsAlarmActionStrategy">SnsAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.IAlarmActionStrategy">IAlarmActionStrategy</a>
+- *Implemented By:* <a href="#cdk-monitoring-constructs.LambdaAlarmActionStrategy">LambdaAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.MultipleAlarmActionStrategy">MultipleAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.NoopAlarmActionStrategy">NoopAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.OpsItemAlarmActionStrategy">OpsItemAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.SnsAlarmActionStrategy">SnsAlarmActionStrategy</a>, <a href="#cdk-monitoring-constructs.IAlarmActionStrategy">IAlarmActionStrategy</a>
 
 An object that appends actions to alarms.
 
diff --git a/lib/common/alarm/action/LambdaAlarmActionStrategy.ts b/lib/common/alarm/action/LambdaAlarmActionStrategy.ts
new file mode 100644
index 00000000..4be0a129
--- /dev/null
+++ b/lib/common/alarm/action/LambdaAlarmActionStrategy.ts
@@ -0,0 +1,28 @@
+import { LambdaAction } from "aws-cdk-lib/aws-cloudwatch-actions";
+import { IAlias, IFunction, IVersion } from "aws-cdk-lib/aws-lambda";
+
+import {
+  AlarmActionStrategyProps,
+  IAlarmActionStrategy,
+} from "./IAlarmActionStrategy";
+
+export function triggerLambda(
+  lambdaFunction: IAlias | IVersion | IFunction,
+): IAlarmActionStrategy {
+  return new LambdaAlarmActionStrategy(lambdaFunction);
+}
+
+/**
+ * Alarm action strategy that triggers a Lambda function.
+ */
+export class LambdaAlarmActionStrategy implements IAlarmActionStrategy {
+  protected readonly lambdaFunction: IAlias | IVersion | IFunction;
+
+  constructor(lambdaFunction: IAlias | IVersion | IFunction) {
+    this.lambdaFunction = lambdaFunction;
+  }
+
+  addAlarmActions(props: AlarmActionStrategyProps): void {
+    props.alarm.addAlarmAction(new LambdaAction(this.lambdaFunction));
+  }
+}
diff --git a/lib/common/alarm/action/index.ts b/lib/common/alarm/action/index.ts
index 7dc6bb1f..9b0e98bb 100644
--- a/lib/common/alarm/action/index.ts
+++ b/lib/common/alarm/action/index.ts
@@ -1,4 +1,5 @@
 export * from "./IAlarmActionStrategy";
+export * from "./LambdaAlarmActionStrategy";
 export * from "./MultipleAlarmActionStrategy";
 export * from "./NoopAlarmActionStrategy";
 export * from "./OpsItemAlarmActionStrategy";


Test patch:
diff --git a/test/common/alarm/action/LambdaAlarmActionStrategy.test.ts b/test/common/alarm/action/LambdaAlarmActionStrategy.test.ts
new file mode 100644
index 00000000..53c92060
--- /dev/null
+++ b/test/common/alarm/action/LambdaAlarmActionStrategy.test.ts
@@ -0,0 +1,65 @@
+import { Stack } from "aws-cdk-lib";
+import { Template } from "aws-cdk-lib/assertions";
+import { Alarm, Metric } from "aws-cdk-lib/aws-cloudwatch";
+import { Function, InlineCode, Runtime } from "aws-cdk-lib/aws-lambda";
+
+import { LambdaAlarmActionStrategy } from "../../../../lib";
+
+test("snapshot test: Lambda function", () => {
+  const stack = new Stack();
+  const onAlarmFunction = new Function(stack, "alarmLambda", {
+    functionName: "DummyLambda",
+    runtime: Runtime.NODEJS_18_X,
+    code: InlineCode.fromInline("{}"),
+    handler: "Dummy::handler",
+  });
+  const alarm = new Alarm(stack, "DummyAlarm", {
+    evaluationPeriods: 1,
+    threshold: 0,
+    metric: new Metric({ namespace: "Dummy", metricName: "Dummy" }),
+  });
+  const action = new LambdaAlarmActionStrategy(onAlarmFunction);
+  action.addAlarmActions({ alarm, action });
+
+  expect(Template.fromStack(stack)).toMatchSnapshot();
+});
+
+test("snapshot test: Lambda alias", () => {
+  const stack = new Stack();
+  const onAlarmFunction = new Function(stack, "alarmLambda", {
+    functionName: "DummyLambda",
+    runtime: Runtime.NODEJS_18_X,
+    code: InlineCode.fromInline("{}"),
+    handler: "Dummy::handler",
+  });
+  const alias = onAlarmFunction.addAlias("aliasName");
+  const alarm = new Alarm(stack, "DummyAlarm", {
+    evaluationPeriods: 1,
+    threshold: 0,
+    metric: new Metric({ namespace: "Dummy", metricName: "Dummy" }),
+  });
+  const action = new LambdaAlarmActionStrategy(alias);
+  action.addAlarmActions({ alarm, action });
+
+  expect(Template.fromStack(stack)).toMatchSnapshot();
+});
+
+test("snapshot test: Lambda version", () => {
+  const stack = new Stack();
+  const onAlarmFunction = new Function(stack, "alarmLambda", {
+    functionName: "DummyLambda",
+    runtime: Runtime.NODEJS_18_X,
+    code: InlineCode.fromInline("{}"),
+    handler: "Dummy::handler",
+  });
+  const version = onAlarmFunction.currentVersion;
+  const alarm = new Alarm(stack, "DummyAlarm", {
+    evaluationPeriods: 1,
+    threshold: 0,
+    metric: new Metric({ namespace: "Dummy", metricName: "Dummy" }),
+  });
+  const action = new LambdaAlarmActionStrategy(version);
+  action.addAlarmActions({ alarm, action });
+
+  expect(Template.fromStack(stack)).toMatchSnapshot();
+});
diff --git a/test/common/alarm/action/__snapshots__/LambdaAlarmActionStrategy.test.ts.snap b/test/common/alarm/action/__snapshots__/LambdaAlarmActionStrategy.test.ts.snap
new file mode 100644
index 00000000..a2b7bc8e
--- /dev/null
+++ b/test/common/alarm/action/__snapshots__/LambdaAlarmActionStrategy.test.ts.snap
@@ -0,0 +1,422 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`snapshot test: Lambda alias 1`] = `
+Object {
+  "Parameters": Object {
+    "BootstrapVersion": Object {
+      "Default": "/cdk-bootstrap/hnb659fds/version",
+      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+      "Type": "AWS::SSM::Parameter::Value<String>",
+    },
+  },
+  "Resources": Object {
+    "DummyAlarm234203A9": Object {
+      "Properties": Object {
+        "AlarmActions": Array [
+          Object {
+            "Ref": "alarmLambdaAliasaliasName41B27313",
+          },
+        ],
+        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
+        "EvaluationPeriods": 1,
+        "MetricName": "Dummy",
+        "Namespace": "Dummy",
+        "Period": 300,
+        "Statistic": "Average",
+        "Threshold": 0,
+      },
+      "Type": "AWS::CloudWatch::Alarm",
+    },
+    "alarmLambda131DB691": Object {
+      "DependsOn": Array [
+        "alarmLambdaServiceRoleCDAABB9D",
+      ],
+      "Properties": Object {
+        "Code": Object {
+          "ZipFile": "{}",
+        },
+        "FunctionName": "DummyLambda",
+        "Handler": "Dummy::handler",
+        "Role": Object {
+          "Fn::GetAtt": Array [
+            "alarmLambdaServiceRoleCDAABB9D",
+            "Arn",
+          ],
+        },
+        "Runtime": "nodejs18.x",
+      },
+      "Type": "AWS::Lambda::Function",
+    },
+    "alarmLambdaAliasaliasName41B27313": Object {
+      "Properties": Object {
+        "FunctionName": Object {
+          "Ref": "alarmLambda131DB691",
+        },
+        "FunctionVersion": Object {
+          "Fn::GetAtt": Array [
+            "alarmLambdaCurrentVersionBDCE825C8cbdd693b2754df9b113fa5d7cbd1972",
+            "Version",
+          ],
+        },
+        "Name": "aliasName",
+      },
+      "Type": "AWS::Lambda::Alias",
+    },
+    "alarmLambdaAliasaliasNameAlarmPermission64A91652": Object {
+      "Properties": Object {
+        "Action": "lambda:InvokeFunction",
+        "FunctionName": Object {
+          "Ref": "alarmLambdaAliasaliasName41B27313",
+        },
+        "Principal": "lambda.alarms.cloudwatch.amazonaws.com",
+        "SourceAccount": Object {
+          "Ref": "AWS::AccountId",
+        },
+        "SourceArn": Object {
+          "Fn::GetAtt": Array [
+            "DummyAlarm234203A9",
+            "Arn",
+          ],
+        },
+      },
+      "Type": "AWS::Lambda::Permission",
+    },
+    "alarmLambdaCurrentVersionBDCE825C8cbdd693b2754df9b113fa5d7cbd1972": Object {
+      "Properties": Object {
+        "FunctionName": Object {
+          "Ref": "alarmLambda131DB691",
+        },
+      },
+      "Type": "AWS::Lambda::Version",
+    },
+    "alarmLambdaServiceRoleCDAABB9D": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "lambda.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+  },
+  "Rules": Object {
+    "CheckBootstrapVersion": Object {
+      "Assertions": Array [
+        Object {
+          "Assert": Object {
+            "Fn::Not": Array [
+              Object {
+                "Fn::Contains": Array [
+                  Array [
+                    "1",
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                  ],
+                  Object {
+                    "Ref": "BootstrapVersion",
+                  },
+                ],
+              },
+            ],
+          },
+          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
+        },
+      ],
+    },
+  },
+}
+`;
+
+exports[`snapshot test: Lambda function 1`] = `
+Object {
+  "Parameters": Object {
+    "BootstrapVersion": Object {
+      "Default": "/cdk-bootstrap/hnb659fds/version",
+      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+      "Type": "AWS::SSM::Parameter::Value<String>",
+    },
+  },
+  "Resources": Object {
+    "DummyAlarm234203A9": Object {
+      "Properties": Object {
+        "AlarmActions": Array [
+          Object {
+            "Fn::GetAtt": Array [
+              "alarmLambda131DB691",
+              "Arn",
+            ],
+          },
+        ],
+        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
+        "EvaluationPeriods": 1,
+        "MetricName": "Dummy",
+        "Namespace": "Dummy",
+        "Period": 300,
+        "Statistic": "Average",
+        "Threshold": 0,
+      },
+      "Type": "AWS::CloudWatch::Alarm",
+    },
+    "alarmLambda131DB691": Object {
+      "DependsOn": Array [
+        "alarmLambdaServiceRoleCDAABB9D",
+      ],
+      "Properties": Object {
+        "Code": Object {
+          "ZipFile": "{}",
+        },
+        "FunctionName": "DummyLambda",
+        "Handler": "Dummy::handler",
+        "Role": Object {
+          "Fn::GetAtt": Array [
+            "alarmLambdaServiceRoleCDAABB9D",
+            "Arn",
+          ],
+        },
+        "Runtime": "nodejs18.x",
+      },
+      "Type": "AWS::Lambda::Function",
+    },
+    "alarmLambdaAlarmPermission43E41C89": Object {
+      "Properties": Object {
+        "Action": "lambda:InvokeFunction",
+        "FunctionName": Object {
+          "Fn::GetAtt": Array [
+            "alarmLambda131DB691",
+            "Arn",
+          ],
+        },
+        "Principal": "lambda.alarms.cloudwatch.amazonaws.com",
+        "SourceAccount": Object {
+          "Ref": "AWS::AccountId",
+        },
+        "SourceArn": Object {
+          "Fn::GetAtt": Array [
+            "DummyAlarm234203A9",
+            "Arn",
+          ],
+        },
+      },
+      "Type": "AWS::Lambda::Permission",
+    },
+    "alarmLambdaServiceRoleCDAABB9D": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "lambda.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+  },
+  "Rules": Object {
+    "CheckBootstrapVersion": Object {
+      "Assertions": Array [
+        Object {
+          "Assert": Object {
+            "Fn::Not": Array [
+              Object {
+                "Fn::Contains": Array [
+                  Array [
+                    "1",
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                  ],
+                  Object {
+                    "Ref": "BootstrapVersion",
+                  },
+                ],
+              },
+            ],
+          },
+          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
+        },
+      ],
+    },
+  },
+}
+`;
+
+exports[`snapshot test: Lambda version 1`] = `
+Object {
+  "Parameters": Object {
+    "BootstrapVersion": Object {
+      "Default": "/cdk-bootstrap/hnb659fds/version",
+      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+      "Type": "AWS::SSM::Parameter::Value<String>",
+    },
+  },
+  "Resources": Object {
+    "DummyAlarm234203A9": Object {
+      "Properties": Object {
+        "AlarmActions": Array [
+          Object {
+            "Ref": "alarmLambdaCurrentVersionBDCE825C8cbdd693b2754df9b113fa5d7cbd1972",
+          },
+        ],
+        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
+        "EvaluationPeriods": 1,
+        "MetricName": "Dummy",
+        "Namespace": "Dummy",
+        "Period": 300,
+        "Statistic": "Average",
+        "Threshold": 0,
+      },
+      "Type": "AWS::CloudWatch::Alarm",
+    },
+    "alarmLambda131DB691": Object {
+      "DependsOn": Array [
+        "alarmLambdaServiceRoleCDAABB9D",
+      ],
+      "Properties": Object {
+        "Code": Object {
+          "ZipFile": "{}",
+        },
+        "FunctionName": "DummyLambda",
+        "Handler": "Dummy::handler",
+        "Role": Object {
+          "Fn::GetAtt": Array [
+            "alarmLambdaServiceRoleCDAABB9D",
+            "Arn",
+          ],
+        },
+        "Runtime": "nodejs18.x",
+      },
+      "Type": "AWS::Lambda::Function",
+    },
+    "alarmLambdaCurrentVersionAlarmPermissionFEBD056F": Object {
+      "Properties": Object {
+        "Action": "lambda:InvokeFunction",
+        "FunctionName": Object {
+          "Ref": "alarmLambdaCurrentVersionBDCE825C8cbdd693b2754df9b113fa5d7cbd1972",
+        },
+        "Principal": "lambda.alarms.cloudwatch.amazonaws.com",
+        "SourceAccount": Object {
+          "Ref": "AWS::AccountId",
+        },
+        "SourceArn": Object {
+          "Fn::GetAtt": Array [
+            "DummyAlarm234203A9",
+            "Arn",
+          ],
+        },
+      },
+      "Type": "AWS::Lambda::Permission",
+    },
+    "alarmLambdaCurrentVersionBDCE825C8cbdd693b2754df9b113fa5d7cbd1972": Object {
+      "Properties": Object {
+        "FunctionName": Object {
+          "Ref": "alarmLambda131DB691",
+        },
+      },
+      "Type": "AWS::Lambda::Version",
+    },
+    "alarmLambdaServiceRoleCDAABB9D": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "lambda.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+  },
+  "Rules": Object {
+    "CheckBootstrapVersion": Object {
+      "Assertions": Array [
+        Object {
+          "Assert": Object {
+            "Fn::Not": Array [
+              Object {
+                "Fn::Contains": Array [
+                  Array [
+                    "1",
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                  ],
+                  Object {
+                    "Ref": "BootstrapVersion",
+                  },
+                ],
+              },
+            ],
+          },
+          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
+        },
+      ],
+    },
+  },
+}
+`;