Skip to content

Commit

Permalink
feat(ec2): lookup available AZs for Interface Endpoints
Browse files Browse the repository at this point in the history
This commit adds a new context provider which will make a DescribeVpcEndpointServices call to figure out what AZs are available. This will filter the user-provided `subnets` parameter to only include subnets for the AZs discovered through DescribeVpcEndpointServices.
  • Loading branch information
flemjame-at-amazon authored May 14, 2020
1 parent 0f948a4 commit 9fa3221
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 12 deletions.
21 changes: 17 additions & 4 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,10 +496,8 @@ Endpoints are virtual devices. They are horizontally scaled, redundant, and high

[example of setting up VPC endpoints](test/integ.vpc-endpoint.lit.ts)

Not all VPC endpoint services are available in all availability zones. By default,
CDK will place a VPC endpoint in one subnet per AZ, because CDK doesn't know about
unavailable AZs. You can determine what the available AZs are from the AWS console.
The AZs CDK places the VPC endpoint in can be configured as follows:
By default, CDK will place a VPC endpoint in one subnet per AZ. If you wish to override the AZs CDK places the VPC endpoint in,
use the `subnets` parameter as follows:

```ts
new InterfaceVpcEndpoint(stack, 'VPC Endpoint', {
Expand All @@ -513,6 +511,21 @@ new InterfaceVpcEndpoint(stack, 'VPC Endpoint', {
});
```

Per the [AWS documentation](https://aws.amazon.com/premiumsupport/knowledge-center/interface-endpoint-availability-zone/), not all
VPC endpoint services are available in all AZs. If you specify the parameter `lookupSupportedAzs`, CDK attempts to discover which
AZs an endpoint service is available in, and will ensure the VPC endpoint is not placed in a subnet that doesn't match those AZs.
These AZs will be stored in cdk.context.json.

```ts
new InterfaceVpcEndpoint(stack, 'VPC Endpoint', {
vpc,
service: new InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443),
// Choose which availability zones to place the VPC endpoint in, based on
// available AZs
lookupSupportedAzs: true
});
```

### Security groups for interface VPC endpoints
By default, interface VPC endpoints create a new security group and traffic is **not**
automatically allowed from the VPC CIDR.
Expand Down
48 changes: 45 additions & 3 deletions packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as iam from '@aws-cdk/aws-iam';
import { Aws, Construct, IResource, Lazy, Resource } from '@aws-cdk/core';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { Aws, Construct, ContextProvider, IResource, Lazy, Resource, Token } from '@aws-cdk/core';
import { Connections, IConnectable } from './connections';
import { CfnVPCEndpoint } from './ec2.generated';
import { Peer } from './peer';
Expand Down Expand Up @@ -360,6 +361,16 @@ export interface InterfaceVpcEndpointOptions {
* @default true
*/
readonly open?: boolean;

/**
* Limit to only those availability zones where the endpoint service can be created
*
* Setting this to 'true' requires a lookup to be performed at synthesis time. Account
* and region must be set on the containing stack for this to work.
*
* @default false
*/
readonly lookupSupportedAzs?: boolean;
}

/**
Expand Down Expand Up @@ -459,8 +470,24 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn
this.connections.allowDefaultPortFrom(Peer.ipv4(props.vpc.vpcCidrBlock));
}

const subnets = props.vpc.selectSubnets({ ...props.subnets, onePerAz: true });
const subnetIds = subnets.subnetIds;
const lookupSupportedAzs = props.lookupSupportedAzs ?? false;
const subnetSelection = props.vpc.selectSubnets({ ...props.subnets, onePerAz: true });
let subnets;

// If we don't have an account/region, we will not be able to do filtering on AZs since
// they will be undefined
// Otherwise, we filter by AZ
const agnostic = (Token.isUnresolved(this.stack.account) || Token.isUnresolved(this.stack.region));

if (agnostic && lookupSupportedAzs) {
throw new Error('Cannot look up VPC endpoint availability zones if account/region are not specified');
} else if (!agnostic && lookupSupportedAzs) {
const availableAZs = this.availableAvailabilityZones(props.service.name);
subnets = subnetSelection.subnets.filter(s => availableAZs.includes(s.availabilityZone));
} else {
subnets = subnetSelection.subnets;
}
const subnetIds = subnets.map(s => s.subnetId);

const endpoint = new CfnVPCEndpoint(this, 'Resource', {
privateDnsEnabled: props.privateDnsEnabled ?? props.service.privateDnsDefault ?? true,
Expand All @@ -477,6 +504,21 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn
this.vpcEndpointDnsEntries = endpoint.attrDnsEntries;
this.vpcEndpointNetworkInterfaceIds = endpoint.attrNetworkInterfaceIds;
}

private availableAvailabilityZones(serviceName: string): string[] {
// Here we check what AZs the endpoint service is available in
// If for whatever reason we can't retrieve the AZs, and no context is set,
// we will fall back to all AZs
const availableAZs = ContextProvider.getValue(this, {
provider: cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER,
dummyValue: this.stack.availabilityZones,
props: {serviceName},
}).value;
if (!Array.isArray(availableAZs)) {
throw new Error(`Discovered AZs for endpoint service ${serviceName} must be an array`);
}
return availableAZs;
}
}

/**
Expand Down
87 changes: 86 additions & 1 deletion packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert';
import { AnyPrincipal, PolicyStatement } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { ContextProvider, Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
// tslint:disable-next-line:max-line-length
import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, InterfaceVpcEndpointService, SecurityGroup, SubnetType, Vpc } from '../lib';
Expand Down Expand Up @@ -385,6 +386,90 @@ export = {
PrivateDnsEnabled: true,
}));

test.done();
},
'test endpoint service context azs discovered'(test: Test) {
// GIVEN
const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } });

// Setup context for stack AZs
stack.node.setContext(
ContextProvider.getKey(stack, {
provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER,
}).key,
['us-east-1a', 'us-east-1b', 'us-east-1c']);
// Setup context for endpoint service AZs
stack.node.setContext(
ContextProvider.getKey(stack, {
provider: cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER,
props: {
serviceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc',
},
}).key,
['us-east-1a', 'us-east-1c']);

const vpc = new Vpc(stack, 'VPC');

// WHEN
vpc.addInterfaceEndpoint('YourService', {
service: {
name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc',
port: 443},
lookupSupportedAzs: true,
});

// THEN
expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', {
ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc',
SubnetIds: [
{
Ref: 'VPCPrivateSubnet1Subnet8BCA10E0',
},
{
Ref: 'VPCPrivateSubnet3Subnet3EDCD457',
},
],
}));

test.done();
},
'endpoint service setup with stack AZ context but no endpoint context'(test: Test) {
// GIVEN
const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } });

// Setup context for stack AZs
stack.node.setContext(
ContextProvider.getKey(stack, {
provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER,
}).key,
['us-east-1a', 'us-east-1b', 'us-east-1c']);

const vpc = new Vpc(stack, 'VPC');

// WHEN
vpc.addInterfaceEndpoint('YourService', {
service: {
name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc',
port: 443},
lookupSupportedAzs: true,
});

// THEN
expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', {
ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc',
SubnetIds: [
{
Ref: 'VPCPrivateSubnet1Subnet8BCA10E0',
},
{
Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A',
},
{
Ref: 'VPCPrivateSubnet3Subnet3EDCD457',
},
],
}));

test.done();
},
},
Expand Down
29 changes: 28 additions & 1 deletion packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export enum ContextProvider {
* VPC Provider
*/
VPC_PROVIDER = 'vpc-provider',

/**
* VPC Endpoint Service AZ Provider
*/
ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones',

}

/**
Expand Down Expand Up @@ -170,8 +176,29 @@ export interface VpcContextQuery {
readonly subnetGroupNameTag?: string;
}

/**
* Query to endpoint service context provider
*/
export interface EndpointServiceAvailabilityZonesContextQuery {
/**
* Query account
*/
readonly account: string;

/**
* Query region
*/
readonly region: string;

/**
* Query service name
*/
readonly serviceName: string;
}

export type ContextQueryProperties = AmiContextQuery
| AvailabilityZonesContextQuery
| HostedZoneContextQuery
| SSMParameterContextQuery
| VpcContextQuery;
| VpcContextQuery
| EndpointServiceAvailabilityZonesContextQuery;
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@
},
{
"$ref": "#/definitions/VpcContextQuery"
},
{
"$ref": "#/definitions/EndpointServiceAvailabilityZonesContextQuery"
}
]
}
Expand All @@ -375,6 +378,7 @@
"enum": [
"ami",
"availability-zones",
"endpoint-service-availability-zones",
"hosted-zone",
"ssm",
"vpc-provider"
Expand Down Expand Up @@ -418,7 +422,7 @@
]
},
"AvailabilityZonesContextQuery": {
"description": "Query to hosted zone context provider",
"description": "Query to availability zone context provider",
"type": "object",
"properties": {
"account": {
Expand Down Expand Up @@ -468,7 +472,7 @@
]
},
"SSMParameterContextQuery": {
"description": "Query to hosted zone context provider",
"description": "Query to SSM Parameter Context Provider",
"type": "object",
"properties": {
"account": {
Expand Down Expand Up @@ -525,6 +529,29 @@
"region"
]
},
"EndpointServiceAvailabilityZonesContextQuery": {
"description": "Query to endpoint service context provider",
"type": "object",
"properties": {
"account": {
"description": "Query account",
"type": "string"
},
"region": {
"description": "Query region",
"type": "string"
},
"serviceName": {
"description": "Query service name",
"type": "string"
}
},
"required": [
"account",
"region",
"serviceName"
]
},
"RuntimeInfo": {
"description": "Information about the application's runtime components.",
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"2.0.0"}
{"version":"3.0.0"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones';

/**
* Query to hosted zone context provider
*/
export interface EndpointServiceAvailabilityZonesContextQuery {
/**
* Query account
*/
readonly account?: string;

/**
* Query region
*/
readonly region?: string;

/**
* Query service name
*/
readonly serviceName?: string;
}

/**
* Response of the AZ provider looks like this
*/
export type EndpointServiceAvailabilityZonesContextResponse = string[];
1 change: 1 addition & 0 deletions packages/@aws-cdk/cx-api/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './cxapi';
export * from './context/vpc';
export * from './context/ami';
export * from './context/availability-zones';
export * from './context/endpoint-service-availability-zones';
export * from './cloud-artifact';
export * from './asset-manifest-artifact';
export * from './cloudformation-artifact';
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/cx-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
"props-default-doc:@aws-cdk/cx-api.AssemblyManifest.runtime",
"props-default-doc:@aws-cdk/cx-api.AvailabilityZonesContextQuery.account",
"props-default-doc:@aws-cdk/cx-api.AvailabilityZonesContextQuery.region",
"props-default-doc:@aws-cdk/cx-api.EndpointServiceAvailabilityZonesContextQuery.account",
"props-default-doc:@aws-cdk/cx-api.EndpointServiceAvailabilityZonesContextQuery.region",
"props-default-doc:@aws-cdk/cx-api.EndpointServiceAvailabilityZonesContextQuery.serviceName",
"props-default-doc:@aws-cdk/cx-api.AwsCloudFormationStackProperties.parameters",
"docs-public-apis:@aws-cdk/cx-api.ContainerImageAssetMetadataEntry",
"docs-public-apis:@aws-cdk/cx-api.FileAssetMetadataEntry",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as cxapi from '@aws-cdk/cx-api';
import { Mode, SdkProvider } from '../api';
import { debug } from '../logging';
import { ContextProviderPlugin } from './provider';

/**
* Plugin to retrieve the Availability Zones for an endpoint service
*/
export class EndpointServiceAZContextProviderPlugin implements ContextProviderPlugin {
constructor(private readonly aws: SdkProvider) {
}

public async getValue(args: {[key: string]: any}) {
const region = args.region;
const account = args.account;
const serviceName = args.serviceName;
debug(`Reading AZs for ${account}:${region}:${serviceName}`);
const ec2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ec2();
const response = await ec2.describeVpcEndpointServices({ServiceNames: [serviceName]}).promise();

// expect a service in the response
if (!response.ServiceDetails || response.ServiceDetails.length === 0) {
debug(`Could not retrieve service details for ${account}:${region}:${serviceName}`);
return [];
}
const azs = response.ServiceDetails[0].AvailabilityZones;
debug(`Endpoint service ${account}:${region}:${serviceName} is available in availability zones ${azs}`);
return azs;
}
}
Loading

0 comments on commit 9fa3221

Please sign in to comment.