lambda: associating multiple LambdaAction instances with a single Lambda function and different CloudWatch alarms.
Describe the bug
The issue arises when associating multiple LambdaAction instances with a single Lambda function and different CloudWatch alarms. AWS CDK automatically creates a permission (AlarmPermission) for each alarm action associated with the Lambda function. However, if multiple alarms are associated with the same Lambda function, CDK attempts to create the same permission (e.g., AlarmPermission) multiple times, causing a conflict.
Regression Issue
- [ ] Select this option if this issue appears to be a regression.
Last Known Working CDK Version
No response
Expected Behavior
AWS CDK should add unique permissions for each LambdaAction associated with the Lambda function when multiple CloudWatch alarms are configured to invoke the same Lambda.
Current Behavior
error output
jsii.errors.JavaScriptError:
@jsii/kernel.RuntimeError: Error: There is already a Construct with name 'AlarmPermission' in Function [JenkinsBitbucketLambdaFunction]
at Kernel._Kernel_ensureSync (/tmp/tmp6m2tuagq/lib/program.js:9510:23)
at Kernel.invoke (/tmp/tmp6m2tuagq/lib/program.js:8874:102)
at KernelHost.processRequest (/tmp/tmp6m2tuagq/lib/program.js:10715:36)
at KernelHost.run (/tmp/tmp6m2tuagq/lib/program.js:10675:22)
at Immediate._onImmediate (/tmp/tmp6m2tuagq/lib/program.js:10676:46)
at process.processImmediate (node:internal/timers:478:21)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/path/to/repo/app.py", line 16, in <module>
jenkins_bitbucket_stack = JenkinsBitbucketMonitoringStack(app, "Jenkins-Bitbucket-Monitoring-Stack", env=aws_env)
File "/path/to/repo/.venv/lib/python3.10/site-packages/jsii/_runtime.py", line 118, in __call__
inst = super(JSIIMeta, cast(JSIIMeta, cls)).__call__(*args, **kwargs)
File "/path/to/repo/jenkins_bitbucket_stack.py", line 166, in __init__
alarm_artifactory.add_alarm_action(cloudwatch_actions.LambdaAction(lambda_function))
File "/path/to/repo/.venv/lib/python3.10/site-packages/aws_cdk/aws_cloudwatch/__init__.py", line 14809, in add_alarm_action
return typing.cast(None, jsii.invoke(self, "addAlarmAction", [*actions]))
File "/path/to/repo/.venv/lib/python3.10/site-packages/jsii/_kernel/__init__.py", line 149, in wrapped
return _recursize_dereference(kernel, fn(kernel, *args, **kwargs))
File "/path/to/repo/.venv/lib/python3.10/site-packages/jsii/_kernel/__init__.py", line 399, in invoke
response = self.provider.invoke(
File "/path/to/repo/.venv/lib/python3.10/site-packages/jsii/_kernel/providers/process.py", line 380, in invoke
return self._process.send(request, InvokeResponse)
File "/path/to/repo/.venv/lib/python3.10/site-packages/jsii/_kernel/providers/process.py", line 342, in send
raise RuntimeError(resp.error) from JavaScriptError(resp.stack)
RuntimeError: Error: There is already a Construct with name 'AlarmPermission' in Function [JenkinsBitbucketLambdaFunction]
Reproduction Steps
CDK code to reproduce issue
from aws_cdk import (
Stack,
aws_cloudwatch as cloudwatch,
aws_lambda as _lambda,
aws_cloudwatch_actions as cloudwatch_actions,
Duration
)
from constructs import Construct
class GenericMonitoringStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Lambda function
lambda_function = _lambda.Function(self, "GenericLambdaFunction",
runtime=_lambda.Runtime.PYTHON_3_12,
role=kwargs.get('lambda_execution_role'), # Pass the execution role as a keyword argument
handler="your_handler.lambda_handler",
timeout=Duration.minutes(5),
function_name="Generic-CloudWatch-AlarmAction",
code=_lambda.Code.from_asset("path/to/your/code"),
environment={
"ALARM_NAME_1": "Generic_Alarm_1",
"ALARM_NAME_2": "Generic_Alarm_2",
"TAG_VALUE_1": "Tag_Value_1",
"TAG_VALUE_2": "Tag_Value_2",
}
)
# CloudWatch Alarm 1 (replace placeholders with actual alarm configuration)
alarm_1 = cloudwatch.Alarm(self, "GenericAlarm1",
alarm_name="Generic_Alarm_1",
metric=kwargs.get('metric_1'), # Pass the metric as a keyword argument
threshold=1,
evaluation_periods=2,
comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treat_missing_data=cloudwatch.TreatMissingData.NOT_BREACHING
)
# CloudWatch Alarm 2 (replace placeholders with actual alarm configuration)
alarm_2 = cloudwatch.Alarm(self, "GenericAlarm2",
alarm_name="Generic_Alarm_2",
metric=kwargs.get('metric_2'), # Pass the metric as a keyword argument
threshold=1,
evaluation_periods=2,
comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treat_missing_data=cloudwatch.TreatMissingData.NOT_BREACHING
)
# Adding CloudWatch Alarms with Lambda actions
alarm_1.add_alarm_action(cloudwatch_actions.LambdaAction(lambda_function))
alarm_2.add_alarm_action(cloudwatch_actions.LambdaAction(lambda_function)) # This line can cause the conflict if the permissions are not handled properly
Possible Solution
created 2 lambda aliases and assigning them to the cloud watch actions
from aws_cdk import (
Stack,
aws_lambda as _lambda,
aws_cloudwatch as cloudwatch,
aws_cloudwatch_actions as cloudwatch_actions,
Duration
)
from constructs import Construct
class MonitoringStackWithAliases(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Lambda function
lambda_function = _lambda.Function(self, "GenericLambdaFunction",
runtime=_lambda.Runtime.PYTHON_3_12,
handler="your_handler.lambda_handler",
timeout=Duration.minutes(5),
code=_lambda.Code.from_asset("path/to/your/code")
)
# Lambda alias for the first alarm
lambda_alias_1 = _lambda.Alias(self, "LambdaAlias1",
alias_name="alias1",
version=lambda_function.current_version
)
# Lambda alias for the second alarm
lambda_alias_2 = _lambda.Alias(self, "LambdaAlias2",
alias_name="alias2",
version=lambda_function.current_version
)
# CloudWatch Alarm 1
alarm_1 = cloudwatch.Alarm(self, "Alarm1",
alarm_name="Alarm1",
metric=kwargs.get('metric_1'), # Pass the metric as a keyword argument
threshold=1,
evaluation_periods=2,
comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treat_missing_data=cloudwatch.TreatMissingData.NOT_BREACHING
)
# CloudWatch Alarm 2
alarm_2 = cloudwatch.Alarm(self, "Alarm2",
alarm_name="Alarm2",
metric=kwargs.get('metric_2'), # Pass the metric as a keyword argument
threshold=1,
evaluation_periods=2,
comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treat_missing_data=cloudwatch.TreatMissingData.NOT_BREACHING
)
# Use Lambda Alias 1 with Alarm 1
alarm_1.add_alarm_action(cloudwatch_actions.LambdaAction(lambda_alias_1))
# Use Lambda Alias 2 with Alarm 2
alarm_2.add_alarm_action(cloudwatch_actions.LambdaAction(lambda_alias_2))
Additional Information/Context
per this document i should be able to tie the function
https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_cloudwatch_actions/README.html
CDK CLI Version
2.151.0
Framework Version
No response
Node.js Version
v21.5.0
OS
Linux tobot-win10 5.15.153.1-microsoft-standard-WSL2 #1 SMP Fri Mar 29 23:14:13 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
Language
Python
Language Version
3.10.12
Other information
No response
@terryobot Good morning. Thanks for reporting the issue. Somehow, I'm unable to reproduce the issue using the below similar TypeScript code with latest CDK version 2.162.1 (build 10aa526).
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
export class CdktestStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const lambdaFunction = new lambda.Function(this, 'someLambda', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'handler',
code: lambda.Code.fromInline('def handler(context):\n\treturn "hi"'),
timeout: cdk.Duration.minutes(5),
functionName: 'TestFunction'
});
const alarm_1 = new cloudwatch.Alarm(this, "GenericAlarm1", {
alarmName: "Generic_Alarm_1",
metric: new cloudwatch.Metric({
metricName: 'metric_1',
namespace: 'MyNamespace'
}),
threshold: 1,
evaluationPeriods: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
});
const alarm_2 = new cloudwatch.Alarm(this, "GenericAlarm2", {
alarmName: "Generic_Alarm_2",
metric: new cloudwatch.Metric({
metricName: 'metric_2',
namespace: 'MyNamespace'
}),
threshold: 1,
evaluationPeriods: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
});
alarm_1.addAlarmAction(new cloudwatchActions.LambdaAction(lambdaFunction));
alarm_2.addAlarmAction(new cloudwatchActions.LambdaAction(lambdaFunction));
}
}
The above CDK code synthesized to below CFN template:
Resources:
someLambdaServiceRole4F404078:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Metadata:
aws:cdk:path: CdktestStack/someLambda/ServiceRole/Resource
someLambdaA2987FE4:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |-
def handler(context):
return "hi"
FunctionName: TestFunction
Handler: handler
Role:
Fn::GetAtt:
- someLambdaServiceRole4F404078
- Arn
Runtime: python3.12
Timeout: 300
DependsOn:
- someLambdaServiceRole4F404078
Metadata:
aws:cdk:path: CdktestStack/someLambda/Resource
someLambdaGenericAlarm1AlarmPermission8A848112:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- someLambdaA2987FE4
- Arn
Principal: lambda.alarms.cloudwatch.amazonaws.com
SourceAccount: "<ACCOUNT_ID>"
SourceArn:
Fn::GetAtt:
- GenericAlarm194B9EE44
- Arn
Metadata:
aws:cdk:path: CdktestStack/someLambda/GenericAlarm1AlarmPermission
someLambdaGenericAlarm2AlarmPermissionE5CC8DA3:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- someLambdaA2987FE4
- Arn
Principal: lambda.alarms.cloudwatch.amazonaws.com
SourceAccount: "<ACCOUNT_ID>"
SourceArn:
Fn::GetAtt:
- GenericAlarm2D2747368
- Arn
Metadata:
aws:cdk:path: CdktestStack/someLambda/GenericAlarm2AlarmPermission
GenericAlarm194B9EE44:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmActions:
- Fn::GetAtt:
- someLambdaA2987FE4
- Arn
AlarmName: Generic_Alarm_1
ComparisonOperator: GreaterThanOrEqualToThreshold
EvaluationPeriods: 2
MetricName: metric_1
Namespace: MyNamespace
Period: 300
Statistic: Average
Threshold: 1
TreatMissingData: notBreaching
Metadata:
aws:cdk:path: CdktestStack/GenericAlarm1/Resource
GenericAlarm2D2747368:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmActions:
- Fn::GetAtt:
- someLambdaA2987FE4
- Arn
AlarmName: Generic_Alarm_2
ComparisonOperator: GreaterThanOrEqualToThreshold
EvaluationPeriods: 2
MetricName: metric_2
Namespace: MyNamespace
Period: 300
Statistic: Average
Threshold: 1
TreatMissingData: notBreaching
Metadata:
aws:cdk:path: CdktestStack/GenericAlarm2/Resource
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Analytics: v2:deflate64:H4sIAAAAAAAA/02KywrCMBAAv6X3ZDVFxKsInqV+gGyTiNvmAdnEHkL+XWo9eJqBmR7UsQfV4cJSm1k6GqHeM+pZ4MKP6tCPBqFeS9CZYhCXZ/j3m02emCmGJgg91CE6u4aVTWgXi1kw6xfUs8Pk1/SV1sRgOZakt/3nTYRoLEy8e6sT9Hs4dBMTyVRCJm9h2PgB8f7j1bcAAAA=
Metadata:
aws:cdk:path: CdktestStack/CDKMetadata/Default
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]
It also deploys successfully:
✨ Synthesis time: 4.86s
CdktestStack: start: Building 6bba642959ca71ec5f8a6a89ebfbd9da078652f88982ca360d283eeb559318ef:<ACCOUNT_ID>-us-east-2
CdktestStack: success: Built 6bba642959ca71ec5f8a6a89ebfbd9da078652f88982ca360d283eeb559318ef:<ACCOUNT_ID>-us-east-2
CdktestStack: start: Publishing 6bba642959ca71ec5f8a6a89ebfbd9da078652f88982ca360d283eeb559318ef:<ACCOUNT_ID>-us-east-2
CdktestStack: success: Published 6bba642959ca71ec5f8a6a89ebfbd9da078652f88982ca360d283eeb559318ef:<ACCOUNT_ID>-us-east-2
Stack undefined
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬───────────────────────────────┬────────┬───────────────────────┬───────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼───────────────────────────────┼────────┼───────────────────────┼───────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ + │ ${someLambda.Arn} │ Allow │ lambda:InvokeFunction │ Service:lambda.alarms.cloudwatch.amazonaws.com │ "ArnLike": { │
│ │ │ │ │ │ "AWS:SourceArn": "${GenericAlarm1.Arn}" │
│ │ │ │ │ │ }, │
│ │ │ │ │ │ "StringEquals": { │
│ │ │ │ │ │ "AWS:SourceAccount": "139480602983" │
│ │ │ │ │ │ } │
│ + │ ${someLambda.Arn} │ Allow │ lambda:InvokeFunction │ Service:lambda.alarms.cloudwatch.amazonaws.com │ "ArnLike": { │
│ │ │ │ │ │ "AWS:SourceArn": "${GenericAlarm2.Arn}" │
│ │ │ │ │ │ }, │
│ │ │ │ │ │ "StringEquals": { │
│ │ │ │ │ │ "AWS:SourceAccount": "139480602983" │
│ │ │ │ │ │ } │
├───┼───────────────────────────────┼────────┼───────────────────────┼───────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ + │ ${someLambda/ServiceRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
└───┴───────────────────────────────┴────────┴───────────────────────┴───────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────┘
IAM Policy Changes
┌───┬───────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼───────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${someLambda/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴───────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
CdktestStack: deploying... [1/1]
CdktestStack: creating CloudFormation changeset...
✅ CdktestStack
✨ Deployment time: 49.8s
Stack ARN:
arn:aws:cloudformation:us-east-2:<ACCOUNT_ID>:stack/CdktestStack/3beb61e0-8b24-11ef-8f60-025ea362026f
✨ Total time: 54.67s
Could you try using the latest CDK version and see if the error goes away?
Thanks, Ashish
This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.
I think this PR https://github.com/aws/aws-cdk/issues/29515 has already fixed the issue. Please try again with latest version.
This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.