CDK中AwsCustomResource调用DescribeVpcEndpoints获取网卡ID失败问题
我需要获取VPC端点的私有IP,将其映射到Application Load Balancer的目标组中,于是通过两步实现:
- 创建AwsCustomResource调用
EC2::describeVpcEndpoints获取网络接口ID列表; - 创建第二个CustomResource调用
EC2::describeNetworkInterfaces传入上一步的ID,获取私有IP。
我的CDK代码如下:
// 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)) }
执行cdk deploy时遇到错误:
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
describeVpcEndpoints的实际返回结果:
{ "VpcEndpoints": [ { "VpcEndpointId": "XXX", "VpcEndpointType": "Interface", "VpcId": "XXX", "ServiceName": "com.amazonaws.eu-west-3.execute-api", "State": "available", "PolicyDocument": "{ \"Statement\": [ { \"Action\": \"*\", \"Effect\": \"Allow\", \"Principal\": \"*\", \"Resource\": \"*\" } ] }", "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" } ] }
明明返回结果里存在VpcEndpoints.0.NetworkInterfaceIds,但getResponseField就是取不到,求解决思路。
修正
outputPaths与getResponseField的匹配逻辑
当设置outputPaths: ['VpcEndpoints.0.NetworkInterfaceIds']时,Custom Resource会提取该路径对应的值作为输出属性,属性键为路径的最后一个节点NetworkInterfaceIds,而非完整路径。因此需要修改getResponseField的参数:const networkInterfaceIds = vpces.getResponseField('NetworkInterfaceIds');或者直接移除
outputPaths配置,让Custom Resource保留完整API响应,再通过完整路径获取值:const vpcEndpointsCall: AwsSdkCall = { service: "EC2", action: "describeVpcEndpoints", // 移除outputPaths,保留完整响应 parameters: { VpcEndpointIds: [myVpcEndpointId], }, physicalResourceId: PhysicalResourceId.of('describeVpcEndpoints'), } const networkInterfaceIds = vpces.getResponseField('VpcEndpoints.0.NetworkInterfaceIds');修复Token传递错误
networkInterfaceIds是CDK Token类型,本身代表数组,无需用Token.asString转换,直接传递给NetworkInterfaceIds参数即可:parameters: { NetworkInterfaceIds: networkInterfaceIds, Filters: [ { Name: "interface-type", Values: ["vpc_endpoint"] } ], },保证PhysicalResourceId唯一性
循环中创建的Custom Resource如果使用相同的PhysicalResourceId,会被CloudFormation判定为同一资源引发冲突,需要给每个实例设置唯一ID:physicalResourceId: PhysicalResourceId.of(`describeNetworkInterfaces-${index}`),优化CustomResource创建逻辑
无需循环创建多个Custom Resource,可一次性获取所有ENI的IP,再循环添加到目标组:const sdkCall: AwsSdkCall = { service: "EC2", action: "describeNetworkInterfaces", parameters: { NetworkInterfaceIds: networkInterfaceIds, Filters: [ { Name: "interface-type", Values: ["vpc_endpoint"] } ], }, physicalResourceId: PhysicalResourceId.of('describeNetworkInterfaces-all'), } const enis = new AwsCustomResource( this.stack, `DescribeNetworkInterfaces-${this.stackProps.deploy.environmentName}`, { onCreate: sdkCall, onUpdate: sdkCall, policy: { statements: [ new PolicyStatement({ actions: ["ec2:DescribeNetworkInterfaces"], resources: ["*"], }), ], }, } ); for (let index = 0; index < this.stackProps.core!.vpc.availabilityZones.length; index++) { const privateIp = enis.getResponseField(`NetworkInterfaces.${index}.PrivateIpAddress`); targetGroup.addTarget(new IpTarget(Token.asString(privateIp), targetPort)); }
内容的提问来源于stack exchange,提问作者Romain Spinelli

