aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.61k stars 3.9k forks source link

lambda: associating multiple LambdaAction instances with a single Lambda function and different CloudWatch alarms. #31754

Open terryobot opened 6 days ago

terryobot commented 6 days ago

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

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

ashishdhingra commented 5 days ago

@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

github-actions[bot] commented 3 days ago

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.

GavinZZ commented 2 days ago

I think this PR https://github.com/aws/aws-cdk/issues/29515 has already fixed the issue. Please try again with latest version.

github-actions[bot] commented 10 hours ago

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.