Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}

Expand Down
25 changes: 25 additions & 0 deletions packages/aws-cdk-lib/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
133 changes: 133 additions & 0 deletions packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down
115 changes: 114 additions & 1 deletion packages/aws-cdk-lib/aws-ec2/test/vpc-endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
Expand Down
Loading