Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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-elasticsearch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,20 @@ const domain = new es.Domain(this, 'Domain', {
},
});
```

## Custom endpoint

Custom endpoints can be configured to reach the ES domain under a custom domain name.

```ts
new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_7,
customEndpoint: {
domainName: 'search.example.com',
},
});
```

It is also possible to specify a custom certificate instead of the auto-generated one.

Additionally, an automatic CNAME-Record is created if a hosted zone is provided for the custom endpoint
56 changes: 56 additions & 0 deletions packages/@aws-cdk/aws-elasticsearch/lib/domain.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { URL } from 'url';

import * as acm from '@aws-cdk/aws-certificatemanager';
import { Metric, MetricOptions, Statistic } from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as logs from '@aws-cdk/aws-logs';
import * as route53 from '@aws-cdk/aws-route53';
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
Expand Down Expand Up @@ -395,6 +397,28 @@ export interface AdvancedSecurityOptions {
readonly masterUserPassword?: cdk.SecretValue;
}

/**
* Configures a custom domain endpoint for the ES domain
*/
export interface CustomEndpointOptions {
/**
* The custom domain name to assign
*/
readonly domainName: string;

/**
* The certificate to use
* @default - create a new one
*/
readonly certificate?: acm.ICertificate;

/**
* The hosted zone in Route53 to create the CNAME record in
* @default - do not create a CNAME
*/
readonly hostedZone?: route53.IHostedZone;
}

/**
* Properties for an AWS Elasticsearch Domain.
*/
Expand Down Expand Up @@ -545,6 +569,13 @@ export interface DomainProps {
*/
readonly enableVersionUpgrade?: boolean;

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

/**
Expand Down Expand Up @@ -1547,6 +1578,18 @@ export class Domain extends DomainBase implements IDomain {
};
}

let customEndpointCertificate: acm.ICertificate | undefined;
if (props.customEndpoint) {
if (props.customEndpoint.certificate) {
customEndpointCertificate = props.customEndpoint.certificate;
} else {
customEndpointCertificate = new acm.Certificate(this, 'CustomEndpointCertificate', {
domainName: props.customEndpoint.domainName,
validation: props.customEndpoint.hostedZone ? acm.CertificateValidation.fromDns(props.customEndpoint.hostedZone) : undefined,
});
}
}

// Create the domain
this.domain = new CfnDomain(this, 'Resource', {
domainName: this.physicalName,
Expand Down Expand Up @@ -1602,6 +1645,11 @@ export class Domain extends DomainBase implements IDomain {
domainEndpointOptions: {
enforceHttps,
tlsSecurityPolicy: props.tlsSecurityPolicy ?? TLSSecurityPolicy.TLS_1_0,
...props.customEndpoint && {
customEndpointEnabled: true,
customEndpoint: props.customEndpoint.domainName,
customEndpointCertificateArn: customEndpointCertificate!.certificateArn,
},
},
advancedSecurityOptions: advancedSecurityEnabled
? {
Expand Down Expand Up @@ -1637,6 +1685,14 @@ export class Domain extends DomainBase implements IDomain {
resourceName: this.physicalName,
});

if (props.customEndpoint?.hostedZone) {
new route53.CnameRecord(this, 'CnameRecord', {
recordName: props.customEndpoint.domainName,
zone: props.customEndpoint.hostedZone,
domainName: this.domainEndpoint,
});
}

const accessPolicyStatements: iam.PolicyStatement[] | undefined = unsignedBasicAuthEnabled
? (props.accessPolicies ?? []).concat(unsignedAccessPolicy)
: props.accessPolicies;
Expand Down
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-elasticsearch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,27 @@
"pkglint": "0.0.0"
},
"dependencies": {
"@aws-cdk/aws-certificatemanager": "0.0.0",
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-ec2": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-kms": "0.0.0",
"@aws-cdk/aws-logs": "0.0.0",
"@aws-cdk/aws-route53": "0.0.0",
"@aws-cdk/aws-secretsmanager": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.2.0"
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-certificatemanager": "0.0.0",
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-ec2": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-kms": "0.0.0",
"@aws-cdk/aws-logs": "0.0.0",
"@aws-cdk/aws-route53": "0.0.0",
"@aws-cdk/aws-secretsmanager": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"@aws-cdk/core": "0.0.0",
Expand Down
130 changes: 130 additions & 0 deletions packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable jest/expect-expect */
import '@aws-cdk/assert/jest';
import * as assert from '@aws-cdk/assert';
import * as acm from '@aws-cdk/aws-certificatemanager';
import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch';
import { Subnet, Vpc, EbsDeviceVolumeType } from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as logs from '@aws-cdk/aws-logs';
import * as route53 from '@aws-cdk/aws-route53';
import { App, Stack, Duration, SecretValue } from '@aws-cdk/core';
import { Domain, ElasticsearchVersion } from '../lib';

Expand Down Expand Up @@ -987,6 +989,134 @@ describe('advanced security options', () => {
});
});

describe('custom endpoints', () => {
const customDomainName = 'search.example.com';

test('custom domain without hosted zone and default cert', () => {
new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_1,
nodeToNodeEncryption: true,
enforceHttps: true,
customEndpoint: {
domainName: customDomainName,
},
});

expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', {
DomainEndpointOptions: {
EnforceHTTPS: true,
CustomEndpointEnabled: true,
CustomEndpoint: customDomainName,
CustomEndpointCertificateArn: {
Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate
},
},
});
expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', {
DomainName: customDomainName,
ValidationMethod: 'EMAIL',
});
});

test('custom domain with hosted zone and default cert', () => {
const zone = new route53.HostedZone(stack, 'DummyZone', { zoneName: 'example.com' });
new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_1,
nodeToNodeEncryption: true,
enforceHttps: true,
customEndpoint: {
domainName: customDomainName,
hostedZone: zone,
},
});

expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', {
DomainEndpointOptions: {
EnforceHTTPS: true,
CustomEndpointEnabled: true,
CustomEndpoint: customDomainName,
CustomEndpointCertificateArn: {
Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate
},
},
});
expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', {
DomainName: customDomainName,
DomainValidationOptions: [
{
DomainName: customDomainName,
HostedZoneId: {
Ref: 'DummyZone03E0FE81',
},
},
],
ValidationMethod: 'DNS',
});
expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', {
Name: 'search.example.com.',
Type: 'CNAME',
HostedZoneId: {
Ref: 'DummyZone03E0FE81',
},
ResourceRecords: [
{
'Fn::GetAtt': [
'Domain66AC69E0',
'DomainEndpoint',
],
},
],
});
});

test('custom domain with hosted zone and given cert', () => {
const zone = new route53.HostedZone(stack, 'DummyZone', {
zoneName: 'example.com',
});
const certificate = new acm.Certificate(stack, 'DummyCert', {
domainName: customDomainName,
});

new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_1,
nodeToNodeEncryption: true,
enforceHttps: true,
customEndpoint: {
domainName: customDomainName,
hostedZone: zone,
certificate,
},
});

expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', {
DomainEndpointOptions: {
EnforceHTTPS: true,
CustomEndpointEnabled: true,
CustomEndpoint: customDomainName,
CustomEndpointCertificateArn: {
Ref: 'DummyCertFA37670B',
},
},
});
expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', {
Name: 'search.example.com.',
Type: 'CNAME',
HostedZoneId: {
Ref: 'DummyZone03E0FE81',
},
ResourceRecords: [
{
'Fn::GetAtt': [
'Domain66AC69E0',
'DomainEndpoint',
],
},
],
});
});

});

describe('custom error responses', () => {

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