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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(elasticsearch): graduate to stable 馃殌 #13900

Merged
merged 8 commits into from Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-elasticsearch/README.md
Expand Up @@ -146,6 +146,33 @@ This sets up the domain with node to node encryption and encryption at
rest. You can also choose to supply your own KMS key to use for encryption at
rest.

## VPC Support

Elasticsearch domains can be placed inside a VPC, providing a secure communication between Amazon ES and other services within the VPC without the need for an internet gateway, NAT device, or VPN connection.

> Visit [VPC Support for Amazon Elasticsearch Service Domains](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html) for more details.

```ts
const vpc = new ec2.Vpc(this, 'Vpc');
const domainProps: es.DomainProps = {
version: es.ElasticsearchVersion.V7_1,
removalPolicy: RemovalPolicy.DESTROY,
vpc,
// must be enabled since our VPC contains multiple private subnets.
zoneAwareness: {
enabled: true,
},
capacity: {
// must be an even number since the default az count is 2.
dataNodes: 2,
},
};
new es.Domain(this, 'Domain', domainProps);
```

In addition, you can use the `vpcSubnets` property to control which specific subnets will be used, and the `securityGroups` property to control
which security groups will be attached to the domain. By default, CDK will select all *private* subnets in the VPC, and create one dedicated security group.

## Metrics

Helper methods exist to access common domain metrics for example:
Expand Down
113 changes: 71 additions & 42 deletions packages/@aws-cdk/aws-elasticsearch/lib/domain.ts
Expand Up @@ -331,32 +331,6 @@ export interface CognitoOptions {
readonly userPoolId: string;
}

/**
* The virtual private cloud (VPC) configuration for the Amazon ES domain. For
* more information, see [VPC Support for Amazon Elasticsearch Service
* Domains](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html)
* in the Amazon Elasticsearch Service Developer Guide.
*/
export interface VpcOptions {
/**
* The list of security groups that are associated with the VPC endpoints
* for the domain. If you don't provide a security group ID, Amazon ES uses
* the default security group for the VPC. To learn more, see [Security Groups for your VPC]
* (https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html) in the Amazon VPC
* User Guide.
*/
readonly securityGroups: ec2.ISecurityGroup[];

/**
* Provide one subnet for each Availability Zone that your domain uses. For
* example, you must specify three subnet IDs for a three Availability Zone
* domain. To learn more, see [VPCs and Subnets]
* (https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html) in the
* Amazon VPC User Guide.
*/
readonly subnets: ec2.ISubnet[];
}

/**
* The minimum TLS version required for traffic to the domain.
*/
Expand Down Expand Up @@ -513,14 +487,35 @@ export interface DomainProps {
readonly automatedSnapshotStartHour?: number;

/**
* The virtual private cloud (VPC) configuration for the Amazon ES domain. For
* more information, see [VPC Support for Amazon Elasticsearch Service
* Domains](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html)
* in the Amazon Elasticsearch Service Developer Guide.
* Place the domain inside this VPC.
*
* @see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html
* @default - Domain is not placed in a VPC.
*/
readonly vpc?: ec2.IVpc;

/**
* The list of security groups that are associated with the VPC endpoints
* for the domain.
*
* Only used if `vpc` is specified.
*
* @default - VPC not used
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html
* @default - One new security group is created.
*/
readonly vpcOptions?: VpcOptions;
readonly securityGroups?: ec2.ISecurityGroup[];

/**
* The specific vpc subnets the domain will be placed in. You must provide one subnet for each Availability Zone
* that your domain uses. For example, you must specify three subnet IDs for a three Availability Zone
* domain.
*
* Only used if `vpc` is specified.
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html
* @default - All private subnets.
*/
readonly vpcSubnets?: ec2.SubnetSelection[];

/**
* True to require that all traffic to the domain arrive over HTTPS.
Expand Down Expand Up @@ -719,7 +714,7 @@ export interface IDomain extends cdk.IResource {
*
* @default maximum over 1 minute
*/
metricClusterIndexWriteBlocked(props?: MetricOptions): Metric;
metricClusterIndexWritesBlocked(props?: MetricOptions): Metric;
Copy link
Contributor Author

Choose a reason for hiding this comment

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


/**
* Metric for the number of nodes.
Expand Down Expand Up @@ -1002,8 +997,8 @@ abstract class DomainBase extends cdk.Resource implements IDomain {
*
* @default maximum over 1 minute
*/
public metricClusterIndexWriteBlocked(props?: MetricOptions): Metric {
return this.metric('ClusterIndexWriteBlocked', {
public metricClusterIndexWritesBlocked(props?: MetricOptions): Metric {
return this.metric('ClusterIndexWritesBlocked', {
statistic: Statistic.MAXIMUM,
period: cdk.Duration.minutes(1),
...props,
Expand Down Expand Up @@ -1177,7 +1172,7 @@ export interface DomainAttributes {
/**
* Provides an Elasticsearch domain.
*/
export class Domain extends DomainBase implements IDomain {
export class Domain extends DomainBase implements IDomain, ec2.IConnectable {
/**
* Creates a Domain construct that represents an external domain via domain endpoint.
*
Expand Down Expand Up @@ -1264,6 +1259,8 @@ export class Domain extends DomainBase implements IDomain {

private readonly domain: CfnDomain;

private readonly _connections: ec2.Connections | undefined;

constructor(scope: Construct, id: string, props: DomainProps) {
super(scope, id, {
physicalName: props.domainName,
Expand Down Expand Up @@ -1300,11 +1297,23 @@ export class Domain extends DomainBase implements IDomain {
props.zoneAwareness?.enabled ??
props.zoneAwareness?.availabilityZoneCount != null;


let securityGroups: ec2.ISecurityGroup[] | undefined;
let subnets: ec2.ISubnet[] | undefined;

if (props.vpc) {
subnets = selectSubnets(props.vpc, props.vpcSubnets ?? [{ subnetType: ec2.SubnetType.PRIVATE }]);
securityGroups = props.securityGroups ?? [new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
description: `Security group for domain ${this.node.id}`,
})];
this._connections = new ec2.Connections({ securityGroups });
}

// If VPC options are supplied ensure that the number of subnets matches the number AZ
if (props.vpcOptions != null && zoneAwarenessEnabled &&
new Set(props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone)).size < availabilityZoneCount) {
if (subnets && zoneAwarenessEnabled && new Set(subnets.map((subnet) => subnet.availabilityZone)).size < availabilityZoneCount) {
throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using');
};
}

if ([dedicatedMasterType, instanceType, warmType].some(t => !t.endsWith('.elasticsearch'))) {
throw new Error('Master, data and UltraWarm node instance types must end with ".elasticsearch".');
Expand Down Expand Up @@ -1491,10 +1500,11 @@ export class Domain extends DomainBase implements IDomain {
}

let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined;
if (props.vpcOptions) {

if (securityGroups && subnets) {
cfnVpcOptions = {
securityGroupIds: props.vpcOptions.securityGroups.map((sg) => sg.securityGroupId),
subnetIds: props.vpcOptions.subnets.map((subnet) => subnet.subnetId),
securityGroupIds: securityGroups.map((sg) => sg.securityGroupId),
subnetIds: subnets.map((subnet) => subnet.subnetId),
};
}

Expand Down Expand Up @@ -1730,6 +1740,17 @@ export class Domain extends DomainBase implements IDomain {
accessPolicy.node.addDependency(this.domain);
}
}

/**
* Manages network connections to the domain. This will throw an error in case the domain
* is not placed inside a VPC.
*/
public get connections(): ec2.Connections {
if (!this._connections) {
throw new Error("Connections are only available on VPC enabled domains. Use the 'vpc' property to place a domain inside a VPC");
}
return this._connections;
}
}

/**
Expand Down Expand Up @@ -1778,3 +1799,11 @@ function parseVersion(version: ElasticsearchVersion): number {
throw new Error(`Invalid Elasticsearch version: ${versionStr}. Version string needs to start with major and minor version (x.y).`);
}
}

function selectSubnets(vpc: ec2.IVpc, vpcSubnets: ec2.SubnetSelection[]): ec2.ISubnet[] {
const selected = [];
for (const selection of vpcSubnets) {
selected.push(...vpc.selectSubnets(selection).subnets);
}
return selected;
}
125 changes: 86 additions & 39 deletions packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts
Expand Up @@ -3,7 +3,7 @@ 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 { Vpc, EbsDeviceVolumeType, SecurityGroup } 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';
Expand All @@ -30,6 +30,84 @@ const readWriteActions = [
...writeActions,
];

test('connections throws if domain is placed inside a vpc', () => {

expect(() => {
new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_1,
}).connections;
}).toThrowError("Connections are only available on VPC enabled domains. Use the 'vpc' property to place a domain inside a VPC");
});

test('subnets and security groups can be provided when vpc is used', () => {

const vpc = new Vpc(stack, 'Vpc');
const securityGroup = new SecurityGroup(stack, 'CustomSecurityGroup', {
vpc,
});
const domain = new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_9,
vpc,
vpcSubnets: [{ subnets: [vpc.privateSubnets[0]] }],
securityGroups: [securityGroup],
});

expect(domain.connections.securityGroups[0].securityGroupId).toEqual(securityGroup.securityGroupId);
expect(stack).toHaveResource('AWS::Elasticsearch::Domain', {
VPCOptions: {
SecurityGroupIds: [
{
'Fn::GetAtt': [
'CustomSecurityGroupE5E500E5',
'GroupId',
],
},
],
SubnetIds: [
{
Ref: 'VpcPrivateSubnet1Subnet536B997A',
},
],
},
});

});

test('default subnets and security group when vpc is used', () => {

const vpc = new Vpc(stack, 'Vpc');
const domain = new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_9,
vpc,
});

expect(stack.resolve(domain.connections.securityGroups[0].securityGroupId)).toEqual({ 'Fn::GetAtt': ['DomainSecurityGroup48AA5FD6', 'GroupId'] });
expect(stack).toHaveResource('AWS::Elasticsearch::Domain', {
VPCOptions: {
SecurityGroupIds: [
{
'Fn::GetAtt': [
'DomainSecurityGroup48AA5FD6',
'GroupId',
],
},
],
SubnetIds: [
{
Ref: 'VpcPrivateSubnet1Subnet536B997A',
},
{
Ref: 'VpcPrivateSubnet2Subnet3788AAA1',
},
{
Ref: 'VpcPrivateSubnet3SubnetF258B56E',
},
],
},
});

});

test('default removalpolicy is retain', () => {
new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_1,
Expand Down Expand Up @@ -709,8 +787,8 @@ describe('metrics', () => {

test('Can use metricClusterIndexWriteBlocked on an Elasticsearch Domain', () => {
testMetric(
(domain) => domain.metricClusterIndexWriteBlocked(),
'ClusterIndexWriteBlocked',
(domain) => domain.metricClusterIndexWritesBlocked(),
'ClusterIndexWritesBlocked',
Statistic.MAXIMUM,
Duration.minutes(1),
);
Expand Down Expand Up @@ -1150,24 +1228,17 @@ describe('custom endpoints', () => {
describe('custom error responses', () => {

test('error when availabilityZoneCount does not match vpcOptions.subnets length', () => {
const vpc = new Vpc(stack, 'Vpc');
const vpc = new Vpc(stack, 'Vpc', {
maxAzs: 1,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simpler way to enforce a single AZ.

});

expect(() => new Domain(stack, 'Domain', {
version: ElasticsearchVersion.V7_4,
zoneAwareness: {
enabled: true,
availabilityZoneCount: 2,
},
vpcOptions: {
subnets: [
new Subnet(stack, 'Subnet', {
availabilityZone: 'testaz',
cidrBlock: vpc.vpcCidrBlock,
vpcId: vpc.vpcId,
}),
],
securityGroups: [],
},
vpc,
})).toThrow(/you need to provide a subnet for each AZ you are using/);
});

Expand Down Expand Up @@ -1357,31 +1428,7 @@ describe('custom error responses', () => {

expect(() => new Domain(stack, 'Domain1', {
version: ElasticsearchVersion.V7_4,
vpcOptions: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This whole config wasn't really needed to assert the test, which validates the availabilityZoneCount property below.

subnets: [
new Subnet(stack, 'Subnet1', {
availabilityZone: 'testaz1',
cidrBlock: vpc.vpcCidrBlock,
vpcId: vpc.vpcId,
}),
new Subnet(stack, 'Subnet2', {
availabilityZone: 'testaz2',
cidrBlock: vpc.vpcCidrBlock,
vpcId: vpc.vpcId,
}),
new Subnet(stack, 'Subnet3', {
availabilityZone: 'testaz3',
cidrBlock: vpc.vpcCidrBlock,
vpcId: vpc.vpcId,
}),
new Subnet(stack, 'Subnet4', {
availabilityZone: 'testaz4',
cidrBlock: vpc.vpcCidrBlock,
vpcId: vpc.vpcId,
}),
],
securityGroups: [],
},
vpc,
zoneAwareness: {
availabilityZoneCount: 4,
},
Expand Down