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.5k stars 3.84k forks source link

aws_elasticloadbalancingv2: using from_lookup falsely assumes Dual Stack #30828

Open timothy-cloudopsguy opened 1 month ago

timothy-cloudopsguy commented 1 month ago

Describe the bug

When running this block of code:

Example 1

        nlb = elbv2.NetworkLoadBalancer.from_lookup(
            self,
            id='nlb',
            load_balancer_arn=env_data['nlb_arn']
        )
        print(f"IP_ADDRESS_TYPE: {nlb.ip_address_type}")

        listener = nlb.add_listener(
            id='listener',
            port=nlb_listener_port,
            protocol=elbv2.Protocol.UDP
        )

The print statement shows DUAL_STACK and not IPV4

IP_ADDRESS_TYPE: IpAddressType.DUAL_STACK

When in reality ... This is not a dual stack NLB.

aws elbv2 describe-load-balancers --load-balancer-arns arn:aws:elasticloadbalancing:REGION:ACCOUNT:loadbalancer/net/NLB_NAME  | jq -r .LoadBalancers[0].IpAddressType

ipv4

Expected Behavior

Expected behavior is for the .from_lookup() function to correctly determine the ip_address_type instead of assume DUAL_STACK, and then properly attach the add_listener() as requested without errors.

NOTE: When using .from_network_load_balancer_attributes(), it works fine.

Current Behavior

Because the returned INetworkLoadBalancer does not actually grab the truthiness of ip_address_type, but assumes it to be DUAL_STACK, the CDK stack FAILS to add the listener with this message:

RuntimeError: Error: UDP or TCP_UDP listeners cannot be added to a dualstack network load balancer.

NOTE: When using .from_network_load_balancer_attributes(), it works fine.

Reproduction Steps

Create an NLB in another stack or manually, and save the ARN into an SSM Parameter. Read in the parameter in a CDK stack and use from_lookup() to get an INetworkLoadBalancer object and add_listener() to add a UDP listener.

        nlb_arn_ssm = ssm.StringParameter.from_string_parameter_name(
            self,
            id="nlb-arn-ssm",
            string_parameter_name="test_nlb_arn")

        nlb = elbv2.NetworkLoadBalancer.from_lookup(
            self,
            id='nlb',
            load_balancer_arn=nlb_arn_ssm.string_value
        )

        print(f"IP_ADDRESS_TYPE: {nlb.ip_address_type}")

        nlb_listener_port = 7777

        listener = nlb.add_listener(
            id='listener',
            port=nlb_listener_port,
            protocol=elbv2.Protocol.UDP
        )

Possible Solution

When pulling in information about the NLB using the ARN, pull in the ip_address_type as well using describe-load-balancers ?

Additional Information/Context

No response

CDK CLI Version

2.142.1 (build ed4e152)

Framework Version

python

Node.js Version

v18.19.0

OS

macOs 14.5 (23F79)

Language

Python

Language Version

Python 3.12.4

Other information

jsii.errors.JavaScriptError: 
  @jsii/kernel.RuntimeError: Error: UDP or TCP_UDP listeners cannot be added to a dualstack network load balancer.
      at Kernel._Kernel_ensureSync (/private/var/folders/qz/dgx8s7357r9bfpywd3q336d80000gn/T/tmp_515if_3/lib/program.js:10502:23)
      at Kernel.invoke (/private/var/folders/qz/dgx8s7357r9bfpywd3q336d80000gn/T/tmp_515if_3/lib/program.js:9866:102)
      at KernelHost.processRequest (/private/var/folders/qz/dgx8s7357r9bfpywd3q336d80000gn/T/tmp_515if_3/lib/program.js:11707:36)
      at KernelHost.run (/private/var/folders/qz/dgx8s7357r9bfpywd3q336d80000gn/T/tmp_515if_3/lib/program.js:11667:22)
      at Immediate._onImmediate (/private/var/folders/qz/dgx8s7357r9bfpywd3q336d80000gn/T/tmp_515if_3/lib/program.js:11668:46)
      at process.processImmediate (node:internal/timers:476:21)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/app.py", line 99, in <module>
    application_stack = ApplicationStack(
                        ^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/jsii/_runtime.py", line 118, in __call__
    inst = super(JSIIMeta, cast(JSIIMeta, cls)).__call__(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/lib/application.py", line 136, in __init__
    self.ecs = _ecs(
               ^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/jsii/_runtime.py", line 118, in __call__
    inst = super(JSIIMeta, cast(JSIIMeta, cls)).__call__(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/lib/ecs.py", line 964, in __init__
    listener = nlb.add_listener(
               ^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/aws_cdk/aws_elasticloadbalancingv2/__init__.py", line 13826, in add_listener
    return typing.cast("NetworkListener", jsii.invoke(self, "addListener", [id, props]))
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/jsii/_kernel/__init__.py", line 149, in wrapped
    return _recursize_dereference(kernel, fn(kernel, *args, **kwargs))
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/jsii/_kernel/__init__.py", line 399, in invoke
    response = self.provider.invoke(
               ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/jsii/_kernel/providers/process.py", line 380, in invoke
    return self._process.send(request, InvokeResponse)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timothywelch/sharpen/src-openvxi/cdk/venv/lib/python3.12/site-packages/jsii/_kernel/providers/process.py", line 342, in send
    raise RuntimeError(resp.error) from JavaScriptError(resp.stack)
RuntimeError: Error: UDP or TCP_UDP listeners cannot be added to a dualstack network load balancer.

Subprocess exited with error 1
timothy-cloudopsguy commented 1 month ago

NOTE: I just tried using .from_network_load_balancer_attributes() and it synthesized just fine. I'll update this if it deploys successfully.

ashishdhingra commented 1 month ago

@timothy-cloudopsguy Good morning. Thanks for reporting the issue. I was able to reproduce the issue when executing cdk synth for the first time using below TypeScript CDK code:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { debug } from 'console';

export class Issue30806Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Manually created IPV4 NLB in AWS console.
    const nlbArn = 'arn:aws:elasticloadbalancing:us-east-2:REDACTED:loadbalancer/net/testnlb/REDACTED';
    const nlb = elbv2.NetworkLoadBalancer.fromLookup(this, 'testnlb', { loadBalancerArn: nlbArn});
    debug(`IP_ADDRESS_TYPE 1: ${nlb.ipAddressType}`);
  }
}

Running cdk synth for the first time produced below output:

IP_ADDRESS_TYPE 1: dualstack
IP_ADDRESS_TYPE 1: ipv4
IP_ADDRESS_TYPE 1: ipv4
Resources:
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/zPSMzSx0DNQTCwv1k1OydbNyUzSqw4uSUzO1glKLc4vLUpO1XFOy4Oxa3Xy8lNS9bKK9cuMDPQMDfUMFbOKMzN1i0rzSjJzU/WCIDQA2v7akFYAAAA=
    Metadata:
      aws:cdk:path: Issue30806Stack/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]
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.

Then running cdk context produced below output (REDACTED):

Context found in cdk.json:

┌────┬───────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────┐
│ #  │ Key                                                                       │ Value                                                                     │
├────┼───────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ 1  │ @aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver         │ true                                                                      │
├────┼───────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ 50 │ load-balancer:account=REDACTED:loadBalancerArn=arn$:aws$:elasticloadb     │ { "loadBalancerArn": "arn:aws:elasticloadbalancing:us-east-2:REDACTED     │
│    │ alancing$:us-east-2$:REDACTED$:loadbalancer/net/testnlb/REDACTED          │ :loadbalancer/net/testnlb/REDACTED", "loadBalancerCanonicalHosted         │
│    │ 79b:loadBalancerType=network:region=us-east-2                             │ ZoneId": "REDACTED", "loadBalancerDnsName": "testnlb-REDACTED             │
│    │                                                                           │ b.elb.us-east-2.amazonaws.com", "vpcId": "vpc-REDACTED", "securi          │
│    │                                                                           │ tyGroupIds": [ "sg-REDACTED" ], "ipAddressType": "ipv4" }                 │
└────┴───────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────┘
Run cdk context --reset KEY_OR_NUMBER to remove a context key. It will be refreshed on the next CDK synthesis run.

Running cdk synth again produces below output:

IP_ADDRESS_TYPE 1: ipv4
Resources:
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/zPSMzSx0DNQTCwv1k1OydbNyUzSqw4uSUzO1glKLc4vLUpO1XFOy4Oxa3Xy8lNS9bKK9cuMDPQMDfUMFbOKMzN1i0rzSjJzU/WCIDQA2v7akFYAAAA=
    Metadata:
      aws:cdk:path: Issue30806Stack/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]
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.

So cdk synth would cache CF resources during synthesis time, refer below links for more details:

So if after running cdk synth for the first time, if I add the below code and run cdk synth again, it synthesizes properly.

const targetGroupArn = 'arn:aws:elasticloadbalancing:us-east-2:REDACTED:targetgroup/testtargetgroupfornlb/REDACTED';
const listener = nlb.addListener('listener', {
      port: nlbListernerPort, 
      protocol: elbv2.Protocol.UDP,
      defaultTargetGroups: [elbv2.NetworkTargetGroup.fromTargetGroupAttributes(this, 'tg', {targetGroupArn: targetGroupArn})]
    });

The reason why NetworkLoadBalancer.fromNetworkLoadBalancerAttributes() could be working is that it defaults to IPV4 here (this could be incorrect if using DUALSTACK balancer). The context value for load balancer is somehow not populated when using NetworkLoadBalancer.fromNetworkLoadBalancerAttributes().

pahud commented 1 month ago

In CDK, all L2 constructs have the fromXxxx() methods and they could be implemented in two major patterns:

  1. Just returns the interface of the resource with all required attributes we provide in the options. Such as arn or other attributes. CDK would NOT call AWS SDKs to retrieve additional information. All information has to be provided by user.

  2. Query additional attributes via context provider, such as Vpc.fromLookup(), and cache additional information in the context variables i.e. cdk.context.json.

elbv2.NetworkLoadBalancer.from_lookup() falls into the 2nd category that returns a new LookedUpNetworkLoadBalancer for you, which is literally a INetworkLoadBalancer. If you look at its implementation, it determines the IpAddressType from here:

https://github.com/aws/aws-cdk/blob/8d55d864183803e2e6cfb3991edced7496eaadeb/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts#L548-L552

And additional props are determined here:

https://github.com/aws/aws-cdk/blob/8d55d864183803e2e6cfb3991edced7496eaadeb/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts#L187-L190

The query logic is defined here:

https://github.com/aws/aws-cdk/blob/8d55d864183803e2e6cfb3991edced7496eaadeb/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts#L130-L157

OK. When dealing with context provider, if that value does not exist in local cdk.context.json, CDK would initially insert a placeholder dummy value and replace it after it stores the real value into the cdk.context.json. This is a little bit tricky though. Unfortunately, the dummy value of ipAddressType is currently defined as

https://github.com/aws/aws-cdk/blob/8d55d864183803e2e6cfb3991edced7496eaadeb/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts#L148-L149

This would be confusing because user code would not be able to initially determine if the value is a dummy value or real value. But CDK would eventually replace that value with correct value. This explains why we see this from the sample @ashishdhingra provided above.

IP_ADDRESS_TYPE 1: dualstack
IP_ADDRESS_TYPE 1: ipv4
IP_ADDRESS_TYPE 1: ipv4

Now my question is - given the value would be eventually replaced with correct value. Would this still be an issue for you? Off the top of my head, the only problem is that if you have a check on the type in your CDK code and execute different logic accordingly, that might be an issue but if you simply CfnOutput that, it should not be an issue.

Also, this only happens when your cdk.context.json does not have that cache. If you execute the 2nd time, it would simply return the cached value from cdk.context.json

I am afraid the only way to work it around is:

if (isHavingDummyValue()) {
  // we got dummy values, skip everything for now.
} else {
  listener = nlb.addListener(…)
}

Full sample in TypeScript

export class DummyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const nlbArn = 'arn:aws:elasticloadbalancing:us-east-1:AWS_ACCOUNT_ID:loadbalancer/net/testnlb/xxxxxxxxxx';
    const nlb = elbv2.NetworkLoadBalancer.fromLookup(this, 'testnlb', { loadBalancerArn: nlbArn});
    const vpc = ec2.Vpc.fromLookup(this, 'vpc', { isDefault: true });

    if (nlb.securityGroups && nlb.securityGroups[0] === 'sg-1234') {
      // https://github.com/aws/aws-cdk/blob/8d55d864183803e2e6cfb3991edced7496eaadeb/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts#L154C28-L154C37
      debug('got dummy value, skip adding listener');
    } else {
      nlb.addListener('mylistener', {
        port: 80,
        defaultTargetGroups: [
          new elbv2.NetworkTargetGroup(this, 'TG', { port: 80, vpc }),
        ]
      });
    }
}