From bbb3f34976ecb3cf44f7d4729d40f8ef24bf6a1e Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 9 Apr 2019 09:57:28 +0200 Subject: [PATCH] feat(ec2): add support for vpc endpoints (#2104) Add support for both gateway and interface VPC endpoints. Static members are exposed for all AWS service endpoints. As gateway endpoints reference route tables, they currently cannot be added to imported VPC networks. BREAKING CHANGE: * `vpc.selectSubnetIds(...)` has been replaced with `vpc.selectSubnets(...).subnetIds`. --- .../aws-autoscaling/lib/auto-scaling-group.ts | 2 +- .../@aws-cdk/aws-codebuild/lib/project.ts | 5 +- packages/@aws-cdk/aws-ec2/README.md | 7 + packages/@aws-cdk/aws-ec2/lib/index.ts | 1 + packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts | 483 +++++++++++++ packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 164 +++-- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 74 +- packages/@aws-cdk/aws-ec2/package.json | 3 +- .../test/integ.vpc-endpoint.lit.expected.json | 650 ++++++++++++++++++ .../aws-ec2/test/integ.vpc-endpoint.lit.ts | 47 ++ .../aws-ec2/test/test.vpc-endpoint.ts | 338 +++++++++ packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 64 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- packages/@aws-cdk/aws-eks/lib/cluster.ts | 10 +- .../lib/shared/base-load-balancer.ts | 6 +- packages/@aws-cdk/aws-lambda/lib/function.ts | 2 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 4 +- .../aws-rds/lib/rotation-single-user.ts | 4 +- 18 files changed, 1756 insertions(+), 110 deletions(-) create mode 100644 packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index c8429013018a7..efc1209770f8b 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -268,7 +268,7 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup throw new Error(`Should have minCapacity (${minCapacity}) <= desiredCapacity (${desiredCapacity}) <= maxCapacity (${maxCapacity})`); } - const subnetIds = props.vpc.subnetIds(props.vpcSubnets); + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets); const asgProps: CfnAutoScalingGroupProps = { cooldown: props.cooldownSeconds !== undefined ? `${props.cooldownSeconds}` : undefined, minSize: minCapacity.toString(), diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index e6bc958412eb9..e178ffd3287c9 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -879,9 +879,6 @@ export class Project extends ProjectBase { }); this._securityGroups = [securityGroup]; } - const subnetSelection: ec2.SubnetSelection = props.subnetSelection ? props.subnetSelection : { - subnetType: ec2.SubnetType.Private - }; this.addToRoleInlinePolicy(new iam.PolicyStatement() .addAllResources() .addActions( @@ -904,7 +901,7 @@ export class Project extends ProjectBase { .addAction('ec2:CreateNetworkInterfacePermission')); return { vpcId: props.vpc.vpcId, - subnets: props.vpc.subnetIds(subnetSelection).map(s => s), + subnets: props.vpc.selectSubnets(props.subnetSelection).subnetIds, securityGroupIds: this._securityGroups.map(s => s.securityGroupId) }; } diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 0912890ad284b..bea0e304a8917 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -399,3 +399,10 @@ const vpnConnection = vpc.addVpnConnection('Dynamic', { }); const state = vpnConnection.metricTunnelState(); ``` + +### VPC endpoints +A VPC endpoint enables you to privately connect your VPC to supported AWS services and VPC endpoint services powered by PrivateLink without requiring an internet gateway, NAT device, VPN connection, or AWS Direct Connect connection. Instances in your VPC do not require public IP addresses to communicate with resources in the service. Traffic between your VPC and the other service does not leave the Amazon network. + +Endpoints are virtual devices. They are horizontally scaled, redundant, and highly available VPC components that allow communication between instances in your VPC and services without imposing availability risks or bandwidth constraints on your network traffic. + +[example of setting up VPC endpoints](test/integ.vpc-endpoint.lit.ts) diff --git a/packages/@aws-cdk/aws-ec2/lib/index.ts b/packages/@aws-cdk/aws-ec2/lib/index.ts index fdfe81aa9b97a..32946b75d470c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/index.ts +++ b/packages/@aws-cdk/aws-ec2/lib/index.ts @@ -7,6 +7,7 @@ export * from './vpc'; export * from './vpc-ref'; export * from './vpc-network-provider'; export * from './vpn'; +export * from './vpc-endpoint'; // AWS::EC2 CloudFormation Resources: export * from './ec2.generated'; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts new file mode 100644 index 0000000000000..416339ee1a4b9 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -0,0 +1,483 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { Connections, IConnectable } from './connections'; +import { CfnVPCEndpoint } from './ec2.generated'; +import { SecurityGroup } from './security-group'; +import { TcpPort, TcpPortFromAttribute } from './security-group-rule'; +import { IVpcNetwork, SubnetSelection, SubnetType } from './vpc-ref'; + +/** + * A VPC endpoint. + */ +export interface IVpcEndpoint extends cdk.IConstruct { + /** + * The VPC endpoint identifier. + */ + readonly vpcEndpointId: string; +} + +export abstract class VpcEndpoint extends cdk.Construct implements IVpcEndpoint { + public abstract readonly vpcEndpointId: string; + + protected policyDocument?: iam.PolicyDocument; + + /** + * Adds a statement to the policy document of the VPC endpoint. The statement + * must have a Principal. + * + * Not all interface VPC endpoints support policy. For more information + * see https://docs.aws.amazon.com/vpc/latest/userguide/vpce-interface.html + * + * @param statement the IAM statement to add + */ + public addToPolicy(statement: iam.PolicyStatement) { + if (!statement.hasPrincipal) { + throw new Error('Statement must have a `Principal`.'); + } + + if (!this.policyDocument) { + this.policyDocument = new iam.PolicyDocument(); + } + + this.policyDocument.addStatement(statement); + } +} + +/** + * A gateway VPC endpoint. + */ +export interface IGatewayVpcEndpoint extends IVpcEndpoint { + /** + * Exports this VPC endpoint from the stack. + */ + export(): GatewayVpcEndpointImportProps; +} + +/** + * The type of VPC endpoint. + */ +export enum VpcEndpointType { + /** + * Interface + * + * An interface endpoint is an elastic network interface with a private IP + * address that serves as an entry point for traffic destined to a supported + * service. + */ + Interface = 'Interface', + + /** + * Gateway + * + * A gateway endpoint is a gateway that is a target for a specified route in + * your route table, used for traffic destined to a supported AWS service. + */ + Gateway = 'Gateway' +} + +/** + * A service for a gateway VPC endpoint. + */ +export interface IGatewayVpcEndpointService { + /** + * The name of the service. + */ + readonly name: string; +} + +/** + * An AWS service for a gateway VPC endpoint. + */ +export class GatewayVpcEndpointAwsService implements IGatewayVpcEndpointService { + public static readonly DynamoDb = new GatewayVpcEndpointAwsService('dynamodb'); + public static readonly S3 = new GatewayVpcEndpointAwsService('s3'); + + /** + * The name of the service. + */ + public readonly name: string; + + constructor(name: string, prefix?: string) { + this.name = `${prefix || 'com.amazonaws'}.${cdk.Aws.region}.${name}`; + } +} + +/** + * Options to add a gateway endpoint to a VPC. + */ +export interface GatewayVpcEndpointOptions { + /** + * The service to use for this gateway VPC endpoint. + */ + readonly service: IGatewayVpcEndpointService; + + /** + * Where to add endpoint routing. + * + * @default private subnets + */ + readonly subnets?: SubnetSelection[] +} + +/** + * Construction properties for a GatewayVpcEndpoint. + */ +export interface GatewayVpcEndpointProps extends GatewayVpcEndpointOptions { + /** + * The VPC network in which the gateway endpoint will be used. + */ + readonly vpc: IVpcNetwork +} + +/** + * A gateway VPC endpoint. + */ +export class GatewayVpcEndpoint extends VpcEndpoint implements IGatewayVpcEndpoint { + /** + * Imports an existing gateway VPC endpoint. + */ + public static import(scope: cdk.Construct, id: string, props: GatewayVpcEndpointImportProps): IGatewayVpcEndpoint { + return new ImportedGatewayVpcEndpoint(scope, id, props); + } + + /** + * The gateway VPC endpoint identifier. + */ + public readonly vpcEndpointId: string; + + /** + * The date and time the gateway VPC endpoint was created. + */ + public readonly vpcEndpointCreationTimestamp: string; + + constructor(scope: cdk.Construct, id: string, props: GatewayVpcEndpointProps) { + super(scope, id); + + const subnets = props.subnets || [{ subnetType: SubnetType.Private }]; + const routeTableIds = [...new Set(Array().concat(...subnets.map(s => props.vpc.selectSubnets(s).routeTableIds)))]; + + if (routeTableIds.length === 0) { + throw new Error(`Can't add a gateway endpoint to VPC; route table IDs are not available`); + } + + const endpoint = new CfnVPCEndpoint(this, 'Resource', { + policyDocument: new cdk.Token(() => this.policyDocument), + routeTableIds, + serviceName: props.service.name, + vpcEndpointType: VpcEndpointType.Gateway, + vpcId: props.vpc.vpcId + }); + + this.vpcEndpointId = endpoint.vpcEndpointId; + this.vpcEndpointCreationTimestamp = endpoint.vpcEndpointCreationTimestamp; + } + + /** + * Exports this gateway VPC endpoint from the stack. + */ + public export(): GatewayVpcEndpointImportProps { + return { + vpcEndpointId: new cdk.CfnOutput(this, 'VpcEndpointId', { value: this.vpcEndpointId }).makeImportValue().toString() + }; + } +} + +/** + * Construction properties for an ImportedGatewayVpcEndpoint. + */ +export interface GatewayVpcEndpointImportProps { + /** + * The gateway VPC endpoint identifier. + */ + readonly vpcEndpointId: string; +} + +/** + * An imported gateway VPC endpoint. + */ +class ImportedGatewayVpcEndpoint extends cdk.Construct implements IGatewayVpcEndpoint { + /** + * The gateway VPC endpoint identifier. + */ + public readonly vpcEndpointId: string; + + constructor(scope: cdk.Construct, id: string, private readonly props: GatewayVpcEndpointImportProps) { + super(scope, id); + + this.vpcEndpointId = props.vpcEndpointId; + } + + /** + * Exports this gateway VPC endpoint from the stack. + */ + public export(): GatewayVpcEndpointImportProps { + return this.props; + } +} + +/** + * A service for an interface VPC endpoint. + */ +export interface IInterfaceVpcEndpointService { + /** + * The name of the service. + */ + readonly name: string; + + /** + * The port of the service. + */ + readonly port: number; +} + +/** + * An AWS service for an interface VPC endpoint. + */ +export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointService { + public static readonly SageMakerNotebook = new InterfaceVpcEndpointAwsService('sagemaker', 'aws.sagemaker'); + public static readonly CloudFormation = new InterfaceVpcEndpointAwsService('cloudformation'); + public static readonly CloudTrail = new InterfaceVpcEndpointAwsService('cloudtrail'); + public static readonly CodeBuild = new InterfaceVpcEndpointAwsService('codebuild'); + public static readonly CodeBuildFips = new InterfaceVpcEndpointAwsService('codebuil-fips'); + public static readonly CodeCommit = new InterfaceVpcEndpointAwsService('codecommit'); + public static readonly CodeCommitFips = new InterfaceVpcEndpointAwsService('codecommit-fips'); + public static readonly CodePipeline = new InterfaceVpcEndpointAwsService('codepipeline'); + public static readonly Config = new InterfaceVpcEndpointAwsService('config'); + public static readonly Ec2 = new InterfaceVpcEndpointAwsService('ec2'); + public static readonly Ec2Messages = new InterfaceVpcEndpointAwsService('ec2messages'); + public static readonly Ecr = new InterfaceVpcEndpointAwsService('ecr.api'); + public static readonly EcrDocker = new InterfaceVpcEndpointAwsService('ecr.dkr'); + public static readonly Ecs = new InterfaceVpcEndpointAwsService('ecs'); + public static readonly EcsAgent = new InterfaceVpcEndpointAwsService('ecs-agent'); + public static readonly EcsTelemetry = new InterfaceVpcEndpointAwsService('ecs-telemetry'); + public static readonly ElasticInferenceRuntime = new InterfaceVpcEndpointAwsService('elastic-inference.runtime'); + public static readonly ElasticLoadBalancing = new InterfaceVpcEndpointAwsService('elasticloadbalancing'); + public static readonly CloudWatchEvents = new InterfaceVpcEndpointAwsService('events'); + public static readonly ApiGateway = new InterfaceVpcEndpointAwsService('execute-api'); + public static readonly CodeCommitGit = new InterfaceVpcEndpointAwsService('git-codecommit'); + public static readonly CodeCommitGitFips = new InterfaceVpcEndpointAwsService('git-codecommit-fips'); + public static readonly KinesisStreams = new InterfaceVpcEndpointAwsService('kinesis-streams'); + public static readonly Kms = new InterfaceVpcEndpointAwsService('kms'); + public static readonly CloudWatchLogs = new InterfaceVpcEndpointAwsService('logs'); + public static readonly CloudWatch = new InterfaceVpcEndpointAwsService('monitoring'); + public static readonly SageMakerApi = new InterfaceVpcEndpointAwsService('sagemaker.api'); + public static readonly SageMakerRuntime = new InterfaceVpcEndpointAwsService('sagemaker.runtime'); + public static readonly SageMakerRuntimeFips = new InterfaceVpcEndpointAwsService('sagemaker.runtime-fips'); + public static readonly SecretsManager = new InterfaceVpcEndpointAwsService('secretsmanager'); + public static readonly ServiceCatalog = new InterfaceVpcEndpointAwsService('servicecatalog'); + public static readonly Sns = new InterfaceVpcEndpointAwsService('sns'); + public static readonly Sqs = new InterfaceVpcEndpointAwsService('sqs'); + public static readonly Ssm = new InterfaceVpcEndpointAwsService('ssm'); + public static readonly SsmMessages = new InterfaceVpcEndpointAwsService('ssmmessages'); + public static readonly Sts = new InterfaceVpcEndpointAwsService('sts'); + public static readonly Transfer = new InterfaceVpcEndpointAwsService('transfer.server'); + + /** + * The name of the service. + */ + public readonly name: string; + + /** + * The port of the service. + */ + public readonly port: number; + + constructor(name: string, prefix?: string, port?: number) { + this.name = `${prefix || 'com.amazonaws'}.${cdk.Aws.region}.${name}`; + this.port = port || 443; + } +} + +/** + * Options to add an interface endpoint to a VPC. + */ +export interface InterfaceVpcEndpointOptions { + /** + * The service to use for this interface VPC endpoint. + */ + readonly service: IInterfaceVpcEndpointService; + + /** + * Whether to associate a private hosted zone with the specified VPC. This + * allows you to make requests to the service using its default DNS hostname. + * + * @default true + */ + readonly privateDnsEnabled?: boolean; + + /** + * The subnets in which to create an endpoint network interface. At most one + * per availability zone. + * + * @default private subnets + */ + readonly subnets?: SubnetSelection; +} + +/** + * Construction properties for an InterfaceVpcEndpoint. + */ +export interface InterfaceVpcEndpointProps extends InterfaceVpcEndpointOptions { + /** + * The VPC network in which the interface endpoint will be used. + */ + readonly vpc: IVpcNetwork +} + +/** + * An interface VPC endpoint. + */ +export interface IInterfaceVpcEndpoint extends IVpcEndpoint, IConnectable { + /** + * Exports this interface VPC endpoint from the stack. + */ + export(): InterfaceVpcEndpointImportProps; +} + +/** + * A interface VPC endpoint. + */ +export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEndpoint { + /** + * Imports an existing interface VPC endpoint. + */ + public static import(scope: cdk.Construct, id: string, props: InterfaceVpcEndpointImportProps): IInterfaceVpcEndpoint { + return new ImportedInterfaceVpcEndpoint(scope, id, props); + } + + /** + * The interface VPC endpoint identifier. + */ + public readonly vpcEndpointId: string; + + /** + * The date and time the interface VPC endpoint was created. + */ + public readonly vpcEndpointCreationTimestamp: string; + + /** + * The DNS entries for the interface VPC endpoint. + */ + public readonly dnsEntries: string[]; + + /** + * One or more network interfaces for the interface VPC endpoint. + */ + public readonly networkInterfaceIds: string[]; + + /** + * The identifier of the security group associated with this interface VPC + * endpoint. + */ + public readonly securityGroupId: string; + + /** + * Access to network connections. + */ + public readonly connections: Connections; + + private readonly port: number; + + constructor(scope: cdk.Construct, id: string, props: InterfaceVpcEndpointProps) { + super(scope, id); + + this.port = props.service.port; + const securityGroup = new SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc + }); + this.securityGroupId = securityGroup.securityGroupId; + this.connections = new Connections({ + defaultPortRange: new TcpPort(props.service.port), + securityGroups: [securityGroup] + }); + + const subnets = props.vpc.selectSubnets({ ...props.subnets, onePerAz: true }); + const subnetIds = subnets.subnetIds; + + const endpoint = new CfnVPCEndpoint(this, 'Resource', { + privateDnsEnabled: props.privateDnsEnabled || true, + policyDocument: new cdk.Token(() => this.policyDocument), + securityGroupIds: [this.securityGroupId], + serviceName: props.service.name, + vpcEndpointType: VpcEndpointType.Interface, + subnetIds, + vpcId: props.vpc.vpcId + }); + + this.vpcEndpointId = endpoint.vpcEndpointId; + this.vpcEndpointCreationTimestamp = endpoint.vpcEndpointCreationTimestamp; + this.dnsEntries = endpoint.vpcEndpointDnsEntries; + this.networkInterfaceIds = endpoint.vpcEndpointNetworkInterfaceIds; + } + + /** + * Exports this interface VPC endpoint from the stack. + */ + public export(): InterfaceVpcEndpointImportProps { + return { + vpcEndpointId: new cdk.CfnOutput(this, 'VpcEndpointId', { value: this.vpcEndpointId }).makeImportValue().toString(), + securityGroupId: new cdk.CfnOutput(this, 'SecurityGroupId', { value: this.securityGroupId }).makeImportValue().toString(), + port: new cdk.CfnOutput(this, 'port', { value: this.port }).makeImportValue().toString() + }; + } +} + +/** + * Construction properties for an ImportedInterfaceVpcEndpoint. + */ +export interface InterfaceVpcEndpointImportProps { + /** + * The interface VPC endpoint identifier. + */ + readonly vpcEndpointId: string; + + /** + * The identifier of the security group associated with the interface VPC endpoint. + */ + readonly securityGroupId: string; + + /** + * The port of the service of the interface VPC endpoint. + */ + readonly port: string; +} + +/** + * An imported VPC interface endpoint. + */ +class ImportedInterfaceVpcEndpoint extends cdk.Construct implements IInterfaceVpcEndpoint { + /** + * The interface VPC endpoint identifier. + */ + public readonly vpcEndpointId: string; + + /** + * The identifier of the security group associated with the interface VPC endpoint. + */ + public readonly securityGroupId: string; + + /** + * Access to network connections. + */ + public readonly connections: Connections; + + constructor(scope: cdk.Construct, id: string, private readonly props: InterfaceVpcEndpointImportProps) { + super(scope, id); + + this.vpcEndpointId = props.vpcEndpointId; + + this.securityGroupId = props.securityGroupId; + + this.connections = new Connections({ + defaultPortRange: new TcpPortFromAttribute(props.port), + securityGroups: [SecurityGroup.import(this, 'SecurityGroup', props)], + }); + } + + /** + * Exports this interface VPC endpoint from the stack. + */ + public export(): InterfaceVpcEndpointImportProps { + return this.props; + } +} diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index cd8c7e6dc515f..87dadb2ee7870 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,5 +1,6 @@ -import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk"; +import { Construct, IConstruct, IDependable } from '@aws-cdk/cdk'; import { DEFAULT_SUBNET_NAME, subnetName } from './util'; +import { InterfaceVpcEndpoint, InterfaceVpcEndpointOptions } from './vpc-endpoint'; import { VpnConnection, VpnConnectionOptions } from './vpn'; export interface IVpcSubnet extends IConstruct { @@ -18,6 +19,11 @@ export interface IVpcSubnet extends IConstruct { */ readonly internetConnectivityEstablished: IDependable; + /** + * Route table ID + */ + readonly routeTableId?: string; + /** * Exports this subnet to another stack. */ @@ -63,18 +69,20 @@ export interface IVpcNetwork extends IConstruct { /** * Return IDs of the subnets appropriate for the given selection strategy * - * Requires that at least once subnet is matched, throws a descriptive + * Requires that at least one subnet is matched, throws a descriptive * error message otherwise. * - * Prefer to use this method over {@link subnets} if you need to pass subnet - * IDs to a CloudFormation Resource. + * @deprecated Use selectSubnets() instead. */ - subnetIds(selection?: SubnetSelection): string[]; + selectSubnetIds(selection?: SubnetSelection): string[]; /** - * Return a dependable object representing internet connectivity for the given subnets + * Return information on the subnets appropriate for the given selection strategy + * + * Requires that at least one subnet is matched, throws a descriptive + * error message otherwise. */ - subnetInternetDependencies(selection?: SubnetSelection): IDependable; + selectSubnets(selection?: SubnetSelection): SelectedSubnets; /** * Return whether all of the given subnets are from the VPC's public subnets. @@ -86,6 +94,11 @@ export interface IVpcNetwork extends IConstruct { */ addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection; + /** + * Adds a new interface endpoint to this VPC + */ + addInterfaceEndpoint(id: string, options: InterfaceVpcEndpointOptions): InterfaceVpcEndpoint + /** * Exports this VPC so it can be consumed by another stack. */ @@ -159,6 +172,38 @@ export interface SubnetSelection { * @default name */ readonly subnetName?: string; + + /** + * If true, return at most one subnet per AZ + * + * @defautl false + */ + readonly onePerAz?: boolean; +} + +/** + * Result of selecting a subset of subnets from a VPC + */ +export interface SelectedSubnets { + /** + * The subnet IDs + */ + readonly subnetIds: string[]; + + /** + * The respective AZs of each subnet + */ + readonly availabilityZones: string[]; + + /** + * Route table IDs of each respective subnet + */ + readonly routeTableIds: string[]; + + /** + * Dependency representing internet connectivity for these subnets + */ + readonly internetConnectedDependency: IDependable; } /** @@ -206,31 +251,22 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { */ public readonly natDependencies = new Array(); - /** - * Returns IDs of selected subnets - */ - public subnetIds(selection: SubnetSelection = {}): string[] { - selection = reifySelectionDefaults(selection); - - const nets = this.subnets(selection); - if (nets.length === 0) { - throw new Error(`There are no ${describeSelection(selection)} in this VPC. Use a different VPC subnet selection.`); - } - - return nets.map(n => n.subnetId); + public selectSubnetIds(selection?: SubnetSelection): string[] { + return this.selectSubnets(selection).subnetIds; } /** - * Return a dependable object representing internet connectivity for the given subnets + * Returns IDs of selected subnets */ - public subnetInternetDependencies(selection: SubnetSelection = {}): IDependable { - selection = reifySelectionDefaults(selection); + public selectSubnets(selection: SubnetSelection = {}): SelectedSubnets { + const subnets = this.selectSubnetObjects(selection); - const ret = new CompositeDependable(); - for (const subnet of this.subnets(selection)) { - ret.add(subnet.internetConnectivityEstablished); - } - return ret; + return { + subnetIds: subnets.map(s => s.subnetId), + availabilityZones: subnets.map(s => s.availabilityZone), + routeTableIds: subnets.map(s => s.routeTableId).filter(notUndefined), // Possibly don't have this information + internetConnectedDependency: tap(new CompositeDependable(), d => subnets.forEach(s => d.add(s.internetConnectivityEstablished))), + }; } /** @@ -243,6 +279,16 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { }); } + /** + * Adds a new interface endpoint to this VPC + */ + public addInterfaceEndpoint(id: string, options: InterfaceVpcEndpointOptions): InterfaceVpcEndpoint { + return new InterfaceVpcEndpoint(this, id, { + vpc: this, + ...options + }); + } + /** * Export this VPC from the stack */ @@ -266,27 +312,31 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { /** * Return the subnets appropriate for the placement strategy */ - protected subnets(selection: SubnetSelection = {}): IVpcSubnet[] { + protected selectSubnetObjects(selection: SubnetSelection = {}): IVpcSubnet[] { selection = reifySelectionDefaults(selection); - - // Select by name - if (selection.subnetName !== undefined) { - const allSubnets = this.privateSubnets.concat(this.publicSubnets).concat(this.isolatedSubnets); - const selectedSubnets = allSubnets.filter(s => subnetName(s) === selection.subnetName); - if (selectedSubnets.length === 0) { - throw new Error(`No subnets with name: ${selection.subnetName}`); + let subnets: IVpcSubnet[] = []; + + if (selection.subnetName !== undefined) { // Select by name + const allSubnets = [...this.publicSubnets, ...this.privateSubnets, ...this.isolatedSubnets]; + subnets = allSubnets.filter(s => subnetName(s) === selection.subnetName); + } else { // Select by type + subnets = { + [SubnetType.Isolated]: this.isolatedSubnets, + [SubnetType.Private]: this.privateSubnets, + [SubnetType.Public]: this.publicSubnets, + }[selection.subnetType || SubnetType.Private]; + + if (selection.onePerAz && subnets.length > 0) { + // Restrict to at most one subnet group + subnets = subnets.filter(s => subnetName(s) === subnetName(subnets[0])); } - return selectedSubnets; } - // Select by type - if (selection.subnetType === undefined) { return this.privateSubnets; } + if (subnets.length === 0) { + throw new Error(`There are no ${describeSelection(selection)} in this VPC. Use a different VPC subnet selection.`); + } - return { - [SubnetType.Isolated]: this.isolatedSubnets, - [SubnetType.Private]: this.privateSubnets, - [SubnetType.Public]: this.publicSubnets, - }[selection.subnetType]; + return subnets; } } @@ -371,15 +421,15 @@ export interface VpcSubnetImportProps { * Returns "private subnets" by default. */ function reifySelectionDefaults(placement: SubnetSelection): SubnetSelection { - if (placement.subnetType !== undefined && placement.subnetName !== undefined) { - throw new Error('Only one of subnetType and subnetName can be supplied'); - } + if (placement.subnetType !== undefined && placement.subnetName !== undefined) { + throw new Error('Only one of subnetType and subnetName can be supplied'); + } - if (placement.subnetType === undefined && placement.subnetName === undefined) { - return { subnetType: SubnetType.Private }; - } + if (placement.subnetType === undefined && placement.subnetName === undefined) { + return { subnetType: SubnetType.Private, onePerAz: placement.onePerAz }; + } - return placement; + return placement; } /** @@ -415,4 +465,16 @@ class CompositeDependable implements IDependable { } return ret; } -} \ No newline at end of file +} + +/** + * Invoke a function on a value (for its side effect) and return the value + */ +function tap(x: T, fn: (x: T) => void): T { + fn(x); + return x; +} + +function notUndefined(x: T | undefined): x is T { + return x !== undefined; +} diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 19de99d8317dc..9ce4f38062d1d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -4,6 +4,7 @@ import { CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnVPNGateway, Cfn import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment } from './ec2.generated'; import { NetworkBuilder } from './network-util'; import { DEFAULT_SUBNET_NAME, ExportSubnetGroup, ImportSubnetGroup, subnetId } from './util'; +import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, GatewayVpcEndpointOptions } from './vpc-endpoint'; import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider'; import { IVpcNetwork, IVpcSubnet, SubnetSelection, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcSubnetImportProps } from './vpc-ref'; import { VpnConnectionOptions, VpnConnectionType } from './vpn'; @@ -143,7 +144,12 @@ export interface VpcNetworkProps { * * @default on the route tables associated with private subnets */ - readonly vpnRoutePropagation?: SubnetType[] + readonly vpnRoutePropagation?: SubnetSelection[] + + /** + * Gateway endpoints to add to this VPC. + */ + readonly gatewayEndpoints?: { [id: string]: GatewayVpcEndpointOptions } } /** @@ -408,19 +414,10 @@ export class VpcNetwork extends VpcNetworkBase { this.vpnGatewayId = vpnGateway.vpnGatewayName; // Propagate routes on route tables associated with the right subnets - const vpnRoutePropagation = props.vpnRoutePropagation || [SubnetType.Private]; - let subnets: IVpcSubnet[] = []; - if (vpnRoutePropagation.includes(SubnetType.Public)) { - subnets = [...subnets, ...this.publicSubnets]; - } - if (vpnRoutePropagation.includes(SubnetType.Private)) { - subnets = [...subnets, ...this.privateSubnets]; - } - if (vpnRoutePropagation.includes(SubnetType.Isolated)) { - subnets = [...subnets, ...this.isolatedSubnets]; - } + const vpnRoutePropagation = props.vpnRoutePropagation || [{ subnetType: SubnetType.Private }]; + const routeTableIds = [...new Set(Array().concat(...vpnRoutePropagation.map(s => this.selectSubnets(s).routeTableIds)))]; const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', { - routeTableIds: (subnets as VpcSubnet[]).map(subnet => subnet.routeTableId), + routeTableIds, vpnGatewayId: this.vpnGatewayId }); @@ -434,6 +431,48 @@ export class VpcNetwork extends VpcNetworkBase { this.addVpnConnection(connectionId, connection); } } + + // Allow creation of gateway endpoints on VPC instantiation as those can be + // immediately functional without further configuration. This is not the case + // for interface endpoints where the security group must be configured. + if (props.gatewayEndpoints) { + const gatewayEndpoints = props.gatewayEndpoints || {}; + for (const [endpointId, endpoint] of Object.entries(gatewayEndpoints)) { + this.addGatewayEndpoint(endpointId, endpoint); + } + } + } + + /** + * Adds a new gateway endpoint to this VPC + */ + public addGatewayEndpoint(id: string, options: GatewayVpcEndpointOptions): GatewayVpcEndpoint { + return new GatewayVpcEndpoint(this, id, { + vpc: this, + ...options + }); + } + + /** + * Adds a new S3 gateway endpoint to this VPC + */ + public addS3Endpoint(id: string, subnets?: SubnetSelection[]): GatewayVpcEndpoint { + return new GatewayVpcEndpoint(this, id, { + service: GatewayVpcEndpointAwsService.S3, + vpc: this, + subnets + }); + } + + /** + * Adds a new DynamoDB gateway endpoint to this VPC + */ + public addDynamoDbEndpoint(id: string, subnets?: SubnetSelection[]): GatewayVpcEndpoint { + return new GatewayVpcEndpoint(this, id, { + service: GatewayVpcEndpointAwsService.DynamoDb, + vpc: this, + subnets + }); } /** @@ -466,7 +505,7 @@ export class VpcNetwork extends VpcNetworkBase { let natSubnets: VpcPublicSubnet[]; if (placement) { - const subnets = this.subnets(placement); + const subnets = this.selectSubnetObjects(placement); for (const sub of subnets) { if (this.publicSubnets.indexOf(sub) === -1) { throw new Error(`natGatewayPlacement ${placement} contains non public subnet ${sub}`); @@ -621,7 +660,7 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet { /** * The routeTableId attached to this subnet. */ - public readonly routeTableId: string; + public readonly routeTableId?: string; private readonly internetDependencies = new ConcreteDependable(); @@ -662,7 +701,7 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet { protected addDefaultRouteToNAT(natGatewayId: string) { const route = new CfnRoute(this, `DefaultRoute`, { - routeTableId: this.routeTableId, + routeTableId: this.routeTableId!, destinationCidrBlock: '0.0.0.0/0', natGatewayId }); @@ -677,7 +716,7 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet { gateway: CfnInternetGateway, gatewayAttachment: CfnVPCGatewayAttachment) { const route = new CfnRoute(this, `DefaultRoute`, { - routeTableId: this.routeTableId, + routeTableId: this.routeTableId!, destinationCidrBlock: '0.0.0.0/0', gatewayId: gateway.ref }); @@ -780,6 +819,7 @@ class ImportedVpcSubnet extends cdk.Construct implements IVpcSubnet { public readonly internetConnectivityEstablished: cdk.IDependable = new cdk.ConcreteDependable(); public readonly availabilityZone: string; public readonly subnetId: string; + public readonly routeTableId?: string = undefined; constructor(scope: cdk.Construct, id: string, private readonly props: VpcSubnetImportProps) { super(scope, id); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 6aba5373e0bcb..f2bdffb2bcfea 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -73,6 +73,7 @@ "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-cloudwatch": "^0.28.0", + "@aws-cdk/aws-iam": "^0.28.0", "@aws-cdk/cdk": "^0.28.0", "@aws-cdk/cx-api": "^0.28.0" }, @@ -84,4 +85,4 @@ "resource-attribute:@aws-cdk/aws-ec2.ISecurityGroup.securityGroupVpcId" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json new file mode 100644 index 0000000000000..e0a19805633f5 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json @@ -0,0 +1,650 @@ +{ + "Resources": { + "MyVpcF9F0CA6F": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc" + } + ] + } + }, + "MyVpcPublicSubnet1SubnetF6608456": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVpcPublicSubnet1RouteTableC46AB2F4": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet1" + } + ] + } + }, + "MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet1RouteTableC46AB2F4" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + } + } + }, + "MyVpcPublicSubnet1DefaultRoute95FDF9EB": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet1RouteTableC46AB2F4" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + }, + "DependsOn": [ + "MyVpcVPCGW488ACE0D" + ] + }, + "MyVpcPublicSubnet1EIP096967CB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet1NATGatewayAD3400C1": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet1EIP096967CB", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet1" + } + ] + } + }, + "MyVpcPublicSubnet2Subnet492B6BFB": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVpcPublicSubnet2RouteTable1DF17386": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet2" + } + ] + } + }, + "MyVpcPublicSubnet2RouteTableAssociation227DE78D": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet2RouteTable1DF17386" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + } + } + }, + "MyVpcPublicSubnet2DefaultRoute052936F6": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet2RouteTable1DF17386" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + }, + "DependsOn": [ + "MyVpcVPCGW488ACE0D" + ] + }, + "MyVpcPublicSubnet2EIP8CCBA239": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet2NATGateway91BFBEC9": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet2EIP8CCBA239", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet2" + } + ] + } + }, + "MyVpcPublicSubnet3Subnet57EEE236": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVpcPublicSubnet3RouteTable15028F08": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet3" + } + ] + } + }, + "MyVpcPublicSubnet3RouteTableAssociation5C27DDA4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet3RouteTable15028F08" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + } + } + }, + "MyVpcPublicSubnet3DefaultRoute3A83AB36": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet3RouteTable15028F08" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + }, + "DependsOn": [ + "MyVpcVPCGW488ACE0D" + ] + }, + "MyVpcPublicSubnet3EIPC5ACADAB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet3NATGatewayD4B50EBE": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet3EIPC5ACADAB", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet3" + } + ] + } + }, + "MyVpcPrivateSubnet1Subnet5057CF7E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVpcPrivateSubnet1RouteTable8819E6E2": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet1" + } + ] + } + }, + "MyVpcPrivateSubnet1RouteTableAssociation56D38C7E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet1Subnet5057CF7E" + } + } + }, + "MyVpcPrivateSubnet1DefaultRouteA8CDE2FA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet1NATGatewayAD3400C1" + } + } + }, + "MyVpcPrivateSubnet2Subnet0040C983": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVpcPrivateSubnet2RouteTableCEDCEECE": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet2" + } + ] + } + }, + "MyVpcPrivateSubnet2RouteTableAssociation86A610DA": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet2Subnet0040C983" + } + } + }, + "MyVpcPrivateSubnet2DefaultRoute9CE96294": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet2NATGateway91BFBEC9" + } + } + }, + "MyVpcPrivateSubnet3Subnet772D6AD7": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVpcPrivateSubnet3RouteTableB790927C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet3" + } + ] + } + }, + "MyVpcPrivateSubnet3RouteTableAssociationD951741C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet3Subnet772D6AD7" + } + } + }, + "MyVpcPrivateSubnet3DefaultRouteEC11C0C5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet3NATGatewayD4B50EBE" + } + } + }, + "MyVpcIGW5C4A4F63": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc" + } + ] + } + }, + "MyVpcVPCGW488ACE0D": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "InternetGatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + } + }, + "MyVpcS3FADC1889": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "RouteTableIds": [ + { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "MyVpcDynamoDbEndpointE6A39B0D": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:DescribeTable", + "dynamodb:ListTables" + ], + "Effect": "Allow", + "Principal": "*", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "RouteTableIds": [ + { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "MyVpcEcrDockerEndpointSecurityGroup47BB9CC1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-ec2-vpc-endpoint/MyVpc/EcrDockerEndpoint/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:443", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc" + } + ], + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcEcrDockerEndpoint0385050C": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".ecr.dkr" + ] + ] + }, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "MyVpcEcrDockerEndpointSecurityGroup47BB9CC1", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + "Ref": "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + "Ref": "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ], + "VpcEndpointType": "Interface" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.ts new file mode 100644 index 0000000000000..4e9dca508c896 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.ts @@ -0,0 +1,47 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import ec2 = require('../lib'); + +const app = new cdk.App(); + +class VpcEndpointStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /// !show + // Add gateway endpoints when creating the VPC + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + gatewayEndpoints: { + S3: { + service: ec2.GatewayVpcEndpointAwsService.S3 + } + } + }); + + // Alternatively gateway endpoints can be added on the VPC + const dynamoDbEndpoint = vpc.addGatewayEndpoint('DynamoDbEndpoint', { + service: ec2.GatewayVpcEndpointAwsService.DynamoDb + }); + + // This allows to customize the endpoint policy + dynamoDbEndpoint.addToPolicy( + new iam.PolicyStatement() // Restrict to listing and describing tables + .addAnyPrincipal() + .addActions('dynamodb:DescribeTable', 'dynamodb:ListTables') + .addAllResources() + ); + + // Add an interface endpoint + const ecrDockerEndpoint = vpc.addInterfaceEndpoint('EcrDockerEndpoint', { + service: ec2.InterfaceVpcEndpointAwsService.EcrDocker + }); + + // When working with an interface endpoint, use the connections object to + // allow traffic to flow to the endpoint. + ecrDockerEndpoint.connections.allowDefaultPortFromAnyIpv4(); + /// !hide + } +} + +new VpcEndpointStack(app, 'aws-cdk-ec2-vpc-endpoint'); +app.run(); diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts new file mode 100644 index 0000000000000..6de8f2f83f38f --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts @@ -0,0 +1,338 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +// tslint:disable-next-line:max-line-length +import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, SubnetType, VpcNetwork } from '../lib'; + +export = { + 'gateway endpoint': { + 'add an endpoint to a vpc'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new VpcNetwork(stack, 'VpcNetwork', { + gatewayEndpoints: { + S3: { + service: GatewayVpcEndpointAwsService.S3 + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region' + }, + '.s3' + ] + ] + }, + VpcId: { + Ref: 'VpcNetworkB258E83A' + }, + RouteTableIds: [ + { + Ref: 'VpcNetworkPrivateSubnet1RouteTableCD085FF1' + }, + { + Ref: 'VpcNetworkPrivateSubnet2RouteTableE97B328B' + }, + { + Ref: 'VpcNetworkPrivateSubnet3RouteTableE0C661A2' + } + ], + VpcEndpointType: 'Gateway' + })); + + test.done(); + }, + + 'routing on private and public subnets'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new VpcNetwork(stack, 'VpcNetwork', { + gatewayEndpoints: { + S3: { + service: GatewayVpcEndpointAwsService.S3, + subnets: [ + { + subnetType: SubnetType.Public + }, + { + subnetType: SubnetType.Private + } + ] + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region' + }, + '.s3' + ] + ] + }, + VpcId: { + Ref: 'VpcNetworkB258E83A' + }, + RouteTableIds: [ + { + Ref: 'VpcNetworkPublicSubnet1RouteTable25CCC53F' + }, + { + Ref: 'VpcNetworkPublicSubnet2RouteTableE5F348DF' + }, + { + Ref: 'VpcNetworkPublicSubnet3RouteTable36E30B07' + }, + { + Ref: 'VpcNetworkPrivateSubnet1RouteTableCD085FF1' + }, + { + Ref: 'VpcNetworkPrivateSubnet2RouteTableE97B328B' + }, + { + Ref: 'VpcNetworkPrivateSubnet3RouteTableE0C661A2' + } + ], + VpcEndpointType: 'Gateway' + })); + + test.done(); + }, + + 'add statements to policy'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VpcNetwork'); + const endpoint = vpc.addGatewayEndpoint('S3', { + service: GatewayVpcEndpointAwsService.S3 + }); + + // WHEN + endpoint.addToPolicy( + new PolicyStatement() + .addAnyPrincipal() + .addActions('s3:GetObject', 's3:ListBucket') + .addAllResources() + ); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject', + 's3:ListBucket' + ], + Effect: 'Allow', + Principal: '*', + Resource: '*' + } + ], + Version: '2012-10-17' + } + })); + + test.done(); + }, + + 'throws when adding a statement without a principal'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VpcNetwork'); + const endpoint = vpc.addGatewayEndpoint('S3', { + service: GatewayVpcEndpointAwsService.S3 + }); + + // THEN + test.throws(() => endpoint.addToPolicy( + new PolicyStatement() + .addActions('s3:GetObject', 's3:ListBucket') + .addAllResources() + ), /`Principal`/); + + test.done(); + }, + + 'import/export'(test: Test) { + // GIVEN + const stack1 = new Stack(); + const stack2 = new Stack(); + const vpc = new VpcNetwork(stack1, 'Vpc1'); + const endpoint = vpc.addGatewayEndpoint('DynamoDB', { + service: GatewayVpcEndpointAwsService.DynamoDb + }); + + // WHEN + GatewayVpcEndpoint.import(stack2, 'ImportedEndpoint', endpoint.export()); + + // THEN: No error + test.done(); + }, + + 'conveniance methods for S3 and DynamoDB'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VpcNetwork'); + + // WHEN + vpc.addS3Endpoint('S3'); + vpc.addDynamoDbEndpoint('DynamoDb'); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region' + }, + '.s3' + ] + ] + }, + })); + + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region' + }, + '.dynamodb' + ] + ] + }, + })); + + test.done(); + }, + + 'throws with an imported vpc'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = VpcNetwork.import(stack, 'VPC', { + vpcId: 'id', + privateSubnetIds: ['1', '2', '3'], + availabilityZones: ['a', 'b', 'c'] + }); + + // THEN + test.throws(() => new GatewayVpcEndpoint(stack, 'Gateway', { + service: GatewayVpcEndpointAwsService.S3, + vpc + }), /route table/); + + test.done(); + } + }, + + 'interface endpoint': { + 'add an endpoint to a vpc'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VpcNetwork'); + + // WHEN + vpc.addInterfaceEndpoint('EcrDocker', { + service: InterfaceVpcEndpointAwsService.EcrDocker + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region' + }, + '.ecr.dkr' + ] + ] + }, + VpcId: { + Ref: 'VpcNetworkB258E83A' + }, + PrivateDnsEnabled: true, + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'VpcNetworkEcrDockerSecurityGroup7C91D347', + 'GroupId' + ] + } + ], + SubnetIds: [ + { + Ref: 'VpcNetworkPrivateSubnet1Subnet07BA143B' + }, + { + Ref: 'VpcNetworkPrivateSubnet2Subnet5E4189D6' + }, + { + Ref: 'VpcNetworkPrivateSubnet3Subnet5D16E0FB' + } + ], + VpcEndpointType: 'Interface' + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'VpcNetwork/EcrDocker/SecurityGroup', + VpcId: { + Ref: 'VpcNetworkB258E83A' + } + })); + + test.done(); + }, + + 'import/export'(test: Test) { + // GIVEN + const stack1 = new Stack(); + const stack2 = new Stack(); + const vpc = new VpcNetwork(stack1, 'Vpc1'); + const endpoint = vpc.addInterfaceEndpoint('EC2', { + service: InterfaceVpcEndpointAwsService.Ec2 + }); + + // WHEN + const importedEndpoint = InterfaceVpcEndpoint.import(stack2, 'ImportedEndpoint', endpoint.export()); + importedEndpoint.connections.allowDefaultPortFromAnyIpv4(); + + // THEN + expect(stack2).to(haveResource('AWS::EC2::SecurityGroupIngress', { + GroupId: { + 'Fn::ImportValue': 'Stack:Vpc1EC2SecurityGroupId3B169C3F' + } + })); + + test.done(); + } + } +}; diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 4e8ce9e304b65..4b384638a46f9 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -142,7 +142,7 @@ export = { expect(stack).to(countResources("AWS::EC2::Subnet", 6)); test.done(); }, - "with reserved subents, any other subnets should not have cidrBlock from within reserved space"(test: Test) { + "with reserved subnets, any other subnets should not have cidrBlock from within reserved space"(test: Test) { const stack = getTestStack(); new VpcNetwork(stack, 'TheVPC', { cidr: '10.0.0.0/16', @@ -183,7 +183,7 @@ export = { } test.done(); }, - "with custom subents, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { + "with custom subnets, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; new VpcNetwork(stack, 'TheVPC', { @@ -439,7 +439,11 @@ export = { { subnetType: SubnetType.Isolated, name: 'Isolated' }, ], vpnGateway: true, - vpnRoutePropagation: [SubnetType.Isolated] + vpnRoutePropagation: [ + { + subnetType: SubnetType.Isolated + } + ] }); expect(stack).to(haveResource('AWS::EC2::VPNGatewayRoutePropagation', { @@ -470,8 +474,12 @@ export = { ], vpnGateway: true, vpnRoutePropagation: [ - SubnetType.Private, - SubnetType.Isolated + { + subnetType: SubnetType.Private + }, + { + subnetType: SubnetType.Isolated + } ] }); @@ -601,10 +609,10 @@ export = { const vpc = new VpcNetwork(stack, 'VPC'); // WHEN - const nets = vpc.subnetIds(); + const { subnetIds } = vpc.selectSubnets(); // THEN - test.deepEqual(nets, vpc.privateSubnets.map(s => s.subnetId)); + test.deepEqual(subnetIds, vpc.privateSubnets.map(s => s.subnetId)); test.done(); }, @@ -614,10 +622,10 @@ export = { const vpc = new VpcNetwork(stack, 'VPC'); // WHEN - const nets = vpc.subnetIds({ subnetType: SubnetType.Public }); + const { subnetIds } = vpc.selectSubnets({ subnetType: SubnetType.Public }); // THEN - test.deepEqual(nets, vpc.publicSubnets.map(s => s.subnetId)); + test.deepEqual(subnetIds, vpc.publicSubnets.map(s => s.subnetId)); test.done(); }, @@ -633,10 +641,10 @@ export = { }); // WHEN - const nets = vpc.subnetIds({ subnetType: SubnetType.Isolated }); + const { subnetIds } = vpc.selectSubnets({ subnetType: SubnetType.Isolated }); // THEN - test.deepEqual(nets, vpc.isolatedSubnets.map(s => s.subnetId)); + test.deepEqual(subnetIds, vpc.isolatedSubnets.map(s => s.subnetId)); test.done(); }, @@ -652,10 +660,10 @@ export = { }); // WHEN - const nets = vpc.subnetIds({ subnetName: 'DontTalkToMe' }); + const { subnetIds } = vpc.selectSubnets({ subnetName: 'DontTalkToMe' }); // THEN - test.deepEqual(nets, vpc.privateSubnets.map(s => s.subnetId)); + test.deepEqual(subnetIds, vpc.privateSubnets.map(s => s.subnetId)); test.done(); }, @@ -669,9 +677,29 @@ export = { }); test.throws(() => { - vpc.subnetIds(); + vpc.selectSubnets(); }, /There are no 'Private' subnets in this VPC/); + test.done(); + }, + + 'select subnets with az restriction'(test: Test) { + // GIVEN + const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'VpcNetwork', { + maxAZs: 1, + subnetConfiguration: [ + {name: 'app', subnetType: SubnetType.Private }, + {name: 'db', subnetType: SubnetType.Private }, + ] + }); + + // WHEN + const { subnetIds } = vpc.selectSubnets({ onePerAz: true }); + + // THEN + test.deepEqual(subnetIds.length, 1); + test.deepEqual(subnetIds[0], vpc.privateSubnets[0].subnetId); test.done(); } }, @@ -733,11 +761,11 @@ export = { }); // WHEN - const nets = importedVpc.subnetIds({ subnetType: SubnetType.Isolated }); + const { subnetIds } = importedVpc.selectSubnets({ subnetType: SubnetType.Isolated }); // THEN test.equal(3, importedVpc.isolatedSubnets.length); - test.deepEqual(nets, importedVpc.isolatedSubnets.map(s => s.subnetId)); + test.deepEqual(subnetIds, importedVpc.isolatedSubnets.map(s => s.subnetId)); test.done(); }, @@ -756,11 +784,11 @@ export = { }); // WHEN - const nets = importedVpc.subnetIds({ subnetName: isolatedName }); + const { subnetIds } = importedVpc.selectSubnets({ subnetName: isolatedName }); // THEN test.equal(3, importedVpc.isolatedSubnets.length); - test.deepEqual(nets, importedVpc.isolatedSubnets.map(s => s.subnetId)); + test.deepEqual(subnetIds, importedVpc.isolatedSubnets.map(s => s.subnetId)); } test.done(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 653676700d77b..be6b0df0cccc1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -234,7 +234,7 @@ export abstract class BaseService extends cdk.Construct this.networkConfiguration = { awsvpcConfiguration: { assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED', - subnets: vpc.subnetIds(vpcSubnets), + subnets: vpc.selectSubnets(vpcSubnets).subnetIds, securityGroups: new cdk.Token(() => [securityGroup!.securityGroupId]).toList(), } }; diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 174d3eff33509..8dcdd35f767f0 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -163,7 +163,7 @@ export class Cluster extends ClusterBase { // Get subnetIds for all selected subnets const placements = props.vpcSubnets || [{ subnetType: ec2.SubnetType.Public }, { subnetType: ec2.SubnetType.Private }]; - const subnetIds = flatMap(placements, p => this.vpc.subnetIds(p)); + const subnetIds = [...new Set(Array().concat(...placements.map(s => props.vpc.selectSubnets(s).subnetIds)))]; const resource = new CfnCluster(this, 'Resource', { name: props.clusterName, @@ -332,11 +332,3 @@ class ImportedCluster extends ClusterBase { } } } - -function flatMap(xs: T[], f: (x: T) => U[]): U[] { - const ret = new Array(); - for (const x of xs) { - ret.push(...f(x)); - } - return ret; -} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 2d2791a1a6f9f..5d3e73bb723ee 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -101,19 +101,19 @@ export abstract class BaseLoadBalancer extends cdk.Construct implements route53. const vpcSubnets = ifUndefined(baseProps.vpcSubnets, { subnetType: internetFacing ? ec2.SubnetType.Public : ec2.SubnetType.Private }); - const subnets = baseProps.vpc.subnetIds(vpcSubnets); + const { subnetIds, internetConnectedDependency } = baseProps.vpc.selectSubnets(vpcSubnets); this.vpc = baseProps.vpc; const resource = new CfnLoadBalancer(this, 'Resource', { name: baseProps.loadBalancerName, - subnets, + subnets: subnetIds, scheme: internetFacing ? 'internet-facing' : 'internal', loadBalancerAttributes: new cdk.Token(() => renderAttributes(this.attributes)), ...additionalProps }); if (internetFacing) { - resource.node.addDependency(baseProps.vpc.subnetInternetDependencies(vpcSubnets)); + resource.node.addDependency(internetConnectedDependency); } if (baseProps.deletionProtection) { this.setAttribute('deletion_protection.enabled', 'true'); } diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index c0a750f248731..324783d9605e8 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -550,7 +550,7 @@ export class Function extends FunctionBase { // won't work because the ENIs don't get a Public IP. // Why are we not simply forcing vpcSubnets? Because you might still be choosing // Isolated networks or selecting among 2 sets of Private subnets by name. - const subnetIds = props.vpc.subnetIds(props.vpcSubnets); + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets); const publicSubnetIds = new Set(props.vpc.publicSubnets.map(s => s.subnetId)); for (const subnetId of subnetIds) { if (publicSubnetIds.has(subnetId)) { diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 0caedce93e1e7..13b81137ad220 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -235,7 +235,7 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu this.vpc = props.instanceProps.vpc; this.vpcSubnets = props.instanceProps.vpcSubnets; - const subnetIds = props.instanceProps.vpc.subnetIds(props.instanceProps.vpcSubnets); + const { subnetIds } = props.instanceProps.vpc.selectSubnets(props.instanceProps.vpcSubnets); // Cannot test whether the subnets are in different AZs, but at least we can test the amount. if (subnetIds.length < 2) { @@ -308,7 +308,7 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu } // Get the actual subnet objects so we can depend on internet connectivity. - const internetConnected = props.instanceProps.vpc.subnetInternetDependencies(props.instanceProps.vpcSubnets); + const internetConnected = props.instanceProps.vpc.selectSubnets(props.instanceProps.vpcSubnets).internetConnectedDependency; for (let i = 0; i < instanceCount; i++) { const instanceIndex = i + 1; diff --git a/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts b/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts index a892fcf021d7e..357e9504a0fd2 100644 --- a/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts @@ -139,7 +139,7 @@ export class RotationSingleUser extends cdk.Construct { vpc: props.vpc }); - const vpcSubnetIds = props.vpc.subnetIds(props.vpcSubnets); + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets); props.target.connections.allowDefaultPortFrom(securityGroup); @@ -149,7 +149,7 @@ export class RotationSingleUser extends cdk.Construct { endpoint: `https://secretsmanager.${this.node.stack.region}.${this.node.stack.urlSuffix}`, functionName: rotationFunctionName, vpcSecurityGroupIds: securityGroup.securityGroupId, - vpcSubnetIds: vpcSubnetIds.join(',') + vpcSubnetIds: subnetIds.join(',') } });