From 8f6f9a8678496e131a43ca4c76e561d50a0a0de8 Mon Sep 17 00:00:00 2001 From: flemjame-at-amazon <57235867+flemjame-at-amazon@users.noreply.github.com> Date: Thu, 17 Dec 2020 02:34:14 -0500 Subject: [PATCH 1/4] feat(route53): Vpc endpoint service private dns (#10780) https://aws.amazon.com/premiumsupport/knowledge-center/vpc-private-dns-name-endpoint-service/ https://docs.aws.amazon.com/vpc/latest/userguide/verify-domains.html AWS added the ability to specify a custom DNS name for an endpoint service earlier this year. It makes it so your clients don't have to create aliases for an InterfaceVpcEndpoint when they connect to your service. This reduces undifferentiated lifting done by clients. This PR creates a construct that will set up the custom DNS. ```ts stack = new Stack(); vpc = new Vpc(stack, 'VPC'); nlb = new NetworkLoadBalancer(stack, 'NLB', { vpc, }); vpces = new VpcEndpointService(stack, 'VPCES', { vpcEndpointServiceLoadBalancers: [nlb], }); // You must use a public hosted zone so domain ownership can be verified zone = new PublicHostedZone(stack, 'PHZ', { zoneName: 'aws-cdk.dev', }); new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { endpointService: vpces, domainName: 'my-stuff.aws-cdk.dev', publicZone: zone, }); ``` Original design ticket: https://github.com/aws/aws-cdk/issues/10580 *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/README.md | 17 + .../aws-ec2/lib/vpc-endpoint-service.ts | 8 + packages/@aws-cdk/aws-route53/README.md | 48 + packages/@aws-cdk/aws-route53/lib/index.ts | 3 +- .../lib/vpc-endpoint-service-domain-name.ts | 230 ++++ packages/@aws-cdk/aws-route53/package.json | 3 + ...endpoint-service-domain-name.expected.json | 1004 +++++++++++++++++ .../integ.vpc-endpoint-service-domain-name.ts | 48 + .../vpc-endpoint-service-domain-name.test.ts | 254 +++++ 9 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts create mode 100644 packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json create mode 100644 packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts create mode 100644 packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 664f3c2b2eff2..b8a583d1a2ea5 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -623,6 +623,23 @@ new VpcEndpointService(this, 'EndpointService', { }); ``` +Endpoint services support private DNS, which makes it easier for clients to connect to your service by automatically setting up DNS in their VPC. +You can enable private DNS on an endpoint service like so: + +```ts +import { VpcEndpointServiceDomainName } from '@aws-cdk/aws-route53'; + +new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, +}); +``` + +Note: The domain name must be owned (registered through Route53) by the account the endpoint service is in, or delegated to the account. +The VpcEndpointServiceDomainName will handle the AWS side of domain verification, the process for which can be found +[here](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-dns-validation.html) + ## Instances You can use the `Instance` class to start up a single EC2 instance. For production setups, we recommend diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts index d5d043eef0dc0..d17e56aab202c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts @@ -27,6 +27,14 @@ export interface IVpcEndpointService extends IResource { * @attribute */ readonly vpcEndpointServiceName: string; + + /** + * The id of the VPC Endpoint Service that clients use to connect to, + * like vpce-svc-xxxxxxxxxxxxxxxx + * + * @attribute + */ + readonly vpcEndpointServiceId: string; } /** diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index d83ba3a547915..116dd217748cd 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -129,3 +129,51 @@ const zone = HostedZone.fromHostedZoneId(this, 'MyZone', { hostedZoneId: 'ZOJJZC49E0EPZ', }); ``` + +## VPC Endpoint Service Private DNS + +When you create a VPC endpoint service, AWS generates endpoint-specific DNS hostnames that consumers use to communicate with the service. +For example, vpce-1234-abcdev-us-east-1.vpce-svc-123345.us-east-1.vpce.amazonaws.com. +By default, your consumers access the service with that DNS name. +This can cause problems with HTTPS traffic because the DNS will not match the backend certificate: + +```console +curl: (60) SSL: no alternative certificate subject name matches target host name 'vpce-abcdefghijklmnopq-rstuvwx.vpce-svc-abcdefghijklmnopq.us-east-1.vpce.amazonaws.com' +``` + +Effectively, the endpoint appears untrustworthy. To mitigate this, clients have to create an alias for this DNS name in Route53. + +Private DNS for an endpoint service lets you configure a private DNS name so consumers can +access the service using an existing DNS name without creating this Route53 DNS alias +This DNS name can also be guaranteed to match up with the backend certificate. + +Before consumers can use the private DNS name, you must verify that you have control of the domain/subdomain. + +Assuming your account has ownership of the particlar domain/subdomain, +this construct sets up the private DNS configuration on the endpoint service, +creates all the necessary Route53 entries, and verifies domain ownership. + +```ts +import { Stack } from '@aws-cdk/core'; +import { Vpc, VpcEndpointService } from '@aws-cdk/aws-ec2'; +import { NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { PublicHostedZone } from '@aws-cdk/aws-route53'; + +stack = new Stack(); +vpc = new Vpc(stack, 'VPC'); +nlb = new NetworkLoadBalancer(stack, 'NLB', { + vpc, +}); +vpces = new VpcEndpointService(stack, 'VPCES', { + vpcEndpointServiceLoadBalancers: [nlb], +}); +// You must use a public hosted zone so domain ownership can be verified +zone = new PublicHostedZone(stack, 'PHZ', { + zoneName: 'aws-cdk.dev', +}); +new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, +}); +``` diff --git a/packages/@aws-cdk/aws-route53/lib/index.ts b/packages/@aws-cdk/aws-route53/lib/index.ts index 9a6ed0a853679..c35c57025fc26 100644 --- a/packages/@aws-cdk/aws-route53/lib/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/index.ts @@ -1,8 +1,9 @@ +export * from './alias-record-target'; export * from './hosted-zone'; export * from './hosted-zone-provider'; export * from './hosted-zone-ref'; export * from './record-set'; -export * from './alias-record-target'; +export * from './vpc-endpoint-service-domain-name'; // AWS::Route53 CloudFormation Resources: export * from './route53.generated'; diff --git a/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts new file mode 100644 index 0000000000000..2af889661098a --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts @@ -0,0 +1,230 @@ +import * as crypto from 'crypto'; +import { IVpcEndpointService } from '@aws-cdk/aws-ec2'; +import { Fn, Names, Stack } from '@aws-cdk/core'; +import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; +import { Construct } from 'constructs'; +import { IPublicHostedZone, TxtRecord } from '../lib'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties to configure a VPC Endpoint Service domain name + */ +export interface VpcEndpointServiceDomainNameProps { + + /** + * The VPC Endpoint Service to configure Private DNS for + */ + readonly endpointService: IVpcEndpointService; + + /** + * The domain name to use. + * + * This domain name must be owned by this account (registered through Route53), + * or delegated to this account. Domain ownership will be verified by AWS before + * private DNS can be used. + * @see https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-dns-validation.html + */ + readonly domainName: string; + + /** + * The public hosted zone to use for the domain. + */ + readonly publicHostedZone: IPublicHostedZone; +} + +/** + * A Private DNS configuration for a VPC endpoint service. + */ +export class VpcEndpointServiceDomainName extends CoreConstruct { + + // Track all domain names created, so someone doesn't accidentally associate two domains with a single service + private static readonly endpointServices: IVpcEndpointService[] = []; + + // Track all domain names created, so someone doesn't accidentally associate two domains with a single service + private static readonly endpointServicesMap: { [endpointService: string]: string} = {}; + + // The way this class works is by using three custom resources and a TxtRecord in conjunction + // The first custom resource tells the VPC endpoint service to use the given DNS name + // The VPC endpoint service will then say: + // "ok, create a TXT record using these two values to prove you own the domain" + // The second custom resource retrieves these two values from the service + // The TxtRecord is created from these two values + // The third custom resource tells the VPC Endpoint Service to verify the domain ownership + constructor(scope: Construct, id: string, props: VpcEndpointServiceDomainNameProps) { + super(scope, id); + + const serviceUniqueId = Names.nodeUniqueId(props.endpointService.node); + const serviceId = props.endpointService.vpcEndpointServiceId; + const privateDnsName = props.domainName; + + // Make sure a user doesn't accidentally add multiple domains + this.validateProps(props); + + VpcEndpointServiceDomainName.endpointServicesMap[serviceUniqueId] = privateDnsName; + VpcEndpointServiceDomainName.endpointServices.push(props.endpointService); + + // Enable Private DNS on the endpoint service and retrieve the AWS-generated configuration + const privateDnsConfiguration = this.getPrivateDnsConfiguration(serviceUniqueId, serviceId, privateDnsName); + + // Tell AWS to verify that this account owns the domain attached to the service + this.verifyPrivateDnsConfiguration(privateDnsConfiguration, props.publicHostedZone); + + // Finally, don't do any of the above before the endpoint service is created + this.node.addDependency(props.endpointService); + } + + private validateProps(props: VpcEndpointServiceDomainNameProps): void { + const serviceUniqueId = Names.nodeUniqueId(props.endpointService.node); + if (serviceUniqueId in VpcEndpointServiceDomainName.endpointServicesMap) { + const endpoint = VpcEndpointServiceDomainName.endpointServicesMap[serviceUniqueId]; + throw new Error( + `Cannot create a VpcEndpointServiceDomainName for service ${serviceUniqueId}, another VpcEndpointServiceDomainName (${endpoint}) is already associated with it`); + } + } + + /** + * Sets up Custom Resources to make AWS calls to set up Private DNS on an endpoint service, + * returning the values to use in a TxtRecord, which AWS uses to verify domain ownership. + */ + private getPrivateDnsConfiguration(serviceUniqueId: string, serviceId: string, privateDnsName: string): PrivateDnsConfiguration { + + // The custom resource which tells AWS to enable Private DNS on the given service, using the given domain name + // AWS will generate a name/value pair for use in a TxtRecord, which is used to verify domain ownership. + const enablePrivateDnsAction = { + service: 'EC2', + action: 'modifyVpcEndpointServiceConfiguration', + parameters: { + ServiceId: serviceId, + PrivateDnsName: privateDnsName, + }, + physicalResourceId: PhysicalResourceId.of(serviceUniqueId), + }; + const removePrivateDnsAction = { + service: 'EC2', + action: 'modifyVpcEndpointServiceConfiguration', + parameters: { + ServiceId: serviceId, + RemovePrivateDnsName: true, + }, + }; + const enable = new AwsCustomResource(this, 'EnableDns', { + onCreate: enablePrivateDnsAction, + onUpdate: enablePrivateDnsAction, + onDelete: removePrivateDnsAction, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [ + Fn.join(':', [ + 'arn', + Stack.of(this).partition, + 'ec2', + Stack.of(this).region, + Stack.of(this).account, + Fn.join('/', [ + 'vpc-endpoint-service', + serviceId, + ]), + ]), + ], + }), + }); + + // Look up the name/value pair if the domain changes, or the service changes, + // which would cause the values to be different. If the unique ID changes, + // the resource may be entirely recreated, so we will need to look it up again. + const lookup = hashcode(Names.uniqueId(this) + serviceUniqueId + privateDnsName); + + // Create the custom resource to look up the name/value pair generated by AWS + // after the previous API call + const retriveNameValuePairAction = { + service: 'EC2', + action: 'describeVpcEndpointServiceConfigurations', + parameters: { + ServiceIds: [serviceId], + }, + physicalResourceId: PhysicalResourceId.of(lookup), + }; + const getNames = new AwsCustomResource(this, 'GetNames', { + onCreate: retriveNameValuePairAction, + onUpdate: retriveNameValuePairAction, + // describeVpcEndpointServiceConfigurations can't take an ARN for granular permissions + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: AwsCustomResourcePolicy.ANY_RESOURCE, + }), + }); + + // We only want to call and get the name/value pair after we've told AWS to enable Private DNS + // If we call before then, we'll get an empty pair of values. + getNames.node.addDependency(enable); + + // Get the references to the name/value pair associated with the endpoint service + const name = getNames.getResponseField('ServiceConfigurations.0.PrivateDnsNameConfiguration.Name'); + const value = getNames.getResponseField('ServiceConfigurations.0.PrivateDnsNameConfiguration.Value'); + + return { name, value, serviceId }; + } + + /** + * Creates a Route53 entry and a Custom Resource which explicitly tells AWS to verify ownership + * of the domain name attached to an endpoint service. + */ + private verifyPrivateDnsConfiguration(config: PrivateDnsConfiguration, publicHostedZone: IPublicHostedZone) { + // Create the TXT record in the provided hosted zone + const verificationRecord = new TxtRecord(this, 'DnsVerificationRecord', { + recordName: config.name, + values: [config.value], + zone: publicHostedZone, + }); + + // Tell the endpoint service to verify the domain ownership + const startVerificationAction = { + service: 'EC2', + action: 'startVpcEndpointServicePrivateDnsVerification', + parameters: { + ServiceId: config.serviceId, + }, + physicalResourceId: PhysicalResourceId.of(Fn.join(':', [config.name, config.value])), + }; + const startVerification = new AwsCustomResource(this, 'StartVerification', { + onCreate: startVerificationAction, + onUpdate: startVerificationAction, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [ + Fn.join(':', [ + 'arn', + Stack.of(this).partition, + 'ec2', + Stack.of(this).region, + Stack.of(this).account, + Fn.join('/', [ + 'vpc-endpoint-service', + config.serviceId, + ]), + ]), + ], + }), + }); + // Only verify after the record has been created + startVerification.node.addDependency(verificationRecord); + } +} + +/** + * Represent the name/value pair associated with a Private DNS enabled endpoint service + */ +interface PrivateDnsConfiguration { + readonly name: string; + readonly value: string; + readonly serviceId: string; +} + +/** + * Hash a string + */ +function hashcode(s: string): string { + const hash = crypto.createHash('md5'); + hash.update(s); + return hash.digest('hex'); +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index c7cdda9c49f38..2145126c0e42d 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -78,6 +78,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^26.6.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, @@ -86,6 +87,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", @@ -94,6 +96,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.2.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json new file mode 100644 index 0000000000000..df7ab45491d39 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json @@ -0,0 +1,1004 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "mylb": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "Type": "network", + "Name": "mylb", + "Scheme": "internal", + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + ] + } + }, + "VPCES3AE7D565": { + "Type": "AWS::EC2::VPCEndpointService", + "Properties": { + "AcceptanceRequired": true, + "NetworkLoadBalancerArns": [ + { + "Ref": "mylb" + } + ] + } + }, + "PHZ45BE903D": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "aws-cdk.dev." + } + }, + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:ModifyVpcEndpointServiceConfiguration", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + ":", + [ + "arn", + { + "Ref": "AWS::Partition" + }, + "ec2", + { + "Ref": "AWS::Region" + }, + { + "Ref": "AWS::AccountId" + }, + { + "Fn::Join": [ + "/", + [ + "vpc-endpoint-service", + { + "Ref": "VPCES3AE7D565" + } + ] + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "VPCES3AE7D565" + ] + }, + "EndpointDomainEnableDnsDACBF5A6": { + "Type": "Custom::AWS", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "service": "EC2", + "action": "modifyVpcEndpointServiceConfiguration", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + }, + "PrivateDnsName": "my-stuff.aws-cdk.dev" + }, + "physicalResourceId": { + "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" + } + }, + "Update": { + "service": "EC2", + "action": "modifyVpcEndpointServiceConfiguration", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + }, + "PrivateDnsName": "my-stuff.aws-cdk.dev" + }, + "physicalResourceId": { + "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" + } + }, + "Delete": { + "service": "EC2", + "action": "modifyVpcEndpointServiceConfiguration", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + }, + "RemovePrivateDnsName": "TRUE:BOOLEAN" + } + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "VPCES3AE7D565" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EndpointDomainGetNamesCustomResourcePolicy141775B1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:DescribeVpcEndpointServiceConfigurations", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EndpointDomainGetNamesCustomResourcePolicy141775B1", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "EndpointDomainEnableDnsDACBF5A6", + "VPCES3AE7D565" + ] + }, + "EndpointDomainGetNames9E697ED2": { + "Type": "Custom::AWS", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "service": "EC2", + "action": "describeVpcEndpointServiceConfigurations", + "parameters": { + "ServiceIds": [ + { + "Ref": "VPCES3AE7D565" + } + ] + }, + "physicalResourceId": { + "id": "0b26ca4969ad06c279e229b1b55b9bc2" + } + }, + "Update": { + "service": "EC2", + "action": "describeVpcEndpointServiceConfigurations", + "parameters": { + "ServiceIds": [ + { + "Ref": "VPCES3AE7D565" + } + ] + }, + "physicalResourceId": { + "id": "0b26ca4969ad06c279e229b1b55b9bc2" + } + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "EndpointDomainEnableDnsDACBF5A6", + "EndpointDomainGetNamesCustomResourcePolicy141775B1", + "VPCES3AE7D565" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EndpointDomainDnsVerificationRecord66623BDA": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + ".aws-cdk.dev." + ] + ] + }, + "Type": "TXT", + "HostedZoneId": { + "Ref": "PHZ45BE903D" + }, + "ResourceRecords": [ + { + "Fn::Join": [ + "", + [ + "\"", + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + }, + "\"" + ] + ] + } + ], + "TTL": "1800" + }, + "DependsOn": [ + "VPCES3AE7D565" + ] + }, + "EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:StartVpcEndpointServicePrivateDnsVerification", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + ":", + [ + "arn", + { + "Ref": "AWS::Partition" + }, + "ec2", + { + "Ref": "AWS::Region" + }, + { + "Ref": "AWS::AccountId" + }, + { + "Fn::Join": [ + "/", + [ + "vpc-endpoint-service", + { + "Ref": "VPCES3AE7D565" + } + ] + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "EndpointDomainDnsVerificationRecord66623BDA", + "VPCES3AE7D565" + ] + }, + "EndpointDomainStartVerification05E2F7A3": { + "Type": "Custom::AWS", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "service": "EC2", + "action": "startVpcEndpointServicePrivateDnsVerification", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + } + }, + "physicalResourceId": { + "id": { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] + ] + } + } + }, + "Update": { + "service": "EC2", + "action": "startVpcEndpointServicePrivateDnsVerification", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + } + }, + "physicalResourceId": { + "id": { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] + ] + } + } + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EndpointDomainDnsVerificationRecord66623BDA", + "EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6", + "VPCES3AE7D565" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] + } + }, + "Parameters": { + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "Type": "String", + "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + }, + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "Type": "String", + "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + }, + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "Type": "String", + "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts new file mode 100644 index 0000000000000..672b2133f077b --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts @@ -0,0 +1,48 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { PublicHostedZone, VpcEndpointServiceDomainName } from '../lib'; + +/** + * A load balancer that can host a VPC Endpoint Service. + * + * Why do this instead of using the NetworkLoadBalancer construct? aws-route53 + * cannot depend on aws-elasticloadbalancingv2 because aws-elasticloadbalancingv2 + * already takes a dependency on aws-route53. + */ +class DummyEndpointLoadBalancer implements ec2.IVpcEndpointServiceLoadBalancer { + /** + * The ARN of the load balancer that hosts the VPC Endpoint Service + */ + public readonly loadBalancerArn: string; + constructor(scope: Construct, id: string, vpc: ec2.Vpc) { + const lb = new cdk.CfnResource(scope, id, { + type: 'AWS::ElasticLoadBalancingV2::LoadBalancer', + properties: { + Type: 'network', + Name: 'mylb', + Scheme: 'internal', + Subnets: [vpc.privateSubnets[0].subnetId], + }, + }); + this.loadBalancerArn = lb.ref; + } +} + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-vpc-endpoint-dns-integ'); +const vpc = new ec2.Vpc(stack, 'VPC'); +const nlb = new DummyEndpointLoadBalancer(stack, 'mylb', vpc); +const vpces = new ec2.VpcEndpointService(stack, 'VPCES', { + vpcEndpointServiceLoadBalancers: [nlb], +}); +const zone = new PublicHostedZone(stack, 'PHZ', { + zoneName: 'aws-cdk.dev', +}); +new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts new file mode 100644 index 0000000000000..70ba07c201d5f --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -0,0 +1,254 @@ +import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { PublicHostedZone, VpcEndpointServiceDomainName } from '../lib'; + +let stack: Stack; +let nlb: IVpcEndpointServiceLoadBalancer; +let vpces: VpcEndpointService; +let zone: PublicHostedZone; + +/** + * A load balancer that can host a VPC Endpoint Service + */ +class DummyEndpointLoadBalancer implements IVpcEndpointServiceLoadBalancer { + /** + * The ARN of the load balancer that hosts the VPC Endpoint Service + */ + public readonly loadBalancerArn: string; + constructor(arn: string) { + this.loadBalancerArn = arn; + } +} + +beforeEach(() => { + stack = new Stack(); + nlb = new DummyEndpointLoadBalancer('arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a'); + vpces = new VpcEndpointService(stack, 'VPCES', { + vpcEndpointServiceLoadBalancers: [nlb], + }); + zone = new PublicHostedZone(stack, 'PHZ', { + zoneName: 'aws-cdk.dev', + }); +}); + +test('create domain name resource', () => { + // GIVEN + + // WHEN + new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Properties: { + Create: { + action: 'modifyVpcEndpointServiceConfiguration', + service: 'EC2', + parameters: { + PrivateDnsName: 'my-stuff.aws-cdk.dev', + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: 'EndpointDomain', + }, + }, + Update: { + action: 'modifyVpcEndpointServiceConfiguration', + service: 'EC2', + parameters: { + PrivateDnsName: 'my-stuff.aws-cdk.dev', + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: 'EndpointDomain', + }, + }, + Delete: { + action: 'modifyVpcEndpointServiceConfiguration', + service: 'EC2', + parameters: { + RemovePrivateDnsName: 'TRUE:BOOLEAN', + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + }, + }, + DependsOn: [ + 'EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB', + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); + + // Have to use `haveResourceLike` because there is a property that, by design, changes on every build + cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Properties: { + Create: { + action: 'describeVpcEndpointServiceConfigurations', + service: 'EC2', + parameters: { + ServiceIds: [{ + Ref: 'VPCES3AE7D565', + }], + }, + }, + Update: { + action: 'describeVpcEndpointServiceConfigurations', + service: 'EC2', + parameters: { + ServiceIds: [{ + Ref: 'VPCES3AE7D565', + }], + }, + }, + }, + DependsOn: [ + 'EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB', + 'EndpointDomainEnableDnsDACBF5A6', + 'EndpointDomainGetNamesCustomResourcePolicy141775B1', + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResource('AWS::Route53::RecordSet', { + Properties: { + Name: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + '.aws-cdk.dev.', + ], + ], + }, + Type: 'TXT', + HostedZoneId: { + Ref: 'PHZ45BE903D', + }, + ResourceRecords: [ + { + 'Fn::Join': [ + '', + [ + '\"', + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + '\"', + ], + ], + }, + ], + TTL: '1800', + }, + DependsOn: [ + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Properties: { + Create: { + action: 'startVpcEndpointServicePrivateDnsVerification', + service: 'EC2', + parameters: { + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], + ], + }, + }, + }, + Update: { + action: 'startVpcEndpointServicePrivateDnsVerification', + service: 'EC2', + parameters: { + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], + ], + }, + }, + }, + }, + DependsOn: [ + 'EndpointDomainDnsVerificationRecord66623BDA', + 'EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6', + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); +}); + +test('throws if creating multiple domains for a single service', () => { + // GIVEN + + new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff-1.aws-cdk.dev', + publicHostedZone: zone, + }); + + // WHEN / THEN + expect(() => { + new VpcEndpointServiceDomainName(stack, 'EndpointDomain2', { + endpointService: vpces, + domainName: 'my-stuff-2.aws-cdk.dev', + publicHostedZone: zone, + }); + }).toThrow(); +}); \ No newline at end of file From 9b19e2953a7bdb4d9fd0fc27cea3b3320ba0a804 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 17 Dec 2020 09:02:58 +0000 Subject: [PATCH 2/4] chore(release): 1.79.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02bad01ae7f3f..61aaf8da350cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.79.0](https://github.com/aws/aws-cdk/compare/v1.78.0...v1.79.0) (2020-12-17) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2:** `HttpApi.fromApiId()` has been replaced with +`HttpApi.fromHttpApiAttributes()`. +* **elasticsearch:** ES Domain LogGroup LogicalId will change, which will trigger new log group resources to be created + +### Features + +* **appmesh:** add timeout support to Routes ([#11973](https://github.com/aws/aws-cdk/issues/11973)) ([78c185d](https://github.com/aws/aws-cdk/commit/78c185d15e64e81ee86ee71cd6430cd80fdbb8fe)) +* **core:** expose custom resource provider's role ([#11923](https://github.com/aws/aws-cdk/issues/11923)) ([06f26d3](https://github.com/aws/aws-cdk/commit/06f26d390707b0e2a4e05e36405a4751c907a234)), closes [/github.com/aws/aws-cdk/pull/9751#issuecomment-723554595](https://github.com/aws//github.com/aws/aws-cdk/pull/9751/issues/issuecomment-723554595) +* **ec2:** add r5b instance type to instance class ([#12027](https://github.com/aws/aws-cdk/issues/12027)) ([d276b02](https://github.com/aws/aws-cdk/commit/d276b020e61ee4455c7ed9f093436d1aab319e76)), closes [#12025](https://github.com/aws/aws-cdk/issues/12025) +* **ecs-patterns:** Add DeploymentController option to Fargate services ([#10452](https://github.com/aws/aws-cdk/issues/10452)) ([2cd233a](https://github.com/aws/aws-cdk/commit/2cd233a94fc2f3cb06211157738e59e8c7ee85e5)), closes [aws/containers-roadmap#130](https://github.com/aws/containers-roadmap/issues/130) [#10971](https://github.com/aws/aws-cdk/issues/10971) +* **eks:** attach cluster security group to self-managed nodes ([#12042](https://github.com/aws/aws-cdk/issues/12042)) ([1078bea](https://github.com/aws/aws-cdk/commit/1078bea4c90afaac76a5e81328a9d6ec44a79e9a)) +* **elasticsearch:** support audit logs ([#12106](https://github.com/aws/aws-cdk/issues/12106)) ([d10ea63](https://github.com/aws/aws-cdk/commit/d10ea631f8699385cadf61d6e0a067b68da37df6)), closes [#12105](https://github.com/aws/aws-cdk/issues/12105) +* **ivs:** add IVS L2 Constructs ([#11454](https://github.com/aws/aws-cdk/issues/11454)) ([f813bff](https://github.com/aws/aws-cdk/commit/f813bff2da4792cfa7bfce6f572a7d2bb5c4759d)) +* **lambda:** encryption key for environment variables ([#11893](https://github.com/aws/aws-cdk/issues/11893)) ([ccbaf83](https://github.com/aws/aws-cdk/commit/ccbaf8399c3a9f3ff6e60758e0b713d82f37420b)), closes [#10837](https://github.com/aws/aws-cdk/issues/10837) +* **lambda-nodejs:** expose more esbuild options ([#12063](https://github.com/aws/aws-cdk/issues/12063)) ([bab21b3](https://github.com/aws/aws-cdk/commit/bab21b377593b7475b047d05a54914344352c054)), closes [#12046](https://github.com/aws/aws-cdk/issues/12046) +* **route53:** Vpc endpoint service private dns ([#10780](https://github.com/aws/aws-cdk/issues/10780)) ([8f6f9a8](https://github.com/aws/aws-cdk/commit/8f6f9a8678496e131a43ca4c76e561d50a0a0de8)) +* **s3-deployment:** support vpc in BucketDeploymentProps ([#12035](https://github.com/aws/aws-cdk/issues/12035)) ([6caf72f](https://github.com/aws/aws-cdk/commit/6caf72f67d6d3373186e57f32671369c2cc8b56e)), closes [#11734](https://github.com/aws/aws-cdk/issues/11734) +* **stepfunctions-tasks:** add support for ModelClientConfig to SageMakerCreateTransformJob ([#11892](https://github.com/aws/aws-cdk/issues/11892)) ([bf05092](https://github.com/aws/aws-cdk/commit/bf050928c033328b259746c0a7f33038aadc4c17)) + + +### Bug Fixes + +* **ec2:** 'encoded list token' error using Vpc imported from deploy-time lists ([#12040](https://github.com/aws/aws-cdk/issues/12040)) ([0690da9](https://github.com/aws/aws-cdk/commit/0690da925144c821a73bfab4ae8d678a8c074357)) +* **ec2:** fromInterfaceVpcEndpointAttributes: Security Groups should not be required ([#11857](https://github.com/aws/aws-cdk/issues/11857)) ([86ae5d6](https://github.com/aws/aws-cdk/commit/86ae5d6ec5291f7a8da37bbf021c31f88e66d283)), closes [#11050](https://github.com/aws/aws-cdk/issues/11050) +* **eks:** failure to deploy cluster since aws-auth configmap exists ([#12068](https://github.com/aws/aws-cdk/issues/12068)) ([dc8a98a](https://github.com/aws/aws-cdk/commit/dc8a98a5436a7a2347fa9676d84f73a8cf00cd49)), closes [#12053](https://github.com/aws/aws-cdk/issues/12053) +* **eks:** k8s resources accidentally deleted due to logical ID change ([#12053](https://github.com/aws/aws-cdk/issues/12053)) ([019852e](https://github.com/aws/aws-cdk/commit/019852e4834327d848c9fe8dc271f1d4d5117fb8)), closes [#10397](https://github.com/aws/aws-cdk/issues/10397) [#10397](https://github.com/aws/aws-cdk/issues/10397) +* **elasticsearch:** Defining 2 domains with logging enabled in the same stack fails on construct id conflict ([#12055](https://github.com/aws/aws-cdk/issues/12055)) ([ec3ce19](https://github.com/aws/aws-cdk/commit/ec3ce19bc8203703cb1abcecdb2afc674c2013f6)), closes [#12017](https://github.com/aws/aws-cdk/issues/12017) +* **elasticsearch:** log policies are overwritten when creating 2 domains which also results in a failure while destroying the stack ([#12056](https://github.com/aws/aws-cdk/issues/12056)) ([889d089](https://github.com/aws/aws-cdk/commit/889d0892bae10243e03900f0ae6db078fc7eb320)), closes [#12016](https://github.com/aws/aws-cdk/issues/12016) +* **stepfunctions-tasks:** policies created for EMR tasks have ARNs that are not partition-aware ([#11553](https://github.com/aws/aws-cdk/issues/11553)) ([1cf6713](https://github.com/aws/aws-cdk/commit/1cf6713b778c789af7a420ad890910a9516473f0)), closes [#11503](https://github.com/aws/aws-cdk/issues/11503) + + +* **apigatewayv2:** apiEndpoint is elevated to the IHttpApi interface ([#11988](https://github.com/aws/aws-cdk/issues/11988)) ([bc5b9b6](https://github.com/aws/aws-cdk/commit/bc5b9b659444bfbef9cfc3c8666fce7e6f45465a)) + ## [1.78.0](https://github.com/aws/aws-cdk/compare/v1.77.0...v1.78.0) (2020-12-11) diff --git a/version.v1.json b/version.v1.json index 0e9c47b816903..1d87fb5f6b025 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.78.0" + "version": "1.79.0" } From d5afb555b983c8c034f63dd58d1fa24b82b6e9fe Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 17 Dec 2020 10:20:39 +0100 Subject: [PATCH 3/4] fix(lambda-nodejs): local bundling fails with relative depsLockFilePath (#12125) Ensure it's a file and make it absolute. Closes #12115 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-lambda-nodejs/lib/function.ts | 5 ++++- .../aws-lambda-nodejs/test/function.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index a1c7611e64825..5e4e160eaa717 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -87,7 +87,10 @@ export class NodejsFunction extends lambda.Function { if (!fs.existsSync(props.depsLockFilePath)) { throw new Error(`Lock file at ${props.depsLockFilePath} doesn't exist`); } - depsLockFilePath = props.depsLockFilePath; + if (!fs.statSync(props.depsLockFilePath).isFile()) { + throw new Error('`depsLockFilePath` should point to a file'); + } + depsLockFilePath = path.resolve(props.depsLockFilePath); } else { const lockFile = findUp(LockFile.YARN) ?? findUp(LockFile.NPM); if (!lockFile) { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 743526d0e3899..d898d312be92c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -107,6 +107,22 @@ test('throws with non existing lock file', () => { })).toThrow(/Lock file at \/does\/not\/exist.lock doesn't exist/); }); +test('throws when depsLockFilePath is not a file', () => { + expect(() => new NodejsFunction(stack, 'handler1', { + depsLockFilePath: __dirname, + })).toThrow(/\`depsLockFilePath\` should point to a file/); +}); + +test('resolves depsLockFilePath to an absolute path', () => { + new NodejsFunction(stack, 'handler1', { + depsLockFilePath: './package.json', + }); + + expect(Bundling.bundle).toHaveBeenCalledWith(expect.objectContaining({ + depsLockFilePath: expect.stringMatching(/@aws-cdk\/aws-lambda-nodejs\/package.json$/), + })); +}); + test('resolves entry to an absolute path', () => { // WHEN new NodejsFunction(stack, 'fn', { From 7a120f3d181f019da1445b31bffe9d3ddc6e9b78 Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Thu, 17 Dec 2020 12:57:24 -0500 Subject: [PATCH 4/4] chore(logs): Fix typo (#12133) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-logs/lib/log-group.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index 83c4a8e170b89..8b24506f11ba0 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -154,7 +154,7 @@ abstract class LogGroupBase extends Resource implements ILogGroup { } /** - * Give permissions to write to create and write to streams in this log group + * Give permissions to create and write to streams in this log group */ public grantWrite(grantee: iam.IGrantable) { return this.grant(grantee, 'logs:CreateLogStream', 'logs:PutLogEvents');