diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.vpc-endpoint.lit.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.vpc-endpoint.lit.ts index e10a3203a187f..5915e4802e2a3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.vpc-endpoint.lit.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.vpc-endpoint.lit.ts @@ -48,6 +48,13 @@ class VpcEndpointStack extends cdk.Stack { service: ec2.InterfaceVpcEndpointAwsService.DYNAMODB, privateDnsEnabled: false, }); + + // Add an interface endpoint with ipAddressType and dnsRecordIpType + vpc.addInterfaceEndpoint('S3ServiceEndpoint', { + service: ec2.InterfaceVpcEndpointAwsService.S3, + ipAddressType: ec2.VpcEndpointIpAddressType.DUALSTACK, + dnsRecordIpType: ec2.VpcEndpointDnsRecordIpType.DUALSTACK, + }); } } diff --git a/packages/aws-cdk-lib/aws-ec2/README.md b/packages/aws-cdk-lib/aws-ec2/README.md index 923024a630393..328c924e7e9ad 100644 --- a/packages/aws-cdk-lib/aws-ec2/README.md +++ b/packages/aws-cdk-lib/aws-ec2/README.md @@ -1092,6 +1092,31 @@ myEndpoint.connections.allowDefaultPortFromAnyIpv4(); Alternatively, existing security groups can be used by specifying the `securityGroups` prop. +#### IPv6 and Dualstack support + +As IPv4 addresses are running out, many AWS services are adding support for IPv6 or Dualstack (IPv4 and IPv6 support) for their VPC Endpoints. + +IPv6 and Dualstack address types can be configured by using: + +```ts +vpc.addInterfaceEndpoint('ExampleEndpoint', { + service: InterfaceVpcEndpointAwsService.ExampleEndpoint, + ipAddressType: VpcEndpointIpAddressType.IPV6, + dnsRecordIpType: VpcEndpointDnsRecordIpType.IPV6, +}); +``` +The possible values for `ipAddressType` are: +* `IPV4` This option is supported only if all selected subnets have IPv4 address ranges and the endpoint service accepts IPv4 requests. +* `IPV6` This option is supported only if all selected subnets are IPv6 only subnets and the endpoint service accepts IPv6 requests. +* `DUALSTACK` Assign both IPv4 and IPv6 addresses to the endpoint network interfaces. This option is supported only if all selected subnets have both IPv4 and IPv6 address ranges and the endpoint service accepts both IPv4 and IPv6 requests. + The possible values for `dnsRecordIpType` are: +* `IPV4` Create A records for the private, Regional, and zonal DNS names. `ipAddressType` MUST be `IPV4` or `DUALSTACK` +* `IPV6` Create AAAA records for the private, Regional, and zonal DNS names. `ipAddressType` MUST be `IPV6` or `DUALSTACK` +* `DUALSTACK` Create A and AAAA records for the private, Regional, and zonal DNS names. `ipAddressType` MUST be `DUALSTACK` +* `SERVICE_DEFINED` Create A records for the private, Regional, and zonal DNS names and AAAA records for the Regional and zonal DNS names. `ipAddressType` MUST be `DUALSTACK` + We can only configure dnsRecordIpType when ipAddressType is specified and private DNS must be enabled to use any DNS related features. To avoid complications, it is recommended to always set `privateDnsEnabled` to true (defaults to true) and set the `ipAddressType` and `dnsRecordIpType` explicitly when needing specific IP type behavior. Furthermore, check that the VPC being used supports the IP address type that is being configued. + More documentation on compatibility and specifications can be found [here](https://docs.aws.amazon.com/vpc/latest/privatelink/create-endpoint-service.html#connect-to-endpoint-service) + ### VPC endpoint services A VPC endpoint service enables you to expose a Network Load Balancer(s) as a provider service to consumers, who connect to your service over a VPC endpoint. You can restrict access to your service via allowed principals (anything that extends ArnPrincipal), and require that new connections be manually accepted. You can also enable Contributor Insight rules. diff --git a/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts b/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts index f5b0463020f5a..7491f65d86fe0 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts @@ -96,6 +96,74 @@ export enum VpcEndpointType { RESOURCE = 'Resource', } +/** + * IP address type for the endpoint. + */ +export enum VpcEndpointIpAddressType { + /** + * Assign IPv4 addresses to the endpoint network interfaces. + * This option is supported only if all selected subnets have IPv4 address ranges + * and the endpoint service accepts IPv4 requests. + */ + IPV4 = 'ipv4', + /** + * Assign IPv6 addresses to the endpoint network interfaces. + * This option is supported only if all selected subnets are IPv6 only subnets + * and the endpoint service accepts IPv6 requests. + */ + IPV6 = 'ipv6', + /** + * Assign both IPv4 and IPv6 addresses to the endpoint network interfaces. + * This option is supported only if all selected subnets have both IPv4 and IPv6 + * address ranges and the endpoint service accepts both IPv4 and IPv6 requests. + */ + DUALSTACK = 'dualstack', +} + +/** + * Enums for all Dns Record IP Address types. + */ +export enum VpcEndpointDnsRecordIpType { + /** + * Create A records for the private, Regional, and zonal DNS names. + * The IP address type must be IPv4 or Dualstack. + */ + IPV4 = 'ipv4', + /** + * Create AAAA records for the private, Regional, and zonal DNS names. + * The IP address type must be IPv6 or Dualstack. + */ + IPV6 = 'ipv6', + /** + * Create A and AAAA records for the private, Regional, and zonal DNS names. + * The IP address type must be Dualstack. + */ + DUALSTACK = 'dualstack', + /** + * Create A records for the private, Regional, and zonal DNS names and + * AAAA records for the Regional and zonal DNS names. + * The IP address type must be Dualstack. + */ + SERVICE_DEFINED = 'service-defined', +} + +/** + * Indicates whether to enable private DNS only for inbound endpoints. + * This option is available only for services that support both gateway and interface endpoints. + * It routes traffic that originates from the VPC to the gateway endpoint and traffic that + * originates from on-premises to the interface endpoint. + */ +export enum VpcEndpointPrivateDnsOnlyForInboundResolverEndpoint { + /** + * Enable private DNS for all resolvers. + */ + ALL_RESOLVERS = 'AllResolvers', + /** + * Enable private DNS only for inbound endpoints. + */ + ONLY_INBOUND_RESOLVER = 'OnlyInboundResolver', +} + /** * A service for a gateway VPC endpoint. */ @@ -822,6 +890,27 @@ export interface InterfaceVpcEndpointOptions { * @default false */ readonly lookupSupportedAzs?: boolean; + + /** + * The IP address type for the endpoint. + * + * @default not specified + */ + readonly ipAddressType?: VpcEndpointIpAddressType; + + /** + * Type of DNS records created for the VPC endpoint. + * + * @default not specified + */ + readonly dnsRecordIpType?: VpcEndpointDnsRecordIpType; + + /** + * Whether to enable private DNS only for inbound endpoints. + * + * @default not specified + */ + readonly privateDnsOnlyForInboundResolverEndpoint?: VpcEndpointPrivateDnsOnlyForInboundResolverEndpoint; } /** @@ -942,6 +1031,8 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn vpcEndpointType: VpcEndpointType.INTERFACE, subnetIds, vpcId: props.vpc.vpcId, + ipAddressType: props.ipAddressType, + dnsOptions: this.getDnsOptions(props), }); this.vpcEndpointId = endpoint.ref; @@ -950,6 +1041,48 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn this.vpcEndpointNetworkInterfaceIds = endpoint.attrNetworkInterfaceIds; } + private getDnsOptions(props: InterfaceVpcEndpointProps): CfnVPCEndpoint.DnsOptionsSpecificationProperty | undefined { + if (!props.privateDnsEnabled && props.privateDnsOnlyForInboundResolverEndpoint !== undefined) { + throw new Error('Enable private DNS to set the private DNS only for inbound endpoints'); + } + + if (!props.ipAddressType && props.dnsRecordIpType !== undefined) { + throw new Error('Configure the ipAddressType to use in the VPC endpoint'); + } + + /** + * Checks to see if dnsRecordIpType and ipAddressType are compatible, throw error if not + * @see https://docs.aws.amazon.com/vpc/latest/privatelink/create-endpoint-service.html#connect-to-endpoint-service + */ + switch (props.dnsRecordIpType) { + case VpcEndpointDnsRecordIpType.IPV4: + if (props.ipAddressType === VpcEndpointIpAddressType.IPV6) { + throw new Error('Cannot create a VPC endpoint with ipAddressType of IPv6 with DNS Records for IPv4'); + } + break; + case VpcEndpointDnsRecordIpType.IPV6: + if (props.ipAddressType === VpcEndpointIpAddressType.IPV4) { + throw new Error('Cannot create a VPC endpoint with ipAddressType of IPv4 with DNS Records for IPv6'); + } + break; + case VpcEndpointDnsRecordIpType.DUALSTACK: + if (props.ipAddressType !== VpcEndpointIpAddressType.DUALSTACK) { + throw new Error('VPC endpoints with dualstack ipAddressType should set dnsRecordIpType to dualstack'); + } + break; + case VpcEndpointDnsRecordIpType.SERVICE_DEFINED: + if (props.ipAddressType !== VpcEndpointIpAddressType.DUALSTACK) { + throw new Error('VPC endpoints with service defined configuration should set dnsRecordIpType to dualstack'); + } + break; + } + + return { + privateDnsOnlyForInboundResolverEndpoint: props.privateDnsOnlyForInboundResolverEndpoint, + dnsRecordIpType: props.dnsRecordIpType, + }; + } + /** * Determine which subnets to place the endpoint in. This is in its own function * because there's a lot of code. diff --git a/packages/aws-cdk-lib/aws-ec2/test/vpc-endpoint.test.ts b/packages/aws-cdk-lib/aws-ec2/test/vpc-endpoint.test.ts index 727e7a7b585ff..eb76b1972680c 100644 --- a/packages/aws-cdk-lib/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/aws-cdk-lib/aws-ec2/test/vpc-endpoint.test.ts @@ -3,7 +3,18 @@ import { AnyPrincipal, PolicyStatement } from '../../aws-iam'; import * as cxschema from '../../cloud-assembly-schema'; import { ContextProvider, Fn, Stack } from '../../core'; // eslint-disable-next-line max-len -import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, InterfaceVpcEndpointService, SecurityGroup, SubnetFilter, SubnetType, Vpc } from '../lib'; +import { + GatewayVpcEndpoint, + GatewayVpcEndpointAwsService, + InterfaceVpcEndpoint, + InterfaceVpcEndpointAwsService, + InterfaceVpcEndpointService, + SecurityGroup, + SubnetFilter, + SubnetType, + Vpc, + VpcEndpointDnsRecordIpType, VpcEndpointIpAddressType, +} from '../lib'; describe('vpc endpoint', () => { describe('gateway endpoint', () => { @@ -152,6 +163,108 @@ describe('vpc endpoint', () => { }))).toThrow(/`Principal`/); }); + test('add an endpoint to a vpc and check that by default the properties are absent', () => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VpcNetwork'); + + // WHEN + vpc.addInterfaceEndpoint('EcrDocker', { + service: InterfaceVpcEndpointAwsService.ECR_DOCKER, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { + IpAddressType: Match.absent(), + DnsOptions: { DnsRecordIpType: Match.absent() }, + }); + }); + + test('throws when adding dnsRecordIpType without private dns enabled', () => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VpcNetwork'); + + // WHEN + expect(() => { + vpc.addInterfaceEndpoint('EcrDocker', { + privateDnsEnabled: false, + service: InterfaceVpcEndpointAwsService.ECR_DOCKER, + dnsRecordIpType: VpcEndpointDnsRecordIpType.DUALSTACK, + }); + // THEN + }).toThrow(); + }); + + test('throws when adding dnsRecordIpType without ipAddressType', () => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VpcNetwork'); + + // WHEN + expect(() => { + vpc.addInterfaceEndpoint('EcrDocker', { + service: InterfaceVpcEndpointAwsService.ECR_DOCKER, + dnsRecordIpType: VpcEndpointDnsRecordIpType.DUALSTACK, + }); + // THEN + }).toThrow(); + }); + + test.each([ + [VpcEndpointIpAddressType.IPV4, VpcEndpointDnsRecordIpType.IPV4], + [VpcEndpointIpAddressType.DUALSTACK, VpcEndpointDnsRecordIpType.IPV4], + [VpcEndpointIpAddressType.IPV6, VpcEndpointDnsRecordIpType.IPV6], + [VpcEndpointIpAddressType.DUALSTACK, VpcEndpointDnsRecordIpType.IPV6], + [VpcEndpointIpAddressType.DUALSTACK, VpcEndpointDnsRecordIpType.DUALSTACK], + [VpcEndpointIpAddressType.DUALSTACK, VpcEndpointDnsRecordIpType.SERVICE_DEFINED], + ])('add an endpoint to a vpc with various matching IP address types', ( + ipAddressType: VpcEndpointIpAddressType, + dnsRecordIpType: VpcEndpointDnsRecordIpType) => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VpcNetwork'); + + // WHEN + vpc.addInterfaceEndpoint('EcrDocker', { + service: InterfaceVpcEndpointAwsService.ECR_DOCKER, + ipAddressType: ipAddressType, + dnsRecordIpType: dnsRecordIpType, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { + IpAddressType: ipAddressType, + DnsOptions: { DnsRecordIpType: dnsRecordIpType }, + }); + }); + + test.each([ + [VpcEndpointIpAddressType.IPV6, VpcEndpointDnsRecordIpType.IPV4], + [VpcEndpointIpAddressType.IPV4, VpcEndpointDnsRecordIpType.IPV6], + [VpcEndpointIpAddressType.IPV4, VpcEndpointDnsRecordIpType.DUALSTACK], + [VpcEndpointIpAddressType.IPV6, VpcEndpointDnsRecordIpType.DUALSTACK], + [VpcEndpointIpAddressType.IPV4, VpcEndpointDnsRecordIpType.SERVICE_DEFINED], + [VpcEndpointIpAddressType.IPV6, VpcEndpointDnsRecordIpType.SERVICE_DEFINED], + ])('add an endpoint to a vpc with mismatched ipAddressType and dnsRecordIpType, which throws error', ( + ipAddressType: VpcEndpointIpAddressType, + dnsRecordIpType: VpcEndpointDnsRecordIpType, + ) => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VpcNetwork'); + + // WHEN + expect(() => { + vpc.addInterfaceEndpoint('EcrDocker', { + service: InterfaceVpcEndpointAwsService.ECR_DOCKER, + ipAddressType: ipAddressType, + dnsRecordIpType: dnsRecordIpType, + }); + // THEN + }).toThrow(); + }); + test('import/export', () => { // GIVEN const stack2 = new Stack();