1

I have a VPC endpoint, and I want to retrieve its private IP addresses (to map to an Application Load Balancer through a Target Group).

I do that in two steps:

  1. First I create an AwsCustomResource to retrieve its Network Interface IDs, using EC2::describeVpcEndpoints
  2. Then I create a second CustomResource to retrieve the private IP addresses, using EC2::describeNetworkInterfaces (with the previous Network Interface IDs passed in parameters)

Here my CDK code:

// 1st get the VPC endpoint network interface IDs
const vpcEndpointsCall: AwsSdkCall = {
    service: "EC2",
    action: "describeVpcEndpoints",
    outputPaths: [`VpcEndpoints.0.NetworkInterfaceIds`],
    parameters: {
        VpcEndpointIds: [myVpcEndpointId],
    },
    physicalResourceId: PhysicalResourceId.of('describeVpcEndpoints'),
}

const vpces = new AwsCustomResource(
    this.stack,
    `DescribeVpcEndpoints-${this.stackProps.deploy.environmentName}`,
    {
        onCreate: vpcEndpointsCall,
        onUpdate: vpcEndpointsCall,
        policy: {
            statements: [
                new PolicyStatement({
                    actions: ["ec2:DescribeVpcEndpoints"],
                    resources: ["*"],
                }),
            ],
        },
    }
);
        
const networkInterfaceIds = vpces.getResponseField(`VpcEndpoints.0.NetworkInterfaceIds`)

for (let index = 0; index < this.stackProps.core!.vpc.availabilityZones.length; index++) {
    
    // 2nd get the private IP addresses of the VPC endpoint network interface IDs
    const sdkCall: AwsSdkCall = {
        service: "EC2",
        action: "describeNetworkInterfaces",
        outputPaths: [`NetworkInterfaces.${index}.PrivateIpAddress`],
        parameters: {
            NetworkInterfaceIds: Token.asString(networkInterfaceIds),
            Filters: [
                { Name: "interface-type", Values: ["vpc_endpoint"] }
            ],
        },
        physicalResourceId: PhysicalResourceId.of('describeNetworkInterfaces'),
    }

    const eni = new AwsCustomResource(
        this.stack,
        `DescribeNetworkInterfaces-${this.stackProps.deploy.environmentName}${index}`,
        {
            onCreate: sdkCall,
            onUpdate: sdkCall,
            policy: {
                statements: [
                    new PolicyStatement({
                        actions: ["ec2:DescribeNetworkInterfaces"],
                        resources: ["*"],
                    }),
                ],
            },
        }
    );

    targetGroup.addTarget(new IpTarget(Token.asString(eni.getResponseField(`NetworkInterfaces.${index}.PrivateIpAddress`)), targetPort))
}

But during the cdk deploy, the following error is raised:

CustomResource attribute error: Vendor response doesn't contain VpcEndpoints.0.NetworkInterfaceIds key in object arn:aws:cloudformation:eu-west-3:XXXXXXXXXX:stack/CDK-Core-EC2-XXXXXXXXXX/XXXXXXXXXX|DescribeVpcEndpointsXXXXXXXXXX|XXXXXXXXXX in S3 bucket cloudformation-custom-resource-storage-euwest3

Here is the result of the DescribeVpcEndpoints call:

{
    "VpcEndpoints": [
        {
            "VpcEndpointId": "XXX",
            "VpcEndpointType": "Interface",
            "VpcId": "XXX",
            "ServiceName": "com.amazonaws.eu-west-3.execute-api",
            "State": "available",
            "PolicyDocument": "{\n  \"Statement\": [\n    {\n      \"Action\": \"*\", \n      \"Effect\": \"Allow\", \n      \"Principal\": \"*\", \n      \"Resource\": \"*\"\n    }\n  ]\n}",       
            "RouteTableIds": [],
            "SubnetIds": [
                "subnet-XXX",
                "subnet-XXX",
                "subnet-XXX"
            ],
            "Groups": [
                {
                    "GroupId": "XXX",
                    "GroupName": "XXX"
                },
                {
                    "GroupId": "XXX",
                    "GroupName": "XXX"
                }
            ],
            "IpAddressType": "ipv4",
            "DnsOptions": {
                "DnsRecordIpType": "ipv4"
            },
            "PrivateDnsEnabled": true,
            "RequesterManaged": false,
            "NetworkInterfaceIds": [
                "eni-XXX",
                "eni-XXX",
                "eni-XXX"
            ],
            "DnsEntries": [
                {
                    "DnsName": "vpce-XXX.execute-api.eu-west-3.vpce.amazonaws.com",
                    "HostedZoneId": "XXX"
                },
                {
                    "DnsName": "vpce-XXX-eu-west-3c.execute-api.eu-west-3.vpce.amazonaws.com",
                    "HostedZoneId": "XXX"
                },
                {
                    "DnsName": "vpce-XXX-eu-west-3b.execute-api.eu-west-3.vpce.amazonaws.com",
                    "HostedZoneId": "XXX"
                },
                {
                    "DnsName": "vpce-XXX-eu-west-3a.execute-api.eu-west-3.vpce.amazonaws.com",
                    "HostedZoneId": "XXX"
                },
                {
                    "DnsName": "execute-api.eu-west-3.amazonaws.com",
                    "HostedZoneId": "XXX"
                },
                {
                    "DnsName": "*.execute-api.eu-west-3.amazonaws.com",
                    "HostedZoneId": "XXX"
                }
            ],
            "CreationTimestamp": "XXX",
            "Tags": [],
            "OwnerId": "XXX"
        }
}

So I don't understand why the getResponseField('VpcEndpoints.0.NetworkInterfaceIds') is not working.

I expect to have the list of the Network Interface IDs in the field VpcEndpoints.0.NetworkInterfaceIds.

Any idea?

gshpychka
  • 8,523
  • 1
  • 11
  • 31
  • How are you getting that response? My guess is the call doesn't return any endpoints with that ID. And this will not work - you cannot pass a token that represents a list as a string. You'd have to select each ID as a separate token – gshpychka Aug 23 '23 at 14:44
  • Consider simplifying your setup with a single Lambda-backed [CustomResource](https://stackoverflow.com/questions/76855476/how-to-reference-json-values-from-a-parameter-store-using-a-awscustomresource-in/76884135#76884135) (you write the handler code) construct instead of a chain of `AwsCustomResource` (CDK deploys a Lambda for you) constructs. Your CR Lambda handler would make all the SDK calls in one go and output whatever end result your app needs. – fedonev Aug 24 '23 at 11:30

2 Answers2

1

Just fixed the same issue: your problem is Token.asString(networkInterfaceIds), Token.asString cannot handle Arrays.

You have to access each array entry itself by index:

Token.asString(networkInterfaceIds)[0]

Token.asString(networkInterfaceIds)[1]

...

Also you should use Token.asString everywhere, so you have to restructure your code a little bit.

droebi
  • 872
  • 1
  • 14
  • 27
0

I answer myself because I found something interesting in the Custom Resource (Lambda) logs:

{
    "Status": "SUCCESS",
    "Reason": "OK",
    "PhysicalResourceId": "1692795531828",
    "StackId": "arn:aws:cloudformation:us-east-1:...:stack/XXX/XXX",
    "RequestId": "XXX",
    "LogicalResourceId": "DescribeVpcEndpoints16CD45EC",
    "NoEcho": false,
    "Data": {. <---- Values to be referenced by “vpces.getResponseField()” method
        "VpcEndpoints.0.NetworkInterfaceIds.0": "eni-...",
        "VpcEndpoints.0.NetworkInterfaceIds.1": "eni-…",
        "VpcEndpoints.0.NetworkInterfaceIds.2": "eni-…",
        "VpcEndpoints.0.NetworkInterfaceIds.3": "eni-…",
        "VpcEndpoints.0.NetworkInterfaceIds.4": "eni-…"
    }
}

So I need to manage each Network Interface individually.

The fixed code is now:

// 1st get the VPC endpoint network interface IDs
const vpcEndpointsCall: AwsSdkCall = {
    service: "EC2",
    action: "describeVpcEndpoints",
    outputPaths: [`VpcEndpoints.0.NetworkInterfaceIds`],
    parameters: {
        VpcEndpointIds: [vpceId],
    },
    physicalResourceId: PhysicalResourceId.of('describeVpcEndpoints'),
}

const vpces = new AwsCustomResource(
    this.stack,
    `DescribeVpcEndpoints-vpceDns${vpcePrivateDnsStatus}-${this.stackProps.deploy.environmentName}`,
    {
        onCreate: vpcEndpointsCall,
        onUpdate: vpcEndpointsCall,
        policy: {
            statements: [
                new PolicyStatement({
                    actions: ["ec2:DescribeVpcEndpoints"],
                    resources: ["*"],
                }),
            ],
        },
    }
);

// const networkInterfaceIds = vpces.getResponseField(`VpcEndpoints.0.NetworkInterfaceIds`)
// console.log("NetworkInterfaceIds: " + Token.asString(networkInterfaceIds))

for (let index = 0; index < this.stackProps.core!.vpc.availabilityZones.length; index++) {
    
    // 2nd get the private IP addresses of the VPC endpoint network interface IDs
    const sdkCall: AwsSdkCall = {
        service: "EC2",
        action: "describeNetworkInterfaces",
        outputPaths: [`NetworkInterfaces.0.PrivateIpAddress`],
        parameters: {
            NetworkInterfaceIds: [vpces.getResponseField(`VpcEndpoints.0.NetworkInterfaceIds.${index}`)],
            Filters: [
                { Name: "interface-type", Values: ["vpc_endpoint"] }
            ],
        },
        physicalResourceId: PhysicalResourceId.of('describeNetworkInterfaces'),
    }

    const eni = new AwsCustomResource(
        this.stack,
        `DescribeNetworkInterfaces-vpceDns${vpcePrivateDnsStatus}-${this.stackProps.deploy.environmentName}${index}`,
        {
            onCreate: sdkCall,
            onUpdate: sdkCall,
            policy: {
                statements: [
                    new PolicyStatement({
                        actions: ["ec2:DescribeNetworkInterfaces"],
                        resources: ["*"],
                    }),
                ],
            },
        }
    );

    targetGroup.addTarget(new IpTarget(Token.asString(eni.getResponseField(`NetworkInterfaces.0.PrivateIpAddress`)), targetPort))
}

It works perfectly now.