Skip to content

Commit f67ab86

Browse files
authored
feat(elasticsearch): add custom endpoint options (#12904)
Closes #12261 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f864e46 commit f67ab86

File tree

4 files changed

+207
-0
lines changed

4 files changed

+207
-0
lines changed

packages/@aws-cdk/aws-elasticsearch/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,20 @@ const domain = new es.Domain(this, 'Domain', {
225225
},
226226
});
227227
```
228+
229+
## Custom endpoint
230+
231+
Custom endpoints can be configured to reach the ES domain under a custom domain name.
232+
233+
```ts
234+
new Domain(stack, 'Domain', {
235+
version: ElasticsearchVersion.V7_7,
236+
customEndpoint: {
237+
domainName: 'search.example.com',
238+
},
239+
});
240+
```
241+
242+
It is also possible to specify a custom certificate instead of the auto-generated one.
243+
244+
Additionally, an automatic CNAME-Record is created if a hosted zone is provided for the custom endpoint

packages/@aws-cdk/aws-elasticsearch/lib/domain.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { URL } from 'url';
22

3+
import * as acm from '@aws-cdk/aws-certificatemanager';
34
import { Metric, MetricOptions, Statistic } from '@aws-cdk/aws-cloudwatch';
45
import * as ec2 from '@aws-cdk/aws-ec2';
56
import * as iam from '@aws-cdk/aws-iam';
67
import * as kms from '@aws-cdk/aws-kms';
78
import * as logs from '@aws-cdk/aws-logs';
9+
import * as route53 from '@aws-cdk/aws-route53';
810
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
911
import * as cdk from '@aws-cdk/core';
1012
import { Construct } from 'constructs';
@@ -395,6 +397,28 @@ export interface AdvancedSecurityOptions {
395397
readonly masterUserPassword?: cdk.SecretValue;
396398
}
397399

400+
/**
401+
* Configures a custom domain endpoint for the ES domain
402+
*/
403+
export interface CustomEndpointOptions {
404+
/**
405+
* The custom domain name to assign
406+
*/
407+
readonly domainName: string;
408+
409+
/**
410+
* The certificate to use
411+
* @default - create a new one
412+
*/
413+
readonly certificate?: acm.ICertificate;
414+
415+
/**
416+
* The hosted zone in Route53 to create the CNAME record in
417+
* @default - do not create a CNAME
418+
*/
419+
readonly hostedZone?: route53.IHostedZone;
420+
}
421+
398422
/**
399423
* Properties for an AWS Elasticsearch Domain.
400424
*/
@@ -545,6 +569,13 @@ export interface DomainProps {
545569
*/
546570
readonly enableVersionUpgrade?: boolean;
547571

572+
/**
573+
* To configure a custom domain configure these options
574+
*
575+
* If you specify a Route53 hosted zone it will create a CNAME record and use DNS validation for the certificate
576+
* @default - no custom domain endpoint will be configured
577+
*/
578+
readonly customEndpoint?: CustomEndpointOptions;
548579
}
549580

550581
/**
@@ -1547,6 +1578,18 @@ export class Domain extends DomainBase implements IDomain {
15471578
};
15481579
}
15491580

1581+
let customEndpointCertificate: acm.ICertificate | undefined;
1582+
if (props.customEndpoint) {
1583+
if (props.customEndpoint.certificate) {
1584+
customEndpointCertificate = props.customEndpoint.certificate;
1585+
} else {
1586+
customEndpointCertificate = new acm.Certificate(this, 'CustomEndpointCertificate', {
1587+
domainName: props.customEndpoint.domainName,
1588+
validation: props.customEndpoint.hostedZone ? acm.CertificateValidation.fromDns(props.customEndpoint.hostedZone) : undefined,
1589+
});
1590+
}
1591+
}
1592+
15501593
// Create the domain
15511594
this.domain = new CfnDomain(this, 'Resource', {
15521595
domainName: this.physicalName,
@@ -1602,6 +1645,11 @@ export class Domain extends DomainBase implements IDomain {
16021645
domainEndpointOptions: {
16031646
enforceHttps,
16041647
tlsSecurityPolicy: props.tlsSecurityPolicy ?? TLSSecurityPolicy.TLS_1_0,
1648+
...props.customEndpoint && {
1649+
customEndpointEnabled: true,
1650+
customEndpoint: props.customEndpoint.domainName,
1651+
customEndpointCertificateArn: customEndpointCertificate!.certificateArn,
1652+
},
16051653
},
16061654
advancedSecurityOptions: advancedSecurityEnabled
16071655
? {
@@ -1637,6 +1685,14 @@ export class Domain extends DomainBase implements IDomain {
16371685
resourceName: this.physicalName,
16381686
});
16391687

1688+
if (props.customEndpoint?.hostedZone) {
1689+
new route53.CnameRecord(this, 'CnameRecord', {
1690+
recordName: props.customEndpoint.domainName,
1691+
zone: props.customEndpoint.hostedZone,
1692+
domainName: this.domainEndpoint,
1693+
});
1694+
}
1695+
16401696
const accessPolicyStatements: iam.PolicyStatement[] | undefined = unsignedBasicAuthEnabled
16411697
? (props.accessPolicies ?? []).concat(unsignedAccessPolicy)
16421698
: props.accessPolicies;

packages/@aws-cdk/aws-elasticsearch/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,27 @@
7878
"pkglint": "0.0.0"
7979
},
8080
"dependencies": {
81+
"@aws-cdk/aws-certificatemanager": "0.0.0",
8182
"@aws-cdk/aws-cloudwatch": "0.0.0",
8283
"@aws-cdk/aws-ec2": "0.0.0",
8384
"@aws-cdk/aws-iam": "0.0.0",
8485
"@aws-cdk/aws-kms": "0.0.0",
8586
"@aws-cdk/aws-logs": "0.0.0",
87+
"@aws-cdk/aws-route53": "0.0.0",
8688
"@aws-cdk/aws-secretsmanager": "0.0.0",
8789
"@aws-cdk/custom-resources": "0.0.0",
8890
"@aws-cdk/core": "0.0.0",
8991
"constructs": "^3.2.0"
9092
},
9193
"homepage": "https://github.com/aws/aws-cdk",
9294
"peerDependencies": {
95+
"@aws-cdk/aws-certificatemanager": "0.0.0",
9396
"@aws-cdk/aws-cloudwatch": "0.0.0",
9497
"@aws-cdk/aws-ec2": "0.0.0",
9598
"@aws-cdk/aws-iam": "0.0.0",
9699
"@aws-cdk/aws-kms": "0.0.0",
97100
"@aws-cdk/aws-logs": "0.0.0",
101+
"@aws-cdk/aws-route53": "0.0.0",
98102
"@aws-cdk/aws-secretsmanager": "0.0.0",
99103
"@aws-cdk/custom-resources": "0.0.0",
100104
"@aws-cdk/core": "0.0.0",

packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* eslint-disable jest/expect-expect */
22
import '@aws-cdk/assert/jest';
33
import * as assert from '@aws-cdk/assert';
4+
import * as acm from '@aws-cdk/aws-certificatemanager';
45
import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch';
56
import { Subnet, Vpc, EbsDeviceVolumeType } from '@aws-cdk/aws-ec2';
67
import * as iam from '@aws-cdk/aws-iam';
78
import * as kms from '@aws-cdk/aws-kms';
89
import * as logs from '@aws-cdk/aws-logs';
10+
import * as route53 from '@aws-cdk/aws-route53';
911
import { App, Stack, Duration, SecretValue } from '@aws-cdk/core';
1012
import { Domain, ElasticsearchVersion } from '../lib';
1113

@@ -987,6 +989,134 @@ describe('advanced security options', () => {
987989
});
988990
});
989991

992+
describe('custom endpoints', () => {
993+
const customDomainName = 'search.example.com';
994+
995+
test('custom domain without hosted zone and default cert', () => {
996+
new Domain(stack, 'Domain', {
997+
version: ElasticsearchVersion.V7_1,
998+
nodeToNodeEncryption: true,
999+
enforceHttps: true,
1000+
customEndpoint: {
1001+
domainName: customDomainName,
1002+
},
1003+
});
1004+
1005+
expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', {
1006+
DomainEndpointOptions: {
1007+
EnforceHTTPS: true,
1008+
CustomEndpointEnabled: true,
1009+
CustomEndpoint: customDomainName,
1010+
CustomEndpointCertificateArn: {
1011+
Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate
1012+
},
1013+
},
1014+
});
1015+
expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', {
1016+
DomainName: customDomainName,
1017+
ValidationMethod: 'EMAIL',
1018+
});
1019+
});
1020+
1021+
test('custom domain with hosted zone and default cert', () => {
1022+
const zone = new route53.HostedZone(stack, 'DummyZone', { zoneName: 'example.com' });
1023+
new Domain(stack, 'Domain', {
1024+
version: ElasticsearchVersion.V7_1,
1025+
nodeToNodeEncryption: true,
1026+
enforceHttps: true,
1027+
customEndpoint: {
1028+
domainName: customDomainName,
1029+
hostedZone: zone,
1030+
},
1031+
});
1032+
1033+
expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', {
1034+
DomainEndpointOptions: {
1035+
EnforceHTTPS: true,
1036+
CustomEndpointEnabled: true,
1037+
CustomEndpoint: customDomainName,
1038+
CustomEndpointCertificateArn: {
1039+
Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate
1040+
},
1041+
},
1042+
});
1043+
expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', {
1044+
DomainName: customDomainName,
1045+
DomainValidationOptions: [
1046+
{
1047+
DomainName: customDomainName,
1048+
HostedZoneId: {
1049+
Ref: 'DummyZone03E0FE81',
1050+
},
1051+
},
1052+
],
1053+
ValidationMethod: 'DNS',
1054+
});
1055+
expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', {
1056+
Name: 'search.example.com.',
1057+
Type: 'CNAME',
1058+
HostedZoneId: {
1059+
Ref: 'DummyZone03E0FE81',
1060+
},
1061+
ResourceRecords: [
1062+
{
1063+
'Fn::GetAtt': [
1064+
'Domain66AC69E0',
1065+
'DomainEndpoint',
1066+
],
1067+
},
1068+
],
1069+
});
1070+
});
1071+
1072+
test('custom domain with hosted zone and given cert', () => {
1073+
const zone = new route53.HostedZone(stack, 'DummyZone', {
1074+
zoneName: 'example.com',
1075+
});
1076+
const certificate = new acm.Certificate(stack, 'DummyCert', {
1077+
domainName: customDomainName,
1078+
});
1079+
1080+
new Domain(stack, 'Domain', {
1081+
version: ElasticsearchVersion.V7_1,
1082+
nodeToNodeEncryption: true,
1083+
enforceHttps: true,
1084+
customEndpoint: {
1085+
domainName: customDomainName,
1086+
hostedZone: zone,
1087+
certificate,
1088+
},
1089+
});
1090+
1091+
expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', {
1092+
DomainEndpointOptions: {
1093+
EnforceHTTPS: true,
1094+
CustomEndpointEnabled: true,
1095+
CustomEndpoint: customDomainName,
1096+
CustomEndpointCertificateArn: {
1097+
Ref: 'DummyCertFA37670B',
1098+
},
1099+
},
1100+
});
1101+
expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', {
1102+
Name: 'search.example.com.',
1103+
Type: 'CNAME',
1104+
HostedZoneId: {
1105+
Ref: 'DummyZone03E0FE81',
1106+
},
1107+
ResourceRecords: [
1108+
{
1109+
'Fn::GetAtt': [
1110+
'Domain66AC69E0',
1111+
'DomainEndpoint',
1112+
],
1113+
},
1114+
],
1115+
});
1116+
});
1117+
1118+
});
1119+
9901120
describe('custom error responses', () => {
9911121

9921122
test('error when availabilityZoneCount does not match vpcOptions.subnets length', () => {

0 commit comments

Comments
 (0)