Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(route53): Vpc endpoint service private dns #10780

Merged
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
446a54c
Merge pull request #21 from aws/master
flemjame-at-amazon Aug 31, 2020
26f6998
Merge pull request #22 from aws/master
flemjame-at-amazon Sep 2, 2020
702316c
Merge pull request #23 from aws/master
flemjame-at-amazon Sep 9, 2020
af13454
Merge pull request #24 from aws/master
flemjame-at-amazon Sep 16, 2020
7e8d2f1
Merge pull request #26 from aws/master
flemjame-at-amazon Oct 1, 2020
4d7ec0f
Merge pull request #27 from aws/master
flemjame-at-amazon Oct 5, 2020
4c617d5
Merge pull request #28 from aws/master
flemjame-at-amazon Oct 6, 2020
8f40f10
Initial commit
flemjame-at-amazon Oct 7, 2020
1fc9648
Fix the tests, comment code, and add error checking for multiplmultip…
flemjame-at-amazon Oct 7, 2020
5d9472d
feat(route53-patterns): Vpc Endpoint Service private DNS
flemjame-at-amazon Oct 8, 2020
bb7e875
Also add a readme blurb in the EC2 section
flemjame-at-amazon Oct 9, 2020
3687738
Merge pull request #29 from aws/master
flemjame-at-amazon Oct 21, 2020
578c851
Update jest to resolve conflict
flemjame-at-amazon Oct 21, 2020
426f771
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Oct 21, 2020
46b4360
Remove elbv2 as a dependency by mocking out the lb in unit testing, a…
flemjame-at-amazon Oct 21, 2020
d6c31f1
review feedback
flemjame-at-amazon Oct 21, 2020
30a654a
Change property name to publicHostedZone
flemjame-at-amazon Oct 21, 2020
e260ffc
Change property name to publicHostedZone
flemjame-at-amazon Oct 21, 2020
e948aab
Review feedback
flemjame-at-amazon Oct 21, 2020
684db32
Move to route53
flemjame-at-amazon Oct 21, 2020
3b026e8
Don't need route53-patterns package.json changes
flemjame-at-amazon Oct 21, 2020
4c294ba
Add integration test
flemjame-at-amazon Oct 22, 2020
da1d66e
Add integration test
flemjame-at-amazon Oct 22, 2020
e83aaa1
Add integration test
flemjame-at-amazon Oct 22, 2020
94a61dd
Switch from random string to hash of parameters that would cause a ch…
flemjame-at-amazon Oct 22, 2020
06c141b
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Oct 22, 2020
b0b4e46
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Oct 27, 2020
4f95f91
Merge pull request #30 from aws/master
flemjame-at-amazon Oct 27, 2020
bf66346
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Oct 29, 2020
ff00622
Resolve conflicts
flemjame-at-amazon Oct 29, 2020
624a2d5
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Oct 29, 2020
2094a72
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 3, 2020
8dd5d53
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 4, 2020
fdf8436
Not sure why the hashes changed after a merge
flemjame-at-amazon Nov 4, 2020
bc913f9
Review feedback
flemjame-at-amazon Nov 4, 2020
46e3645
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 9, 2020
e8c91fd
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 16, 2020
f2f6fa4
Review feedback
flemjame-at-amazon Nov 16, 2020
dd506ff
Switch to user ARN
flemjame-at-amazon Nov 16, 2020
31d80c6
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 16, 2020
5a90d35
last element of an ARN needs to be joined by a / not a :
flemjame-at-amazon Nov 16, 2020
1e94c9b
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 17, 2020
31f5b10
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 19, 2020
c2a80f6
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 23, 2020
722a94e
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Nov 30, 2020
09354d2
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Dec 8, 2020
c99da4d
Update README.md
flemjame-at-amazon Dec 8, 2020
8d10ca2
Merge branch 'master' into vpc-endpoint-service-private-dns
flemjame-at-amazon Dec 16, 2020
b56d705
Merge branch 'master' into vpc-endpoint-service-private-dns
mergify[bot] Dec 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,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
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +31 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a test that uses this property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in ec2:

new cdk.CfnOutput(this, 'MyVpcEndpointServiceWithPrincipalsEndpointServiceId', {
exportName: 'EndpointServiceId',
value: service2.vpcEndpointServiceId,
description: 'Reference this service from other stacks',

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it an attribute of IVpcEndpointService because I would rather have it take an IVpcEndpointService as an argument, vs a VpcEndpointService

}

/**
Expand Down
47 changes: 47 additions & 0 deletions packages/@aws-cdk/aws-route53/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,51 @@ you know the ID and the retrieval for the `zoneName` is undesirable.
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I love this section. That's because I badly need it, I have no idea why you would care about this otherwise :)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not just the same as an R53 Alias though? Guess I'm asking: why is it a different concept?

(Just for my understanding of the feature, nothing to do with the PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It kind of is a R53 alias, as if AWS was managing the hosted zone and ARecord for a service client.

For services exposed over a PrivateLink, a client has to set up the alias themselves -- create the hosted zone, ARecord, etc, because the service has no idea what the VPC endpoint DNS name is going to be (autogenerated on creation).

Private DNS basically means AWS will manage the DNS for the client, so the hostname will resolve correctly to the VPC endpoint without the client having to create a hosted zone or an ARecord.

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', {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be even better if we can import a hosted zone from different account by providing IAM access. DNS delegation may have been set up in a central account.

zoneName: 'aws-cdk.dev',
});
new VpcEndpointServiceDomainName(stack, 'EndpointDomain', {
endpointService: vpces,
domainName: 'my-stuff.aws-cdk.dev',
publicHostedZone: zone,
});
```
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-route53/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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';
230 changes: 230 additions & 0 deletions packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts
Original file line number Diff line number Diff line change
@@ -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');
};
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-route53/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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"
},
Expand All @@ -85,6 +86,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",
Expand All @@ -93,6 +95,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": {
Expand Down
Loading