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

aws_elasticloadbalancingv2: using from_lookup falsely assumes Dual Stack #30828

Open
timothy-cloudopsguy opened this issue Jul 11, 2024 · 3 comments
Labels
@aws-cdk/aws-elasticloadbalancingv2 Related to Amazon Elastic Load Balancing V2 bug This issue is a bug. effort/medium Medium work item – several days of effort p2

Comments

@timothy-cloudopsguy
Copy link

timothy-cloudopsguy commented Jul 11, 2024

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 timothy-cloudopsguy added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Jul 11, 2024
@github-actions github-actions bot added the @aws-cdk/aws-elasticloadbalancingv2 Related to Amazon Elastic Load Balancing V2 label Jul 11, 2024
@timothy-cloudopsguy
Copy link
Author

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

@ashishdhingra ashishdhingra self-assigned this Jul 11, 2024
@ashishdhingra ashishdhingra added needs-reproduction This issue needs reproduction. and removed needs-triage This issue or PR still needs to be triaged. labels Jul 11, 2024
@ashishdhingra
Copy link
Contributor

ashishdhingra commented Jul 11, 2024

@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().

@ashishdhingra ashishdhingra added p2 effort/medium Medium work item – several days of effort and removed needs-reproduction This issue needs reproduction. labels Jul 11, 2024
@pahud
Copy link
Contributor

pahud commented Jul 11, 2024

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:

if (props.ipAddressType === cxapi.LoadBalancerIpAddressType.IPV4) {
this.ipAddressType = IpAddressType.IPV4;
} else if (props.ipAddressType === cxapi.LoadBalancerIpAddressType.DUAL_STACK) {
this.ipAddressType = IpAddressType.DUAL_STACK;
}

And additional props are determined here:

const props = BaseLoadBalancer._queryContextProvider(scope, {
userOptions: options,
loadBalancerType: cxschema.LoadBalancerType.NETWORK,
});

The query logic is defined here:

protected static _queryContextProvider(scope: Construct, options: LoadBalancerQueryContextProviderOptions) {
if (Token.isUnresolved(options.userOptions.loadBalancerArn)
|| Object.values(options.userOptions.loadBalancerTags ?? {}).some(Token.isUnresolved)) {
throw new Error('All arguments to look up a load balancer must be concrete (no Tokens)');
}
let cxschemaTags: cxschema.Tag[] | undefined;
if (options.userOptions.loadBalancerTags) {
cxschemaTags = mapTagMapToCxschema(options.userOptions.loadBalancerTags);
}
const props: cxapi.LoadBalancerContextResponse = ContextProvider.getValue(scope, {
provider: cxschema.ContextProvider.LOAD_BALANCER_PROVIDER,
props: {
loadBalancerArn: options.userOptions.loadBalancerArn,
loadBalancerTags: cxschemaTags,
loadBalancerType: options.loadBalancerType,
} as cxschema.LoadBalancerContextQuery,
dummyValue: {
ipAddressType: cxapi.LoadBalancerIpAddressType.DUAL_STACK,
// eslint-disable-next-line @aws-cdk/no-literal-partition
loadBalancerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188`,
loadBalancerCanonicalHostedZoneId: 'Z3DZXE0EXAMPLE',
loadBalancerDnsName: 'my-load-balancer-1234567890.us-west-2.elb.amazonaws.com',
securityGroupIds: ['sg-1234'],
vpcId: 'vpc-12345',
} as cxapi.LoadBalancerContextResponse,
}).value;

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

dummyValue: {
ipAddressType: cxapi.LoadBalancerIpAddressType.DUAL_STACK,

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 }),
        ]
      });
    }
} 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-elasticloadbalancingv2 Related to Amazon Elastic Load Balancing V2 bug This issue is a bug. effort/medium Medium work item – several days of effort p2
Projects
None yet
Development

No branches or pull requests

3 participants