From 6a27aef358a913d6da422c46f6ee14c163de78cb Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Thu, 4 Jun 2020 19:00:40 +1000 Subject: [PATCH 01/45] feat(elasticsearch): Add l2 cdk construct for Elasticsearch domain Add a level 2 cdk construct for Elasticsearch Domain. Also add a custom resource for setting up log group resource policies. This is required to allow Elasticsearch to log to the log groups. --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 611 ++++++++++++++++++ .../@aws-cdk/aws-elasticsearch/lib/index.ts | 2 + .../lib/log-group-resource-policy.ts | 56 ++ .../@aws-cdk/aws-elasticsearch/package.json | 12 + 4 files changed, 681 insertions(+) create mode 100644 packages/@aws-cdk/aws-elasticsearch/lib/domain.ts create mode 100644 packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts new file mode 100644 index 0000000000000..7c8578cf9897d --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -0,0 +1,611 @@ +import * as cdk from '@aws-cdk/core'; +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 { Metric, MetricOptions, Statistic } from '@aws-cdk/aws-cloudwatch'; +import { EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { LogGroupResourcePolicy } from './log-group-resource-policy'; +import { CfnDomain } from './elasticsearch.generated'; + +export interface ClusterConfig { + readonly masterNodes: number; + readonly masterNodeInstanceType: string; + readonly dataNodes: number; + readonly dataNodeInstanceType: string; + /** + * The number of AZs that you want the domain to use. When you enable zone + * awareness, Amazon ES allocates the nodes and replica index shards that + * belong to a cluster across the specified number of Availability Zones (AZs) + * in the same region to prevent data loss and minimize downtime in the event + * of node or data center failure. Don't enable zone awareness if your cluster + * has no replica index shards or is a single-node cluster. + * + * @default - Zone awareness is not enabled. + */ + readonly availabilityZoneCount?: number; +} + +export interface EbsOptions { + readonly iops?: number; + readonly volumeSize: number; + readonly volumeType: EbsDeviceVolumeType; +} + +export interface LoggingOptions { + /** + * Specify if slow search logging should be set up. + * + * @default - false + */ + readonly slowSearchLogEnabed?: boolean; + + /** + * Log slow searches to this log group. + * + * @default - a new log group is created if slow search logging is enabled + */ + readonly slowSearchLogGroup?: logs.LogGroup; + + /** + * Specify if slow index logging should be set up. + * + * @default - false + */ + readonly slowIndexLogEnabed?: boolean; + + /** + * Log slow indecies to this log group. + * + * @default - a new log group is created if slow index logging is enabled + */ + readonly slowIndexLogGroup?: logs.LogGroup; + + /** + * Specify if Elasticsearch application logging should be set up. + * + * @default - false + */ + readonly appLogEnabed?: boolean; + + /** + * Log Elasticsearch application logs to this log group. + * + * @default - a new log group is created if app logging is enabled + */ + readonly appLogGroup?: logs.LogGroup; +} + +/** + * Whether the domain should encrypt data at rest, and if so, the AWS Key + * Management Service (KMS) key to use. Can only be used to create a new domain, + * not update an existing one + */ +export interface EncryptionAtRestOptions { + /** + * Specify true to enable encryption at rest. + */ + readonly enabled?: boolean; + + /** + * Supply if using KMS key for encryption at rest. + */ + readonly kmsKey?: kms.Key; +} + +/** + * Properties for a AWS Elasticsearch Domain. + */ +export interface DomainProps { + /** + * Domain Access policies. + * + * @default - No access policies. + */ + readonly accessPolicies?: PolicyStatement[]; + + /** + * Additional options to specify for the Amazon ES domain. + * + * @default - no advanced options are specified + */ + readonly advancedOptions?: { [key: string]: (string) }; + + /** + * `AWS::Elasticsearch::Domain.CognitoOptions` + */ + readonly cognitoOptions?: CfnDomain.CognitoOptionsProperty | cdk.IResolvable; + + /** + * Enforces a particular physical domain name. + * + * @default + */ + readonly domainName?: string; + + /** + * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that + * are attached to data nodes in the Amazon ES domain. + * + * @default - No EBS volumes attached. + */ + readonly ebsOptions?: EbsOptions; + + /** + * The cluster configuration for the Amazon ES domain. + * + */ + readonly clusterConfig: ClusterConfig; + + /** + * The Elasticsearch Version + * + */ + readonly elasticsearchVersion: string; + + /** + * Encryption at rest options for the cluster. + * + * @default - No encryption at rest + */ + readonly encryptionAtRestOptions?: EncryptionAtRestOptions; + + + /** + * Configuration log publishing configuration options. + * + * @default - No logs are published + */ + readonly logPublishingOptions?: LoggingOptions; + + + /** + * Specify true to enable node to node encryption. + * + * @default - Node to node encryption is not enabled. + */ + readonly nodeToNodeEncryptionEnabled?: boolean; + + /** + * The hour in UTC during which the service takes an automated daily snapshot + * of the indices in the Amazon ES domain. Only applies for Elasticsearch + * versions below 5.3. + */ + readonly automatedSnapshotStartHour?: number; + + /** + * `AWS::Elasticsearch::Domain.VPCOptions` + */ + readonly vpcOptions?: CfnDomain.VPCOptionsProperty | cdk.IResolvable; +} + +/** + * An interface that represents an Elasticsearch domain - either created with the CDK, or an existing one. + */ +export interface IDomain extends cdk.IResource { + /** + * Arn of the Elasticsearch table. + * + * @attribute + */ + readonly domainArn: string; + + /** + * Domain name of the Elasticsearch domain. + * + * @attribute + */ + readonly domainName: string; + + /** + * Endpoint of the Elasticsearch domain. + * + * @attribute + */ + readonly domainEndpoint: string; + + /** + * Return the given named metric for this Domain. + */ + metric(metricName: string, clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for the time the cluster status is red. + * + * @default maximum over a minute + */ + metricClusterStatusRed(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for the time the cluster status is yellow. + * + * @default maximum over a minute + */ + metricClusterStatusYellow(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for the storage space of nodes in the cluster. + * + * @default minimum over a minute + */ + metricFreeStorageSpace(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for the cluster blocking index writes. + * + * @default maximum over 5 minutes + */ + metricClusterIndexWriteBlocked(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for the number of nodes. + * + * @default minimum over 1 hour + */ + metricNodes(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for automated snapshot failures. + * + * @default maximum over a minute + */ + metricAutomatedSnapshotFailure(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for CPU utilization. + * + * @default maximum over a minute + */ + metricCPUUtilization(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for JVM memory pressure. + * + * @default maximum over a minute + */ + metricJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for master CPU utilization. + * + * @default maximum over a minute + */ + metricMasterCPUUtilization(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for master JVM memory pressure. + * + * @default maximum over a minute + */ + metricMasterJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for KMS key errors. + * + * @default maximum over a minute + */ + metricKMSKeyError(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for KMS key being inaccessible. + * + * @default maximum over a minute + */ + metricKMSKeyInaccessible(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for number of searchable documents. + * + * @default maximum over a minute + */ + metricSearchableDocuments(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for search latency. + * + * @default maximum over a minute + */ + metricSearchLatency(clientId: string, props?: MetricOptions): Metric; + + /** + * Metric for indexing latency. + * + * @default maximum over a minute + */ + metricIndexingLatency(clientId: string, props?: MetricOptions): Metric; +} +export class Domain extends cdk.Resource implements IDomain { + /** + * @attribute + */ + public readonly domainArn: string; + + /** + * @attribute + */ + public readonly domainName: string; + + /** + * @attribute + */ + public readonly domainEndpoint: string; + + + private readonly domain: CfnDomain; + + private readonly slowSearchLogGroup?: logs.LogGroup; + + private readonly slowIndexLogGroup?: logs.LogGroup; + + private readonly appLogGroup?: logs.LogGroup; + + constructor(scope: cdk.Construct, id: string, props: DomainProps) { + super(scope, id, { + physicalName: props.domainName, + }); + + // Setup logging + const logGroups: logs.LogGroup[] = []; + + if (props.logPublishingOptions?.slowSearchLogGroup) { + this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup; + logGroups.push(this.slowSearchLogGroup); + } else if (props.logPublishingOptions?.slowSearchLogEnabed) { + this.slowSearchLogGroup = new logs.LogGroup(this, 'SlowSearchLogs', { + logGroupName: `elasticsearch/domains/${props.domainName ?? id}/slow-search-logs`, + retention: logs.RetentionDays.ONE_MONTH, + }); + logGroups.push(this.slowSearchLogGroup); + } + + if (props.logPublishingOptions?.slowIndexLogGroup) { + this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup; + logGroups.push(this.slowIndexLogGroup); + } else if (props.logPublishingOptions?.slowIndexLogEnabed) { + this.slowIndexLogGroup = new logs.LogGroup(this, 'SlowIndexLogs', { + logGroupName: `elasticsearch/domains/${props.domainName ?? id}/slow-index-logs`, + retention: logs.RetentionDays.ONE_MONTH, + }); + logGroups.push(this.slowIndexLogGroup); + } + + if (props.logPublishingOptions?.appLogGroup) { + this.appLogGroup = props.logPublishingOptions.appLogGroup; + logGroups.push(this.appLogGroup); + } else if (props.logPublishingOptions?.appLogEnabed) { + this.appLogGroup = new logs.LogGroup(this, 'AppLogs', { + logGroupName: `elasticsearch/domains/${props.domainName ?? id}/application-logs`, + retention: logs.RetentionDays.ONE_MONTH, + }); + logGroups.push(this.appLogGroup); + } + + let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; + if (logGroups.length > 0) { + const logPolicyStatement = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], + resources: logGroups.map((lg) => lg.logGroupArn), + principals: [new iam.ServicePrincipal('es.amazonaws.com')], + }); + + // Use a custom resource to set the log group resource policy since it is not supported by CDK and cfn. + // https://github.com/aws/aws-cdk/issues/5343 + logGroupResourcePolicy = new LogGroupResourcePolicy(this, 'ESLogGroupPolicy', { + policyName: 'ESLogPolicy', + policyStatements: [logPolicyStatement], + }); + } + + // Create the domain + this.domain = new CfnDomain(this, 'Resource', { + domainName: this.physicalName, + elasticsearchVersion: props.elasticsearchVersion, + elasticsearchClusterConfig: { + dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, + dedicatedMasterCount: props.clusterConfig.masterNodes, + dedicatedMasterType: props.clusterConfig.masterNodeInstanceType, + instanceCount: props.clusterConfig.dataNodes, + instanceType: props.clusterConfig.dataNodeInstanceType, + zoneAwarenessEnabled: props.clusterConfig.availabilityZoneCount != null, + zoneAwarenessConfig: { availabilityZoneCount: props.clusterConfig.availabilityZoneCount }, + }, + ebsOptions: { + ebsEnabled: props.ebsOptions != null, + volumeSize: props.ebsOptions?.volumeSize, + volumeType: props.ebsOptions?.volumeType, + iops: props.ebsOptions?.iops, + }, + encryptionAtRestOptions: { + enabled: props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null), + kmsKeyId: props.encryptionAtRestOptions?.kmsKey?.keyId, + }, + nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryptionEnabled ?? false }, + logPublishingOptions: { + ES_APPLICATION_LOGS: { + enabled: this.appLogGroup != null, + cloudWatchLogsLogGroupArn: this.appLogGroup?.logGroupArn, + }, + SEARCH_SLOW_LOGS: { + enabled: this.slowSearchLogGroup != null, + cloudWatchLogsLogGroupArn: this.slowSearchLogGroup?.logGroupArn, + }, + INDEX_SLOW_LOGS: { + enabled: this.slowIndexLogGroup != null, + cloudWatchLogsLogGroupArn: this.slowIndexLogGroup?.logGroupArn, + }, + }, + }); + + if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } + + if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + + this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { + service: 'es', + resource: 'domain', + resourceName: this.physicalName, + }); + this.domainName = this.getResourceNameAttribute(this.domain.ref); + + this.domainEndpoint = `https://${this.domain.attrDomainEndpoint}`; + } + + /** + * Return the given named metric for this Domain. + */ + public metric(metricName: string, clientId: string, props?: MetricOptions): Metric { + return new Metric({ + namespace: 'AWS/ES', + metricName, + dimensions: { + DomainName: this.domainName, + ClientId: clientId, + }, + ...props, + }); + } + + /** + * Metric for the time the cluster status is red. + * + * @default maximum over a minute + */ + public metricClusterStatusRed(clientId: string, props?: MetricOptions): Metric { + return this.metric('ClusterStatus.red', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for the time the cluster status is yellow. + * + * @default maximum over a minute + */ + public metricClusterStatusYellow(clientId: string, props?: MetricOptions): Metric { + return this.metric('ClusterStatus.yellow', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for the storage space of nodes in the cluster. + * + * @default minimum over a minute + */ + public metricFreeStorageSpace(clientId: string, props?: MetricOptions): Metric { + return this.metric('FreeStorageSpace', clientId, { statistic: Statistic.MINIMUM, ...props }); + } + + /** + * Metric for the cluster blocking index writes. + * + * @default maximum over 5 minutes + */ + public metricClusterIndexWriteBlocked(clientId: string, props?: MetricOptions): Metric { + return this.metric('ClusterIndexWriteBlocked', clientId, { + statistic: Statistic.MAXIMUM, + period: cdk.Duration.minutes(1), + ...props, + }); + } + + /** + * Metric for the number of nodes. + * + * @default minimum over 1 hour + */ + public metricNodes(clientId: string, props?: MetricOptions): Metric { + return this.metric('Nodes', clientId, { + statistic: Statistic.MAXIMUM, + period: cdk.Duration.hours(1), + ...props, + }); + } + + /** + * Metric for automated snapshot failures. + * + * @default maximum over a minute + */ + public metricAutomatedSnapshotFailure(clientId: string, props?: MetricOptions): Metric { + return this.metric('AutomatedSnapshotFailure', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for CPU utilization. + * + * @default maximum over a minute + */ + public metricCPUUtilization(clientId: string, props?: MetricOptions): Metric { + return this.metric('CPUUtilization', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for JVM memory pressure. + * + * @default maximum over a minute + */ + public metricJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric { + return this.metric('JVMMemoryPressure', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for master CPU utilization. + * + * @default maximum over a minute + */ + public metricMasterCPUUtilization(clientId: string, props?: MetricOptions): Metric { + return this.metric('MasterCPUUtilization', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for master JVM memory pressure. + * + * @default maximum over a minute + */ + public metricMasterJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric { + return this.metric('MasterJVMMemoryPressure', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for KMS key errors. + * + * @default maximum over a minute + */ + public metricKMSKeyError(clientId: string, props?: MetricOptions): Metric { + return this.metric('KMSKeyError', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for KMS key being inaccessible. + * + * @default maximum over a minute + */ + public metricKMSKeyInaccessible(clientId: string, props?: MetricOptions): Metric { + return this.metric('KMSKeyInaccessible', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for number of searchable documents. + * + * @default maximum over a minute + */ + public metricSearchableDocuments(clientId: string, props?: MetricOptions): Metric { + return this.metric('SearchableDocuments', clientId, { statistic: Statistic.MAXIMUM, ...props }); + } + + /** + * Metric for search latency. + * + * @default maximum over a minute + */ + public metricSearchLatency(clientId: string, props?: MetricOptions): Metric { + return this.metric('SearchLatencyP99', clientId, { statistic: 'p99', ...props }); + } + + /** + * Metric for indexing latency. + * + * @default maximum over a minute + */ + public metricIndexingLatency(clientId: string, props?: MetricOptions): Metric { + return this.metric('IndexingLatencyP99', clientId, { statistic: 'p99', ...props }); + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/index.ts b/packages/@aws-cdk/aws-elasticsearch/lib/index.ts index 2ae3564622996..3a3d943f9825d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/index.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/index.ts @@ -1,2 +1,4 @@ +export * from './domain'; + // AWS::Elasticsearch CloudFormation Resources: export * from './elasticsearch.generated'; diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts new file mode 100644 index 0000000000000..84a5067771368 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts @@ -0,0 +1,56 @@ +import * as cdk from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cr from '@aws-cdk/custom-resources'; + +/** + * Construction properties for LogGroupResourcePolicy + */ +export interface ILogGroupResourcePolicyProps { + /** + * The log group resource policy name + */ + readonly policyName: string; + /** + * The policy statements for the log group resource logs + */ + readonly policyStatements: [iam.PolicyStatement]; +} + +/** + * Creates LogGroup resource policies. + */ +export class LogGroupResourcePolicy extends cr.AwsCustomResource { + constructor(scope: cdk.Construct, id: string, props: ILogGroupResourcePolicyProps) { + const policy = new iam.Policy(scope, props.policyName, { + statements: props.policyStatements, + }); + + super(scope, id, { + resourceType: 'Custom::CloudwatchLogResourcePolicy', + onUpdate: { + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName: props.policyName, + policyDocument: JSON.stringify(policy.document), + }, + physicalResourceId: cr.PhysicalResourceId.of(id), + }, + onDelete: { + service: 'CloudWatchLogs', + action: 'deleteResourcePolicy', + parameters: { + policyName: props.policyName, + }, + ignoreErrorCodesMatching: '400', + }, + policy: cr.AwsCustomResourcePolicy.fromStatements([ + new iam.PolicyStatement({ + actions: ['logs:PutResourcePolicy', 'logs:DeleteResourcePolicy'], + // Resource Policies are global in Cloudwatch Logs per-region, per-account. + resources: ['*'], + }), + ]), + }); + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index c58a9eb82ed20..3123df4e4d4e2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -77,11 +77,23 @@ "pkglint": "0.0.0" }, "dependencies": { + "@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/custom-resources": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.4" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@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/custom-resources": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.4" }, From 7e6ff04bbbb9f694779373ccc343fe869dbb77b2 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 22 Jun 2020 22:06:44 +1000 Subject: [PATCH 02/45] Add Elasticsearch Domain details to README --- packages/@aws-cdk/aws-elasticsearch/README.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index bba696bd2a6c0..4801c704cbd1d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -9,4 +9,75 @@ --- +To create an Elasticsearch domain: + +```ts +import * as es from '@aws-cdk/aws-elasticsearch'; + +const cluster = new es.Cluster(this, 'Domain', { + elasticsearchVersion: '7.4', + clusterConfig: { + masterNodes: 3, + masterNodeInstanceType: 'c5.large.elasticsearch', + dataNodes: 3, + dataNodeInstanceType: 'r5.large.elasticsearch', + }, + logPublishingOptions: { + slowSearchLogEnabed: true, + appLogEnabled: true + }, +}); +``` + +This creates an Elasticsearch cluster and automatically sets up log groups for +logging the cluster logs and slow search logs. + + +### Permissions + +#### IAM + +Helper methods also exist for managing access to the cluster. + +```ts +const lambda = new lambda.Function(this, 'Lambda', { /* ... */ }); +// Grant the lambda functiomn read and write access to app-search index +cluster.grantReadWriteForIndex(lambda, 'app-search'); +``` + +### Encryption + +The cluster can also be created with encryption enabled: + +```ts +const cluster = new es.Cluster(this, 'Domain', { + elasticsearchVersion: '7.4', + clusterConfig: { + masterNodes: 3, + masterNodeInstanceType: 'c5.large.elasticsearch', + dataNodes: 3, + dataNodeInstanceType: 'r5.large.elasticsearch', + }, + nodeToNodeEncryptionEnabled: true, + encryptionAtRestOptions: { + enabled: true, + }, +}); + +``` + +This sets up the cluster 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. + +### Metrics + +Helper methods exist to access common cluster metrics for example: + +```ts + +const freeStorageSpace = cluster.metricFreeStorageSpace('account-id'); +const masterSysMemoryUtilization = cluster.metric('MasterSysMemoryUtilization', 'account-id'); +``` + This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. From f4e84eed5f08a36c0cd89e56cdf29312e2b185eb Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Thu, 9 Jul 2020 10:07:40 +1000 Subject: [PATCH 03/45] Address PR feedback * Add proper support for Cognito and VPC --- packages/@aws-cdk/aws-elasticsearch/README.md | 20 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 243 ++++++++++++++---- .../lib/log-group-resource-policy.ts | 6 +- 3 files changed, 205 insertions(+), 64 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 4801c704cbd1d..36f5a0a9b69bd 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -14,7 +14,7 @@ To create an Elasticsearch domain: ```ts import * as es from '@aws-cdk/aws-elasticsearch'; -const cluster = new es.Cluster(this, 'Domain', { +const domain = new es.Domain(this, 'Domain', { elasticsearchVersion: '7.4', clusterConfig: { masterNodes: 3, @@ -30,27 +30,27 @@ const cluster = new es.Cluster(this, 'Domain', { ``` This creates an Elasticsearch cluster and automatically sets up log groups for -logging the cluster logs and slow search logs. +logging the domain logs and slow search logs. ### Permissions #### IAM -Helper methods also exist for managing access to the cluster. +Helper methods also exist for managing access to the domain. ```ts const lambda = new lambda.Function(this, 'Lambda', { /* ... */ }); // Grant the lambda functiomn read and write access to app-search index -cluster.grantReadWriteForIndex(lambda, 'app-search'); +domain.grantReadWriteForIndex(lambda, 'app-search'); ``` ### Encryption -The cluster can also be created with encryption enabled: +The domain can also be created with encryption enabled: ```ts -const cluster = new es.Cluster(this, 'Domain', { +const domain = new es.Domain(this, 'Domain', { elasticsearchVersion: '7.4', clusterConfig: { masterNodes: 3, @@ -66,18 +66,18 @@ const cluster = new es.Cluster(this, 'Domain', { ``` -This sets up the cluster with node to node encryption and encryption at +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. ### Metrics -Helper methods exist to access common cluster metrics for example: +Helper methods exist to access common domain metrics for example: ```ts -const freeStorageSpace = cluster.metricFreeStorageSpace('account-id'); -const masterSysMemoryUtilization = cluster.metric('MasterSysMemoryUtilization', 'account-id'); +const freeStorageSpace = domain.metricFreeStorageSpace('account-id'); +const masterSysMemoryUtilization = domain.metric('MasterSysMemoryUtilization', 'account-id'); ``` This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 7c8578cf9897d..1af130facbc7e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1,18 +1,45 @@ -import * as cdk from '@aws-cdk/core'; +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 { Metric, MetricOptions, Statistic } from '@aws-cdk/aws-cloudwatch'; -import { EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; -import { PolicyStatement } from '@aws-cdk/aws-iam'; -import { LogGroupResourcePolicy } from './log-group-resource-policy'; +import * as cdk from '@aws-cdk/core'; + import { CfnDomain } from './elasticsearch.generated'; +import { LogGroupResourcePolicy } from './log-group-resource-policy'; +/** + * Configures the makeup of the cluster such as number of nodes and instance + * type. + */ export interface ClusterConfig { + /** + * The number of instances to use for the master node + */ readonly masterNodes: number; + + /** + * The hardware configuration of the computer that hosts the dedicated master + * node, such as `m3.medium.elasticsearch`. For valid values, see [Supported + * Instance Types] + * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html) + * in the Amazon Elasticsearch Service Developer Guide. + */ readonly masterNodeInstanceType: string; + + /** + * The number of data nodes to use in the Amazon ES domain. + */ readonly dataNodes: number; + + /** + * The instance type for your data nodes, such as + * `m3.medium.elasticsearch`. For valid values, see [Supported Instance + * Types](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html) + * in the Amazon Elasticsearch Service Developer Guide. + */ readonly dataNodeInstanceType: string; + /** * The number of AZs that you want the domain to use. When you enable zone * awareness, Amazon ES allocates the nodes and replica index shards that @@ -26,12 +53,45 @@ export interface ClusterConfig { readonly availabilityZoneCount?: number; } +/** + * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that + * are attached to data nodes in the Amazon ES domain. For more information, see + * [Configuring EBS-based Storage] + * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-ebs) + * in the Amazon Elasticsearch Service Developer Guide. + */ export interface EbsOptions { + /** + * The number of I/O operations per second (IOPS) that the volume + * supports. This property applies only to the Provisioned IOPS (SSD) EBS + * volume type. + * + * @default - iops are not set. + */ readonly iops?: number; + + /** + * The size (in GiB) of the EBS volume for each data node. The minimum and + * maximum size of an EBS volume depends on the EBS volume type and the + * instance type to which it is attached. For more information, see + * [Configuring EBS-based Storage] + * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-ebs) + * in the Amazon Elasticsearch Service Developer Guide + */ readonly volumeSize: number; - readonly volumeType: EbsDeviceVolumeType; + + /** + * The EBS volume type to use with the Amazon ES domain, such as standard, gp2, io1, st1, or sc1. + * For more information, see[Configuring EBS-based Storage] + * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-ebs) + * in the Amazon Elasticsearch Service Developer Guide + */ + readonly volumeType: ec2.EbsDeviceVolumeType; } +/** + * Configures log settings for the domain. + */ export interface LoggingOptions { /** * Specify if slow search logging should be set up. @@ -45,7 +105,7 @@ export interface LoggingOptions { * * @default - a new log group is created if slow search logging is enabled */ - readonly slowSearchLogGroup?: logs.LogGroup; + readonly slowSearchLogGroup?: logs.ILogGroup; /** * Specify if slow index logging should be set up. @@ -59,21 +119,21 @@ export interface LoggingOptions { * * @default - a new log group is created if slow index logging is enabled */ - readonly slowIndexLogGroup?: logs.LogGroup; + readonly slowIndexLogGroup?: logs.ILogGroup; /** * Specify if Elasticsearch application logging should be set up. * * @default - false */ - readonly appLogEnabed?: boolean; + readonly appLogEnabled?: boolean; /** * Log Elasticsearch application logs to this log group. * * @default - a new log group is created if app logging is enabled */ - readonly appLogGroup?: logs.LogGroup; + readonly appLogGroup?: logs.ILogGroup; } /** @@ -84,17 +144,67 @@ export interface LoggingOptions { export interface EncryptionAtRestOptions { /** * Specify true to enable encryption at rest. + * + * @default - encryption at rest is disabled. */ readonly enabled?: boolean; /** * Supply if using KMS key for encryption at rest. + * + * @default - uses default aws/es KMS key. + */ + readonly kmsKey?: kms.IKey; +} + +/** + * Configures Amazon ES to use Amazon Cognito authentication for Kibana. + */ +export interface CognitoOptions { + /** + * The Amazon Cognito identity pool ID that you want Amazon ES to use for Kibana authentication. */ - readonly kmsKey?: kms.Key; + readonly identityPoolId: string; + + /** + * The AmazonESCognitoAccess role that allows Amazon ES to configure your user pool and identity pool. + */ + readonly role: iam.IRole; + + /** + * The Amazon Cognito user pool ID that you want Amazon ES to use for Kibana authentication. + */ + readonly userPoolId: string; } /** - * Properties for a AWS Elasticsearch Domain. + * 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[]; +} + +/** + * Properties for an AWS Elasticsearch Domain. */ export interface DomainProps { /** @@ -102,7 +212,7 @@ export interface DomainProps { * * @default - No access policies. */ - readonly accessPolicies?: PolicyStatement[]; + readonly accessPolicies?: iam.PolicyStatement[]; /** * Additional options to specify for the Amazon ES domain. @@ -112,14 +222,16 @@ export interface DomainProps { readonly advancedOptions?: { [key: string]: (string) }; /** - * `AWS::Elasticsearch::Domain.CognitoOptions` + * Configures Amazon ES to use Amazon Cognito authentication for Kibana. + * + * @default - Cognito not used for authentication to Kibana. */ - readonly cognitoOptions?: CfnDomain.CognitoOptionsProperty | cdk.IResolvable; + readonly cognitoOptions?: CognitoOptions; /** * Enforces a particular physical domain name. * - * @default + * @default - A name will be auto-generated. */ readonly domainName?: string; @@ -170,15 +282,23 @@ export interface DomainProps { * The hour in UTC during which the service takes an automated daily snapshot * of the indices in the Amazon ES domain. Only applies for Elasticsearch * versions below 5.3. + * + * @default - Not used for Elasticsearch versions above 5.3. */ readonly automatedSnapshotStartHour?: number; /** - * `AWS::Elasticsearch::Domain.VPCOptions` + * 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. + * + * @default - VPC not used */ - readonly vpcOptions?: CfnDomain.VPCOptionsProperty | cdk.IResolvable; + readonly vpcOptions?: VpcOptions; } + /** * An interface that represents an Elasticsearch domain - either created with the CDK, or an existing one. */ @@ -314,7 +434,19 @@ export interface IDomain extends cdk.IResource { */ metricIndexingLatency(clientId: string, props?: MetricOptions): Metric; } + + +/** + * Provides an Elasticsearch domain. + */ export class Domain extends cdk.Resource implements IDomain { + private static createLogGroup(parent: cdk.Construct, domainName: string, id: string, name: string): logs.ILogGroup { + return new logs.LogGroup(parent, id, { + logGroupName: `elasticsearch/domains/${domainName}/${name}`, + retention: logs.RetentionDays.ONE_MONTH, + }); + } + /** * @attribute */ @@ -333,52 +465,42 @@ export class Domain extends cdk.Resource implements IDomain { private readonly domain: CfnDomain; - private readonly slowSearchLogGroup?: logs.LogGroup; + private readonly slowSearchLogGroup?: logs.ILogGroup; - private readonly slowIndexLogGroup?: logs.LogGroup; + private readonly slowIndexLogGroup?: logs.ILogGroup; - private readonly appLogGroup?: logs.LogGroup; + private readonly appLogGroup?: logs.ILogGroup; constructor(scope: cdk.Construct, id: string, props: DomainProps) { super(scope, id, { physicalName: props.domainName, }); + this.domainName = this.physicalName; + // Setup logging - const logGroups: logs.LogGroup[] = []; + const logGroups: logs.ILogGroup[] = []; + + if (props.logPublishingOptions?.slowSearchLogEnabed) { + this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup ?? + Domain.createLogGroup(this, this.domainName, 'SlowSearchLogs', 'slow-search-logs'); - if (props.logPublishingOptions?.slowSearchLogGroup) { - this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup; - logGroups.push(this.slowSearchLogGroup); - } else if (props.logPublishingOptions?.slowSearchLogEnabed) { - this.slowSearchLogGroup = new logs.LogGroup(this, 'SlowSearchLogs', { - logGroupName: `elasticsearch/domains/${props.domainName ?? id}/slow-search-logs`, - retention: logs.RetentionDays.ONE_MONTH, - }); logGroups.push(this.slowSearchLogGroup); - } + }; + + if (props.logPublishingOptions?.slowIndexLogEnabed) { + this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup ?? + Domain.createLogGroup(this, this.domainName, 'SlowIndexLogs', 'slow-index-logs'); - if (props.logPublishingOptions?.slowIndexLogGroup) { - this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup; - logGroups.push(this.slowIndexLogGroup); - } else if (props.logPublishingOptions?.slowIndexLogEnabed) { - this.slowIndexLogGroup = new logs.LogGroup(this, 'SlowIndexLogs', { - logGroupName: `elasticsearch/domains/${props.domainName ?? id}/slow-index-logs`, - retention: logs.RetentionDays.ONE_MONTH, - }); logGroups.push(this.slowIndexLogGroup); - } + }; + + if (props.logPublishingOptions?.appLogEnabled) { + this.appLogGroup = props.logPublishingOptions.appLogGroup ?? + Domain.createLogGroup(this, this.domainName, 'AppLogs', 'application-logs'); - if (props.logPublishingOptions?.appLogGroup) { - this.appLogGroup = props.logPublishingOptions.appLogGroup; - logGroups.push(this.appLogGroup); - } else if (props.logPublishingOptions?.appLogEnabed) { - this.appLogGroup = new logs.LogGroup(this, 'AppLogs', { - logGroupName: `elasticsearch/domains/${props.domainName ?? id}/application-logs`, - retention: logs.RetentionDays.ONE_MONTH, - }); logGroups.push(this.appLogGroup); - } + }; let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; if (logGroups.length > 0) { @@ -397,6 +519,19 @@ export class Domain extends cdk.Resource implements IDomain { }); } + // If VPC options are supplied ensure that the number of subnets matches the number AZ + if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.clusterConfig.availabilityZoneCount) { + throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); + }; + + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; + if (props.vpcOptions) { + cfnVpcOptions = { + securityGroupIds: props.vpcOptions.securityGroups.map((sg) => sg.securityGroupId), + subnetIds: props.vpcOptions.subnets.map((subnet) => subnet.subnetId), + }; + } + // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, @@ -435,6 +570,13 @@ export class Domain extends cdk.Resource implements IDomain { cloudWatchLogsLogGroupArn: this.slowIndexLogGroup?.logGroupArn, }, }, + cognitoOptions: { + enabled: props.cognitoOptions != null, + identityPoolId: props.cognitoOptions?.identityPoolId, + roleArn: props.cognitoOptions?.role.roleArn, + userPoolId: props.cognitoOptions?.userPoolId, + }, + vpcOptions: cfnVpcOptions, }); if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } @@ -446,9 +588,8 @@ export class Domain extends cdk.Resource implements IDomain { resource: 'domain', resourceName: this.physicalName, }); - this.domainName = this.getResourceNameAttribute(this.domain.ref); - this.domainEndpoint = `https://${this.domain.attrDomainEndpoint}`; + this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); } /** diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts index 84a5067771368..1d9dcc5de03ce 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts @@ -1,11 +1,11 @@ -import * as cdk from '@aws-cdk/core'; import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; import * as cr from '@aws-cdk/custom-resources'; /** * Construction properties for LogGroupResourcePolicy */ -export interface ILogGroupResourcePolicyProps { +export interface LogGroupResourcePolicyProps { /** * The log group resource policy name */ @@ -20,7 +20,7 @@ export interface ILogGroupResourcePolicyProps { * Creates LogGroup resource policies. */ export class LogGroupResourcePolicy extends cr.AwsCustomResource { - constructor(scope: cdk.Construct, id: string, props: ILogGroupResourcePolicyProps) { + constructor(scope: cdk.Construct, id: string, props: LogGroupResourcePolicyProps) { const policy = new iam.Policy(scope, props.policyName, { statements: props.policyStatements, }); From 30b99223535ed0ef064cec9d5ac1776c123da79a Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Sun, 19 Jul 2020 17:11:54 +1000 Subject: [PATCH 04/45] Add grant helper methods --- packages/@aws-cdk/aws-elasticsearch/README.md | 4 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 85 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 36f5a0a9b69bd..fd5a4355c79a2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -41,8 +41,8 @@ Helper methods also exist for managing access to the domain. ```ts const lambda = new lambda.Function(this, 'Lambda', { /* ... */ }); -// Grant the lambda functiomn read and write access to app-search index -domain.grantReadWriteForIndex(lambda, 'app-search'); +// Grant the lambda functiomn read access to app-search index +domain.grantIndex(lambda, 'app-search', 'es:HttpGet'); ``` ### Encryption diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 1af130facbc7e..cee2877ab0241 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -324,6 +324,35 @@ export interface IDomain extends cdk.IResource { */ readonly domainEndpoint: string; + /** + * Adds an IAM policy statement associated with this domain to an IAM + * principal's policy. + * + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + + /** + * Adds an IAM policy statement associated with an index in this domain to an IAM + * principal's policy. + * + * @param index The index to grant permissions for + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + */ + grantIndex(index: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + + /** + * Adds an IAM policy statement associated with a path in this domain to an IAM + * principal's policy. + * + * @param path The path to grant permissions for + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + */ + grantPath(path: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + /** * Return the given named metric for this Domain. */ @@ -592,6 +621,62 @@ export class Domain extends cdk.Resource implements IDomain { this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); } + /** + * Adds an IAM policy statement associated with this domain to an IAM + * principal's policy. + * + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + */ + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [ + this.domainArn, + `${this.domainArn}/*`, + ], + scope: this, + }); + } + + /** + * Adds an IAM policy statement associated with an index in this domain to an IAM + * principal's policy. + * + * @param index The index to grant permissions for + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + */ + public grantIndex(index: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [ + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ], + scope: this, + }); + } + + /** + * Adds an IAM policy statement associated with a path in this domain to an IAM + * principal's policy. + * + * @param path The path to grant permissions for + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + */ + public grantPath(path: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [`${this.domainArn}/${path}`], + scope: this, + }); + } + /** * Return the given named metric for this Domain. */ From da6ad9feb45bf5afe4b13bbe7d27df14cf1e15a2 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 25 Jul 2020 06:30:30 -0500 Subject: [PATCH 05/45] abstract out DomainBase; fromXXX members on Domain --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 404 +++++++++++------- 1 file changed, 261 insertions(+), 143 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index cee2877ab0241..268b9c3a51d29 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -304,7 +304,7 @@ export interface DomainProps { */ export interface IDomain extends cdk.IResource { /** - * Arn of the Elasticsearch table. + * Arn of the Elasticsearch domain. * * @attribute */ @@ -466,160 +466,23 @@ export interface IDomain extends cdk.IResource { /** - * Provides an Elasticsearch domain. + * A new or imported domain. */ -export class Domain extends cdk.Resource implements IDomain { - private static createLogGroup(parent: cdk.Construct, domainName: string, id: string, name: string): logs.ILogGroup { - return new logs.LogGroup(parent, id, { - logGroupName: `elasticsearch/domains/${domainName}/${name}`, - retention: logs.RetentionDays.ONE_MONTH, - }); - } - +abstract class DomainBase extends cdk.Resource implements IDomain { /** * @attribute */ - public readonly domainArn: string; + public abstract readonly domainArn: string; /** * @attribute */ - public readonly domainName: string; + public abstract readonly domainName: string; /** * @attribute */ - public readonly domainEndpoint: string; - - - private readonly domain: CfnDomain; - - private readonly slowSearchLogGroup?: logs.ILogGroup; - - private readonly slowIndexLogGroup?: logs.ILogGroup; - - private readonly appLogGroup?: logs.ILogGroup; - - constructor(scope: cdk.Construct, id: string, props: DomainProps) { - super(scope, id, { - physicalName: props.domainName, - }); - - this.domainName = this.physicalName; - - // Setup logging - const logGroups: logs.ILogGroup[] = []; - - if (props.logPublishingOptions?.slowSearchLogEnabed) { - this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup ?? - Domain.createLogGroup(this, this.domainName, 'SlowSearchLogs', 'slow-search-logs'); - - logGroups.push(this.slowSearchLogGroup); - }; - - if (props.logPublishingOptions?.slowIndexLogEnabed) { - this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup ?? - Domain.createLogGroup(this, this.domainName, 'SlowIndexLogs', 'slow-index-logs'); - - logGroups.push(this.slowIndexLogGroup); - }; - - if (props.logPublishingOptions?.appLogEnabled) { - this.appLogGroup = props.logPublishingOptions.appLogGroup ?? - Domain.createLogGroup(this, this.domainName, 'AppLogs', 'application-logs'); - - logGroups.push(this.appLogGroup); - }; - - let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; - if (logGroups.length > 0) { - const logPolicyStatement = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], - resources: logGroups.map((lg) => lg.logGroupArn), - principals: [new iam.ServicePrincipal('es.amazonaws.com')], - }); - - // Use a custom resource to set the log group resource policy since it is not supported by CDK and cfn. - // https://github.com/aws/aws-cdk/issues/5343 - logGroupResourcePolicy = new LogGroupResourcePolicy(this, 'ESLogGroupPolicy', { - policyName: 'ESLogPolicy', - policyStatements: [logPolicyStatement], - }); - } - - // If VPC options are supplied ensure that the number of subnets matches the number AZ - if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.clusterConfig.availabilityZoneCount) { - throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); - }; - - let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; - if (props.vpcOptions) { - cfnVpcOptions = { - securityGroupIds: props.vpcOptions.securityGroups.map((sg) => sg.securityGroupId), - subnetIds: props.vpcOptions.subnets.map((subnet) => subnet.subnetId), - }; - } - - // Create the domain - this.domain = new CfnDomain(this, 'Resource', { - domainName: this.physicalName, - elasticsearchVersion: props.elasticsearchVersion, - elasticsearchClusterConfig: { - dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, - dedicatedMasterCount: props.clusterConfig.masterNodes, - dedicatedMasterType: props.clusterConfig.masterNodeInstanceType, - instanceCount: props.clusterConfig.dataNodes, - instanceType: props.clusterConfig.dataNodeInstanceType, - zoneAwarenessEnabled: props.clusterConfig.availabilityZoneCount != null, - zoneAwarenessConfig: { availabilityZoneCount: props.clusterConfig.availabilityZoneCount }, - }, - ebsOptions: { - ebsEnabled: props.ebsOptions != null, - volumeSize: props.ebsOptions?.volumeSize, - volumeType: props.ebsOptions?.volumeType, - iops: props.ebsOptions?.iops, - }, - encryptionAtRestOptions: { - enabled: props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null), - kmsKeyId: props.encryptionAtRestOptions?.kmsKey?.keyId, - }, - nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryptionEnabled ?? false }, - logPublishingOptions: { - ES_APPLICATION_LOGS: { - enabled: this.appLogGroup != null, - cloudWatchLogsLogGroupArn: this.appLogGroup?.logGroupArn, - }, - SEARCH_SLOW_LOGS: { - enabled: this.slowSearchLogGroup != null, - cloudWatchLogsLogGroupArn: this.slowSearchLogGroup?.logGroupArn, - }, - INDEX_SLOW_LOGS: { - enabled: this.slowIndexLogGroup != null, - cloudWatchLogsLogGroupArn: this.slowIndexLogGroup?.logGroupArn, - }, - }, - cognitoOptions: { - enabled: props.cognitoOptions != null, - identityPoolId: props.cognitoOptions?.identityPoolId, - roleArn: props.cognitoOptions?.role.roleArn, - userPoolId: props.cognitoOptions?.userPoolId, - }, - vpcOptions: cfnVpcOptions, - }); - - if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } - - if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } - - this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { - service: 'es', - resource: 'domain', - resourceName: this.physicalName, - }); - - this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); - } + public abstract readonly domainEndpoint: string; /** * Adds an IAM policy statement associated with this domain to an IAM @@ -835,3 +698,258 @@ export class Domain extends cdk.Resource implements IDomain { return this.metric('IndexingLatencyP99', clientId, { statistic: 'p99', ...props }); } } + + +/** + * Reference to an Elasticsearch domain. + */ +export interface DomainAttributes { + /** + * The ARN of the Elasticsearch domain. + * One of this, or {@link domainName}, is required. + * + * @default - no domain arn + */ + readonly domainArn?: string; + + /** + * The domain name of the Elasticsearch domain. + * One of this, or {@link domainArn}, is required. + * + * @default - no domain name + */ + readonly domainName?: string; + + /** + * The domain endpoint of the Elasticsearch domain. + */ + readonly domainEndpoint: string; +} + + +/** + * Provides an Elasticsearch domain. + */ +export class Domain extends DomainBase implements IDomain { + /** + * Creates a Domain construct that represents an external domain via domain arn. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param domainName The domain's name. + * @param domainEndpoint The domain's endpoint. + */ + public static fromDomainName(scope: cdk.Construct, id: string, domainName: string, domainEndpoint: string): IDomain { + return Domain.fromDomainAttributes(scope, id, { domainName, domainEndpoint }); + } + + /** + * Creates a Domain construct that represents an external domain via domain arn. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param domainArn The domain's ARN. + * @param domainEndpoint The domain's endpoint. + */ + public static fromDomainArn(scope: cdk.Construct, id: string, domainArn: string, domainEndpoint: string): IDomain { + return Domain.fromDomainAttributes(scope, id, { domainArn, domainEndpoint }); + } + + /** + * Creates a Domain construct that represents an external domain. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param attrs A `DomainAttributes` object. + */ + public static fromDomainAttributes(scope: cdk.Construct, id: string, attrs: DomainAttributes): IDomain { + + class Import extends DomainBase { + + public readonly domainArn: string; + public readonly domainName: string; + public readonly domainEndpoint: string; + + constructor(_domainArn: string, domainName: string, domainEndpoint: string) { + super(scope, id); + this.domainArn = _domainArn; + this.domainName = domainName; + this.domainEndpoint = domainEndpoint; + } + } + + let name: string; + let arn: string; + const stack = cdk.Stack.of(scope); + if (!attrs.domainName) { + if (!attrs.domainArn) { throw new Error('One of domainName or domainArn is required!'); } + + arn = attrs.domainArn; + const maybeDomainName = stack.parseArn(attrs.domainArn).resourceName; + if (!maybeDomainName) { throw new Error('ARN for Elasticsearch domain must be in the form: ...'); } + name = maybeDomainName; + } else { + if (attrs.domainArn) { throw new Error('Only one of domainArn or domainName can be provided'); } + name = attrs.domainName; + arn = stack.formatArn({ + service: 'elasticsearch', + resource: 'domain', + resourceName: attrs.domainName, + }); + } + + return new Import(arn, name, attrs.domainEndpoint); + } + + private static createLogGroup(parent: cdk.Construct, domainName: string, id: string, name: string): logs.ILogGroup { + return new logs.LogGroup(parent, id, { + logGroupName: `elasticsearch/domains/${domainName}/${name}`, + retention: logs.RetentionDays.ONE_MONTH, + }); + } + + /** + * @attribute + */ + public readonly domainArn: string; + + /** + * @attribute + */ + public readonly domainName: string; + + /** + * @attribute + */ + public readonly domainEndpoint: string; + + + private readonly domain: CfnDomain; + + private readonly slowSearchLogGroup?: logs.ILogGroup; + + private readonly slowIndexLogGroup?: logs.ILogGroup; + + private readonly appLogGroup?: logs.ILogGroup; + + constructor(scope: cdk.Construct, id: string, props: DomainProps) { + super(scope, id, { + physicalName: props.domainName, + }); + + this.domainName = this.physicalName; + + // Setup logging + const logGroups: logs.ILogGroup[] = []; + + if (props.logPublishingOptions?.slowSearchLogEnabed) { + this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup ?? + Domain.createLogGroup(this, this.domainName, 'SlowSearchLogs', 'slow-search-logs'); + + logGroups.push(this.slowSearchLogGroup); + }; + + if (props.logPublishingOptions?.slowIndexLogEnabed) { + this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup ?? + Domain.createLogGroup(this, this.domainName, 'SlowIndexLogs', 'slow-index-logs'); + + logGroups.push(this.slowIndexLogGroup); + }; + + if (props.logPublishingOptions?.appLogEnabled) { + this.appLogGroup = props.logPublishingOptions.appLogGroup ?? + Domain.createLogGroup(this, this.domainName, 'AppLogs', 'application-logs'); + + logGroups.push(this.appLogGroup); + }; + + let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; + if (logGroups.length > 0) { + const logPolicyStatement = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], + resources: logGroups.map((lg) => lg.logGroupArn), + principals: [new iam.ServicePrincipal('es.amazonaws.com')], + }); + + // Use a custom resource to set the log group resource policy since it is not supported by CDK and cfn. + // https://github.com/aws/aws-cdk/issues/5343 + logGroupResourcePolicy = new LogGroupResourcePolicy(this, 'ESLogGroupPolicy', { + policyName: 'ESLogPolicy', + policyStatements: [logPolicyStatement], + }); + } + + // If VPC options are supplied ensure that the number of subnets matches the number AZ + if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.clusterConfig.availabilityZoneCount) { + throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); + }; + + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; + if (props.vpcOptions) { + cfnVpcOptions = { + securityGroupIds: props.vpcOptions.securityGroups.map((sg) => sg.securityGroupId), + subnetIds: props.vpcOptions.subnets.map((subnet) => subnet.subnetId), + }; + } + + // Create the domain + this.domain = new CfnDomain(this, 'Resource', { + domainName: this.physicalName, + elasticsearchVersion: props.elasticsearchVersion, + elasticsearchClusterConfig: { + dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, + dedicatedMasterCount: props.clusterConfig.masterNodes, + dedicatedMasterType: props.clusterConfig.masterNodeInstanceType, + instanceCount: props.clusterConfig.dataNodes, + instanceType: props.clusterConfig.dataNodeInstanceType, + zoneAwarenessEnabled: props.clusterConfig.availabilityZoneCount != null, + zoneAwarenessConfig: { availabilityZoneCount: props.clusterConfig.availabilityZoneCount }, + }, + ebsOptions: { + ebsEnabled: props.ebsOptions != null, + volumeSize: props.ebsOptions?.volumeSize, + volumeType: props.ebsOptions?.volumeType, + iops: props.ebsOptions?.iops, + }, + encryptionAtRestOptions: { + enabled: props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null), + kmsKeyId: props.encryptionAtRestOptions?.kmsKey?.keyId, + }, + nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryptionEnabled ?? false }, + logPublishingOptions: { + ES_APPLICATION_LOGS: { + enabled: this.appLogGroup != null, + cloudWatchLogsLogGroupArn: this.appLogGroup?.logGroupArn, + }, + SEARCH_SLOW_LOGS: { + enabled: this.slowSearchLogGroup != null, + cloudWatchLogsLogGroupArn: this.slowSearchLogGroup?.logGroupArn, + }, + INDEX_SLOW_LOGS: { + enabled: this.slowIndexLogGroup != null, + cloudWatchLogsLogGroupArn: this.slowIndexLogGroup?.logGroupArn, + }, + }, + cognitoOptions: { + enabled: props.cognitoOptions != null, + identityPoolId: props.cognitoOptions?.identityPoolId, + roleArn: props.cognitoOptions?.role.roleArn, + userPoolId: props.cognitoOptions?.userPoolId, + }, + vpcOptions: cfnVpcOptions, + }); + + if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } + + if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + + this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { + service: 'es', + resource: 'domain', + resourceName: this.physicalName, + }); + + this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); + } +} From 96b442f92bf0d038b610a5d09431750674b9eed9 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Wed, 29 Jul 2020 15:10:41 -0500 Subject: [PATCH 06/45] update stability banner --- packages/@aws-cdk/aws-elasticsearch/README.md | 10 ++++++++-- packages/@aws-cdk/aws-elasticsearch/package.json | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index fd5a4355c79a2..b52b8ebd13057 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -1,10 +1,16 @@ ## Amazon Elasticsearch Service Construct Library + --- -![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) +| Features | Stability | +| --- | --- | +| CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) | +| Higher level constructs for Domain | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) | + +> **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. -> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +> **Experimental:** Higher level constructs in this module that are marked as experimental are under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. --- diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 3123df4e4d4e2..7de6532623790 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -101,7 +101,12 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "features": [ + { + "name": "Higher level constructs for Domain", + "stability": "Experimental" + } + ], "awscdkio": { "announce": false } From 37e6776ae51c768fca59e322c2c6a05fd84c8643 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Thu, 30 Jul 2020 01:49:58 -0500 Subject: [PATCH 07/45] initial tests --- packages/@aws-cdk/aws-elasticsearch/README.md | 26 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 305 +++++----- .../@aws-cdk/aws-elasticsearch/package.json | 1 + .../aws-elasticsearch/test/domain.test.ts | 533 ++++++++++++++++++ .../test/elasticsearch.test.ts | 6 - .../test/integ.elasticsearch.expected.json | 284 ++++++++++ .../test/integ.elasticsearch.ts | 35 ++ .../test/log-group-resource-policy.test.ts | 65 +++ 8 files changed, 1104 insertions(+), 151 deletions(-) create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts delete mode 100644 packages/@aws-cdk/aws-elasticsearch/test/elasticsearch.test.ts create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index b52b8ebd13057..1053bec115348 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -21,13 +21,17 @@ To create an Elasticsearch domain: import * as es from '@aws-cdk/aws-elasticsearch'; const domain = new es.Domain(this, 'Domain', { - elasticsearchVersion: '7.4', + elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_1, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'c5.large.elasticsearch', dataNodes: 3, dataNodeInstanceType: 'r5.large.elasticsearch', }, + ebsOptions: { + volumeSize: 100, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, logPublishingOptions: { slowSearchLogEnabed: true, appLogEnabled: true @@ -38,6 +42,16 @@ const domain = new es.Domain(this, 'Domain', { This creates an Elasticsearch cluster and automatically sets up log groups for logging the domain logs and slow search logs. +### Importing existing domains + +To import an existing domain into your CDK application, use the `Domain.fromDomainEndpoint` factory method. +This method accepts a domain endpoint of an already existing domain: + +```ts +const domainEndpoint = 'https://my-domain-jcjotrt6f7otem4sqcwbch3c4u.us-east-1.es.amazonaws.com'; +const domain = Domain.fromDomainEndpoint(this, 'ImportedDomain', domainEndpoint); +domain.grantIndex('existing-index', myLambdaFunction, 'es:ESHttpGet', 'es:ESHttpPut'); +``` ### Permissions @@ -48,7 +62,7 @@ Helper methods also exist for managing access to the domain. ```ts const lambda = new lambda.Function(this, 'Lambda', { /* ... */ }); // Grant the lambda functiomn read access to app-search index -domain.grantIndex(lambda, 'app-search', 'es:HttpGet'); +domain.grantIndex('app-search', lambda, 'es:ESHttpGet'); ``` ### Encryption @@ -57,7 +71,7 @@ The domain can also be created with encryption enabled: ```ts const domain = new es.Domain(this, 'Domain', { - elasticsearchVersion: '7.4', + elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_4, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'c5.large.elasticsearch', @@ -69,7 +83,6 @@ const domain = new es.Domain(this, 'Domain', { enabled: true, }, }); - ``` This sets up the domain with node to node encryption and encryption at @@ -81,9 +94,8 @@ rest. Helper methods exist to access common domain metrics for example: ```ts - -const freeStorageSpace = domain.metricFreeStorageSpace('account-id'); -const masterSysMemoryUtilization = domain.metric('MasterSysMemoryUtilization', 'account-id'); +const freeStorageSpace = domain.metricFreeStorageSpace(); +const masterSysMemoryUtilization = domain.metric('MasterSysMemoryUtilization'); ``` This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 268b9c3a51d29..a7e2671e7ce51 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1,3 +1,5 @@ +import { URL } from 'url'; + 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'; @@ -252,8 +254,9 @@ export interface DomainProps { /** * The Elasticsearch Version * + * @default ElasticsearchVersion.ES_VERSION_7_4 */ - readonly elasticsearchVersion: string; + readonly elasticsearchVersion?: ElasticsearchVersion; /** * Encryption at rest options for the cluster. @@ -329,7 +332,7 @@ export interface IDomain extends cdk.IResource { * principal's policy. * * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) */ grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; @@ -339,7 +342,7 @@ export interface IDomain extends cdk.IResource { * * @param index The index to grant permissions for * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) */ grantIndex(index: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant; @@ -349,119 +352,119 @@ export interface IDomain extends cdk.IResource { * * @param path The path to grant permissions for * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) */ grantPath(path: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant; /** * Return the given named metric for this Domain. */ - metric(metricName: string, clientId: string, props?: MetricOptions): Metric; + metric(metricName: string, props?: MetricOptions): Metric; /** * Metric for the time the cluster status is red. * * @default maximum over a minute */ - metricClusterStatusRed(clientId: string, props?: MetricOptions): Metric; + metricClusterStatusRed(props?: MetricOptions): Metric; /** * Metric for the time the cluster status is yellow. * * @default maximum over a minute */ - metricClusterStatusYellow(clientId: string, props?: MetricOptions): Metric; + metricClusterStatusYellow(props?: MetricOptions): Metric; /** * Metric for the storage space of nodes in the cluster. * * @default minimum over a minute */ - metricFreeStorageSpace(clientId: string, props?: MetricOptions): Metric; + metricFreeStorageSpace(props?: MetricOptions): Metric; /** * Metric for the cluster blocking index writes. * * @default maximum over 5 minutes */ - metricClusterIndexWriteBlocked(clientId: string, props?: MetricOptions): Metric; + metricClusterIndexWriteBlocked(props?: MetricOptions): Metric; /** * Metric for the number of nodes. * * @default minimum over 1 hour */ - metricNodes(clientId: string, props?: MetricOptions): Metric; + metricNodes(props?: MetricOptions): Metric; /** * Metric for automated snapshot failures. * * @default maximum over a minute */ - metricAutomatedSnapshotFailure(clientId: string, props?: MetricOptions): Metric; + metricAutomatedSnapshotFailure(props?: MetricOptions): Metric; /** * Metric for CPU utilization. * * @default maximum over a minute */ - metricCPUUtilization(clientId: string, props?: MetricOptions): Metric; + metricCPUUtilization(props?: MetricOptions): Metric; /** * Metric for JVM memory pressure. * * @default maximum over a minute */ - metricJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric; + metricJVMMemoryPressure(props?: MetricOptions): Metric; /** * Metric for master CPU utilization. * * @default maximum over a minute */ - metricMasterCPUUtilization(clientId: string, props?: MetricOptions): Metric; + metricMasterCPUUtilization(props?: MetricOptions): Metric; /** * Metric for master JVM memory pressure. * * @default maximum over a minute */ - metricMasterJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric; + metricMasterJVMMemoryPressure(props?: MetricOptions): Metric; /** * Metric for KMS key errors. * * @default maximum over a minute */ - metricKMSKeyError(clientId: string, props?: MetricOptions): Metric; + metricKMSKeyError(props?: MetricOptions): Metric; /** * Metric for KMS key being inaccessible. * * @default maximum over a minute */ - metricKMSKeyInaccessible(clientId: string, props?: MetricOptions): Metric; + metricKMSKeyInaccessible(props?: MetricOptions): Metric; /** * Metric for number of searchable documents. * * @default maximum over a minute */ - metricSearchableDocuments(clientId: string, props?: MetricOptions): Metric; + metricSearchableDocuments(props?: MetricOptions): Metric; /** * Metric for search latency. * * @default maximum over a minute */ - metricSearchLatency(clientId: string, props?: MetricOptions): Metric; + metricSearchLatency(props?: MetricOptions): Metric; /** * Metric for indexing latency. * * @default maximum over a minute */ - metricIndexingLatency(clientId: string, props?: MetricOptions): Metric; + metricIndexingLatency(props?: MetricOptions): Metric; } @@ -489,7 +492,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * principal's policy. * * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) */ public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { return iam.Grant.addToPrincipal({ @@ -509,7 +512,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * * @param index The index to grant permissions for * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) */ public grantIndex(index: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant { return iam.Grant.addToPrincipal({ @@ -529,7 +532,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * * @param path The path to grant permissions for * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:HttpGet", "es:HttpPut", ...) + * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) */ public grantPath(path: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant { return iam.Grant.addToPrincipal({ @@ -543,13 +546,13 @@ abstract class DomainBase extends cdk.Resource implements IDomain { /** * Return the given named metric for this Domain. */ - public metric(metricName: string, clientId: string, props?: MetricOptions): Metric { + public metric(metricName: string, props?: MetricOptions): Metric { return new Metric({ namespace: 'AWS/ES', metricName, dimensions: { DomainName: this.domainName, - ClientId: clientId, + ClientId: this.stack.account, }, ...props, }); @@ -558,37 +561,37 @@ abstract class DomainBase extends cdk.Resource implements IDomain { /** * Metric for the time the cluster status is red. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricClusterStatusRed(clientId: string, props?: MetricOptions): Metric { - return this.metric('ClusterStatus.red', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricClusterStatusRed(props?: MetricOptions): Metric { + return this.metric('ClusterStatus.red', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for the time the cluster status is yellow. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricClusterStatusYellow(clientId: string, props?: MetricOptions): Metric { - return this.metric('ClusterStatus.yellow', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricClusterStatusYellow(props?: MetricOptions): Metric { + return this.metric('ClusterStatus.yellow', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for the storage space of nodes in the cluster. * - * @default minimum over a minute + * @default minimum over 5 minutes */ - public metricFreeStorageSpace(clientId: string, props?: MetricOptions): Metric { - return this.metric('FreeStorageSpace', clientId, { statistic: Statistic.MINIMUM, ...props }); + public metricFreeStorageSpace(props?: MetricOptions): Metric { + return this.metric('FreeStorageSpace', { statistic: Statistic.MINIMUM, ...props }); } /** * Metric for the cluster blocking index writes. * - * @default maximum over 5 minutes + * @default maximum over 1 minute */ - public metricClusterIndexWriteBlocked(clientId: string, props?: MetricOptions): Metric { - return this.metric('ClusterIndexWriteBlocked', clientId, { + public metricClusterIndexWriteBlocked(props?: MetricOptions): Metric { + return this.metric('ClusterIndexWriteBlocked', { statistic: Statistic.MAXIMUM, period: cdk.Duration.minutes(1), ...props, @@ -598,10 +601,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { /** * Metric for the number of nodes. * - * @default minimum over 1 hour + * @default maximum over 1 hour */ - public metricNodes(clientId: string, props?: MetricOptions): Metric { - return this.metric('Nodes', clientId, { + public metricNodes(props?: MetricOptions): Metric { + return this.metric('Nodes', { statistic: Statistic.MAXIMUM, period: cdk.Duration.hours(1), ...props, @@ -611,91 +614,91 @@ abstract class DomainBase extends cdk.Resource implements IDomain { /** * Metric for automated snapshot failures. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricAutomatedSnapshotFailure(clientId: string, props?: MetricOptions): Metric { - return this.metric('AutomatedSnapshotFailure', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricAutomatedSnapshotFailure(props?: MetricOptions): Metric { + return this.metric('AutomatedSnapshotFailure', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for CPU utilization. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricCPUUtilization(clientId: string, props?: MetricOptions): Metric { - return this.metric('CPUUtilization', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricCPUUtilization(props?: MetricOptions): Metric { + return this.metric('CPUUtilization', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for JVM memory pressure. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric { - return this.metric('JVMMemoryPressure', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricJVMMemoryPressure(props?: MetricOptions): Metric { + return this.metric('JVMMemoryPressure', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for master CPU utilization. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricMasterCPUUtilization(clientId: string, props?: MetricOptions): Metric { - return this.metric('MasterCPUUtilization', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricMasterCPUUtilization(props?: MetricOptions): Metric { + return this.metric('MasterCPUUtilization', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for master JVM memory pressure. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricMasterJVMMemoryPressure(clientId: string, props?: MetricOptions): Metric { - return this.metric('MasterJVMMemoryPressure', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricMasterJVMMemoryPressure(props?: MetricOptions): Metric { + return this.metric('MasterJVMMemoryPressure', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for KMS key errors. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricKMSKeyError(clientId: string, props?: MetricOptions): Metric { - return this.metric('KMSKeyError', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricKMSKeyError(props?: MetricOptions): Metric { + return this.metric('KMSKeyError', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for KMS key being inaccessible. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricKMSKeyInaccessible(clientId: string, props?: MetricOptions): Metric { - return this.metric('KMSKeyInaccessible', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricKMSKeyInaccessible(props?: MetricOptions): Metric { + return this.metric('KMSKeyInaccessible', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for number of searchable documents. * - * @default maximum over a minute + * @default maximum over 5 minutes */ - public metricSearchableDocuments(clientId: string, props?: MetricOptions): Metric { - return this.metric('SearchableDocuments', clientId, { statistic: Statistic.MAXIMUM, ...props }); + public metricSearchableDocuments(props?: MetricOptions): Metric { + return this.metric('SearchableDocuments', { statistic: Statistic.MAXIMUM, ...props }); } /** * Metric for search latency. * - * @default maximum over a minute + * @default p99 over 5 minutes */ - public metricSearchLatency(clientId: string, props?: MetricOptions): Metric { - return this.metric('SearchLatencyP99', clientId, { statistic: 'p99', ...props }); + public metricSearchLatency(props?: MetricOptions): Metric { + return this.metric('SearchLatencyP99', { statistic: 'p99', ...props }); } /** * Metric for indexing latency. * - * @default maximum over a minute + * @default p99 over 5 minutes */ - public metricIndexingLatency(clientId: string, props?: MetricOptions): Metric { - return this.metric('IndexingLatencyP99', clientId, { statistic: 'p99', ...props }); + public metricIndexingLatency(props?: MetricOptions): Metric { + return this.metric('IndexingLatencyP99', { statistic: 'p99', ...props }); } } @@ -706,19 +709,13 @@ abstract class DomainBase extends cdk.Resource implements IDomain { export interface DomainAttributes { /** * The ARN of the Elasticsearch domain. - * One of this, or {@link domainName}, is required. - * - * @default - no domain arn */ - readonly domainArn?: string; + readonly domainArn: string; /** * The domain name of the Elasticsearch domain. - * One of this, or {@link domainArn}, is required. - * - * @default - no domain name */ - readonly domainName?: string; + readonly domainName: string; /** * The domain endpoint of the Elasticsearch domain. @@ -732,27 +729,21 @@ export interface DomainAttributes { */ export class Domain extends DomainBase implements IDomain { /** - * Creates a Domain construct that represents an external domain via domain arn. - * - * @param scope The parent creating construct (usually `this`). - * @param id The construct's name. - * @param domainName The domain's name. - * @param domainEndpoint The domain's endpoint. - */ - public static fromDomainName(scope: cdk.Construct, id: string, domainName: string, domainEndpoint: string): IDomain { - return Domain.fromDomainAttributes(scope, id, { domainName, domainEndpoint }); - } - - /** - * Creates a Domain construct that represents an external domain via domain arn. + * Creates a Domain construct that represents an external domain via domain endpoint. * * @param scope The parent creating construct (usually `this`). * @param id The construct's name. - * @param domainArn The domain's ARN. * @param domainEndpoint The domain's endpoint. */ - public static fromDomainArn(scope: cdk.Construct, id: string, domainArn: string, domainEndpoint: string): IDomain { - return Domain.fromDomainAttributes(scope, id, { domainArn, domainEndpoint }); + public static fromDomainEndpoint(scope: cdk.Construct, id: string, domainEndpoint: string): IDomain { + const stack = cdk.Stack.of(scope); + const domainName = extractNameFromEndpoint(domainEndpoint); + const domainArn = stack.formatArn({ + service: 'es', + resource: 'domain', + resourceName: domainName, + }); + return Domain.fromDomainAttributes(scope, id, { domainArn, domainName, domainEndpoint }); } /** @@ -763,49 +754,18 @@ export class Domain extends DomainBase implements IDomain { * @param attrs A `DomainAttributes` object. */ public static fromDomainAttributes(scope: cdk.Construct, id: string, attrs: DomainAttributes): IDomain { - - class Import extends DomainBase { - + return new class extends DomainBase implements IDomain { public readonly domainArn: string; public readonly domainName: string; public readonly domainEndpoint: string; - constructor(_domainArn: string, domainName: string, domainEndpoint: string) { + constructor() { super(scope, id); - this.domainArn = _domainArn; - this.domainName = domainName; - this.domainEndpoint = domainEndpoint; + this.domainArn = attrs.domainArn; + this.domainName = attrs.domainName; + this.domainEndpoint = attrs.domainEndpoint; } - } - - let name: string; - let arn: string; - const stack = cdk.Stack.of(scope); - if (!attrs.domainName) { - if (!attrs.domainArn) { throw new Error('One of domainName or domainArn is required!'); } - - arn = attrs.domainArn; - const maybeDomainName = stack.parseArn(attrs.domainArn).resourceName; - if (!maybeDomainName) { throw new Error('ARN for Elasticsearch domain must be in the form: ...'); } - name = maybeDomainName; - } else { - if (attrs.domainArn) { throw new Error('Only one of domainArn or domainName can be provided'); } - name = attrs.domainName; - arn = stack.formatArn({ - service: 'elasticsearch', - resource: 'domain', - resourceName: attrs.domainName, - }); - } - - return new Import(arn, name, attrs.domainEndpoint); - } - - private static createLogGroup(parent: cdk.Construct, domainName: string, id: string, name: string): logs.ILogGroup { - return new logs.LogGroup(parent, id, { - logGroupName: `elasticsearch/domains/${domainName}/${name}`, - retention: logs.RetentionDays.ONE_MONTH, - }); + }; } /** @@ -837,28 +797,32 @@ export class Domain extends DomainBase implements IDomain { physicalName: props.domainName, }); - this.domainName = this.physicalName; - // Setup logging const logGroups: logs.ILogGroup[] = []; if (props.logPublishingOptions?.slowSearchLogEnabed) { this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup ?? - Domain.createLogGroup(this, this.domainName, 'SlowSearchLogs', 'slow-search-logs'); + new logs.LogGroup(scope, 'SlowSearchLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); logGroups.push(this.slowSearchLogGroup); }; if (props.logPublishingOptions?.slowIndexLogEnabed) { this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup ?? - Domain.createLogGroup(this, this.domainName, 'SlowIndexLogs', 'slow-index-logs'); + new logs.LogGroup(scope, 'SlowIndexLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); logGroups.push(this.slowIndexLogGroup); }; if (props.logPublishingOptions?.appLogEnabled) { this.appLogGroup = props.logPublishingOptions.appLogGroup ?? - Domain.createLogGroup(this, this.domainName, 'AppLogs', 'application-logs'); + new logs.LogGroup(scope, 'AppLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); logGroups.push(this.appLogGroup); }; @@ -896,7 +860,7 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, - elasticsearchVersion: props.elasticsearchVersion, + elasticsearchVersion: props.elasticsearchVersion ?? ElasticsearchVersion.ES_VERSION_7_4, elasticsearchClusterConfig: { dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, dedicatedMasterCount: props.clusterConfig.masterNodes, @@ -904,7 +868,10 @@ export class Domain extends DomainBase implements IDomain { instanceCount: props.clusterConfig.dataNodes, instanceType: props.clusterConfig.dataNodeInstanceType, zoneAwarenessEnabled: props.clusterConfig.availabilityZoneCount != null, - zoneAwarenessConfig: { availabilityZoneCount: props.clusterConfig.availabilityZoneCount }, + zoneAwarenessConfig: + props.clusterConfig.availabilityZoneCount != null + ? { availabilityZoneCount: props.clusterConfig.availabilityZoneCount } + : undefined, }, ebsOptions: { ebsEnabled: props.ebsOptions != null, @@ -949,7 +916,69 @@ export class Domain extends DomainBase implements IDomain { resource: 'domain', resourceName: this.physicalName, }); + this.domainName = this.getResourceNameAttribute(this.domain.ref); this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); } } + +/** + * The Elasticsearch version that your domain will leverage. + * + * Per https://aws.amazon.com/elasticsearch-service/faqs/, Amazon Elasticsearch Service + * currently supports Elasticsearch versions 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, 6.0, + * 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. + */ +export enum ElasticsearchVersion { + /** Elasticsearch Version 7.4 */ + ES_VERSION_7_4 = '7.4', + /** Elasticsearch Version 7.1 */ + ES_VERSION_7_1 = '7.1', + /** Elasticsearch Version 6.8 */ + ES_VERSION_6_8 = '6.8', + /** Elasticsearch Version 6.7 */ + ES_VERSION_6_7 = '6.7', + /** Elasticsearch Version 6.5 */ + ES_VERSION_6_5 = '6.5', + /** Elasticsearch Version 6.4 */ + ES_VERSION_6_4 = '6.4', + /** Elasticsearch Version 6.3 */ + ES_VERSION_6_3 = '6.3', + /** Elasticsearch Version 6.2 */ + ES_VERSION_6_2 = '6.2', + /** Elasticsearch Version 6.0 */ + ES_VERSION_6_0 = '6.0', + /** Elasticsearch Version 5.6 */ + ES_VERSION_5_6 = '5.6', + /** Elasticsearch Version 5.5 */ + ES_VERSION_5_5 = '5.5', + /** Elasticsearch Version 5.3 */ + ES_VERSION_5_3 = '5.3', + /** Elasticsearch Version 5.1 */ + ES_VERSION_5_1 = '5.1', + /** Elasticsearch Version 2.3 */ + ES_VERSION_2_3 = '2.3', + /** Elasticsearch Version 1.5 */ + ES_VERSION_1_5 = '1.5', +} + +/** + * Given an Elasticsearch domain endpoint, returns a CloudFormation expression that + * extracts the domain name. + * + * Domain endpoints look like this: + * + * https://example-domain-jcjotrt6f7otem4sqcwbch3c4u.us-east-1.es.amazonaws.com + * https://-..es.amazonaws.com + * + * ..which means that in order to extract the domain name from the endpoint, we can + * split the endpoint using "-" and select the component in index 0. + * + * @param domainEndpoint The Elasticsearch domain endpoint + */ +function extractNameFromEndpoint(domainEndpoint: string) { + const { hostname } = new URL(domainEndpoint); + const domain = hostname.split('.')[0]; + const suffix = '-' + domain.split('-').slice(-1)[0]; + return domain.split(suffix)[0]; +} diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 7de6532623790..003e67cd95dc3 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -73,6 +73,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts new file mode 100644 index 0000000000000..876fbd2608cbb --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -0,0 +1,533 @@ +import '@aws-cdk/assert/jest'; +import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; +import { Subnet, Vpc } from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, Stack, Duration } from '@aws-cdk/core'; +import { Domain, ElasticsearchVersion } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); + + jest.resetAllMocks(); +}); + +const defaultClusterConfig = { + masterNodes: 3, + masterNodeInstanceType: 'c5.large.elasticsearch', + dataNodes: 3, + dataNodeInstanceType: 'r5.large.elasticsearch', +}; + +test('minimal example renders correctly', () => { + new Domain(stack, 'Domain', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_7_1, + clusterConfig: defaultClusterConfig, + }); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + CognitoOptions: { + Enabled: false, + }, + EBSOptions: { + EBSEnabled: false, + }, + ElasticsearchClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + DedicatedMasterType: 'c5.large.elasticsearch', + InstanceCount: 3, + InstanceType: 'r5.large.elasticsearch', + ZoneAwarenessEnabled: false, + }, + ElasticsearchVersion: '7.1', + EncryptionAtRestOptions: { + Enabled: false, + }, + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + NodeToNodeEncryptionOptions: { + Enabled: false, + }, + }); +}); + +describe('log groups', () => { + + test('slowSearchLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + clusterConfig: defaultClusterConfig, + logPublishingOptions: { + slowSearchLogEnabed: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'SlowSearchLogsE00DC2E7', + 'Arn', + ], + }, + Enabled: true, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + }); + }); + + test('slowIndexLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + clusterConfig: defaultClusterConfig, + logPublishingOptions: { + slowIndexLogEnabed: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'SlowIndexLogsAD49DED0', + 'Arn', + ], + }, + Enabled: true, + }, + }, + }); + }); + + test('appLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + clusterConfig: defaultClusterConfig, + logPublishingOptions: { + appLogEnabled: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'AppLogsC5DF83A6', + 'Arn', + ], + }, + Enabled: true, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + }); + }); + +}); + +describe('grants', () => { + + test('"grant" allows adding arbitrary actions associated with this domain resource', () => { + const domain = new Domain(stack, 'Domain', { + clusterConfig: defaultClusterConfig, + }); + const user = new iam.User(stack, 'user'); + + domain.grant(user, 'es:ESHttpGet'); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'es:ESHttpGet', + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'userDefaultPolicy083DF682', + Users: [ + { + Ref: 'user2C2B57AE', + }, + ], + }); + }); + + test('"grant" allows adding arbitrary actions associated with this domain resource', () => { + testGrant( + ['action1', 'action2'], (p, t) => t.grant(p, 'es:action1', 'es:action2')); + }); + + test('"grantIndex" allows adding arbitrary actions associated with an index in this domain resource', () => { + testGrant( + ['action1', 'action2'], + (p, d) => d.grantIndex('my-index', p, 'es:action1', 'es:action2'), + false, + ['/my-index', '/my-index/*'], + ); + }); + + test('"grantPath" allows adding arbitrary actions associated with a given path in this domain resource', () => { + testGrant( + ['action1', 'action2'], + (p, d) => d.grantPath('my-index/my-path', p, 'es:action1', 'es:action2'), + false, + ['/my-index/my-path'], + ); + }); + + test('"grant" for an imported domain', () => { + const domainEndpoint = 'https://test-domain-2w2x2u3tifly-jcjotrt6f7otem4sqcwbch3c4u.testregion.es.amazonaws.com'; + const domain = Domain.fromDomainEndpoint(stack, 'Domain', domainEndpoint); + const user = new iam.User(stack, 'user'); + + domain.grant(user, 'es:*'); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'es:*', + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':es:testregion:1234:domain/test-domain-2w2x2u3tifly', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':es:testregion:1234:domain/test-domain-2w2x2u3tifly/*', + ], + ], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'userDefaultPolicy083DF682', + Users: [ + { + Ref: 'user2C2B57AE', + }, + ], + }); + }); + +}); + +describe('metrics', () => { + + test('Can use metricClusterStatusRed on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricClusterStatusRed(), + 'ClusterStatus.red', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricClusterStatusYellow on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricClusterStatusYellow(), + 'ClusterStatus.yellow', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricFreeStorageSpace on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricFreeStorageSpace(), + 'FreeStorageSpace', + Statistic.MINIMUM, + ); + }); + + test('Can use metricClusterIndexWriteBlocked on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricClusterIndexWriteBlocked(), + 'ClusterIndexWriteBlocked', + Statistic.MAXIMUM, + Duration.minutes(1), + ); + }); + + test('Can use metricNodes on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricNodes(), + 'Nodes', + Statistic.MAXIMUM, + Duration.hours(1), + ); + }); + + test('Can use metricAutomatedSnapshotFailure on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricAutomatedSnapshotFailure(), + 'AutomatedSnapshotFailure', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricCPUUtilization on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricCPUUtilization(), + 'CPUUtilization', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricJVMMemoryPressure on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricJVMMemoryPressure(), + 'JVMMemoryPressure', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricMasterCPUUtilization on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricMasterCPUUtilization(), + 'MasterCPUUtilization', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricMasterJVMMemoryPressure on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricMasterJVMMemoryPressure(), + 'MasterJVMMemoryPressure', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricKMSKeyError on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricKMSKeyError(), + 'KMSKeyError', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricKMSKeyInaccessible on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricKMSKeyInaccessible(), + 'KMSKeyInaccessible', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricSearchableDocuments on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricSearchableDocuments(), + 'SearchableDocuments', + Statistic.MAXIMUM, + ); + }); + + test('Can use metricSearchLatency on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricSearchLatency(), + 'SearchLatencyP99', + 'p99', + ); + }); + + test('Can use metricIndexingLatency on an Elasticsearch Domain', () => { + testMetric( + (domain) => domain.metricIndexingLatency(), + 'IndexingLatencyP99', + 'p99', + ); + }); + +}); + +describe('import', () => { + + test('static fromDomainEndpoint(endpoint) allows importing an external/existing domain', () => { + const domainName = 'test-domain-2w2x2u3tifly'; + const domainEndpoint = `https://${domainName}-jcjotrt6f7otem4sqcwbch3c4u.testregion.es.amazonaws.com`; + const imported = Domain.fromDomainEndpoint(stack, 'Domain', domainEndpoint); + + expect(imported.domainName).toEqual(domainName); + expect(imported.domainArn).toMatch(RegExp(`es:testregion:1234:domain/${domainName}$`)); + + expect(stack).not.toHaveResource('AWS::Elasticsearch::Domain'); + }); + +}); + +describe('custom error responses', () => { + + test('error when availabilityZoneCount does not match vpcOptions.subnets length', () => { + const vpc = new Vpc(stack, 'Vpc'); + + expect(() => new Domain(stack, 'Domain', { + clusterConfig: { + ...defaultClusterConfig, + availabilityZoneCount: 2, + }, + vpcOptions: { + subnets: [ + new Subnet(stack, 'Subnet', { + availabilityZone: 'testaz', + cidrBlock: vpc.vpcCidrBlock, + vpcId: vpc.vpcId, + }), + ], + securityGroups: [], + }, + })).toThrow(/you need to provide a subnet for each AZ you are using/); + }); + +}); + +function testGrant( + expectedActions: string[], + invocation: (user: iam.IPrincipal, domain: Domain) => void, + appliesToDomainRoot: Boolean = true, + paths: string[] = ['/*'], +) { + const domain = new Domain(stack, 'Domain', { + clusterConfig: defaultClusterConfig, + }); + const user = new iam.User(stack, 'user'); + + invocation(user, domain); + + const action = expectedActions.length > 1 ? expectedActions.map(a => `es:${a}`) : `es:${expectedActions[0]}`; + const domainArn = { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'Arn', + ], + }; + const resolvedPaths = paths.map(path => { + return { + 'Fn::Join': [ + '', + [ + domainArn, + path, + ], + ], + }; + }); + const resource = appliesToDomainRoot + ? [domainArn, ...resolvedPaths] + : resolvedPaths.length > 1 + ? resolvedPaths + : resolvedPaths[0]; + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: action, + Effect: 'Allow', + Resource: resource, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'userDefaultPolicy083DF682', + Users: [ + { + Ref: 'user2C2B57AE', + }, + ], + }); +} + +function testMetric( + invocation: (domain: Domain) => Metric, + metricName: string, + statistic: string = Statistic.SUM, + period: Duration = Duration.minutes(5), +) { + const domain = new Domain(stack, 'Domain', { + clusterConfig: defaultClusterConfig, + }); + + const metric = invocation(domain); + + expect(metric).toMatchObject({ + metricName, + namespace: 'AWS/ES', + period, + statistic, + dimensions: { + ClientId: '1234', + }, + }); + expect(metric.dimensions).toHaveProperty('DomainName'); +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json new file mode 100644 index 0000000000000..7998e0bbc9207 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -0,0 +1,284 @@ +{ + "Resources": { + "DomainESLogGroupPolicy5373A2E8": { + "Type": "Custom::CloudwatchLogResourcePolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "service": "CloudWatchLogs", + "action": "putResourcePolicy", + "parameters": { + "policyName": "ESLogPolicy", + "policyDocument": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", + { + "Fn::GetAtt": [ + "SlowSearchLogsE00DC2E7", + "Arn" + ] + }, + "\",\"", + { + "Fn::GetAtt": [ + "AppLogsC5DF83A6", + "Arn" + ] + }, + "\"]}],\"Version\":\"2012-10-17\"}" + ] + ] + } + }, + "physicalResourceId": { + "id": "ESLogGroupPolicy" + } + }, + "Update": { + "service": "CloudWatchLogs", + "action": "putResourcePolicy", + "parameters": { + "policyName": "ESLogPolicy", + "policyDocument": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", + { + "Fn::GetAtt": [ + "SlowSearchLogsE00DC2E7", + "Arn" + ] + }, + "\",\"", + { + "Fn::GetAtt": [ + "AppLogsC5DF83A6", + "Arn" + ] + }, + "\"]}],\"Version\":\"2012-10-17\"}" + ] + ] + } + }, + "physicalResourceId": { + "id": "ESLogGroupPolicy" + } + }, + "Delete": { + "service": "CloudWatchLogs", + "action": "deleteResourcePolicy", + "parameters": { + "policyName": "ESLogPolicy" + }, + "ignoreErrorCodesMatching": "400" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Domain66AC69E0": { + "Type": "AWS::Elasticsearch::Domain", + "Properties": { + "CognitoOptions": { + "Enabled": false + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10, + "VolumeType": "gp2" + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "DedicatedMasterType": "m4.large.elasticsearch", + "InstanceCount": 3, + "InstanceType": "m4.large.elasticsearch", + "ZoneAwarenessEnabled": false + }, + "ElasticsearchVersion": "7.1", + "EncryptionAtRestOptions": { + "Enabled": true + }, + "LogPublishingOptions": { + "ES_APPLICATION_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "AppLogsC5DF83A6", + "Arn" + ] + }, + "Enabled": true + }, + "SEARCH_SLOW_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "SlowSearchLogsE00DC2E7", + "Arn" + ] + }, + "Enabled": true + }, + "INDEX_SLOW_LOGS": { + "Enabled": false + } + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + }, + "DependsOn": [ + "DomainESLogGroupPolicy5373A2E8" + ] + }, + "SlowSearchLogsE00DC2E7": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLogsC5DF83A6": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "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" + ] + ] + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutResourcePolicy", + "logs:DeleteResourcePolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3Bucket67234880" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] + } + }, + "Parameters": { + "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3Bucket67234880": { + "Type": "String", + "Description": "S3 bucket for asset \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" + }, + "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96": { + "Type": "String", + "Description": "S3 key for asset version \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" + }, + "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061ArtifactHash9212BF97": { + "Type": "String", + "Description": "Artifact hash for asset \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts new file mode 100644 index 0000000000000..ae4271337210b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -0,0 +1,35 @@ +import { EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; +import { App, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as es from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + new es.Domain(this, 'Domain', { + elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_1, + clusterConfig: { + masterNodes: 3, + masterNodeInstanceType: 'm4.large.elasticsearch', + dataNodes: 3, + dataNodeInstanceType: 'm4.large.elasticsearch', + }, + ebsOptions: { + volumeSize: 10, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, + logPublishingOptions: { + slowSearchLogEnabed: true, + appLogEnabled: true, + }, + nodeToNodeEncryptionEnabled: true, + encryptionAtRestOptions: { + enabled: true, + }, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-elasticsearch'); +app.synth(); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts new file mode 100644 index 0000000000000..4fefd1d13f8b6 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts @@ -0,0 +1,65 @@ +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, Stack } from '@aws-cdk/core'; +import { LogGroupResourcePolicy } from '../lib/log-group-resource-policy'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('minimal example renders correctly', () => { + new LogGroupResourcePolicy(stack, 'LogGroupResourcePolicy', { + policyName: 'TestPolicy', + policyStatements: [new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], + resources: ['*'], + principals: [new iam.ServicePrincipal('es.amazonaws.com')], + })], + }); + + expect(stack).toHaveResource('Custom::CloudwatchLogResourcePolicy', { + ServiceToken: { + 'Fn::GetAtt': [ + 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', + 'Arn', + ], + }, + Create: { + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName: 'TestPolicy', + policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', + }, + physicalResourceId: { + id: 'LogGroupResourcePolicy', + }, + }, + Update: { + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName: 'TestPolicy', + policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', + }, + physicalResourceId: { + id: 'LogGroupResourcePolicy', + }, + }, + Delete: { + service: 'CloudWatchLogs', + action: 'deleteResourcePolicy', + parameters: { + policyName: 'TestPolicy', + }, + ignoreErrorCodesMatching: '400', + }, + }); +}); From f562e67d231ba0a32faa6dcc995020bbb07b8b50 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Thu, 30 Jul 2020 05:06:18 -0500 Subject: [PATCH 08/45] better grant methods --- packages/@aws-cdk/aws-elasticsearch/README.md | 9 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 518 +++++++++++++----- .../@aws-cdk/aws-elasticsearch/lib/perms.ts | 32 ++ .../aws-elasticsearch/test/domain.test.ts | 118 ++-- 4 files changed, 485 insertions(+), 192 deletions(-) create mode 100644 packages/@aws-cdk/aws-elasticsearch/lib/perms.ts diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 1053bec115348..d8b5b364ce5ff 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -50,7 +50,6 @@ This method accepts a domain endpoint of an already existing domain: ```ts const domainEndpoint = 'https://my-domain-jcjotrt6f7otem4sqcwbch3c4u.us-east-1.es.amazonaws.com'; const domain = Domain.fromDomainEndpoint(this, 'ImportedDomain', domainEndpoint); -domain.grantIndex('existing-index', myLambdaFunction, 'es:ESHttpGet', 'es:ESHttpPut'); ``` ### Permissions @@ -61,8 +60,12 @@ Helper methods also exist for managing access to the domain. ```ts const lambda = new lambda.Function(this, 'Lambda', { /* ... */ }); -// Grant the lambda functiomn read access to app-search index -domain.grantIndex('app-search', lambda, 'es:ESHttpGet'); + +// Grant write access to the app-search index +domain.grantIndexWrite('app-search', lambda); + +// Grant read access to the 'app-search/_search' path +domain.grantPathRead('app-search/_search', lambda); ``` ### Encryption diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index a7e2671e7ce51..9acc393788069 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -9,6 +9,7 @@ import * as cdk from '@aws-cdk/core'; import { CfnDomain } from './elasticsearch.generated'; import { LogGroupResourcePolicy } from './log-group-resource-policy'; +import * as perms from './perms'; /** * Configures the makeup of the cluster such as number of nodes and instance @@ -141,7 +142,7 @@ export interface LoggingOptions { /** * Whether the domain should encrypt data at rest, and if so, the AWS Key * Management Service (KMS) key to use. Can only be used to create a new domain, - * not update an existing one + * not update an existing one. */ export interface EncryptionAtRestOptions { /** @@ -328,33 +329,114 @@ export interface IDomain extends cdk.IResource { readonly domainEndpoint: string; /** - * Adds an IAM policy statement associated with this domain to an IAM - * principal's policy. + * Optional KMS encryption key associated with this domain. + */ + readonly encryptionKey?: kms.IKey; + + /** + * Grant read permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to decrypt the contents + * of the domain will also be granted to the same principal. + * + * @param identity The principal + */ + grantRead(identity: iam.IGrantable): iam.Grant; + + /** + * Grant write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt the contents + * of the domain will also be granted to the same principal. + * + * @param identity The principal + */ + grantWrite(identity: iam.IGrantable): iam.Grant; + + /** + * Grant read/write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). * - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) + * If encryption is used, permission to use the key to encrypt and decrypt the + * contents of the domain will also be granted to the same principal. + * + * @param identity The principal */ - grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + grantReadWrite(identity: iam.IGrantable): iam.Grant; /** - * Adds an IAM policy statement associated with an index in this domain to an IAM - * principal's policy. + * Grant read permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to decrypt the contents + * of the index will also be granted to the same principal. * * @param index The index to grant permissions for - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) + * @param identity The principal */ - grantIndex(index: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + grantIndexRead(index: string, identity: iam.IGrantable): iam.Grant; /** - * Adds an IAM policy statement associated with a path in this domain to an IAM - * principal's policy. + * Grant write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt the contents + * of the index will also be granted to the same principal. + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexWrite(index: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant read/write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt/decrypt the contents + * of the index will also be granted to the same principal. + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexReadWrite(index: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant read permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to decrypt the contents + * of the path will also be granted to the same principal. * * @param path The path to grant permissions for - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) + * @param identity The principal */ - grantPath(path: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + grantPathRead(path: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt the contents + * of the path will also be granted to the same principal. + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathWrite(path: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant read/write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt/decrypt the contents + * of the path will also be granted to the same principal. + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathReadWrite(path: string, identity: iam.IGrantable): iam.Grant; /** * Return the given named metric for this Domain. @@ -364,105 +446,105 @@ export interface IDomain extends cdk.IResource { /** * Metric for the time the cluster status is red. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricClusterStatusRed(props?: MetricOptions): Metric; /** * Metric for the time the cluster status is yellow. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricClusterStatusYellow(props?: MetricOptions): Metric; /** * Metric for the storage space of nodes in the cluster. * - * @default minimum over a minute + * @default minimum over 5 minutes */ metricFreeStorageSpace(props?: MetricOptions): Metric; /** * Metric for the cluster blocking index writes. * - * @default maximum over 5 minutes + * @default maximum over 1 minute */ metricClusterIndexWriteBlocked(props?: MetricOptions): Metric; /** * Metric for the number of nodes. * - * @default minimum over 1 hour + * @default maximum over 1 hour */ metricNodes(props?: MetricOptions): Metric; /** * Metric for automated snapshot failures. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricAutomatedSnapshotFailure(props?: MetricOptions): Metric; /** * Metric for CPU utilization. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricCPUUtilization(props?: MetricOptions): Metric; /** * Metric for JVM memory pressure. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricJVMMemoryPressure(props?: MetricOptions): Metric; /** * Metric for master CPU utilization. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricMasterCPUUtilization(props?: MetricOptions): Metric; /** * Metric for master JVM memory pressure. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricMasterJVMMemoryPressure(props?: MetricOptions): Metric; /** * Metric for KMS key errors. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricKMSKeyError(props?: MetricOptions): Metric; /** * Metric for KMS key being inaccessible. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricKMSKeyInaccessible(props?: MetricOptions): Metric; /** * Metric for number of searchable documents. * - * @default maximum over a minute + * @default maximum over 5 minutes */ metricSearchableDocuments(props?: MetricOptions): Metric; /** * Metric for search latency. * - * @default maximum over a minute + * @default p99 over 5 minutes */ metricSearchLatency(props?: MetricOptions): Metric; /** * Metric for indexing latency. * - * @default maximum over a minute + * @default p99 over 5 minutes */ metricIndexingLatency(props?: MetricOptions): Metric; } @@ -472,75 +554,187 @@ export interface IDomain extends cdk.IResource { * A new or imported domain. */ abstract class DomainBase extends cdk.Resource implements IDomain { - /** - * @attribute - */ public abstract readonly domainArn: string; + public abstract readonly domainName: string; + public abstract readonly domainEndpoint: string; /** - * @attribute + * Optional KMS encryption key associated with this domain. */ - public abstract readonly domainName: string; + public abstract readonly encryptionKey?: kms.IKey; /** - * @attribute - */ - public abstract readonly domainEndpoint: string; + * Grant read permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to decrypt the contents + * of the domain will also be granted to the same principal. + * + * @param identity The principal + */ + grantRead(identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_ACTIONS, + perms.KEY_READ_ACTIONS, + this.domainArn, + `${this.domainArn}/*`, + ); + } /** - * Adds an IAM policy statement associated with this domain to an IAM - * principal's policy. + * Grant write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). * - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) - */ - public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { - return iam.Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [ - this.domainArn, - `${this.domainArn}/*`, - ], - scope: this, - }); + * If encryption is used, permission to use the key to encrypt the contents + * of the domain will also be granted to the same principal. + * + * @param identity The principal + */ + grantWrite(identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_WRITE_ACTIONS, + perms.KEY_WRITE_ACTIONS, + this.domainArn, + `${this.domainArn}/*`, + ); } /** - * Adds an IAM policy statement associated with an index in this domain to an IAM - * principal's policy. + * Grant read/write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). * - * @param index The index to grant permissions for - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) - */ - public grantIndex(index: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant { - return iam.Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [ - `${this.domainArn}/${index}`, - `${this.domainArn}/${index}/*`, - ], - scope: this, - }); + * If encryption is used, permission to use the key to encrypt and decrypt the + * contents of the domain will also be granted to the same principal. + * + * @param identity The principal + */ + grantReadWrite(identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_WRITE_ACTIONS, + perms.KEY_READ_WRITE_ACTIONS, + this.domainArn, + `${this.domainArn}/*`, + ); } /** - * Adds an IAM policy statement associated with a path in this domain to an IAM - * principal's policy. + * Grant read permissions for an index in this domain to an IAM + * principal (Role/Group/User). * - * @param path The path to grant permissions for - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "es:ESHttpGet", "es:ESHttpPut", ...) - */ - public grantPath(path: string, grantee: iam.IGrantable, ...actions: string[]): iam.Grant { - return iam.Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [`${this.domainArn}/${path}`], - scope: this, - }); + * If encryption is used, permission to use the key to decrypt the contents + * of the index will also be granted to the same principal. + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexRead(index: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_ACTIONS, + perms.KEY_READ_ACTIONS, + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ); + } + + /** + * Grant write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt the contents + * of the index will also be granted to the same principal. + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexWrite(index: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_WRITE_ACTIONS, + perms.KEY_WRITE_ACTIONS, + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ); + } + + /** + * Grant read/write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt/decrypt the contents + * of the index will also be granted to the same principal. + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexReadWrite(index: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_WRITE_ACTIONS, + perms.KEY_READ_WRITE_ACTIONS, + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ); + } + + /** + * Grant read permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to decrypt the contents + * of the path will also be granted to the same principal. + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathRead(path: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_ACTIONS, + perms.KEY_READ_ACTIONS, + `${this.domainArn}/${path}`, + ); + } + + /** + * Grant write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt the contents + * of the path will also be granted to the same principal. + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathWrite(path: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_WRITE_ACTIONS, + perms.KEY_WRITE_ACTIONS, + `${this.domainArn}/${path}`, + ); + } + + /** + * Grant read/write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * If encryption is used, permission to use the key to encrypt/decrypt the contents + * of the path will also be granted to the same principal. + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathReadWrite(path: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_WRITE_ACTIONS, + perms.KEY_READ_WRITE_ACTIONS, + `${this.domainArn}/${path}`, + ); } /** @@ -564,7 +758,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricClusterStatusRed(props?: MetricOptions): Metric { - return this.metric('ClusterStatus.red', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('ClusterStatus.red', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -573,7 +770,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricClusterStatusYellow(props?: MetricOptions): Metric { - return this.metric('ClusterStatus.yellow', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('ClusterStatus.yellow', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -582,7 +782,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default minimum over 5 minutes */ public metricFreeStorageSpace(props?: MetricOptions): Metric { - return this.metric('FreeStorageSpace', { statistic: Statistic.MINIMUM, ...props }); + return this.metric('FreeStorageSpace', { + statistic: Statistic.MINIMUM, + ...props, + }); } /** @@ -617,7 +820,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricAutomatedSnapshotFailure(props?: MetricOptions): Metric { - return this.metric('AutomatedSnapshotFailure', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('AutomatedSnapshotFailure', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -626,7 +832,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricCPUUtilization(props?: MetricOptions): Metric { - return this.metric('CPUUtilization', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('CPUUtilization', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -635,7 +844,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricJVMMemoryPressure(props?: MetricOptions): Metric { - return this.metric('JVMMemoryPressure', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('JVMMemoryPressure', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -644,7 +856,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricMasterCPUUtilization(props?: MetricOptions): Metric { - return this.metric('MasterCPUUtilization', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('MasterCPUUtilization', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -653,7 +868,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricMasterJVMMemoryPressure(props?: MetricOptions): Metric { - return this.metric('MasterJVMMemoryPressure', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('MasterJVMMemoryPressure', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -662,7 +880,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricKMSKeyError(props?: MetricOptions): Metric { - return this.metric('KMSKeyError', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('KMSKeyError', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -671,7 +892,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricKMSKeyInaccessible(props?: MetricOptions): Metric { - return this.metric('KMSKeyInaccessible', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('KMSKeyInaccessible', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -680,7 +904,10 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default maximum over 5 minutes */ public metricSearchableDocuments(props?: MetricOptions): Metric { - return this.metric('SearchableDocuments', { statistic: Statistic.MAXIMUM, ...props }); + return this.metric('SearchableDocuments', { + statistic: Statistic.MAXIMUM, + ...props, + }); } /** @@ -700,6 +927,29 @@ abstract class DomainBase extends cdk.Resource implements IDomain { public metricIndexingLatency(props?: MetricOptions): Metric { return this.metric('IndexingLatencyP99', { statistic: 'p99', ...props }); } + + private grant( + grantee: iam.IGrantable, + domainActions: string[], + keyActions: string[], + resourceArn: string, + ...otherResourceArns: string[] + ): iam.Grant { + const resourceArns = [resourceArn, ...otherResourceArns]; + + const grant = iam.Grant.addToPrincipal({ + grantee, + actions: domainActions, + resourceArns, + scope: this, + }); + + if (this.encryptionKey && keyActions && keyActions.length !== 0) { + this.encryptionKey.grant(grantee, ...keyActions); + } + + return grant; + } } @@ -721,6 +971,13 @@ export interface DomainAttributes { * The domain endpoint of the Elasticsearch domain. */ readonly domainEndpoint: string; + + /** + * Optional KMS encryption key associated with this domain. + * + * @default no encryption key + */ + readonly encryptionKey?: kms.IKey; } @@ -735,7 +992,11 @@ export class Domain extends DomainBase implements IDomain { * @param id The construct's name. * @param domainEndpoint The domain's endpoint. */ - public static fromDomainEndpoint(scope: cdk.Construct, id: string, domainEndpoint: string): IDomain { + public static fromDomainEndpoint( + scope: cdk.Construct, + id: string, + domainEndpoint: string, + ): IDomain { const stack = cdk.Stack.of(scope); const domainName = extractNameFromEndpoint(domainEndpoint); const domainArn = stack.formatArn({ @@ -743,7 +1004,12 @@ export class Domain extends DomainBase implements IDomain { resource: 'domain', resourceName: domainName, }); - return Domain.fromDomainAttributes(scope, id, { domainArn, domainName, domainEndpoint }); + + return Domain.fromDomainAttributes(scope, id, { + domainArn, + domainName, + domainEndpoint, + }); } /** @@ -754,42 +1020,25 @@ export class Domain extends DomainBase implements IDomain { * @param attrs A `DomainAttributes` object. */ public static fromDomainAttributes(scope: cdk.Construct, id: string, attrs: DomainAttributes): IDomain { - return new class extends DomainBase implements IDomain { - public readonly domainArn: string; - public readonly domainName: string; - public readonly domainEndpoint: string; - - constructor() { - super(scope, id); - this.domainArn = attrs.domainArn; - this.domainName = attrs.domainName; - this.domainEndpoint = attrs.domainEndpoint; - } + return new class extends DomainBase { + public readonly domainArn = attrs.domainArn; + public readonly domainName = attrs.domainName; + public readonly domainEndpoint = attrs.domainEndpoint; + public readonly encryptionKey = attrs.encryptionKey; + + constructor() { super(scope, id); } }; } - /** - * @attribute - */ public readonly domainArn: string; - - /** - * @attribute - */ public readonly domainName: string; - - /** - * @attribute - */ public readonly domainEndpoint: string; + public readonly encryptionKey?: kms.IKey; private readonly domain: CfnDomain; - private readonly slowSearchLogGroup?: logs.ILogGroup; - private readonly slowIndexLogGroup?: logs.ILogGroup; - private readonly appLogGroup?: logs.ILogGroup; constructor(scope: cdk.Construct, id: string, props: DomainProps) { @@ -797,6 +1046,19 @@ export class Domain extends DomainBase implements IDomain { physicalName: props.domainName, }); + // If VPC options are supplied ensure that the number of subnets matches the number AZ + if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.clusterConfig.availabilityZoneCount) { + throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); + }; + + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; + if (props.vpcOptions) { + cfnVpcOptions = { + securityGroupIds: props.vpcOptions.securityGroups.map((sg) => sg.securityGroupId), + subnetIds: props.vpcOptions.subnets.map((subnet) => subnet.subnetId), + }; + } + // Setup logging const logGroups: logs.ILogGroup[] = []; @@ -844,19 +1106,6 @@ export class Domain extends DomainBase implements IDomain { }); } - // If VPC options are supplied ensure that the number of subnets matches the number AZ - if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.clusterConfig.availabilityZoneCount) { - throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); - }; - - let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; - if (props.vpcOptions) { - cfnVpcOptions = { - securityGroupIds: props.vpcOptions.securityGroups.map((sg) => sg.securityGroupId), - subnetIds: props.vpcOptions.subnets.map((subnet) => subnet.subnetId), - }; - } - // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, @@ -911,6 +1160,7 @@ export class Domain extends DomainBase implements IDomain { if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + this.encryptionKey = props.encryptionAtRestOptions?.kmsKey; this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { service: 'es', resource: 'domain', diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts b/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts new file mode 100644 index 0000000000000..38133c2d6ca86 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts @@ -0,0 +1,32 @@ +export const ES_READ_ACTIONS = [ + 'es:ESHttpGet', + 'es:ESHttpHead', +]; + +export const ES_WRITE_ACTIONS = [ + 'es:ESHttpDelete', + 'es:ESHttpPost', + 'es:ESHttpPut', + 'es:ESHttpPatch', +]; + +export const ES_READ_WRITE_ACTIONS = [ + ...ES_READ_ACTIONS, + ...ES_WRITE_ACTIONS, +]; + +export const KEY_READ_ACTIONS = [ + 'kms:Decrypt', + 'kms:DescribeKey', +]; + +export const KEY_WRITE_ACTIONS = [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', +]; + +export const KEY_READ_WRITE_ACTIONS = [ + ...KEY_READ_ACTIONS, + ...KEY_WRITE_ACTIONS, +]; diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 876fbd2608cbb..52bb223bc763f 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -24,6 +24,13 @@ const defaultClusterConfig = { dataNodeInstanceType: 'r5.large.elasticsearch', }; +const readActions = ['ESHttpGet', 'ESHttpHead']; +const writeActions = ['ESHttpDelete', 'ESHttpPost', 'ESHttpPut', 'ESHttpPatch']; +const readWriteActions = [ + ...readActions, + ...writeActions, +]; + test('minimal example renders correctly', () => { new Domain(stack, 'Domain', { elasticsearchVersion: ElasticsearchVersion.ES_VERSION_7_1, @@ -159,73 +166,67 @@ describe('log groups', () => { describe('grants', () => { - test('"grant" allows adding arbitrary actions associated with this domain resource', () => { - const domain = new Domain(stack, 'Domain', { - clusterConfig: defaultClusterConfig, - }); - const user = new iam.User(stack, 'user'); + test('"grantRead" allows read actions associated with this domain resource', () => { + testGrant(readActions, (p, t) => t.grantRead(p)); + }); - domain.grant(user, 'es:ESHttpGet'); + test('"grantWrite" allows write actions associated with this domain resource', () => { + testGrant(writeActions, (p, t) => t.grantWrite(p)); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'es:ESHttpGet', - Effect: 'Allow', - Resource: [ - { - 'Fn::GetAtt': [ - 'Domain66AC69E0', - 'Arn', - ], - }, - { - 'Fn::Join': [ - '', - [ - { - 'Fn::GetAtt': [ - 'Domain66AC69E0', - 'Arn', - ], - }, - '/*', - ], - ], - }, - ], - }, - ], - Version: '2012-10-17', - }, - PolicyName: 'userDefaultPolicy083DF682', - Users: [ - { - Ref: 'user2C2B57AE', - }, - ], - }); + test('"grantReadWrite" allows read and write actions associated with this domain resource', () => { + testGrant(readWriteActions, (p, t) => t.grantReadWrite(p)); }); - test('"grant" allows adding arbitrary actions associated with this domain resource', () => { + test('"grantIndexRead" allows read actions associated with an index in this domain resource', () => { testGrant( - ['action1', 'action2'], (p, t) => t.grant(p, 'es:action1', 'es:action2')); + readActions, + (p, d) => d.grantIndexRead('my-index', p), + false, + ['/my-index', '/my-index/*'], + ); }); - test('"grantIndex" allows adding arbitrary actions associated with an index in this domain resource', () => { + test('"grantIndexWrite" allows write actions associated with an index in this domain resource', () => { testGrant( - ['action1', 'action2'], - (p, d) => d.grantIndex('my-index', p, 'es:action1', 'es:action2'), + writeActions, + (p, d) => d.grantIndexWrite('my-index', p), false, ['/my-index', '/my-index/*'], ); }); - test('"grantPath" allows adding arbitrary actions associated with a given path in this domain resource', () => { + test('"grantIndexReadWrite" allows read and write actions associated with an index in this domain resource', () => { testGrant( - ['action1', 'action2'], - (p, d) => d.grantPath('my-index/my-path', p, 'es:action1', 'es:action2'), + readWriteActions, + (p, d) => d.grantIndexReadWrite('my-index', p), + false, + ['/my-index', '/my-index/*'], + ); + }); + + test('"grantPathRead" allows read actions associated with a given path in this domain resource', () => { + testGrant( + readActions, + (p, d) => d.grantPathRead('my-index/my-path', p), + false, + ['/my-index/my-path'], + ); + }); + + test('"grantPathWrite" allows write actions associated with a given path in this domain resource', () => { + testGrant( + writeActions, + (p, d) => d.grantPathWrite('my-index/my-path', p), + false, + ['/my-index/my-path'], + ); + }); + + test('"grantPathReadWrite" allows read and write actions associated with a given path in this domain resource', () => { + testGrant( + readWriteActions, + (p, d) => d.grantPathReadWrite('my-index/my-path', p), false, ['/my-index/my-path'], ); @@ -236,13 +237,20 @@ describe('grants', () => { const domain = Domain.fromDomainEndpoint(stack, 'Domain', domainEndpoint); const user = new iam.User(stack, 'user'); - domain.grant(user, 'es:*'); + domain.grantReadWrite(user); expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { - Action: 'es:*', + Action: [ + 'es:ESHttpGet', + 'es:ESHttpHead', + 'es:ESHttpDelete', + 'es:ESHttpPost', + 'es:ESHttpPut', + 'es:ESHttpPatch', + ], Effect: 'Allow', Resource: [ { From ebc698bdb93fccbf12d6da22c0e6e5aca8e7d14b Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Thu, 30 Jul 2020 07:09:34 -0500 Subject: [PATCH 09/45] more input validation --- packages/@aws-cdk/aws-elasticsearch/README.md | 6 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 103 ++++++++-- .../aws-elasticsearch/test/domain.test.ts | 189 +++++++++++++++++- .../test/integ.elasticsearch.ts | 2 +- 4 files changed, 279 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index d8b5b364ce5ff..84dec2761c0a4 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -33,7 +33,7 @@ const domain = new es.Domain(this, 'Domain', { volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, }, logPublishingOptions: { - slowSearchLogEnabed: true, + slowSearchLogEnabled: true, appLogEnabled: true }, }); @@ -81,6 +81,10 @@ const domain = new es.Domain(this, 'Domain', { dataNodes: 3, dataNodeInstanceType: 'r5.large.elasticsearch', }, + ebsOptions: { + volumeSize: 100, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, nodeToNodeEncryptionEnabled: true, encryptionAtRestOptions: { enabled: true, diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 9acc393788069..846f398b714cd 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -98,10 +98,11 @@ export interface EbsOptions { export interface LoggingOptions { /** * Specify if slow search logging should be set up. + * Requires Elasticsearch version 5.1 or later. * * @default - false */ - readonly slowSearchLogEnabed?: boolean; + readonly slowSearchLogEnabled?: boolean; /** * Log slow searches to this log group. @@ -112,13 +113,14 @@ export interface LoggingOptions { /** * Specify if slow index logging should be set up. + * Requires Elasticsearch version 5.1 or later. * * @default - false */ - readonly slowIndexLogEnabed?: boolean; + readonly slowIndexLogEnabled?: boolean; /** - * Log slow indecies to this log group. + * Log slow indices to this log group. * * @default - a new log group is created if slow index logging is enabled */ @@ -126,6 +128,7 @@ export interface LoggingOptions { /** * Specify if Elasticsearch application logging should be set up. + * Requires Elasticsearch version 5.1 or later. * * @default - false */ @@ -142,7 +145,7 @@ export interface LoggingOptions { /** * Whether the domain should encrypt data at rest, and if so, the AWS Key * Management Service (KMS) key to use. Can only be used to create a new domain, - * not update an existing one. + * not update an existing one. Requires Elasticsearch version 5.1 or later. */ export interface EncryptionAtRestOptions { /** @@ -277,6 +280,7 @@ export interface DomainProps { /** * Specify true to enable node to node encryption. + * Requires Elasticsearch version 6.0 or later. * * @default - Node to node encryption is not enabled. */ @@ -284,10 +288,9 @@ export interface DomainProps { /** * The hour in UTC during which the service takes an automated daily snapshot - * of the indices in the Amazon ES domain. Only applies for Elasticsearch - * versions below 5.3. + * of the indices in the Amazon ES domain. Requires Elasticsearch version 5.3 and later. * - * @default - Not used for Elasticsearch versions above 5.3. + * @default - Hourly automated snapshots not used */ readonly automatedSnapshotStartHour?: number; @@ -1051,6 +1054,79 @@ export class Domain extends DomainBase implements IDomain { throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); }; + const masterInstanceType = props.clusterConfig.masterNodeInstanceType.toLowerCase(); + const dataInstanceType = props.clusterConfig.dataNodeInstanceType.toLowerCase(); + + if ([masterInstanceType, dataInstanceType].some(instanceType => !instanceType.endsWith('.elasticsearch'))) { + throw new Error('Master and data node instance types must end with ".elasticsearch".'); + } + + const elasticsearchVersion = props.elasticsearchVersion ?? ElasticsearchVersion.ES_VERSION_7_4; + const versionNumber = parseFloat(elasticsearchVersion.toString()); + const encryptionAtRestEnabled = props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null); + const ebsEnabled = props.ebsOptions != null; + + const isInstanceType = function(instanceType: string) : Boolean { + return masterInstanceType.startsWith(instanceType) || dataInstanceType.startsWith(instanceType); + }; + + const isSomeInstanceType = function(...instanceTypes: string[]) : Boolean { + return instanceTypes.some(isInstanceType); + }; + + const isEveryInstanceType = function(...instanceTypes: string[]) : Boolean { + return instanceTypes.every(isInstanceType); + }; + + // Validate feature support for the given Elasticsearch version, per + // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-features-by-version.html + if (versionNumber < 5.1) { + if ( + props.logPublishingOptions?.slowIndexLogEnabled + || props.logPublishingOptions?.appLogEnabled + || props.logPublishingOptions?.slowSearchLogEnabled + ) { + throw new Error('Error and slow logs publishing requires Elasticsearch version 5.1 or later.'); + } + if (props.encryptionAtRestOptions?.enabled) { + throw new Error('Encryption of data at rest requires Elasticsearch version 5.1 or later.'); + } + if (props.cognitoOptions != null) { + throw new Error('Cognito authentication for Kibana requires Elasticsearch version 5.1 or later.'); + } + if (isSomeInstanceType('c5', 'i3', 'm5', 'r5')) { + throw new Error('C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later.'); + } + } else if (versionNumber < 5.3) { + if (props.automatedSnapshotStartHour) { + throw new Error('Hourly automated snapshots requires Elasticsearch version 5.3 or later.'); + } + } else if (versionNumber < 6.0) { + if (props.nodeToNodeEncryptionEnabled) { + throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.'); + } + } + + // Validate against instance type restrictions, per + // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html + if (isInstanceType('i3') && ebsEnabled) { + throw new Error('I3 instance types do not support EBS storage volumes.'); + } + + if (isSomeInstanceType('m3', 'r3', 't2') && encryptionAtRestEnabled) { + throw new Error('M3, R3, and T2 instance types do not support encryption of data at rest.'); + } + + if (isInstanceType('t2.micro') && versionNumber > 2.3) { + throw new Error('The t2.micro.elasticsearch instance type supports only Elasticsearch 1.5 and 2.3.'); + } + + // Only R3 and I3 support instance storage, per + // https://aws.amazon.com/elasticsearch-service/pricing/ + if (!ebsEnabled && !isEveryInstanceType('r3', 'i3')) { + throw new Error('EBS volumes are required for all instance types except R3 and I3.'); + } + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; if (props.vpcOptions) { cfnVpcOptions = { @@ -1062,7 +1138,7 @@ export class Domain extends DomainBase implements IDomain { // Setup logging const logGroups: logs.ILogGroup[] = []; - if (props.logPublishingOptions?.slowSearchLogEnabed) { + if (props.logPublishingOptions?.slowSearchLogEnabled) { this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup ?? new logs.LogGroup(scope, 'SlowSearchLogs', { retention: logs.RetentionDays.ONE_MONTH, @@ -1071,7 +1147,7 @@ export class Domain extends DomainBase implements IDomain { logGroups.push(this.slowSearchLogGroup); }; - if (props.logPublishingOptions?.slowIndexLogEnabed) { + if (props.logPublishingOptions?.slowIndexLogEnabled) { this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup ?? new logs.LogGroup(scope, 'SlowIndexLogs', { retention: logs.RetentionDays.ONE_MONTH, @@ -1109,7 +1185,7 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, - elasticsearchVersion: props.elasticsearchVersion ?? ElasticsearchVersion.ES_VERSION_7_4, + elasticsearchVersion: elasticsearchVersion, elasticsearchClusterConfig: { dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, dedicatedMasterCount: props.clusterConfig.masterNodes, @@ -1123,13 +1199,13 @@ export class Domain extends DomainBase implements IDomain { : undefined, }, ebsOptions: { - ebsEnabled: props.ebsOptions != null, + ebsEnabled, volumeSize: props.ebsOptions?.volumeSize, volumeType: props.ebsOptions?.volumeType, iops: props.ebsOptions?.iops, }, encryptionAtRestOptions: { - enabled: props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null), + enabled: encryptionAtRestEnabled, kmsKeyId: props.encryptionAtRestOptions?.kmsKey?.keyId, }, nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryptionEnabled ?? false }, @@ -1154,6 +1230,9 @@ export class Domain extends DomainBase implements IDomain { userPoolId: props.cognitoOptions?.userPoolId, }, vpcOptions: cfnVpcOptions, + snapshotOptions: props.automatedSnapshotStartHour + ? { automatedSnapshotStartHour: props.automatedSnapshotStartHour } + : undefined, }); if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 52bb223bc763f..83400fdaad79c 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; -import { Subnet, Vpc } from '@aws-cdk/aws-ec2'; +import { Subnet, Vpc, EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { App, Stack, Duration } from '@aws-cdk/core'; import { Domain, ElasticsearchVersion } from '../lib'; @@ -19,9 +19,9 @@ beforeEach(() => { const defaultClusterConfig = { masterNodes: 3, - masterNodeInstanceType: 'c5.large.elasticsearch', + masterNodeInstanceType: 'i3.large.elasticsearch', dataNodes: 3, - dataNodeInstanceType: 'r5.large.elasticsearch', + dataNodeInstanceType: 'r3.large.elasticsearch', }; const readActions = ['ESHttpGet', 'ESHttpHead']; @@ -47,9 +47,9 @@ test('minimal example renders correctly', () => { ElasticsearchClusterConfig: { DedicatedMasterCount: 3, DedicatedMasterEnabled: true, - DedicatedMasterType: 'c5.large.elasticsearch', + DedicatedMasterType: 'i3.large.elasticsearch', InstanceCount: 3, - InstanceType: 'r5.large.elasticsearch', + InstanceType: 'r3.large.elasticsearch', ZoneAwarenessEnabled: false, }, ElasticsearchVersion: '7.1', @@ -79,7 +79,7 @@ describe('log groups', () => { new Domain(stack, 'Domain', { clusterConfig: defaultClusterConfig, logPublishingOptions: { - slowSearchLogEnabed: true, + slowSearchLogEnabled: true, }, }); @@ -108,7 +108,7 @@ describe('log groups', () => { new Domain(stack, 'Domain', { clusterConfig: defaultClusterConfig, logPublishingOptions: { - slowIndexLogEnabed: true, + slowIndexLogEnabled: true, }, }); @@ -457,6 +457,181 @@ describe('custom error responses', () => { })).toThrow(/you need to provide a subnet for each AZ you are using/); }); + test('error when master or data node instance types do not end with .elasticsearch', () => { + const error = /instance types must end with ".elasticsearch"/; + expect(() => new Domain(stack, 'Domain1', { + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'c5.large', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + clusterConfig: { + ...defaultClusterConfig, + dataNodeInstanceType: 'c5.2xlarge', + }, + })).toThrow(error); + }); + + test('error when log publishing is enabled for elasticsearch version < 5.1', () => { + const error = /logs publishing requires Elasticsearch version 5.1 or later/; + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + clusterConfig: defaultClusterConfig, + logPublishingOptions: { + appLogEnabled: true, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + clusterConfig: defaultClusterConfig, + logPublishingOptions: { + slowSearchLogEnabled: true, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain3', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + clusterConfig: defaultClusterConfig, + logPublishingOptions: { + slowIndexLogEnabled: true, + }, + })).toThrow(error); + }); + + test('error when encryption at rest is enabled for elasticsearch version < 5.1', () => { + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + clusterConfig: defaultClusterConfig, + encryptionAtRestOptions: { + enabled: true, + }, + })).toThrow(/Encryption of data at rest requires Elasticsearch version 5.1 or later/); + }); + + test('error when cognito for kibana is enabled for elasticsearch version < 5.1', () => { + const user = new iam.User(stack, 'user'); + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + clusterConfig: defaultClusterConfig, + cognitoOptions: { + identityPoolId: 'test-identity-pool-id', + role: new iam.Role(stack, 'Role', { assumedBy: user }), + userPoolId: 'test-user-pool-id', + }, + })).toThrow(/Cognito authentication for Kibana requires Elasticsearch version 5.1 or later/); + }); + + test('error when C5, I3, M5, or R5 instance types are specified for elasticsearch version < 5.1', () => { + const error = /C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later/; + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'c5.medium.elasticsearch', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + clusterConfig: { + ...defaultClusterConfig, + dataNodeInstanceType: 'i3.2xlarge.elasticsearch', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain3', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + clusterConfig: { + ...defaultClusterConfig, + dataNodeInstanceType: 'm5.2xlarge.elasticsearch', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain4', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'r5.2xlarge.elasticsearch', + }, + })).toThrow(error); + }); + + test('error when automated snapshots are enabled for elasticsearch version < 5.3', () => { + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_5_1, + clusterConfig: defaultClusterConfig, + automatedSnapshotStartHour: 2, + })).toThrow(/Hourly automated snapshots requires Elasticsearch version 5.3 or later/); + }); + + test('error when node to node encryption is enabled for elasticsearch version < 6.0', () => { + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_5_6, + clusterConfig: defaultClusterConfig, + nodeToNodeEncryptionEnabled: true, + })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later/); + }); + + test('error when i3 instance types are specified with EBS enabled', () => { + expect(() => new Domain(stack, 'Domain1', { + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'i3.2xlarge.elasticsearch', + }, + ebsOptions: { + volumeSize: 100, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, + })).toThrow(/I3 instance types do not support EBS storage volumes/); + }); + + test('error when m3, r3, or t2 instance types are specified with encryption at rest enabled', () => { + const error = /M3, R3, and T2 instance types do not support encryption of data at rest/; + expect(() => new Domain(stack, 'Domain1', { + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'm3.2xlarge.elasticsearch', + }, + encryptionAtRestOptions: { + enabled: true, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + clusterConfig: { + ...defaultClusterConfig, + dataNodeInstanceType: 'r3.2xlarge.elasticsearch', + }, + encryptionAtRestOptions: { + enabled: true, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain3', { + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 't2.2xlarge.elasticsearch', + }, + encryptionAtRestOptions: { + enabled: true, + }, + })).toThrow(error); + }); + + test('error when t2.micro is specified with elasticsearch version > 2.3', () => { + expect(() => new Domain(stack, 'Domain1', { + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_6_7, + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 't2.micro.elasticsearch', + }, + })).toThrow(/t2.micro.elasticsearch instance type supports only Elasticsearch 1.5 and 2.3/); + }); + + test('error when any instance type other than R3 and I3 are specified without EBS enabled', () => { + expect(() => new Domain(stack, 'Domain1', { + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'm5.large.elasticsearch', + }, + })).toThrow(/EBS volumes are required for all instance types except R3 and I3/); + }); + }); function testGrant( diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index ae4271337210b..dc9c2bd69d385 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -19,7 +19,7 @@ class TestStack extends Stack { volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, }, logPublishingOptions: { - slowSearchLogEnabed: true, + slowSearchLogEnabled: true, appLogEnabled: true, }, nodeToNodeEncryptionEnabled: true, From 01ff057c46489ce77a03a61f56340d8910ab7c28 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 05:01:28 -0500 Subject: [PATCH 10/45] minimum instead of maximum nodes metric --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 6 +++--- packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 846f398b714cd..9f022d16d731e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -477,7 +477,7 @@ export interface IDomain extends cdk.IResource { /** * Metric for the number of nodes. * - * @default maximum over 1 hour + * @default minimum over 1 hour */ metricNodes(props?: MetricOptions): Metric; @@ -807,11 +807,11 @@ abstract class DomainBase extends cdk.Resource implements IDomain { /** * Metric for the number of nodes. * - * @default maximum over 1 hour + * @default minimum over 1 hour */ public metricNodes(props?: MetricOptions): Metric { return this.metric('Nodes', { - statistic: Statistic.MAXIMUM, + statistic: Statistic.MINIMUM, period: cdk.Duration.hours(1), ...props, }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 83400fdaad79c..f2173d60f3ed2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -332,7 +332,7 @@ describe('metrics', () => { testMetric( (domain) => domain.metricNodes(), 'Nodes', - Statistic.MAXIMUM, + Statistic.MINIMUM, Duration.hours(1), ); }); From 2eacb1170ec7dc4164811d4696864298d9f75cbe Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 05:09:45 -0500 Subject: [PATCH 11/45] remove key read/write permissions --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 88 ------------------- .../@aws-cdk/aws-elasticsearch/lib/perms.ts | 16 ---- 2 files changed, 104 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 9f022d16d731e..6ebe6d264f00e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -331,18 +331,10 @@ export interface IDomain extends cdk.IResource { */ readonly domainEndpoint: string; - /** - * Optional KMS encryption key associated with this domain. - */ - readonly encryptionKey?: kms.IKey; - /** * Grant read permissions for this domain and its contents to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to decrypt the contents - * of the domain will also be granted to the same principal. - * * @param identity The principal */ grantRead(identity: iam.IGrantable): iam.Grant; @@ -351,9 +343,6 @@ export interface IDomain extends cdk.IResource { * Grant write permissions for this domain and its contents to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt the contents - * of the domain will also be granted to the same principal. - * * @param identity The principal */ grantWrite(identity: iam.IGrantable): iam.Grant; @@ -362,9 +351,6 @@ export interface IDomain extends cdk.IResource { * Grant read/write permissions for this domain and its contents to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt and decrypt the - * contents of the domain will also be granted to the same principal. - * * @param identity The principal */ grantReadWrite(identity: iam.IGrantable): iam.Grant; @@ -373,9 +359,6 @@ export interface IDomain extends cdk.IResource { * Grant read permissions for an index in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to decrypt the contents - * of the index will also be granted to the same principal. - * * @param index The index to grant permissions for * @param identity The principal */ @@ -385,9 +368,6 @@ export interface IDomain extends cdk.IResource { * Grant write permissions for an index in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt the contents - * of the index will also be granted to the same principal. - * * @param index The index to grant permissions for * @param identity The principal */ @@ -397,9 +377,6 @@ export interface IDomain extends cdk.IResource { * Grant read/write permissions for an index in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt/decrypt the contents - * of the index will also be granted to the same principal. - * * @param index The index to grant permissions for * @param identity The principal */ @@ -409,9 +386,6 @@ export interface IDomain extends cdk.IResource { * Grant read permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to decrypt the contents - * of the path will also be granted to the same principal. - * * @param path The path to grant permissions for * @param identity The principal */ @@ -421,9 +395,6 @@ export interface IDomain extends cdk.IResource { * Grant write permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt the contents - * of the path will also be granted to the same principal. - * * @param path The path to grant permissions for * @param identity The principal */ @@ -433,9 +404,6 @@ export interface IDomain extends cdk.IResource { * Grant read/write permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt/decrypt the contents - * of the path will also be granted to the same principal. - * * @param path The path to grant permissions for * @param identity The principal */ @@ -561,25 +529,16 @@ abstract class DomainBase extends cdk.Resource implements IDomain { public abstract readonly domainName: string; public abstract readonly domainEndpoint: string; - /** - * Optional KMS encryption key associated with this domain. - */ - public abstract readonly encryptionKey?: kms.IKey; - /** * Grant read permissions for this domain and its contents to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to decrypt the contents - * of the domain will also be granted to the same principal. - * * @param identity The principal */ grantRead(identity: iam.IGrantable): iam.Grant { return this.grant( identity, perms.ES_READ_ACTIONS, - perms.KEY_READ_ACTIONS, this.domainArn, `${this.domainArn}/*`, ); @@ -589,16 +548,12 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant write permissions for this domain and its contents to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt the contents - * of the domain will also be granted to the same principal. - * * @param identity The principal */ grantWrite(identity: iam.IGrantable): iam.Grant { return this.grant( identity, perms.ES_WRITE_ACTIONS, - perms.KEY_WRITE_ACTIONS, this.domainArn, `${this.domainArn}/*`, ); @@ -608,16 +563,12 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant read/write permissions for this domain and its contents to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt and decrypt the - * contents of the domain will also be granted to the same principal. - * * @param identity The principal */ grantReadWrite(identity: iam.IGrantable): iam.Grant { return this.grant( identity, perms.ES_READ_WRITE_ACTIONS, - perms.KEY_READ_WRITE_ACTIONS, this.domainArn, `${this.domainArn}/*`, ); @@ -627,9 +578,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant read permissions for an index in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to decrypt the contents - * of the index will also be granted to the same principal. - * * @param index The index to grant permissions for * @param identity The principal */ @@ -637,7 +585,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { return this.grant( identity, perms.ES_READ_ACTIONS, - perms.KEY_READ_ACTIONS, `${this.domainArn}/${index}`, `${this.domainArn}/${index}/*`, ); @@ -647,9 +594,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant write permissions for an index in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt the contents - * of the index will also be granted to the same principal. - * * @param index The index to grant permissions for * @param identity The principal */ @@ -657,7 +601,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { return this.grant( identity, perms.ES_WRITE_ACTIONS, - perms.KEY_WRITE_ACTIONS, `${this.domainArn}/${index}`, `${this.domainArn}/${index}/*`, ); @@ -667,9 +610,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant read/write permissions for an index in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt/decrypt the contents - * of the index will also be granted to the same principal. - * * @param index The index to grant permissions for * @param identity The principal */ @@ -677,7 +617,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { return this.grant( identity, perms.ES_READ_WRITE_ACTIONS, - perms.KEY_READ_WRITE_ACTIONS, `${this.domainArn}/${index}`, `${this.domainArn}/${index}/*`, ); @@ -687,9 +626,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant read permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to decrypt the contents - * of the path will also be granted to the same principal. - * * @param path The path to grant permissions for * @param identity The principal */ @@ -697,7 +633,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { return this.grant( identity, perms.ES_READ_ACTIONS, - perms.KEY_READ_ACTIONS, `${this.domainArn}/${path}`, ); } @@ -706,9 +641,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant write permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt the contents - * of the path will also be granted to the same principal. - * * @param path The path to grant permissions for * @param identity The principal */ @@ -716,7 +648,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { return this.grant( identity, perms.ES_WRITE_ACTIONS, - perms.KEY_WRITE_ACTIONS, `${this.domainArn}/${path}`, ); } @@ -725,9 +656,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * Grant read/write permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * - * If encryption is used, permission to use the key to encrypt/decrypt the contents - * of the path will also be granted to the same principal. - * * @param path The path to grant permissions for * @param identity The principal */ @@ -735,7 +663,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { return this.grant( identity, perms.ES_READ_WRITE_ACTIONS, - perms.KEY_READ_WRITE_ACTIONS, `${this.domainArn}/${path}`, ); } @@ -934,7 +861,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { private grant( grantee: iam.IGrantable, domainActions: string[], - keyActions: string[], resourceArn: string, ...otherResourceArns: string[] ): iam.Grant { @@ -947,10 +873,6 @@ abstract class DomainBase extends cdk.Resource implements IDomain { scope: this, }); - if (this.encryptionKey && keyActions && keyActions.length !== 0) { - this.encryptionKey.grant(grantee, ...keyActions); - } - return grant; } } @@ -974,13 +896,6 @@ export interface DomainAttributes { * The domain endpoint of the Elasticsearch domain. */ readonly domainEndpoint: string; - - /** - * Optional KMS encryption key associated with this domain. - * - * @default no encryption key - */ - readonly encryptionKey?: kms.IKey; } @@ -1027,7 +942,6 @@ export class Domain extends DomainBase implements IDomain { public readonly domainArn = attrs.domainArn; public readonly domainName = attrs.domainName; public readonly domainEndpoint = attrs.domainEndpoint; - public readonly encryptionKey = attrs.encryptionKey; constructor() { super(scope, id); } }; @@ -1036,7 +950,6 @@ export class Domain extends DomainBase implements IDomain { public readonly domainArn: string; public readonly domainName: string; public readonly domainEndpoint: string; - public readonly encryptionKey?: kms.IKey; private readonly domain: CfnDomain; @@ -1239,7 +1152,6 @@ export class Domain extends DomainBase implements IDomain { if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } - this.encryptionKey = props.encryptionAtRestOptions?.kmsKey; this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { service: 'es', resource: 'domain', diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts b/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts index 38133c2d6ca86..f5804c9f059d5 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/perms.ts @@ -14,19 +14,3 @@ export const ES_READ_WRITE_ACTIONS = [ ...ES_READ_ACTIONS, ...ES_WRITE_ACTIONS, ]; - -export const KEY_READ_ACTIONS = [ - 'kms:Decrypt', - 'kms:DescribeKey', -]; - -export const KEY_WRITE_ACTIONS = [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', -]; - -export const KEY_READ_WRITE_ACTIONS = [ - ...KEY_READ_ACTIONS, - ...KEY_WRITE_ACTIONS, -]; From 5c8ae51005b290712c1e0c65dd512206d0e91f2a Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 05:13:55 -0500 Subject: [PATCH 12/45] function declaration tweaks --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 6ebe6d264f00e..b42a55593197a 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -979,15 +979,15 @@ export class Domain extends DomainBase implements IDomain { const encryptionAtRestEnabled = props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null); const ebsEnabled = props.ebsOptions != null; - const isInstanceType = function(instanceType: string) : Boolean { + function isInstanceType(instanceType: string): Boolean { return masterInstanceType.startsWith(instanceType) || dataInstanceType.startsWith(instanceType); }; - const isSomeInstanceType = function(...instanceTypes: string[]) : Boolean { + function isSomeInstanceType(...instanceTypes: string[]): Boolean { return instanceTypes.some(isInstanceType); }; - const isEveryInstanceType = function(...instanceTypes: string[]) : Boolean { + function isEveryInstanceType(...instanceTypes: string[]): Boolean { return instanceTypes.every(isInstanceType); }; From c889745947ae7b0d39512bcd325629a6d10123ea Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 05:25:20 -0500 Subject: [PATCH 13/45] replace else if statements --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 8 ++++++-- packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index b42a55593197a..dc7bc432dbebb 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1010,11 +1010,15 @@ export class Domain extends DomainBase implements IDomain { if (isSomeInstanceType('c5', 'i3', 'm5', 'r5')) { throw new Error('C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later.'); } - } else if (versionNumber < 5.3) { + } + + if (versionNumber < 5.3) { if (props.automatedSnapshotStartHour) { throw new Error('Hourly automated snapshots requires Elasticsearch version 5.3 or later.'); } - } else if (versionNumber < 6.0) { + } + + if (versionNumber < 6.0) { if (props.nodeToNodeEncryptionEnabled) { throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.'); } diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index f2173d60f3ed2..b3e07a62c06e2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -555,8 +555,12 @@ describe('custom error responses', () => { test('error when automated snapshots are enabled for elasticsearch version < 5.3', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_5_1, - clusterConfig: defaultClusterConfig, + elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + clusterConfig: { + ...defaultClusterConfig, + masterNodeInstanceType: 'm4.2xlarge.elasticsearch', + dataNodeInstanceType: 'm4.2xlarge.elasticsearch', + }, automatedSnapshotStartHour: 2, })).toThrow(/Hourly automated snapshots requires Elasticsearch version 5.3 or later/); }); From 944a71ff4563291468b0dfd6f1e7dcb8862f4f97 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 05:30:24 -0500 Subject: [PATCH 14/45] remove faulty validation around snapshots --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 9 ++------- .../@aws-cdk/aws-elasticsearch/test/domain.test.ts | 12 ------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index dc7bc432dbebb..e0fa09ee6563e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -288,7 +288,8 @@ export interface DomainProps { /** * The hour in UTC during which the service takes an automated daily snapshot - * of the indices in the Amazon ES domain. Requires Elasticsearch version 5.3 and later. + * of the indices in the Amazon ES domain. Only applies for Elasticsearch + * versions below 5.3. * * @default - Hourly automated snapshots not used */ @@ -1012,12 +1013,6 @@ export class Domain extends DomainBase implements IDomain { } } - if (versionNumber < 5.3) { - if (props.automatedSnapshotStartHour) { - throw new Error('Hourly automated snapshots requires Elasticsearch version 5.3 or later.'); - } - } - if (versionNumber < 6.0) { if (props.nodeToNodeEncryptionEnabled) { throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.'); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index b3e07a62c06e2..da13ac2fbcd0e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -553,18 +553,6 @@ describe('custom error responses', () => { })).toThrow(error); }); - test('error when automated snapshots are enabled for elasticsearch version < 5.3', () => { - expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, - clusterConfig: { - ...defaultClusterConfig, - masterNodeInstanceType: 'm4.2xlarge.elasticsearch', - dataNodeInstanceType: 'm4.2xlarge.elasticsearch', - }, - automatedSnapshotStartHour: 2, - })).toThrow(/Hourly automated snapshots requires Elasticsearch version 5.3 or later/); - }); - test('error when node to node encryption is enabled for elasticsearch version < 6.0', () => { expect(() => new Domain(stack, 'Domain1', { elasticsearchVersion: ElasticsearchVersion.ES_VERSION_5_6, From 454bb60ce1ca748d1df9cdfdf691031d3798852f Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 05:45:38 -0500 Subject: [PATCH 15/45] fix isEveryInstanceType logic --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index e0fa09ee6563e..f24878ec940f2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -989,7 +989,8 @@ export class Domain extends DomainBase implements IDomain { }; function isEveryInstanceType(...instanceTypes: string[]): Boolean { - return instanceTypes.every(isInstanceType); + return instanceTypes.some(t => masterInstanceType.startsWith(t)) + && instanceTypes.some(t => dataInstanceType.startsWith(t)); }; // Validate feature support for the given Elasticsearch version, per From 4bd189913ce6d56d4271210f1658f40dd28c4ece Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 06:02:19 -0500 Subject: [PATCH 16/45] address some review feedback --- packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts | 6 +++--- .../test/integ.elasticsearch.expected.json | 4 ++-- .../@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index da13ac2fbcd0e..c9cf3ffbb6ee8 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -167,15 +167,15 @@ describe('log groups', () => { describe('grants', () => { test('"grantRead" allows read actions associated with this domain resource', () => { - testGrant(readActions, (p, t) => t.grantRead(p)); + testGrant(readActions, (p, d) => d.grantRead(p)); }); test('"grantWrite" allows write actions associated with this domain resource', () => { - testGrant(writeActions, (p, t) => t.grantWrite(p)); + testGrant(writeActions, (p, d) => d.grantWrite(p)); }); test('"grantReadWrite" allows read and write actions associated with this domain resource', () => { - testGrant(readWriteActions, (p, t) => t.grantReadWrite(p)); + testGrant(readWriteActions, (p, d) => d.grantReadWrite(p)); }); test('"grantIndexRead" allows read actions associated with an index in this domain resource', () => { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 7998e0bbc9207..2888bc3fceb99 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -99,9 +99,9 @@ "ElasticsearchClusterConfig": { "DedicatedMasterCount": 3, "DedicatedMasterEnabled": true, - "DedicatedMasterType": "m4.large.elasticsearch", + "DedicatedMasterType": "m5.large.elasticsearch", "InstanceCount": 3, - "InstanceType": "m4.large.elasticsearch", + "InstanceType": "m5.large.elasticsearch", "ZoneAwarenessEnabled": false }, "ElasticsearchVersion": "7.1", diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index dc9c2bd69d385..2e9588bd0da06 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -10,9 +10,9 @@ class TestStack extends Stack { elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_1, clusterConfig: { masterNodes: 3, - masterNodeInstanceType: 'm4.large.elasticsearch', + masterNodeInstanceType: 'm5.large.elasticsearch', dataNodes: 3, - dataNodeInstanceType: 'm4.large.elasticsearch', + dataNodeInstanceType: 'm5.large.elasticsearch', }, ebsOptions: { volumeSize: 10, From 7fceefa3e350a0d87664a918bba6ae1445b3bc79 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Sat, 1 Aug 2020 06:53:23 -0500 Subject: [PATCH 17/45] elasticsearchVersion as a number --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 71 ++++++------------- .../aws-elasticsearch/test/domain.test.ts | 33 +++++---- .../test/integ.elasticsearch.ts | 2 +- 3 files changed, 43 insertions(+), 63 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index f24878ec940f2..6d76cc1028e7f 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -256,11 +256,15 @@ export interface DomainProps { readonly clusterConfig: ClusterConfig; /** - * The Elasticsearch Version + * The Elasticsearch version that your domain will leverage. * - * @default ElasticsearchVersion.ES_VERSION_7_4 + * Per https://aws.amazon.com/elasticsearch-service/faqs/, Amazon Elasticsearch Service + * currently supports Elasticsearch versions 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, 6.0, + * 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. + * + * @default 7.4 */ - readonly elasticsearchVersion?: ElasticsearchVersion; + readonly elasticsearchVersion?: number; /** * Encryption at rest options for the cluster. @@ -975,8 +979,17 @@ export class Domain extends DomainBase implements IDomain { throw new Error('Master and data node instance types must end with ".elasticsearch".'); } - const elasticsearchVersion = props.elasticsearchVersion ?? ElasticsearchVersion.ES_VERSION_7_4; - const versionNumber = parseFloat(elasticsearchVersion.toString()); + const elasticsearchVersion = props.elasticsearchVersion ?? 7.4; + if ( + elasticsearchVersion <= 7.4 && + ![ + 1.5, 2.3, 5.1, 5.3, 5.5, 5.6, 6.0, + 6.2, 6.3, 6.4, 6.5, 6.7, 6.8, 7.1, 7.4, + ].includes(elasticsearchVersion) + ) { + throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); + } + const encryptionAtRestEnabled = props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null); const ebsEnabled = props.ebsOptions != null; @@ -995,7 +1008,7 @@ export class Domain extends DomainBase implements IDomain { // Validate feature support for the given Elasticsearch version, per // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-features-by-version.html - if (versionNumber < 5.1) { + if (elasticsearchVersion < 5.1) { if ( props.logPublishingOptions?.slowIndexLogEnabled || props.logPublishingOptions?.appLogEnabled @@ -1014,7 +1027,7 @@ export class Domain extends DomainBase implements IDomain { } } - if (versionNumber < 6.0) { + if (elasticsearchVersion < 6.0) { if (props.nodeToNodeEncryptionEnabled) { throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.'); } @@ -1030,7 +1043,7 @@ export class Domain extends DomainBase implements IDomain { throw new Error('M3, R3, and T2 instance types do not support encryption of data at rest.'); } - if (isInstanceType('t2.micro') && versionNumber > 2.3) { + if (isInstanceType('t2.micro') && elasticsearchVersion > 2.3) { throw new Error('The t2.micro.elasticsearch instance type supports only Elasticsearch 1.5 and 2.3.'); } @@ -1098,7 +1111,7 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, - elasticsearchVersion: elasticsearchVersion, + elasticsearchVersion: elasticsearchVersion.toString(), elasticsearchClusterConfig: { dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, dedicatedMasterCount: props.clusterConfig.masterNodes, @@ -1163,46 +1176,6 @@ export class Domain extends DomainBase implements IDomain { } } -/** - * The Elasticsearch version that your domain will leverage. - * - * Per https://aws.amazon.com/elasticsearch-service/faqs/, Amazon Elasticsearch Service - * currently supports Elasticsearch versions 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, 6.0, - * 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. - */ -export enum ElasticsearchVersion { - /** Elasticsearch Version 7.4 */ - ES_VERSION_7_4 = '7.4', - /** Elasticsearch Version 7.1 */ - ES_VERSION_7_1 = '7.1', - /** Elasticsearch Version 6.8 */ - ES_VERSION_6_8 = '6.8', - /** Elasticsearch Version 6.7 */ - ES_VERSION_6_7 = '6.7', - /** Elasticsearch Version 6.5 */ - ES_VERSION_6_5 = '6.5', - /** Elasticsearch Version 6.4 */ - ES_VERSION_6_4 = '6.4', - /** Elasticsearch Version 6.3 */ - ES_VERSION_6_3 = '6.3', - /** Elasticsearch Version 6.2 */ - ES_VERSION_6_2 = '6.2', - /** Elasticsearch Version 6.0 */ - ES_VERSION_6_0 = '6.0', - /** Elasticsearch Version 5.6 */ - ES_VERSION_5_6 = '5.6', - /** Elasticsearch Version 5.5 */ - ES_VERSION_5_5 = '5.5', - /** Elasticsearch Version 5.3 */ - ES_VERSION_5_3 = '5.3', - /** Elasticsearch Version 5.1 */ - ES_VERSION_5_1 = '5.1', - /** Elasticsearch Version 2.3 */ - ES_VERSION_2_3 = '2.3', - /** Elasticsearch Version 1.5 */ - ES_VERSION_1_5 = '1.5', -} - /** * Given an Elasticsearch domain endpoint, returns a CloudFormation expression that * extracts the domain name. diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index c9cf3ffbb6ee8..0605018b016d0 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -3,7 +3,7 @@ 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 { App, Stack, Duration } from '@aws-cdk/core'; -import { Domain, ElasticsearchVersion } from '../lib'; +import { Domain } from '../lib'; let app: App; let stack: Stack; @@ -33,7 +33,7 @@ const readWriteActions = [ test('minimal example renders correctly', () => { new Domain(stack, 'Domain', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_7_1, + elasticsearchVersion: 7.1, clusterConfig: defaultClusterConfig, }); @@ -473,24 +473,31 @@ describe('custom error responses', () => { })).toThrow(error); }); + test('error when elasticsearchVersion is unsupported/unknown', () => { + expect(() => new Domain(stack, 'Domain1', { + clusterConfig: defaultClusterConfig, + elasticsearchVersion: 5.4, + })).toThrow(/Unknown Elasticsearch version: 5\.4/); + }); + test('error when log publishing is enabled for elasticsearch version < 5.1', () => { const error = /logs publishing requires Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + elasticsearchVersion: 2.3, clusterConfig: defaultClusterConfig, logPublishingOptions: { appLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + elasticsearchVersion: 1.5, clusterConfig: defaultClusterConfig, logPublishingOptions: { slowSearchLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + elasticsearchVersion: 1.5, clusterConfig: defaultClusterConfig, logPublishingOptions: { slowIndexLogEnabled: true, @@ -500,7 +507,7 @@ describe('custom error responses', () => { test('error when encryption at rest is enabled for elasticsearch version < 5.1', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + elasticsearchVersion: 2.3, clusterConfig: defaultClusterConfig, encryptionAtRestOptions: { enabled: true, @@ -511,7 +518,7 @@ describe('custom error responses', () => { test('error when cognito for kibana is enabled for elasticsearch version < 5.1', () => { const user = new iam.User(stack, 'user'); expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + elasticsearchVersion: 2.3, clusterConfig: defaultClusterConfig, cognitoOptions: { identityPoolId: 'test-identity-pool-id', @@ -524,28 +531,28 @@ describe('custom error responses', () => { test('error when C5, I3, M5, or R5 instance types are specified for elasticsearch version < 5.1', () => { const error = /C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_2_3, + elasticsearchVersion: 2.3, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'c5.medium.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + elasticsearchVersion: 1.5, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'i3.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + elasticsearchVersion: 1.5, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'm5.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain4', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_1_5, + elasticsearchVersion: 1.5, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'r5.2xlarge.elasticsearch', @@ -555,7 +562,7 @@ describe('custom error responses', () => { test('error when node to node encryption is enabled for elasticsearch version < 6.0', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_5_6, + elasticsearchVersion: 5.6, clusterConfig: defaultClusterConfig, nodeToNodeEncryptionEnabled: true, })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later/); @@ -607,7 +614,7 @@ describe('custom error responses', () => { test('error when t2.micro is specified with elasticsearch version > 2.3', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: ElasticsearchVersion.ES_VERSION_6_7, + elasticsearchVersion: 6.7, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 't2.micro.elasticsearch', diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index 2e9588bd0da06..b46794e446dde 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -7,7 +7,7 @@ class TestStack extends Stack { super(scope, id, props); new es.Domain(this, 'Domain', { - elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_1, + elasticsearchVersion: 7.1, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'm5.large.elasticsearch', From 6bb5bbf1a291103309213cca1497c08fa95f3fb0 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Sun, 2 Aug 2020 11:41:42 +1000 Subject: [PATCH 18/45] Elasticsearch version as string with existing versions as constants --- packages/@aws-cdk/aws-elasticsearch/README.md | 4 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 91 +++++++++++++++++-- .../aws-elasticsearch/test/domain.test.ts | 40 +++++--- .../test/integ.elasticsearch.ts | 2 +- 4 files changed, 111 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 84dec2761c0a4..2698830471382 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -21,7 +21,7 @@ To create an Elasticsearch domain: import * as es from '@aws-cdk/aws-elasticsearch'; const domain = new es.Domain(this, 'Domain', { - elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_1, + elasticsearchVersion: es.Version.ES_7_1, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'c5.large.elasticsearch', @@ -74,7 +74,7 @@ The domain can also be created with encryption enabled: ```ts const domain = new es.Domain(this, 'Domain', { - elasticsearchVersion: es.ElasticsearchVersion.ES_VERSION_7_4, + elasticsearchVersion: es.Version.ES_7_4, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'c5.large.elasticsearch', diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 6d76cc1028e7f..82f811259a5d9 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -11,6 +11,56 @@ import { CfnDomain } from './elasticsearch.generated'; import { LogGroupResourcePolicy } from './log-group-resource-policy'; import * as perms from './perms'; +/** + * Supported Elasticsearch Versions + */ +export class Version { + /** AWS Elasticsearch 1.5 */ + public static readonly ES_1_5 = '1.5'; + + /** AWS Elasticsearch 2.3 */ + public static readonly ES_2_3 = '2.3'; + + /** AWS Elasticsearch 5.1 */ + public static readonly ES_5_1 = '5.1'; + + /** AWS Elasticsearch 5.3 */ + public static readonly ES_5_3 = '5.3'; + + /** AWS Elasticsearch 5.5 */ + public static readonly ES_5_5 = '5.5'; + + /** AWS Elasticsearch 5.6 */ + public static readonly ES_5_6 = '5.6'; + + /** AWS Elasticsearch 6.0 */ + public static readonly ES_6_0 = '6.0'; + + /** AWS Elasticsearch 6.2 */ + public static readonly ES_6_2 = '6.2'; + + /** AWS Elasticsearch 6.3 */ + public static readonly ES_6_3 = '6.3'; + + /** AWS Elasticsearch 6.4 */ + public static readonly ES_6_4 = '6.4'; + + /** AWS Elasticsearch 6.5 */ + public static readonly ES_6_5 = '6.5'; + + /** AWS Elasticsearch 6.7 */ + public static readonly ES_6_7 = '6.7'; + + /** AWS Elasticsearch 6.8 */ + public static readonly ES_6_8 = '6.8'; + + /** AWS Elasticsearch 7.1 */ + public static readonly ES_7_1 = '7.1'; + + /** AWS Elasticsearch 7.4 */ + public static readonly ES_7_4 = '7.4'; +} + /** * Configures the makeup of the cluster such as number of nodes and instance * type. @@ -262,9 +312,9 @@ export interface DomainProps { * currently supports Elasticsearch versions 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, 6.0, * 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. * - * @default 7.4 + * @default '7.4' */ - readonly elasticsearchVersion?: number; + readonly elasticsearchVersion?: string; /** * Encryption at rest options for the cluster. @@ -688,7 +738,8 @@ abstract class DomainBase extends cdk.Resource implements IDomain { } /** - * Metric for the time the cluster status is red. + + * Metric for the time the cluster status is red. * * @default maximum over 5 minutes */ @@ -979,13 +1030,35 @@ export class Domain extends DomainBase implements IDomain { throw new Error('Master and data node instance types must end with ".elasticsearch".'); } - const elasticsearchVersion = props.elasticsearchVersion ?? 7.4; + const elasticsearchVersion = props.elasticsearchVersion ?? '7.4'; + const elasticsearchVersionNum = parseVersion(elasticsearchVersion); + + function parseVersion(version: string): number { + const firstDot = version.indexOf('.'); + + if (firstDot < 1) { + throw new Error(`Invalid Elasticsearch version: ${version}. Version string needs to start with major and minor version (x.y).`); + } + + const secondDot = version.indexOf('.', firstDot + 1); + + try { + if (secondDot == -1) { + return parseFloat(version); + } else { + return parseFloat(version.substring(0, secondDot)); + } + } catch (error) { + throw new Error(`Invalid Elasticsearch version: ${version}. Version string needs to start with major and minor version (x.y).`); + } + } + if ( - elasticsearchVersion <= 7.4 && + elasticsearchVersionNum <= 7.4 && ![ 1.5, 2.3, 5.1, 5.3, 5.5, 5.6, 6.0, 6.2, 6.3, 6.4, 6.5, 6.7, 6.8, 7.1, 7.4, - ].includes(elasticsearchVersion) + ].includes(elasticsearchVersionNum) ) { throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); } @@ -1008,7 +1081,7 @@ export class Domain extends DomainBase implements IDomain { // Validate feature support for the given Elasticsearch version, per // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-features-by-version.html - if (elasticsearchVersion < 5.1) { + if (elasticsearchVersionNum < 5.1) { if ( props.logPublishingOptions?.slowIndexLogEnabled || props.logPublishingOptions?.appLogEnabled @@ -1027,7 +1100,7 @@ export class Domain extends DomainBase implements IDomain { } } - if (elasticsearchVersion < 6.0) { + if (elasticsearchVersionNum < 6.0) { if (props.nodeToNodeEncryptionEnabled) { throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.'); } @@ -1043,7 +1116,7 @@ export class Domain extends DomainBase implements IDomain { throw new Error('M3, R3, and T2 instance types do not support encryption of data at rest.'); } - if (isInstanceType('t2.micro') && elasticsearchVersion > 2.3) { + if (isInstanceType('t2.micro') && elasticsearchVersionNum > 2.3) { throw new Error('The t2.micro.elasticsearch instance type supports only Elasticsearch 1.5 and 2.3.'); } diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 0605018b016d0..0d1d893a7afd3 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -3,7 +3,7 @@ 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 { App, Stack, Duration } from '@aws-cdk/core'; -import { Domain } from '../lib'; +import { Domain, Version } from '../lib'; let app: App; let stack: Stack; @@ -33,7 +33,7 @@ const readWriteActions = [ test('minimal example renders correctly', () => { new Domain(stack, 'Domain', { - elasticsearchVersion: 7.1, + elasticsearchVersion: Version.ES_7_1, clusterConfig: defaultClusterConfig, }); @@ -476,28 +476,28 @@ describe('custom error responses', () => { test('error when elasticsearchVersion is unsupported/unknown', () => { expect(() => new Domain(stack, 'Domain1', { clusterConfig: defaultClusterConfig, - elasticsearchVersion: 5.4, + elasticsearchVersion: '5.4', })).toThrow(/Unknown Elasticsearch version: 5\.4/); }); test('error when log publishing is enabled for elasticsearch version < 5.1', () => { const error = /logs publishing requires Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: 2.3, + elasticsearchVersion: Version.ES_2_3, clusterConfig: defaultClusterConfig, logPublishingOptions: { appLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { - elasticsearchVersion: 1.5, + elasticsearchVersion: Version.ES_1_5, clusterConfig: defaultClusterConfig, logPublishingOptions: { slowSearchLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { - elasticsearchVersion: 1.5, + elasticsearchVersion: Version.ES_1_5, clusterConfig: defaultClusterConfig, logPublishingOptions: { slowIndexLogEnabled: true, @@ -507,7 +507,7 @@ describe('custom error responses', () => { test('error when encryption at rest is enabled for elasticsearch version < 5.1', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: 2.3, + elasticsearchVersion: Version.ES_2_3, clusterConfig: defaultClusterConfig, encryptionAtRestOptions: { enabled: true, @@ -518,7 +518,7 @@ describe('custom error responses', () => { test('error when cognito for kibana is enabled for elasticsearch version < 5.1', () => { const user = new iam.User(stack, 'user'); expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: 2.3, + elasticsearchVersion: Version.ES_2_3, clusterConfig: defaultClusterConfig, cognitoOptions: { identityPoolId: 'test-identity-pool-id', @@ -531,28 +531,28 @@ describe('custom error responses', () => { test('error when C5, I3, M5, or R5 instance types are specified for elasticsearch version < 5.1', () => { const error = /C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: 2.3, + elasticsearchVersion: Version.ES_2_3, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'c5.medium.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { - elasticsearchVersion: 1.5, + elasticsearchVersion: Version.ES_1_5, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'i3.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { - elasticsearchVersion: 1.5, + elasticsearchVersion: Version.ES_1_5, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'm5.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain4', { - elasticsearchVersion: 1.5, + elasticsearchVersion: Version.ES_1_5, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'r5.2xlarge.elasticsearch', @@ -562,7 +562,7 @@ describe('custom error responses', () => { test('error when node to node encryption is enabled for elasticsearch version < 6.0', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: 5.6, + elasticsearchVersion: Version.ES_5_6, clusterConfig: defaultClusterConfig, nodeToNodeEncryptionEnabled: true, })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later/); @@ -614,7 +614,7 @@ describe('custom error responses', () => { test('error when t2.micro is specified with elasticsearch version > 2.3', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: 6.7, + elasticsearchVersion: Version.ES_6_7, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 't2.micro.elasticsearch', @@ -633,6 +633,18 @@ describe('custom error responses', () => { }); +test('can specify future version', () => { + new Domain(stack, 'Domain', { + elasticsearchVersion: '8.2', + clusterConfig: defaultClusterConfig, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + ElasticsearchVersion: '8.2', + }); +}); + + function testGrant( expectedActions: string[], invocation: (user: iam.IPrincipal, domain: Domain) => void, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index b46794e446dde..9f232c84d4863 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -7,7 +7,7 @@ class TestStack extends Stack { super(scope, id, props); new es.Domain(this, 'Domain', { - elasticsearchVersion: 7.1, + elasticsearchVersion: es.Version.ES_7_1, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'm5.large.elasticsearch', From 1b13c0265f3e4a57113ebe548a808f78e969f360 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:02:54 +1000 Subject: [PATCH 19/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 82f811259a5d9..b2ac672ed91b6 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -282,7 +282,7 @@ export interface DomainProps { * * @default - Cognito not used for authentication to Kibana. */ - readonly cognitoOptions?: CognitoOptions; + readonly cognitoKibanaAuth?: CognitoOptions; /** * Enforces a particular physical domain name. From c4ec26931a7398d107a8bd6cda29f53461472d25 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:03:22 +1000 Subject: [PATCH 20/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index b2ac672ed91b6..77154c4ee7876 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -297,7 +297,7 @@ export interface DomainProps { * * @default - No EBS volumes attached. */ - readonly ebsOptions?: EbsOptions; + readonly ebs?: EbsOptions; /** * The cluster configuration for the Amazon ES domain. From e0a61add1697163296b54388e5994fe5cd09627e Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:16:09 +1000 Subject: [PATCH 21/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 77154c4ee7876..a8ab0629cd9fa 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -215,6 +215,8 @@ export interface EncryptionAtRestOptions { /** * Configures Amazon ES to use Amazon Cognito authentication for Kibana. + +@see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-cognito-auth.html */ export interface CognitoOptions { /** From 9eb08a04e7a1acd21457392c1dc24b9f5c647f19 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:16:26 +1000 Subject: [PATCH 22/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index a8ab0629cd9fa..596fca4286952 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -225,7 +225,9 @@ export interface CognitoOptions { readonly identityPoolId: string; /** - * The AmazonESCognitoAccess role that allows Amazon ES to configure your user pool and identity pool. + * A role that allows Amazon ES to configure your user pool and identity pool. It must have the `AmazonESCognitoAccess` policy attached to it. + +@see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-cognito-auth.html#es-cognito-auth-prereq */ readonly role: iam.IRole; From a9233da42dd446fadd915e4bff50a8f0320c6a8c Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:17:08 +1000 Subject: [PATCH 23/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 596fca4286952..1de2b8dd77c21 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -325,7 +325,7 @@ export interface DomainProps { * * @default - No encryption at rest */ - readonly encryptionAtRestOptions?: EncryptionAtRestOptions; + readonly encryptionAtRest?: EncryptionAtRestOptions; /** From 6e138521fed4b1946a2279fa0667dbccbf43c64b Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:17:41 +1000 Subject: [PATCH 24/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 1de2b8dd77c21..5687a1d02bb0b 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -333,7 +333,7 @@ export interface DomainProps { * * @default - No logs are published */ - readonly logPublishingOptions?: LoggingOptions; + readonly logging?: LoggingOptions; /** From d4dd65daf6c496d6de495176230c298de21f6f7a Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 17 Aug 2020 14:17:55 +1000 Subject: [PATCH 25/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 5687a1d02bb0b..04591f48d5724 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -342,7 +342,7 @@ export interface DomainProps { * * @default - Node to node encryption is not enabled. */ - readonly nodeToNodeEncryptionEnabled?: boolean; + readonly nodeToNodeEncryption?: boolean; /** * The hour in UTC during which the service takes an automated daily snapshot From f2229c4bba451345abe6f96d34f4ce975797a827 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 05:49:50 -0500 Subject: [PATCH 26/45] fix renamed prop references; builds successfully --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 49 +++++++++---------- .../aws-elasticsearch/test/domain.test.ts | 26 +++++----- .../test/integ.elasticsearch.ts | 8 +-- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 04591f48d5724..c9726fc46d8e9 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -215,8 +215,7 @@ export interface EncryptionAtRestOptions { /** * Configures Amazon ES to use Amazon Cognito authentication for Kibana. - -@see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-cognito-auth.html + * @see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-cognito-auth.html */ export interface CognitoOptions { /** @@ -1067,8 +1066,8 @@ export class Domain extends DomainBase implements IDomain { throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); } - const encryptionAtRestEnabled = props.encryptionAtRestOptions?.enabled ?? (props.encryptionAtRestOptions?.kmsKey != null); - const ebsEnabled = props.ebsOptions != null; + const encryptionAtRestEnabled = props.encryptionAtRest?.enabled ?? (props.encryptionAtRest?.kmsKey != null); + const ebsEnabled = props.ebs != null; function isInstanceType(instanceType: string): Boolean { return masterInstanceType.startsWith(instanceType) || dataInstanceType.startsWith(instanceType); @@ -1087,16 +1086,16 @@ export class Domain extends DomainBase implements IDomain { // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-features-by-version.html if (elasticsearchVersionNum < 5.1) { if ( - props.logPublishingOptions?.slowIndexLogEnabled - || props.logPublishingOptions?.appLogEnabled - || props.logPublishingOptions?.slowSearchLogEnabled + props.logging?.slowIndexLogEnabled + || props.logging?.appLogEnabled + || props.logging?.slowSearchLogEnabled ) { throw new Error('Error and slow logs publishing requires Elasticsearch version 5.1 or later.'); } - if (props.encryptionAtRestOptions?.enabled) { + if (props.encryptionAtRest?.enabled) { throw new Error('Encryption of data at rest requires Elasticsearch version 5.1 or later.'); } - if (props.cognitoOptions != null) { + if (props.cognitoKibanaAuth != null) { throw new Error('Cognito authentication for Kibana requires Elasticsearch version 5.1 or later.'); } if (isSomeInstanceType('c5', 'i3', 'm5', 'r5')) { @@ -1105,7 +1104,7 @@ export class Domain extends DomainBase implements IDomain { } if (elasticsearchVersionNum < 6.0) { - if (props.nodeToNodeEncryptionEnabled) { + if (props.nodeToNodeEncryption) { throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.'); } } @@ -1141,8 +1140,8 @@ export class Domain extends DomainBase implements IDomain { // Setup logging const logGroups: logs.ILogGroup[] = []; - if (props.logPublishingOptions?.slowSearchLogEnabled) { - this.slowSearchLogGroup = props.logPublishingOptions.slowSearchLogGroup ?? + if (props.logging?.slowSearchLogEnabled) { + this.slowSearchLogGroup = props.logging.slowSearchLogGroup ?? new logs.LogGroup(scope, 'SlowSearchLogs', { retention: logs.RetentionDays.ONE_MONTH, }); @@ -1150,8 +1149,8 @@ export class Domain extends DomainBase implements IDomain { logGroups.push(this.slowSearchLogGroup); }; - if (props.logPublishingOptions?.slowIndexLogEnabled) { - this.slowIndexLogGroup = props.logPublishingOptions.slowIndexLogGroup ?? + if (props.logging?.slowIndexLogEnabled) { + this.slowIndexLogGroup = props.logging.slowIndexLogGroup ?? new logs.LogGroup(scope, 'SlowIndexLogs', { retention: logs.RetentionDays.ONE_MONTH, }); @@ -1159,8 +1158,8 @@ export class Domain extends DomainBase implements IDomain { logGroups.push(this.slowIndexLogGroup); }; - if (props.logPublishingOptions?.appLogEnabled) { - this.appLogGroup = props.logPublishingOptions.appLogGroup ?? + if (props.logging?.appLogEnabled) { + this.appLogGroup = props.logging.appLogGroup ?? new logs.LogGroup(scope, 'AppLogs', { retention: logs.RetentionDays.ONE_MONTH, }); @@ -1203,15 +1202,15 @@ export class Domain extends DomainBase implements IDomain { }, ebsOptions: { ebsEnabled, - volumeSize: props.ebsOptions?.volumeSize, - volumeType: props.ebsOptions?.volumeType, - iops: props.ebsOptions?.iops, + volumeSize: props.ebs?.volumeSize, + volumeType: props.ebs?.volumeType, + iops: props.ebs?.iops, }, encryptionAtRestOptions: { enabled: encryptionAtRestEnabled, - kmsKeyId: props.encryptionAtRestOptions?.kmsKey?.keyId, + kmsKeyId: props.encryptionAtRest?.kmsKey?.keyId, }, - nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryptionEnabled ?? false }, + nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryption ?? false }, logPublishingOptions: { ES_APPLICATION_LOGS: { enabled: this.appLogGroup != null, @@ -1227,10 +1226,10 @@ export class Domain extends DomainBase implements IDomain { }, }, cognitoOptions: { - enabled: props.cognitoOptions != null, - identityPoolId: props.cognitoOptions?.identityPoolId, - roleArn: props.cognitoOptions?.role.roleArn, - userPoolId: props.cognitoOptions?.userPoolId, + enabled: props.cognitoKibanaAuth != null, + identityPoolId: props.cognitoKibanaAuth?.identityPoolId, + roleArn: props.cognitoKibanaAuth?.role.roleArn, + userPoolId: props.cognitoKibanaAuth?.userPoolId, }, vpcOptions: cfnVpcOptions, snapshotOptions: props.automatedSnapshotStartHour diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 0d1d893a7afd3..c488d6f8c1831 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -78,7 +78,7 @@ describe('log groups', () => { test('slowSearchLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { clusterConfig: defaultClusterConfig, - logPublishingOptions: { + logging: { slowSearchLogEnabled: true, }, }); @@ -107,7 +107,7 @@ describe('log groups', () => { test('slowIndexLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { clusterConfig: defaultClusterConfig, - logPublishingOptions: { + logging: { slowIndexLogEnabled: true, }, }); @@ -136,7 +136,7 @@ describe('log groups', () => { test('appLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { clusterConfig: defaultClusterConfig, - logPublishingOptions: { + logging: { appLogEnabled: true, }, }); @@ -485,21 +485,21 @@ describe('custom error responses', () => { expect(() => new Domain(stack, 'Domain1', { elasticsearchVersion: Version.ES_2_3, clusterConfig: defaultClusterConfig, - logPublishingOptions: { + logging: { appLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { elasticsearchVersion: Version.ES_1_5, clusterConfig: defaultClusterConfig, - logPublishingOptions: { + logging: { slowSearchLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { elasticsearchVersion: Version.ES_1_5, clusterConfig: defaultClusterConfig, - logPublishingOptions: { + logging: { slowIndexLogEnabled: true, }, })).toThrow(error); @@ -509,7 +509,7 @@ describe('custom error responses', () => { expect(() => new Domain(stack, 'Domain1', { elasticsearchVersion: Version.ES_2_3, clusterConfig: defaultClusterConfig, - encryptionAtRestOptions: { + encryptionAtRest: { enabled: true, }, })).toThrow(/Encryption of data at rest requires Elasticsearch version 5.1 or later/); @@ -520,7 +520,7 @@ describe('custom error responses', () => { expect(() => new Domain(stack, 'Domain1', { elasticsearchVersion: Version.ES_2_3, clusterConfig: defaultClusterConfig, - cognitoOptions: { + cognitoKibanaAuth: { identityPoolId: 'test-identity-pool-id', role: new iam.Role(stack, 'Role', { assumedBy: user }), userPoolId: 'test-user-pool-id', @@ -564,7 +564,7 @@ describe('custom error responses', () => { expect(() => new Domain(stack, 'Domain1', { elasticsearchVersion: Version.ES_5_6, clusterConfig: defaultClusterConfig, - nodeToNodeEncryptionEnabled: true, + nodeToNodeEncryption: true, })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later/); }); @@ -574,7 +574,7 @@ describe('custom error responses', () => { ...defaultClusterConfig, masterNodeInstanceType: 'i3.2xlarge.elasticsearch', }, - ebsOptions: { + ebs: { volumeSize: 100, volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, }, @@ -588,7 +588,7 @@ describe('custom error responses', () => { ...defaultClusterConfig, masterNodeInstanceType: 'm3.2xlarge.elasticsearch', }, - encryptionAtRestOptions: { + encryptionAtRest: { enabled: true, }, })).toThrow(error); @@ -597,7 +597,7 @@ describe('custom error responses', () => { ...defaultClusterConfig, dataNodeInstanceType: 'r3.2xlarge.elasticsearch', }, - encryptionAtRestOptions: { + encryptionAtRest: { enabled: true, }, })).toThrow(error); @@ -606,7 +606,7 @@ describe('custom error responses', () => { ...defaultClusterConfig, masterNodeInstanceType: 't2.2xlarge.elasticsearch', }, - encryptionAtRestOptions: { + encryptionAtRest: { enabled: true, }, })).toThrow(error); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index 9f232c84d4863..2455f808c49a4 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -14,16 +14,16 @@ class TestStack extends Stack { dataNodes: 3, dataNodeInstanceType: 'm5.large.elasticsearch', }, - ebsOptions: { + ebs: { volumeSize: 10, volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, }, - logPublishingOptions: { + logging: { slowSearchLogEnabled: true, appLogEnabled: true, }, - nodeToNodeEncryptionEnabled: true, - encryptionAtRestOptions: { + nodeToNodeEncryption: true, + encryptionAtRest: { enabled: true, }, }); From 78e85ace126f96a52af5df28bd353227cd3f6a82 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 06:08:59 -0500 Subject: [PATCH 27/45] version renaming --- packages/@aws-cdk/aws-elasticsearch/README.md | 10 ++-- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 51 +++++++++++-------- .../aws-elasticsearch/test/domain.test.ts | 43 ++++++++++------ .../test/integ.elasticsearch.ts | 2 +- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 2698830471382..b01d19a6099e2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -21,7 +21,7 @@ To create an Elasticsearch domain: import * as es from '@aws-cdk/aws-elasticsearch'; const domain = new es.Domain(this, 'Domain', { - elasticsearchVersion: es.Version.ES_7_1, + version: es.ElasticsearchVersion.V7_1, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'c5.large.elasticsearch', @@ -74,19 +74,19 @@ The domain can also be created with encryption enabled: ```ts const domain = new es.Domain(this, 'Domain', { - elasticsearchVersion: es.Version.ES_7_4, + version: es.ElasticsearchVersion.V7_4, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'c5.large.elasticsearch', dataNodes: 3, dataNodeInstanceType: 'r5.large.elasticsearch', }, - ebsOptions: { + ebs: { volumeSize: 100, volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, }, - nodeToNodeEncryptionEnabled: true, - encryptionAtRestOptions: { + nodeToNodeEncryption: true, + encryptionAtRest: { enabled: true, }, }); diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index c9726fc46d8e9..bba30468c49b0 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -14,51 +14,63 @@ import * as perms from './perms'; /** * Supported Elasticsearch Versions */ -export class Version { +export class ElasticsearchVersion { /** AWS Elasticsearch 1.5 */ - public static readonly ES_1_5 = '1.5'; + public static readonly V1_5 = ElasticsearchVersion.of('1.5'); /** AWS Elasticsearch 2.3 */ - public static readonly ES_2_3 = '2.3'; + public static readonly V2_3 = ElasticsearchVersion.of('2.3'); /** AWS Elasticsearch 5.1 */ - public static readonly ES_5_1 = '5.1'; + public static readonly V5_1 = ElasticsearchVersion.of('5.1'); /** AWS Elasticsearch 5.3 */ - public static readonly ES_5_3 = '5.3'; + public static readonly V5_3 = ElasticsearchVersion.of('5.3'); /** AWS Elasticsearch 5.5 */ - public static readonly ES_5_5 = '5.5'; + public static readonly V5_5 = ElasticsearchVersion.of('5.5'); /** AWS Elasticsearch 5.6 */ - public static readonly ES_5_6 = '5.6'; + public static readonly V5_6 = ElasticsearchVersion.of('5.6'); /** AWS Elasticsearch 6.0 */ - public static readonly ES_6_0 = '6.0'; + public static readonly V6_0 = ElasticsearchVersion.of('6.0'); /** AWS Elasticsearch 6.2 */ - public static readonly ES_6_2 = '6.2'; + public static readonly V6_2 = ElasticsearchVersion.of('6.2'); /** AWS Elasticsearch 6.3 */ - public static readonly ES_6_3 = '6.3'; + public static readonly V6_3 = ElasticsearchVersion.of('6.3'); /** AWS Elasticsearch 6.4 */ - public static readonly ES_6_4 = '6.4'; + public static readonly V6_4 = ElasticsearchVersion.of('6.4'); /** AWS Elasticsearch 6.5 */ - public static readonly ES_6_5 = '6.5'; + public static readonly V6_5 = ElasticsearchVersion.of('6.5'); /** AWS Elasticsearch 6.7 */ - public static readonly ES_6_7 = '6.7'; + public static readonly V6_7 = ElasticsearchVersion.of('6.7'); /** AWS Elasticsearch 6.8 */ - public static readonly ES_6_8 = '6.8'; + public static readonly V6_8 = ElasticsearchVersion.of('6.8'); /** AWS Elasticsearch 7.1 */ - public static readonly ES_7_1 = '7.1'; + public static readonly V7_1 = ElasticsearchVersion.of('7.1'); /** AWS Elasticsearch 7.4 */ - public static readonly ES_7_4 = '7.4'; + public static readonly V7_4 = ElasticsearchVersion.of('7.4'); + + /** + * Custom Elasticsearch version + * @param version custom version number + */ + public static of(version: string) { return new ElasticsearchVersion(version); } + + /** + * + * @param version Elasticsearch version number + */ + private constructor(public readonly version: string) { } } /** @@ -315,9 +327,8 @@ export interface DomainProps { * currently supports Elasticsearch versions 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, 6.0, * 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. * - * @default '7.4' */ - readonly elasticsearchVersion?: string; + readonly version: ElasticsearchVersion; /** * Encryption at rest options for the cluster. @@ -1033,7 +1044,7 @@ export class Domain extends DomainBase implements IDomain { throw new Error('Master and data node instance types must end with ".elasticsearch".'); } - const elasticsearchVersion = props.elasticsearchVersion ?? '7.4'; + const elasticsearchVersion = props.version.version; const elasticsearchVersionNum = parseVersion(elasticsearchVersion); function parseVersion(version: string): number { @@ -1187,7 +1198,7 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, - elasticsearchVersion: elasticsearchVersion.toString(), + elasticsearchVersion, elasticsearchClusterConfig: { dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, dedicatedMasterCount: props.clusterConfig.masterNodes, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index c488d6f8c1831..7f0492eb01403 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -3,7 +3,7 @@ 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 { App, Stack, Duration } from '@aws-cdk/core'; -import { Domain, Version } from '../lib'; +import { Domain, ElasticsearchVersion } from '../lib'; let app: App; let stack: Stack; @@ -33,7 +33,7 @@ const readWriteActions = [ test('minimal example renders correctly', () => { new Domain(stack, 'Domain', { - elasticsearchVersion: Version.ES_7_1, + version: ElasticsearchVersion.V7_1, clusterConfig: defaultClusterConfig, }); @@ -77,6 +77,7 @@ describe('log groups', () => { test('slowSearchLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, clusterConfig: defaultClusterConfig, logging: { slowSearchLogEnabled: true, @@ -106,6 +107,7 @@ describe('log groups', () => { test('slowIndexLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, clusterConfig: defaultClusterConfig, logging: { slowIndexLogEnabled: true, @@ -135,6 +137,7 @@ describe('log groups', () => { test('appLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, clusterConfig: defaultClusterConfig, logging: { appLogEnabled: true, @@ -440,6 +443,7 @@ describe('custom error responses', () => { const vpc = new Vpc(stack, 'Vpc'); expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, availabilityZoneCount: 2, @@ -460,12 +464,14 @@ describe('custom error responses', () => { test('error when master or data node instance types do not end with .elasticsearch', () => { const error = /instance types must end with ".elasticsearch"/; expect(() => new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'c5.large', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'c5.2xlarge', @@ -476,28 +482,28 @@ describe('custom error responses', () => { test('error when elasticsearchVersion is unsupported/unknown', () => { expect(() => new Domain(stack, 'Domain1', { clusterConfig: defaultClusterConfig, - elasticsearchVersion: '5.4', + version: ElasticsearchVersion.of('5.4'), })).toThrow(/Unknown Elasticsearch version: 5\.4/); }); test('error when log publishing is enabled for elasticsearch version < 5.1', () => { const error = /logs publishing requires Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: Version.ES_2_3, + version: ElasticsearchVersion.V2_3, clusterConfig: defaultClusterConfig, logging: { appLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { - elasticsearchVersion: Version.ES_1_5, + version: ElasticsearchVersion.V1_5, clusterConfig: defaultClusterConfig, logging: { slowSearchLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { - elasticsearchVersion: Version.ES_1_5, + version: ElasticsearchVersion.V1_5, clusterConfig: defaultClusterConfig, logging: { slowIndexLogEnabled: true, @@ -507,7 +513,7 @@ describe('custom error responses', () => { test('error when encryption at rest is enabled for elasticsearch version < 5.1', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: Version.ES_2_3, + version: ElasticsearchVersion.V2_3, clusterConfig: defaultClusterConfig, encryptionAtRest: { enabled: true, @@ -518,7 +524,7 @@ describe('custom error responses', () => { test('error when cognito for kibana is enabled for elasticsearch version < 5.1', () => { const user = new iam.User(stack, 'user'); expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: Version.ES_2_3, + version: ElasticsearchVersion.V2_3, clusterConfig: defaultClusterConfig, cognitoKibanaAuth: { identityPoolId: 'test-identity-pool-id', @@ -531,28 +537,28 @@ describe('custom error responses', () => { test('error when C5, I3, M5, or R5 instance types are specified for elasticsearch version < 5.1', () => { const error = /C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: Version.ES_2_3, + version: ElasticsearchVersion.V2_3, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'c5.medium.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { - elasticsearchVersion: Version.ES_1_5, + version: ElasticsearchVersion.V1_5, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'i3.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { - elasticsearchVersion: Version.ES_1_5, + version: ElasticsearchVersion.V1_5, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'm5.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain4', { - elasticsearchVersion: Version.ES_1_5, + version: ElasticsearchVersion.V1_5, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'r5.2xlarge.elasticsearch', @@ -562,7 +568,7 @@ describe('custom error responses', () => { test('error when node to node encryption is enabled for elasticsearch version < 6.0', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: Version.ES_5_6, + version: ElasticsearchVersion.V5_6, clusterConfig: defaultClusterConfig, nodeToNodeEncryption: true, })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later/); @@ -570,6 +576,7 @@ describe('custom error responses', () => { test('error when i3 instance types are specified with EBS enabled', () => { expect(() => new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'i3.2xlarge.elasticsearch', @@ -584,6 +591,7 @@ describe('custom error responses', () => { test('error when m3, r3, or t2 instance types are specified with encryption at rest enabled', () => { const error = /M3, R3, and T2 instance types do not support encryption of data at rest/; expect(() => new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'm3.2xlarge.elasticsearch', @@ -593,6 +601,7 @@ describe('custom error responses', () => { }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, dataNodeInstanceType: 'r3.2xlarge.elasticsearch', @@ -602,6 +611,7 @@ describe('custom error responses', () => { }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 't2.2xlarge.elasticsearch', @@ -614,7 +624,7 @@ describe('custom error responses', () => { test('error when t2.micro is specified with elasticsearch version > 2.3', () => { expect(() => new Domain(stack, 'Domain1', { - elasticsearchVersion: Version.ES_6_7, + version: ElasticsearchVersion.V6_7, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 't2.micro.elasticsearch', @@ -624,6 +634,7 @@ describe('custom error responses', () => { test('error when any instance type other than R3 and I3 are specified without EBS enabled', () => { expect(() => new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, clusterConfig: { ...defaultClusterConfig, masterNodeInstanceType: 'm5.large.elasticsearch', @@ -635,7 +646,7 @@ describe('custom error responses', () => { test('can specify future version', () => { new Domain(stack, 'Domain', { - elasticsearchVersion: '8.2', + version: ElasticsearchVersion.of('8.2'), clusterConfig: defaultClusterConfig, }); @@ -652,6 +663,7 @@ function testGrant( paths: string[] = ['/*'], ) { const domain = new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, clusterConfig: defaultClusterConfig, }); const user = new iam.User(stack, 'user'); @@ -709,6 +721,7 @@ function testMetric( period: Duration = Duration.minutes(5), ) { const domain = new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, clusterConfig: defaultClusterConfig, }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index 2455f808c49a4..ff93d24478be5 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -7,7 +7,7 @@ class TestStack extends Stack { super(scope, id, props); new es.Domain(this, 'Domain', { - elasticsearchVersion: es.Version.ES_7_1, + version: es.ElasticsearchVersion.V7_1, clusterConfig: { masterNodes: 3, masterNodeInstanceType: 'm5.large.elasticsearch', From 9e158cd309ff80077b23ae03fde0422ffdadfd8b Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 07:13:51 -0500 Subject: [PATCH 28/45] add ElasticsearchVersion 7.7 --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index bba30468c49b0..0ca63c289d7da 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -12,7 +12,7 @@ import { LogGroupResourcePolicy } from './log-group-resource-policy'; import * as perms from './perms'; /** - * Supported Elasticsearch Versions + * Elasticsearch version */ export class ElasticsearchVersion { /** AWS Elasticsearch 1.5 */ @@ -60,6 +60,9 @@ export class ElasticsearchVersion { /** AWS Elasticsearch 7.4 */ public static readonly V7_4 = ElasticsearchVersion.of('7.4'); + /** AWS Elasticsearch 7.7 */ + public static readonly V7_7 = ElasticsearchVersion.of('7.7'); + /** * Custom Elasticsearch version * @param version custom version number @@ -324,8 +327,8 @@ export interface DomainProps { * The Elasticsearch version that your domain will leverage. * * Per https://aws.amazon.com/elasticsearch-service/faqs/, Amazon Elasticsearch Service - * currently supports Elasticsearch versions 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, 6.0, - * 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. + * currently supports Elasticsearch versions 7.7, 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, + * 6.0, 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. * */ readonly version: ElasticsearchVersion; @@ -1068,10 +1071,11 @@ export class Domain extends DomainBase implements IDomain { } if ( - elasticsearchVersionNum <= 7.4 && + elasticsearchVersionNum <= 7.7 && ![ 1.5, 2.3, 5.1, 5.3, 5.5, 5.6, 6.0, 6.2, 6.3, 6.4, 6.5, 6.7, 6.8, 7.1, 7.4, + 7.7, ].includes(elasticsearchVersionNum) ) { throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); From efe6b6cc74c8fdf7475e02928531cb12bebd27c7 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 09:41:33 -0500 Subject: [PATCH 29/45] `ClusterConfig` -> capacity and zone awareness --- packages/@aws-cdk/aws-elasticsearch/README.md | 18 +-- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 137 ++++++++++++------ .../aws-elasticsearch/test/domain.test.ts | 94 ++++-------- .../test/integ.elasticsearch.expected.json | 8 +- .../test/integ.elasticsearch.ts | 6 - 5 files changed, 128 insertions(+), 135 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index b01d19a6099e2..2864aea79106d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -22,17 +22,7 @@ import * as es from '@aws-cdk/aws-elasticsearch'; const domain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, - clusterConfig: { - masterNodes: 3, - masterNodeInstanceType: 'c5.large.elasticsearch', - dataNodes: 3, - dataNodeInstanceType: 'r5.large.elasticsearch', - }, - ebsOptions: { - volumeSize: 100, - volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, - }, - logPublishingOptions: { + logging: { slowSearchLogEnabled: true, appLogEnabled: true }, @@ -75,12 +65,6 @@ The domain can also be created with encryption enabled: ```ts const domain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_4, - clusterConfig: { - masterNodes: 3, - masterNodeInstanceType: 'c5.large.elasticsearch', - dataNodes: 3, - dataNodeInstanceType: 'r5.large.elasticsearch', - }, ebs: { volumeSize: 100, volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 0ca63c289d7da..9697dfc9401f9 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -77,14 +77,16 @@ export class ElasticsearchVersion { } /** - * Configures the makeup of the cluster such as number of nodes and instance - * type. + * Configures the capacity of the cluster such as the instance type and the + * number of instances. */ -export interface ClusterConfig { +export interface CapacityConfig { /** - * The number of instances to use for the master node + * The number of instances to use for the master node. + * + * @default - no dedicated master nodes */ - readonly masterNodes: number; + readonly masterNodes?: number; /** * The hardware configuration of the computer that hosts the dedicated master @@ -92,31 +94,53 @@ export interface ClusterConfig { * Instance Types] * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html) * in the Amazon Elasticsearch Service Developer Guide. + * + * @default - r5.large.elasticsearch */ - readonly masterNodeInstanceType: string; + readonly masterNodeInstanceType?: string; /** - * The number of data nodes to use in the Amazon ES domain. + * The number of data nodes (instances) to use in the Amazon ES domain. + * + * @default - 1 */ - readonly dataNodes: number; + readonly dataNodes?: number; /** * The instance type for your data nodes, such as * `m3.medium.elasticsearch`. For valid values, see [Supported Instance * Types](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html) * in the Amazon Elasticsearch Service Developer Guide. + * + * @default - r5.large.elasticsearch */ - readonly dataNodeInstanceType: string; + readonly dataNodeInstanceType?: string; +} +/** + * Specifies zone awareness configuration options. + */ +export interface ZoneAwarenessConfig { /** - * The number of AZs that you want the domain to use. When you enable zone - * awareness, Amazon ES allocates the nodes and replica index shards that - * belong to a cluster across the specified number of Availability Zones (AZs) + * Indicates whether to enable zone awareness for the Amazon ES domain. + * When you enable zone awareness, Amazon ES allocates the nodes and replica + * index shards that belong to a cluster across two Availability Zones (AZs) * in the same region to prevent data loss and minimize downtime in the event * of node or data center failure. Don't enable zone awareness if your cluster - * has no replica index shards or is a single-node cluster. + * has no replica index shards or is a single-node cluster. For more information, + * see [Configuring a Multi-AZ Domain] + * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-managedomains-multiaz) + * in the Amazon Elasticsearch Service Developer Guide. * - * @default - Zone awareness is not enabled. + * @default - false + */ + readonly enabled?: boolean; + + /** + * If you enabled multiple Availability Zones (AZs), the number of AZs that you + * want the domain to use. Valid values are 2 and 3. + * + * @default - 2 if zone awareness is enabled. */ readonly availabilityZoneCount?: number; } @@ -129,6 +153,14 @@ export interface ClusterConfig { * in the Amazon Elasticsearch Service Developer Guide. */ export interface EbsOptions { + /** + * Specifies whether Amazon EBS volumes are attached to data nodes in the + * Amazon ES domain. + * + * @default - true + */ + readonly enabled?: boolean; + /** * The number of I/O operations per second (IOPS) that the volume * supports. This property applies only to the Provisioned IOPS (SSD) EBS @@ -144,17 +176,21 @@ export interface EbsOptions { * instance type to which it is attached. For more information, see * [Configuring EBS-based Storage] * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-ebs) - * in the Amazon Elasticsearch Service Developer Guide + * in the Amazon Elasticsearch Service Developer Guide. + * + * @default 10 */ - readonly volumeSize: number; + readonly volumeSize?: number; /** * The EBS volume type to use with the Amazon ES domain, such as standard, gp2, io1, st1, or sc1. * For more information, see[Configuring EBS-based Storage] * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-ebs) - * in the Amazon Elasticsearch Service Developer Guide + * in the Amazon Elasticsearch Service Developer Guide. + * + * @default gp2 */ - readonly volumeType: ec2.EbsDeviceVolumeType; + readonly volumeType?: ec2.EbsDeviceVolumeType; } /** @@ -313,15 +349,23 @@ export interface DomainProps { * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that * are attached to data nodes in the Amazon ES domain. * - * @default - No EBS volumes attached. + * @default - 10 GiB General Purpose (SSD) volumes per node. */ readonly ebs?: EbsOptions; /** - * The cluster configuration for the Amazon ES domain. + * The cluster capacity configuration for the Amazon ES domain. * + * @default - 1 r5.large.elasticsearch data node; no dedicated master nodes. */ - readonly clusterConfig: ClusterConfig; + readonly capacity?: CapacityConfig; + + /** + * The cluster zone awareness configuration for the Amazon ES domain. + * + * @default - no zone awareness (1 AZ) + */ + readonly zoneAwareness?: ZoneAwarenessConfig; /** * The Elasticsearch version that your domain will leverage. @@ -1036,14 +1080,23 @@ export class Domain extends DomainBase implements IDomain { }); // If VPC options are supplied ensure that the number of subnets matches the number AZ - if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.clusterConfig.availabilityZoneCount) { + if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.zoneAwareness?.availabilityZoneCount) { throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); }; - const masterInstanceType = props.clusterConfig.masterNodeInstanceType.toLowerCase(); - const dataInstanceType = props.clusterConfig.dataNodeInstanceType.toLowerCase(); + const defaultInstanceType = 'r5.large.elasticsearch'; + + const dedicatedMasterType = props.capacity?.masterNodeInstanceType?.toLowerCase() ?? defaultInstanceType; + const dedicatedMasterCount = props.capacity?.masterNodes ?? 0; + const dedicatedMasterEnabled = dedicatedMasterCount > 0; + + const instanceType = props.capacity?.dataNodeInstanceType?.toLowerCase() ?? defaultInstanceType; + const instanceCount = props.capacity?.dataNodes ?? 1; + + const zoneAwarenessEnabled = props.zoneAwareness?.enabled ?? false; + const availabilityZoneCount = props.zoneAwareness?.availabilityZoneCount ?? 2; - if ([masterInstanceType, dataInstanceType].some(instanceType => !instanceType.endsWith('.elasticsearch'))) { + if ([dedicatedMasterType, instanceType].some(t => !t.endsWith('.elasticsearch'))) { throw new Error('Master and data node instance types must end with ".elasticsearch".'); } @@ -1082,10 +1135,12 @@ export class Domain extends DomainBase implements IDomain { } const encryptionAtRestEnabled = props.encryptionAtRest?.enabled ?? (props.encryptionAtRest?.kmsKey != null); - const ebsEnabled = props.ebs != null; + const volumeSize = props.ebs?.volumeSize ?? 10; + const volumeType = props.ebs?.volumeType ?? ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; + const ebsEnabled = props.ebs?.enabled ?? true; - function isInstanceType(instanceType: string): Boolean { - return masterInstanceType.startsWith(instanceType) || dataInstanceType.startsWith(instanceType); + function isInstanceType(t: string): Boolean { + return dedicatedMasterType.startsWith(t) || instanceType.startsWith(t); }; function isSomeInstanceType(...instanceTypes: string[]): Boolean { @@ -1093,8 +1148,8 @@ export class Domain extends DomainBase implements IDomain { }; function isEveryInstanceType(...instanceTypes: string[]): Boolean { - return instanceTypes.some(t => masterInstanceType.startsWith(t)) - && instanceTypes.some(t => dataInstanceType.startsWith(t)); + return instanceTypes.some(t => dedicatedMasterType.startsWith(t)) + && instanceTypes.some(t => instanceType.startsWith(t)); }; // Validate feature support for the given Elasticsearch version, per @@ -1204,22 +1259,22 @@ export class Domain extends DomainBase implements IDomain { domainName: this.physicalName, elasticsearchVersion, elasticsearchClusterConfig: { - dedicatedMasterEnabled: props.clusterConfig.masterNodes != null, - dedicatedMasterCount: props.clusterConfig.masterNodes, - dedicatedMasterType: props.clusterConfig.masterNodeInstanceType, - instanceCount: props.clusterConfig.dataNodes, - instanceType: props.clusterConfig.dataNodeInstanceType, - zoneAwarenessEnabled: props.clusterConfig.availabilityZoneCount != null, + dedicatedMasterEnabled, + dedicatedMasterCount: dedicatedMasterEnabled ? dedicatedMasterCount : undefined, + dedicatedMasterType: dedicatedMasterEnabled ? dedicatedMasterType : undefined, + instanceCount, + instanceType, + zoneAwarenessEnabled, zoneAwarenessConfig: - props.clusterConfig.availabilityZoneCount != null - ? { availabilityZoneCount: props.clusterConfig.availabilityZoneCount } + zoneAwarenessEnabled + ? { availabilityZoneCount } : undefined, }, ebsOptions: { ebsEnabled, - volumeSize: props.ebs?.volumeSize, - volumeType: props.ebs?.volumeType, - iops: props.ebs?.iops, + volumeSize: ebsEnabled ? volumeSize : undefined, + volumeType: ebsEnabled ? volumeType : undefined, + iops: ebsEnabled ? props.ebs?.iops : undefined, }, encryptionAtRestOptions: { enabled: encryptionAtRestEnabled, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 7f0492eb01403..26240cff282c4 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -17,13 +17,6 @@ beforeEach(() => { jest.resetAllMocks(); }); -const defaultClusterConfig = { - masterNodes: 3, - masterNodeInstanceType: 'i3.large.elasticsearch', - dataNodes: 3, - dataNodeInstanceType: 'r3.large.elasticsearch', -}; - const readActions = ['ESHttpGet', 'ESHttpHead']; const writeActions = ['ESHttpDelete', 'ESHttpPost', 'ESHttpPut', 'ESHttpPatch']; const readWriteActions = [ @@ -32,24 +25,21 @@ const readWriteActions = [ ]; test('minimal example renders correctly', () => { - new Domain(stack, 'Domain', { - version: ElasticsearchVersion.V7_1, - clusterConfig: defaultClusterConfig, - }); + new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_1 }); expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { CognitoOptions: { Enabled: false, }, EBSOptions: { - EBSEnabled: false, + EBSEnabled: true, + VolumeSize: 10, + VolumeType: 'gp2', }, ElasticsearchClusterConfig: { - DedicatedMasterCount: 3, - DedicatedMasterEnabled: true, - DedicatedMasterType: 'i3.large.elasticsearch', - InstanceCount: 3, - InstanceType: 'r3.large.elasticsearch', + DedicatedMasterEnabled: false, + InstanceCount: 1, + InstanceType: 'r5.large.elasticsearch', ZoneAwarenessEnabled: false, }, ElasticsearchVersion: '7.1', @@ -78,7 +68,6 @@ describe('log groups', () => { test('slowSearchLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_4, - clusterConfig: defaultClusterConfig, logging: { slowSearchLogEnabled: true, }, @@ -108,7 +97,6 @@ describe('log groups', () => { test('slowIndexLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_4, - clusterConfig: defaultClusterConfig, logging: { slowIndexLogEnabled: true, }, @@ -138,7 +126,6 @@ describe('log groups', () => { test('appLogEnabled should create a custom log group', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_4, - clusterConfig: defaultClusterConfig, logging: { appLogEnabled: true, }, @@ -444,8 +431,8 @@ describe('custom error responses', () => { expect(() => new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + zoneAwareness: { + enabled: true, availabilityZoneCount: 2, }, vpcOptions: { @@ -465,15 +452,13 @@ describe('custom error responses', () => { const error = /instance types must end with ".elasticsearch"/; expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + capacity: { masterNodeInstanceType: 'c5.large', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + capacity: { dataNodeInstanceType: 'c5.2xlarge', }, })).toThrow(error); @@ -481,7 +466,6 @@ describe('custom error responses', () => { test('error when elasticsearchVersion is unsupported/unknown', () => { expect(() => new Domain(stack, 'Domain1', { - clusterConfig: defaultClusterConfig, version: ElasticsearchVersion.of('5.4'), })).toThrow(/Unknown Elasticsearch version: 5\.4/); }); @@ -490,21 +474,18 @@ describe('custom error responses', () => { const error = /logs publishing requires Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V2_3, - clusterConfig: defaultClusterConfig, logging: { appLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { version: ElasticsearchVersion.V1_5, - clusterConfig: defaultClusterConfig, logging: { slowSearchLogEnabled: true, }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { version: ElasticsearchVersion.V1_5, - clusterConfig: defaultClusterConfig, logging: { slowIndexLogEnabled: true, }, @@ -514,7 +495,6 @@ describe('custom error responses', () => { test('error when encryption at rest is enabled for elasticsearch version < 5.1', () => { expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V2_3, - clusterConfig: defaultClusterConfig, encryptionAtRest: { enabled: true, }, @@ -525,7 +505,6 @@ describe('custom error responses', () => { const user = new iam.User(stack, 'user'); expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V2_3, - clusterConfig: defaultClusterConfig, cognitoKibanaAuth: { identityPoolId: 'test-identity-pool-id', role: new iam.Role(stack, 'Role', { assumedBy: user }), @@ -538,29 +517,25 @@ describe('custom error responses', () => { const error = /C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V2_3, - clusterConfig: { - ...defaultClusterConfig, + capacity: { masterNodeInstanceType: 'c5.medium.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { version: ElasticsearchVersion.V1_5, - clusterConfig: { - ...defaultClusterConfig, + capacity: { dataNodeInstanceType: 'i3.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { version: ElasticsearchVersion.V1_5, - clusterConfig: { - ...defaultClusterConfig, + capacity: { dataNodeInstanceType: 'm5.2xlarge.elasticsearch', }, })).toThrow(error); expect(() => new Domain(stack, 'Domain4', { version: ElasticsearchVersion.V1_5, - clusterConfig: { - ...defaultClusterConfig, + capacity: { masterNodeInstanceType: 'r5.2xlarge.elasticsearch', }, })).toThrow(error); @@ -569,7 +544,6 @@ describe('custom error responses', () => { test('error when node to node encryption is enabled for elasticsearch version < 6.0', () => { expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V5_6, - clusterConfig: defaultClusterConfig, nodeToNodeEncryption: true, })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later/); }); @@ -577,9 +551,8 @@ describe('custom error responses', () => { test('error when i3 instance types are specified with EBS enabled', () => { expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, - masterNodeInstanceType: 'i3.2xlarge.elasticsearch', + capacity: { + dataNodeInstanceType: 'i3.2xlarge.elasticsearch', }, ebs: { volumeSize: 100, @@ -592,8 +565,7 @@ describe('custom error responses', () => { const error = /M3, R3, and T2 instance types do not support encryption of data at rest/; expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + capacity: { masterNodeInstanceType: 'm3.2xlarge.elasticsearch', }, encryptionAtRest: { @@ -602,8 +574,7 @@ describe('custom error responses', () => { })).toThrow(error); expect(() => new Domain(stack, 'Domain2', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + capacity: { dataNodeInstanceType: 'r3.2xlarge.elasticsearch', }, encryptionAtRest: { @@ -612,8 +583,7 @@ describe('custom error responses', () => { })).toThrow(error); expect(() => new Domain(stack, 'Domain3', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + capacity: { masterNodeInstanceType: 't2.2xlarge.elasticsearch', }, encryptionAtRest: { @@ -625,8 +595,7 @@ describe('custom error responses', () => { test('error when t2.micro is specified with elasticsearch version > 2.3', () => { expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V6_7, - clusterConfig: { - ...defaultClusterConfig, + capacity: { masterNodeInstanceType: 't2.micro.elasticsearch', }, })).toThrow(/t2.micro.elasticsearch instance type supports only Elasticsearch 1.5 and 2.3/); @@ -635,8 +604,10 @@ describe('custom error responses', () => { test('error when any instance type other than R3 and I3 are specified without EBS enabled', () => { expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V7_4, - clusterConfig: { - ...defaultClusterConfig, + ebs: { + enabled: false, + }, + capacity: { masterNodeInstanceType: 'm5.large.elasticsearch', }, })).toThrow(/EBS volumes are required for all instance types except R3 and I3/); @@ -645,10 +616,7 @@ describe('custom error responses', () => { }); test('can specify future version', () => { - new Domain(stack, 'Domain', { - version: ElasticsearchVersion.of('8.2'), - clusterConfig: defaultClusterConfig, - }); + new Domain(stack, 'Domain', { version: ElasticsearchVersion.of('8.2') }); expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { ElasticsearchVersion: '8.2', @@ -662,10 +630,7 @@ function testGrant( appliesToDomainRoot: Boolean = true, paths: string[] = ['/*'], ) { - const domain = new Domain(stack, 'Domain', { - version: ElasticsearchVersion.V7_4, - clusterConfig: defaultClusterConfig, - }); + const domain = new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_4 }); const user = new iam.User(stack, 'user'); invocation(user, domain); @@ -720,10 +685,7 @@ function testMetric( statistic: string = Statistic.SUM, period: Duration = Duration.minutes(5), ) { - const domain = new Domain(stack, 'Domain', { - version: ElasticsearchVersion.V7_4, - clusterConfig: defaultClusterConfig, - }); + const domain = new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_4 }); const metric = invocation(domain); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 2888bc3fceb99..aff1205f2e633 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -97,11 +97,9 @@ "VolumeType": "gp2" }, "ElasticsearchClusterConfig": { - "DedicatedMasterCount": 3, - "DedicatedMasterEnabled": true, - "DedicatedMasterType": "m5.large.elasticsearch", - "InstanceCount": 3, - "InstanceType": "m5.large.elasticsearch", + "DedicatedMasterEnabled": false, + "InstanceCount": 1, + "InstanceType": "r5.large.elasticsearch", "ZoneAwarenessEnabled": false }, "ElasticsearchVersion": "7.1", diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index ff93d24478be5..876b2ec4bd139 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -8,12 +8,6 @@ class TestStack extends Stack { new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, - clusterConfig: { - masterNodes: 3, - masterNodeInstanceType: 'm5.large.elasticsearch', - dataNodes: 3, - dataNodeInstanceType: 'm5.large.elasticsearch', - }, ebs: { volumeSize: 10, volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, From 8924139d0c54bd84c41ccf68bd9eadc290ed69c6 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 09:57:55 -0500 Subject: [PATCH 30/45] zone awareness validation --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 22 +++++++++--- .../aws-elasticsearch/test/domain.test.ts | 36 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 9697dfc9401f9..c0ede73b2290b 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1086,15 +1086,27 @@ export class Domain extends DomainBase implements IDomain { const defaultInstanceType = 'r5.large.elasticsearch'; - const dedicatedMasterType = props.capacity?.masterNodeInstanceType?.toLowerCase() ?? defaultInstanceType; + const dedicatedMasterType = + props.capacity?.masterNodeInstanceType?.toLowerCase() ?? + defaultInstanceType; const dedicatedMasterCount = props.capacity?.masterNodes ?? 0; const dedicatedMasterEnabled = dedicatedMasterCount > 0; - const instanceType = props.capacity?.dataNodeInstanceType?.toLowerCase() ?? defaultInstanceType; + const instanceType = + props.capacity?.dataNodeInstanceType?.toLowerCase() ?? + defaultInstanceType; const instanceCount = props.capacity?.dataNodes ?? 1; - const zoneAwarenessEnabled = props.zoneAwareness?.enabled ?? false; - const availabilityZoneCount = props.zoneAwareness?.availabilityZoneCount ?? 2; + const availabilityZoneCount = + props.zoneAwareness?.availabilityZoneCount ?? 2; + + if (![1, 2, 3].includes(availabilityZoneCount)) { + throw new Error('Invalid zone awareness configuration; availabilityZoneCount must be 1, 2, or 3'); + } + + const zoneAwarenessEnabled = + props.zoneAwareness?.enabled ?? + props.zoneAwareness?.availabilityZoneCount != null; if ([dedicatedMasterType, instanceType].some(t => !t.endsWith('.elasticsearch'))) { throw new Error('Master and data node instance types must end with ".elasticsearch".'); @@ -1278,7 +1290,7 @@ export class Domain extends DomainBase implements IDomain { }, encryptionAtRestOptions: { enabled: encryptionAtRestEnabled, - kmsKeyId: props.encryptionAtRest?.kmsKey?.keyId, + kmsKeyId: encryptionAtRestEnabled ? props.encryptionAtRest?.kmsKey?.keyId : undefined, }, nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryption ?? false }, logPublishingOptions: { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 26240cff282c4..301b5452b4298 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -613,6 +613,42 @@ describe('custom error responses', () => { })).toThrow(/EBS volumes are required for all instance types except R3 and I3/); }); + test('error when availabilityZoneCount is not 1, 2, or 3', () => { + const vpc = new Vpc(stack, 'Vpc'); + + expect(() => new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, + vpcOptions: { + 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: [], + }, + zoneAwareness: { + availabilityZoneCount: 4, + }, + })).toThrow(/Invalid zone awareness configuration; availabilityZoneCount must be 1, 2, or 3/); + }); + }); test('can specify future version', () => { From aae1e3c84518205b685009d3949d77d0fc539c0a Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 10:30:03 -0500 Subject: [PATCH 31/45] fix availabilityZoneCount validation --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 4 ++-- packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index c0ede73b2290b..3938e70e44d13 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1100,8 +1100,8 @@ export class Domain extends DomainBase implements IDomain { const availabilityZoneCount = props.zoneAwareness?.availabilityZoneCount ?? 2; - if (![1, 2, 3].includes(availabilityZoneCount)) { - throw new Error('Invalid zone awareness configuration; availabilityZoneCount must be 1, 2, or 3'); + if (![2, 3].includes(availabilityZoneCount)) { + throw new Error('Invalid zone awareness configuration; availabilityZoneCount must be 2 or 3'); } const zoneAwarenessEnabled = diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 301b5452b4298..133e66803400b 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -613,7 +613,7 @@ describe('custom error responses', () => { })).toThrow(/EBS volumes are required for all instance types except R3 and I3/); }); - test('error when availabilityZoneCount is not 1, 2, or 3', () => { + test('error when availabilityZoneCount is not 2 or 3', () => { const vpc = new Vpc(stack, 'Vpc'); expect(() => new Domain(stack, 'Domain1', { @@ -646,7 +646,7 @@ describe('custom error responses', () => { zoneAwareness: { availabilityZoneCount: 4, }, - })).toThrow(/Invalid zone awareness configuration; availabilityZoneCount must be 1, 2, or 3/); + })).toThrow(/Invalid zone awareness configuration; availabilityZoneCount must be 2 or 3/); }); }); From 0cb92ebb87879c76514b1e9dcd92e7b29c86c580 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Mon, 17 Aug 2020 10:54:47 -0500 Subject: [PATCH 32/45] make log groups public --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 3938e70e44d13..eed499afaea69 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -447,6 +447,27 @@ export interface IDomain extends cdk.IResource { */ readonly domainEndpoint: string; + /** + * Log group that slow searches are logged to. + * + * @attribute + */ + readonly slowSearchLogGroup?: logs.ILogGroup; + + /** + * Log group that slow indices are logged to. + * + * @attribute + */ + readonly slowIndexLogGroup?: logs.ILogGroup; + + /** + * Log group that application logs are logged to. + * + * @attribute + */ + readonly appLogGroup?: logs.ILogGroup; + /** * Grant read permissions for this domain and its contents to an IAM * principal (Role/Group/User). @@ -1067,12 +1088,12 @@ export class Domain extends DomainBase implements IDomain { public readonly domainArn: string; public readonly domainName: string; public readonly domainEndpoint: string; + public readonly slowSearchLogGroup?: logs.ILogGroup; + public readonly slowIndexLogGroup?: logs.ILogGroup; + public readonly appLogGroup?: logs.ILogGroup; private readonly domain: CfnDomain; - private readonly slowSearchLogGroup?: logs.ILogGroup; - private readonly slowIndexLogGroup?: logs.ILogGroup; - private readonly appLogGroup?: logs.ILogGroup; constructor(scope: cdk.Construct, id: string, props: DomainProps) { super(scope, id, { From 841a35887fd37d4049d967861487ba97b057cf25 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Wed, 19 Aug 2020 09:36:10 +1000 Subject: [PATCH 33/45] Update README.md --- packages/@aws-cdk/aws-elasticsearch/README.md | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 2864aea79106d..8c25952be2bef 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -20,13 +20,32 @@ To create an Elasticsearch domain: ```ts import * as es from '@aws-cdk/aws-elasticsearch'; -const domain = new es.Domain(this, 'Domain', { +const devDomain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, logging: { slowSearchLogEnabled: true, appLogEnabled: true }, }); + +const prodDomain = new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + capacity: { + masterNodes: 5, + dataNodes: 20 + }, + ebs: { + volumeSize: 20 + }, + zoneAwareness: { + availabilityZoneCount: 3 + }, + logging: { + slowSearchLogEnabled: true, + appLogEnabled: true, + slowIndexLogEnabled: true + }, +}); ``` This creates an Elasticsearch cluster and automatically sets up log groups for From a22e4d40ff0e6199c1250defc01920dcfab55a7d Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Wed, 26 Aug 2020 07:17:43 -0500 Subject: [PATCH 34/45] chore: more review feedback (#3) * fix comment * remove domainName from DomainAttributes * domainEndpointOptions * support AdvancedSecurityOptions * throw if encryption/https settings aren't enabled * Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Stephan Hoermann --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 168 +++++++++++++++--- .../@aws-cdk/aws-elasticsearch/package.json | 2 + .../aws-elasticsearch/test/domain.test.ts | 149 +++++++++++++++- ...asticsearch.advancedsecurity.expected.json | 60 +++++++ .../integ.elasticsearch.advancedsecurity.ts | 27 +++ .../test/integ.elasticsearch.expected.json | 3 + 6 files changed, 387 insertions(+), 22 deletions(-) create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.ts diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index eed499afaea69..486ff5138b315 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -5,6 +5,7 @@ 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 secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; import { CfnDomain } from './elasticsearch.generated'; @@ -276,8 +277,8 @@ export interface CognitoOptions { /** * A role that allows Amazon ES to configure your user pool and identity pool. It must have the `AmazonESCognitoAccess` policy attached to it. - -@see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-cognito-auth.html#es-cognito-auth-prereq + * + * @see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-cognito-auth.html#es-cognito-auth-prereq */ readonly role: iam.IRole; @@ -313,6 +314,46 @@ export interface VpcOptions { readonly subnets: ec2.ISubnet[]; } +/** + * The minimum TLS version required for traffic to the domain. + */ +export enum TLSSecurityPolicy { + /** Cipher suite TLS 1.0 */ + TLS_1_0 = 'Policy-Min-TLS-1-0-2019-07', + /** Cipher suite TLS 1.2 */ + TLS_1_2 = 'Policy-Min-TLS-1-2-2019-07' +} + +/** + * Specifies options for fine-grained access control. + */ +export interface AdvancedSecurityOptions { + /** + * ARN for the master user. Only specify this or masterUserName, but not both. + * + * @default - fine-grained access control is disabled + */ + readonly masterUserArn?: string; + + /** + * Username for the master user. Only specify this or masterUserArn, but not both. + * + * @default - fine-grained access control is disabled + */ + readonly masterUserName?: string; + + /** + * Password for the master user. + * + * You can use `SecretValue.plainText` to specify a password in plain text or + * use `secretsmanager.Secret.fromSecretAttributes` to reference a secret in + * Secrets Manager. + * + * @default - A Secrets Manager generated password + */ + readonly masterUserPasswordSecret?: cdk.SecretValue; +} + /** * Properties for an AWS Elasticsearch Domain. */ @@ -419,8 +460,31 @@ export interface DomainProps { * @default - VPC not used */ readonly vpcOptions?: VpcOptions; -} + /** + * True to require that all traffic to the domain arrive over HTTPS. + * + * @default - false + */ + readonly enforceHttps?: boolean; + + /** + * The minimum TLS version required for traffic to the domain. + * + * @default - TLSSecurityPolicy.TLS_1_0 + */ + readonly tlsSecurityPolicy?: TLSSecurityPolicy; + + /** + * Specifies options for fine-grained access control. + * Requires Elasticsearch version 6.7 or later. Enabling fine-grained access control + * also requires encryption of data at rest and node-to-node encryption, along with + * enforced HTTPS. + * + * @default - fine-grained access control is disabled + */ + readonly fineGrainedAccessControl?: AdvancedSecurityOptions; +} /** * An interface that represents an Elasticsearch domain - either created with the CDK, or an existing one. @@ -1025,11 +1089,6 @@ export interface DomainAttributes { */ readonly domainArn: string; - /** - * The domain name of the Elasticsearch domain. - */ - readonly domainName: string; - /** * The domain endpoint of the Elasticsearch domain. */ @@ -1063,7 +1122,6 @@ export class Domain extends DomainBase implements IDomain { return Domain.fromDomainAttributes(scope, id, { domainArn, - domainName, domainEndpoint, }); } @@ -1076,10 +1134,13 @@ export class Domain extends DomainBase implements IDomain { * @param attrs A `DomainAttributes` object. */ public static fromDomainAttributes(scope: cdk.Construct, id: string, attrs: DomainAttributes): IDomain { + const { domainArn, domainEndpoint } = attrs; + const domainName = extractNameFromEndpoint(domainEndpoint); + return new class extends DomainBase { - public readonly domainArn = attrs.domainArn; - public readonly domainName = attrs.domainName; - public readonly domainEndpoint = attrs.domainEndpoint; + public readonly domainArn = domainArn; + public readonly domainName = domainName; + public readonly domainEndpoint = domainEndpoint; constructor() { super(scope, id); } }; @@ -1167,10 +1228,35 @@ export class Domain extends DomainBase implements IDomain { throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); } - const encryptionAtRestEnabled = props.encryptionAtRest?.enabled ?? (props.encryptionAtRest?.kmsKey != null); + const masterUserArn = props.fineGrainedAccessControl?.masterUserArn; + const masterUserName = props.fineGrainedAccessControl?.masterUserName; + + const advancedSecurityEnabled = (masterUserArn ?? masterUserName) != null; + const internalUserDatabaseEnabled = masterUserName != null; + const masterUserPasswordString = + props.fineGrainedAccessControl?.masterUserPasswordSecret?.toString() + function createMasterUserPasswordSecret(): string { + new secretsmanager.Secret(this, id, { + generateSecretString: { + secretStringTemplate: JSON.stringify({ + username: masterUserName, + }), + generateStringKey: 'password', + }, + }) + .secretValueFromJson('password') + .toString() + const masterUserPassword = + masterUserPasswordString ?? + internalUserDatabaseEnabled ? createMasterUserPasswordSecret() : undefined; + + const encryptionAtRestEnabled = + props.encryptionAtRest?.enabled ?? props.encryptionAtRest?.kmsKey != null; + const nodeToNodeEncryptionEnabled = props.nodeToNodeEncryption ?? false; const volumeSize = props.ebs?.volumeSize ?? 10; const volumeType = props.ebs?.volumeType ?? ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; const ebsEnabled = props.ebs?.enabled ?? true; + const enforceHttps = props.enforceHttps; function isInstanceType(t: string): Boolean { return dedicatedMasterType.startsWith(t) || instanceType.startsWith(t); @@ -1212,6 +1298,12 @@ export class Domain extends DomainBase implements IDomain { } } + if (elasticsearchVersionNum < 6.7) { + if (advancedSecurityEnabled) { + throw new Error('Fine-grained access logging requires Elasticsearch version 6.7 or later.'); + } + } + // Validate against instance type restrictions, per // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html if (isInstanceType('i3') && ebsEnabled) { @@ -1232,6 +1324,20 @@ export class Domain extends DomainBase implements IDomain { throw new Error('EBS volumes are required for all instance types except R3 and I3.'); } + // Fine-grained access control requires node-to-node encryption, encryption at rest, + // and enforced HTTPS. + if (advancedSecurityEnabled) { + if (!nodeToNodeEncryptionEnabled) { + throw new Error('Node-to-node encryption is required when fine-grained access control is enabled.'); + } + if (!encryptionAtRestEnabled) { + throw new Error('Encryption-at-rest is required when fine-grained access control is enabled.'); + } + if (!enforceHttps) { + throw new Error('Enforce HTTPS is required when fine-grained access control is enabled.'); + } + } + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; if (props.vpcOptions) { cfnVpcOptions = { @@ -1293,15 +1399,18 @@ export class Domain extends DomainBase implements IDomain { elasticsearchVersion, elasticsearchClusterConfig: { dedicatedMasterEnabled, - dedicatedMasterCount: dedicatedMasterEnabled ? dedicatedMasterCount : undefined, - dedicatedMasterType: dedicatedMasterEnabled ? dedicatedMasterType : undefined, + dedicatedMasterCount: dedicatedMasterEnabled + ? dedicatedMasterCount + : undefined, + dedicatedMasterType: dedicatedMasterEnabled + ? dedicatedMasterType + : undefined, instanceCount, instanceType, zoneAwarenessEnabled, - zoneAwarenessConfig: - zoneAwarenessEnabled - ? { availabilityZoneCount } - : undefined, + zoneAwarenessConfig: zoneAwarenessEnabled + ? { availabilityZoneCount } + : undefined, }, ebsOptions: { ebsEnabled, @@ -1311,9 +1420,11 @@ export class Domain extends DomainBase implements IDomain { }, encryptionAtRestOptions: { enabled: encryptionAtRestEnabled, - kmsKeyId: encryptionAtRestEnabled ? props.encryptionAtRest?.kmsKey?.keyId : undefined, + kmsKeyId: encryptionAtRestEnabled + ? props.encryptionAtRest?.kmsKey?.keyId + : undefined, }, - nodeToNodeEncryptionOptions: { enabled: props.nodeToNodeEncryption ?? false }, + nodeToNodeEncryptionOptions: { enabled: nodeToNodeEncryptionEnabled }, logPublishingOptions: { ES_APPLICATION_LOGS: { enabled: this.appLogGroup != null, @@ -1338,6 +1449,21 @@ export class Domain extends DomainBase implements IDomain { snapshotOptions: props.automatedSnapshotStartHour ? { automatedSnapshotStartHour: props.automatedSnapshotStartHour } : undefined, + domainEndpointOptions: { + enforceHttps, + tlsSecurityPolicy: props.tlsSecurityPolicy ?? TLSSecurityPolicy.TLS_1_0, + }, + advancedSecurityOptions: advancedSecurityEnabled + ? { + enabled: true, + internalUserDatabaseEnabled, + masterUserOptions: { + masterUserArn: masterUserArn, + masterUserName: masterUserName, + masterUserPassword: masterUserPassword, + }, + } + : undefined, }); if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 003e67cd95dc3..69947f1186589 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -83,6 +83,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "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.0.4" @@ -94,6 +95,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "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.0.4" diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 133e66803400b..ee45f4597cb2d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; 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 { App, Stack, Duration } from '@aws-cdk/core'; +import { App, Stack, Duration, SecretValue } from '@aws-cdk/core'; import { Domain, ElasticsearchVersion } from '../lib'; let app: App; @@ -424,6 +424,153 @@ describe('import', () => { }); +describe('advanced security options', () => { + const masterUserArn = 'arn:aws:iam::123456789012:user/JohnDoe'; + const masterUserName = 'JohnDoe'; + const masterUserPasswordSecret = SecretValue.plainText('password'); + + test('enable fine-grained access logging with a master user ARN', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: false, + MasterUserOptions: { + MasterUserARN: masterUserArn, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('enable fine-grained access logging with a master user name and password', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + fineGrainedAccessControl: { + masterUserName, + masterUserPasswordSecret, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'Domain15DC21D9', + }, + ':SecretString:password::}}', + ], + ], + }, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + GenerateSecretString: { + GenerateStringKey: 'password', + }, + }); + }); + + test('enabling fine-grained access logging throws with Elasticsearch < 6.7', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V6_5, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + })).toThrow(/Fine-grained access logging requires Elasticsearch version 6\.7 or later/); + }); + + test('enabling fine-grained access logging throws without node-to-node encryption enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: false, + enforceHttps: true, + })).toThrow(/Node-to-node encryption is required when fine-grained access control is enabled/); + }); + + test('enabling fine-grained access logging throws without encryption-at-rest enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: false, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + })).toThrow(/Encryption-at-rest is required when fine-grained access control is enabled/); + }); + + test('enabling fine-grained access logging throws without enforceHttps enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: false, + })).toThrow(/Enforce HTTPS is required when fine-grained access control is enabled/); + }); +}); + describe('custom error responses', () => { test('error when availabilityZoneCount does not match vpcOptions.subnets length', () => { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json new file mode 100644 index 0000000000000..5a5275e4c52a9 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json @@ -0,0 +1,60 @@ +{ + "Resources": { + "User00B015A1": { + "Type": "AWS::IAM::User" + }, + "Domain66AC69E0": { + "Type": "AWS::Elasticsearch::Domain", + "Properties": { + "AdvancedSecurityOptions": { + "Enabled": true, + "InternalUserDatabaseEnabled": false, + "MasterUserOptions": { + "MasterUserARN": { + "Fn::GetAtt": [ + "User00B015A1", + "Arn" + ] + } + } + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": true, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10, + "VolumeType": "gp2" + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterEnabled": false, + "InstanceCount": 1, + "InstanceType": "r5.large.elasticsearch", + "ZoneAwarenessEnabled": false + }, + "ElasticsearchVersion": "7.1", + "EncryptionAtRestOptions": { + "Enabled": true + }, + "LogPublishingOptions": { + "ES_APPLICATION_LOGS": { + "Enabled": false + }, + "SEARCH_SLOW_LOGS": { + "Enabled": false + }, + "INDEX_SLOW_LOGS": { + "Enabled": false + } + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.ts new file mode 100644 index 0000000000000..e31fc1ecec6f4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.ts @@ -0,0 +1,27 @@ +import { User } from '@aws-cdk/aws-iam'; +import { App, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as es from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const user = new User(this, 'User'); + + new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + fineGrainedAccessControl: { + masterUserArn: user.userArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-elasticsearch-advancedsecurity'); +app.synth(); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index aff1205f2e633..8f748ced758b4 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -91,6 +91,9 @@ "CognitoOptions": { "Enabled": false }, + "DomainEndpointOptions": { + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, "EBSOptions": { "EBSEnabled": true, "VolumeSize": 10, From d24d53380abd15069148ed20b9886544f36ed3a1 Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Wed, 26 Aug 2020 08:17:12 -0500 Subject: [PATCH 35/45] fix build --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 486ff5138b315..912f5a0b3a9ac 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1233,19 +1233,19 @@ export class Domain extends DomainBase implements IDomain { const advancedSecurityEnabled = (masterUserArn ?? masterUserName) != null; const internalUserDatabaseEnabled = masterUserName != null; - const masterUserPasswordString = - props.fineGrainedAccessControl?.masterUserPasswordSecret?.toString() - function createMasterUserPasswordSecret(): string { - new secretsmanager.Secret(this, id, { - generateSecretString: { - secretStringTemplate: JSON.stringify({ - username: masterUserName, - }), - generateStringKey: 'password', - }, - }) - .secretValueFromJson('password') - .toString() + const masterUserPasswordString = props.fineGrainedAccessControl?.masterUserPasswordSecret?.toString(); + const createMasterUserPasswordSecret = (): string => { + return new secretsmanager.Secret(this, id, { + generateSecretString: { + secretStringTemplate: JSON.stringify({ + username: masterUserName, + }), + generateStringKey: 'password', + }, + }) + .secretValueFromJson('password') + .toString(); + }; const masterUserPassword = masterUserPasswordString ?? internalUserDatabaseEnabled ? createMasterUserPasswordSecret() : undefined; From d99d2d3582c6cafda0983f6645f3e0cdd40c42ff Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Thu, 27 Aug 2020 04:46:11 -0500 Subject: [PATCH 36/45] create a PolicyDocument, not a Policy --- .../lib/log-group-resource-policy.ts | 4 ++-- .../test/integ.elasticsearch.expected.json | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts index 1d9dcc5de03ce..ae76c84619fcd 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts @@ -21,7 +21,7 @@ export interface LogGroupResourcePolicyProps { */ export class LogGroupResourcePolicy extends cr.AwsCustomResource { constructor(scope: cdk.Construct, id: string, props: LogGroupResourcePolicyProps) { - const policy = new iam.Policy(scope, props.policyName, { + const policyDocument = new iam.PolicyDocument({ statements: props.policyStatements, }); @@ -32,7 +32,7 @@ export class LogGroupResourcePolicy extends cr.AwsCustomResource { action: 'putResourcePolicy', parameters: { policyName: props.policyName, - policyDocument: JSON.stringify(policy.document), + policyDocument: JSON.stringify(policyDocument), }, physicalResourceId: cr.PhysicalResourceId.of(id), }, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 8f748ced758b4..5faa6b3714e2d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -80,7 +80,8 @@ "policyName": "ESLogPolicy" }, "ignoreErrorCodesMatching": "400" - } + }, + "InstallLatestAwsSdk": true }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -216,7 +217,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3Bucket67234880" + "Ref": "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3BucketA250C084" }, "S3Key": { "Fn::Join": [ @@ -229,7 +230,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96" + "Ref": "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3VersionKeyDC4F0CD7" } ] } @@ -242,7 +243,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96" + "Ref": "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3VersionKeyDC4F0CD7" } ] } @@ -269,17 +270,17 @@ } }, "Parameters": { - "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3Bucket67234880": { + "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3BucketA250C084": { "Type": "String", - "Description": "S3 bucket for asset \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" + "Description": "S3 bucket for asset \"d731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557\"" }, - "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96": { + "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3VersionKeyDC4F0CD7": { "Type": "String", - "Description": "S3 key for asset version \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" + "Description": "S3 key for asset version \"d731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557\"" }, - "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061ArtifactHash9212BF97": { + "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557ArtifactHash5701DE73": { "Type": "String", - "Description": "Artifact hash for asset \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" + "Description": "Artifact hash for asset \"d731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557\"" } } } \ No newline at end of file From daa2b47b81c89d804061920b16d51e39e157a7d8 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Mon, 31 Aug 2020 14:18:49 +1000 Subject: [PATCH 37/45] Updates integration test references --- .../test/integ.elasticsearch.expected.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 5faa6b3714e2d..b20da010c0e52 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -217,7 +217,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3BucketA250C084" + "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3Bucket1EBE4391" }, "S3Key": { "Fn::Join": [ @@ -230,7 +230,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3VersionKeyDC4F0CD7" + "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8" } ] } @@ -243,7 +243,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3VersionKeyDC4F0CD7" + "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8" } ] } @@ -270,17 +270,17 @@ } }, "Parameters": { - "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3BucketA250C084": { + "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3Bucket1EBE4391": { "Type": "String", - "Description": "S3 bucket for asset \"d731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557\"" + "Description": "S3 bucket for asset \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" }, - "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557S3VersionKeyDC4F0CD7": { + "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8": { "Type": "String", - "Description": "S3 key for asset version \"d731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557\"" + "Description": "S3 key for asset version \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" }, - "AssetParametersd731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557ArtifactHash5701DE73": { + "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feArtifactHashC36CC496": { "Type": "String", - "Description": "Artifact hash for asset \"d731b1475f16a318a48a76c83d255f7422cfa5f025c5bff93537b8f0b8e94557\"" + "Description": "Artifact hash for asset \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" } } -} \ No newline at end of file +} From ec6dcefa29a4d13ba6614a4df40f096ed1286425 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Tue, 1 Sep 2020 22:21:56 +1000 Subject: [PATCH 38/45] Update packages/@aws-cdk/aws-elasticsearch/lib/domain.ts Co-authored-by: Eli Polonsky --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 912f5a0b3a9ac..8423fb3d435f3 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1321,7 +1321,7 @@ export class Domain extends DomainBase implements IDomain { // Only R3 and I3 support instance storage, per // https://aws.amazon.com/elasticsearch-service/pricing/ if (!ebsEnabled && !isEveryInstanceType('r3', 'i3')) { - throw new Error('EBS volumes are required for all instance types except R3 and I3.'); + throw new Error('EBS volumes are required when using instance types other than r3 or i3.'); } // Fine-grained access control requires node-to-node encryption, encryption at rest, From bd41e227634b1dc5486f28095e0cb912115bbba5 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Tue, 1 Sep 2020 22:26:06 +1000 Subject: [PATCH 39/45] Addresses PR feedback --- packages/@aws-cdk/aws-elasticsearch/README.md | 33 +++++-- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 90 ++++++++++--------- .../aws-elasticsearch/test/domain.test.ts | 72 ++++++++++++--- 3 files changed, 134 insertions(+), 61 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 8c25952be2bef..e40fa009a1d3e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -15,19 +15,19 @@ --- -To create an Elasticsearch domain: +Create a development cluster by simply specifying the version: ```ts import * as es from '@aws-cdk/aws-elasticsearch'; const devDomain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, - logging: { - slowSearchLogEnabled: true, - appLogEnabled: true - }, }); +``` + +Create a production grade cluster by also specifying things like capacity and az distribution +```ts const prodDomain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, capacity: { @@ -43,7 +43,7 @@ const prodDomain = new es.Domain(this, 'Domain', { logging: { slowSearchLogEnabled: true, appLogEnabled: true, - slowIndexLogEnabled: true + slowIndexLogEnabled: true, }, }); ``` @@ -109,3 +109,24 @@ const masterSysMemoryUtilization = domain.metric('MasterSysMemoryUtilization'); ``` This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +### Fine grained access control + +The domain can also be created with a master user configured. The password can +be supplied or dynamically created if not supplied. + +```ts +const domain = new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + enforceHttps: true, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + fineGrainedAccessControl: { + masterUserName: 'master-user', + }, +}); + +const masterUserPassword = domain.masterUserPassword; +``` diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 8423fb3d435f3..c7d13ca999591 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -351,7 +351,7 @@ export interface AdvancedSecurityOptions { * * @default - A Secrets Manager generated password */ - readonly masterUserPasswordSecret?: cdk.SecretValue; + readonly masterUserPassword?: cdk.SecretValue; } /** @@ -388,7 +388,10 @@ export interface DomainProps { /** * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that - * are attached to data nodes in the Amazon ES domain. + * are attached to data nodes in the Amazon ES domain. For more information, see + * [Configuring EBS-based Storage] + * (https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-ebs) + * in the Amazon Elasticsearch Service Developer Guide. * * @default - 10 GiB General Purpose (SSD) volumes per node. */ @@ -410,11 +413,6 @@ export interface DomainProps { /** * The Elasticsearch version that your domain will leverage. - * - * Per https://aws.amazon.com/elasticsearch-service/faqs/, Amazon Elasticsearch Service - * currently supports Elasticsearch versions 7.7, 7.4, 7.1, 6.8, 6.7, 6.5, 6.4, 6.3, 6.2, - * 6.0, 5.6, 5.5, 5.3, 5.1, 2.3, and 1.5. - * */ readonly version: ElasticsearchVersion; @@ -425,7 +423,6 @@ export interface DomainProps { */ readonly encryptionAtRest?: EncryptionAtRestOptions; - /** * Configuration log publishing configuration options. * @@ -433,7 +430,6 @@ export interface DomainProps { */ readonly logging?: LoggingOptions; - /** * Specify true to enable node to node encryption. * Requires Elasticsearch version 6.0 or later. @@ -511,27 +507,6 @@ export interface IDomain extends cdk.IResource { */ readonly domainEndpoint: string; - /** - * Log group that slow searches are logged to. - * - * @attribute - */ - readonly slowSearchLogGroup?: logs.ILogGroup; - - /** - * Log group that slow indices are logged to. - * - * @attribute - */ - readonly slowIndexLogGroup?: logs.ILogGroup; - - /** - * Log group that application logs are logged to. - * - * @attribute - */ - readonly appLogGroup?: logs.ILogGroup; - /** * Grant read permissions for this domain and its contents to an IAM * principal (Role/Group/User). @@ -1149,10 +1124,33 @@ export class Domain extends DomainBase implements IDomain { public readonly domainArn: string; public readonly domainName: string; public readonly domainEndpoint: string; + + /** + * Log group that slow searches are logged to. + * + * @attribute + */ public readonly slowSearchLogGroup?: logs.ILogGroup; + + /** + * Log group that slow indices are logged to. + * + * @attribute + */ public readonly slowIndexLogGroup?: logs.ILogGroup; + + /** + * Log group that application logs are logged to. + * + * @attribute + */ public readonly appLogGroup?: logs.ILogGroup; + /** + * Master user password if fine grained access control is configured. + */ + public readonly masterUserPassword?: cdk.SecretValue; + private readonly domain: CfnDomain; @@ -1161,11 +1159,6 @@ export class Domain extends DomainBase implements IDomain { physicalName: props.domainName, }); - // If VPC options are supplied ensure that the number of subnets matches the number AZ - if (props.vpcOptions?.subnets.map((subnet) => subnet.availabilityZone).length != props?.zoneAwareness?.availabilityZoneCount) { - throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); - }; - const defaultInstanceType = 'r5.large.elasticsearch'; const dedicatedMasterType = @@ -1190,6 +1183,12 @@ export class Domain extends DomainBase implements IDomain { props.zoneAwareness?.enabled ?? props.zoneAwareness?.availabilityZoneCount != null; + // 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) { + throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); + }; + if ([dedicatedMasterType, instanceType].some(t => !t.endsWith('.elasticsearch'))) { throw new Error('Master and data node instance types must end with ".elasticsearch".'); } @@ -1231,11 +1230,15 @@ export class Domain extends DomainBase implements IDomain { const masterUserArn = props.fineGrainedAccessControl?.masterUserArn; const masterUserName = props.fineGrainedAccessControl?.masterUserName; + if (masterUserArn != null && masterUserName != null) { + throw new Error('Invalid fine grained access control settings. Only provide one of master user ARN or master user name. Not both.'); + } + const advancedSecurityEnabled = (masterUserArn ?? masterUserName) != null; const internalUserDatabaseEnabled = masterUserName != null; - const masterUserPasswordString = props.fineGrainedAccessControl?.masterUserPasswordSecret?.toString(); - const createMasterUserPasswordSecret = (): string => { - return new secretsmanager.Secret(this, id, { + const masterUserPasswordProp = props.fineGrainedAccessControl?.masterUserPassword; + const createMasterUserPassword = (): cdk.SecretValue => { + return new secretsmanager.Secret(this, `${id}Password`, { generateSecretString: { secretStringTemplate: JSON.stringify({ username: masterUserName, @@ -1243,12 +1246,11 @@ export class Domain extends DomainBase implements IDomain { generateStringKey: 'password', }, }) - .secretValueFromJson('password') - .toString(); + .secretValueFromJson('password'); }; - const masterUserPassword = - masterUserPasswordString ?? - internalUserDatabaseEnabled ? createMasterUserPasswordSecret() : undefined; + this.masterUserPassword = internalUserDatabaseEnabled ? + (masterUserPasswordProp ?? createMasterUserPassword()) + : undefined; const encryptionAtRestEnabled = props.encryptionAtRest?.enabled ?? props.encryptionAtRest?.kmsKey != null; @@ -1460,7 +1462,7 @@ export class Domain extends DomainBase implements IDomain { masterUserOptions: { masterUserArn: masterUserArn, masterUserName: masterUserName, - masterUserPassword: masterUserPassword, + masterUserPassword: this.masterUserPassword?.toString(), }, } : undefined, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index ee45f4597cb2d..0f3c3d11b55af 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -422,14 +422,30 @@ describe('import', () => { expect(stack).not.toHaveResource('AWS::Elasticsearch::Domain'); }); + test('static fromDomainAttributes(attributes) allows importing an external/existing domain', () => { + const domainName = 'test-domain-2w2x2u3tifly'; + const domainArn = `es:testregion:1234:domain/${domainName}`; + const domainEndpoint = `https://${domainName}-jcjotrt6f7otem4sqcwbch3c4u.testregion.es.amazonaws.com`; + const imported = Domain.fromDomainAttributes(stack, 'Domain', { + domainArn, + domainEndpoint, + }); + + expect(imported.domainName).toEqual(domainName); + expect(imported.domainArn).toEqual(domainArn); + + expect(stack).not.toHaveResource('AWS::Elasticsearch::Domain'); + }); + }); describe('advanced security options', () => { const masterUserArn = 'arn:aws:iam::123456789012:user/JohnDoe'; const masterUserName = 'JohnDoe'; - const masterUserPasswordSecret = SecretValue.plainText('password'); + const password = 'password'; + const masterUserPassword = SecretValue.plainText(password); - test('enable fine-grained access logging with a master user ARN', () => { + test('enable fine-grained access control with a master user ARN', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_1, fineGrainedAccessControl: { @@ -462,12 +478,46 @@ describe('advanced security options', () => { }); }); - test('enable fine-grained access logging with a master user name and password', () => { + test('enable fine-grained access control with a master user name and password', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + fineGrainedAccessControl: { + masterUserName, + masterUserPassword, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: password, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('enable fine-grained access control with a master user name and dynamically generated password', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_1, fineGrainedAccessControl: { masterUserName, - masterUserPasswordSecret, }, encryptionAtRest: { enabled: true, @@ -488,7 +538,7 @@ describe('advanced security options', () => { [ '{{resolve:secretsmanager:', { - Ref: 'Domain15DC21D9', + Ref: 'DomainDomainPassword65DAD325', }, ':SecretString:password::}}', ], @@ -514,7 +564,7 @@ describe('advanced security options', () => { }); }); - test('enabling fine-grained access logging throws with Elasticsearch < 6.7', () => { + test('enabling fine-grained access control throws with Elasticsearch < 6.7', () => { expect(() => new Domain(stack, 'Domain', { version: ElasticsearchVersion.V6_5, fineGrainedAccessControl: { @@ -525,10 +575,10 @@ describe('advanced security options', () => { }, nodeToNodeEncryption: true, enforceHttps: true, - })).toThrow(/Fine-grained access logging requires Elasticsearch version 6\.7 or later/); + })).toThrow(/Fine-grained access control requires Elasticsearch version 6\.7 or later/); }); - test('enabling fine-grained access logging throws without node-to-node encryption enabled', () => { + test('enabling fine-grained access control throws without node-to-node encryption enabled', () => { expect(() => new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_7, fineGrainedAccessControl: { @@ -542,7 +592,7 @@ describe('advanced security options', () => { })).toThrow(/Node-to-node encryption is required when fine-grained access control is enabled/); }); - test('enabling fine-grained access logging throws without encryption-at-rest enabled', () => { + test('enabling fine-grained access control throws without encryption-at-rest enabled', () => { expect(() => new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_7, fineGrainedAccessControl: { @@ -556,7 +606,7 @@ describe('advanced security options', () => { })).toThrow(/Encryption-at-rest is required when fine-grained access control is enabled/); }); - test('enabling fine-grained access logging throws without enforceHttps enabled', () => { + test('enabling fine-grained access control throws without enforceHttps enabled', () => { expect(() => new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_7, fineGrainedAccessControl: { @@ -757,7 +807,7 @@ describe('custom error responses', () => { capacity: { masterNodeInstanceType: 'm5.large.elasticsearch', }, - })).toThrow(/EBS volumes are required for all instance types except R3 and I3/); + })).toThrow(/EBS volumes are required when using instance types other than r3 or i3/); }); test('error when availabilityZoneCount is not 2 or 3', () => { From e7ad0613e82664b7d3565ef8da39058c9f36996b Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Sat, 19 Sep 2020 10:38:44 +1000 Subject: [PATCH 40/45] Enable unsigned basic auth --- packages/@aws-cdk/aws-elasticsearch/README.md | 28 ++++ .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 113 ++++++++++---- .../aws-elasticsearch/test/domain.test.ts | 146 ++++++++++++++++++ .../test/integ.elasticsearch.expected.json | 32 ++-- 4 files changed, 274 insertions(+), 45 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index e40fa009a1d3e..9aa5d4baa40ef 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -130,3 +130,31 @@ const domain = new es.Domain(this, 'Domain', { const masterUserPassword = domain.masterUserPassword; ``` + +### Using unsigned basic auth + +For convenience, the domain can be configured to allow unsigned HTTP requests +that use basic auth. Unless the domain is configured to be part of a VPC this +means anyone can access the domain using the configured master username and +password. + +To enable unsigned basic auth access the domain is configured with an access +policy that allows anyonmous requests, HTTPS required, node to node encryption, +encryption at rest and fine grained access control. + +If the above settings are not set they will be configured as part of enabling +unsigned basic auth. + +If no master user is configured a default master user is created with the +username `admin`. + +```ts +const domain = new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + useUnsignedBasicAuth: true, +}); + +const masterUserPassword = domain.masterUserPassword; +``` + +``` diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index c7d13ca999591..74fa93e9a0a79 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -480,6 +480,20 @@ export interface DomainProps { * @default - fine-grained access control is disabled */ readonly fineGrainedAccessControl?: AdvancedSecurityOptions; + + /** + * Configures the domain so that unsigned basic auth is enabled. If no master user is provided a default master user + * with username `admin` and a dynamically generated password stored in KMS is created. The password can be retrieved + * by getting `masterUserPassword` from the domain instance. + * + * Setting this to true will also add an access policy that allows unsigned + * access, enable node to node encryption, encryption at rest. If conflicting + * settings are encountered (like disabling encryption at rest) enabling this + * setting will cause a failure. + * + * @default - false + */ + readonly useUnsignedBasicAuth?: boolean; } /** @@ -859,8 +873,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { } /** - - * Metric for the time the cluster status is red. + * Metric for the time the cluster status is red. * * @default maximum over 5 minutes */ @@ -1194,27 +1207,7 @@ export class Domain extends DomainBase implements IDomain { } const elasticsearchVersion = props.version.version; - const elasticsearchVersionNum = parseVersion(elasticsearchVersion); - - function parseVersion(version: string): number { - const firstDot = version.indexOf('.'); - - if (firstDot < 1) { - throw new Error(`Invalid Elasticsearch version: ${version}. Version string needs to start with major and minor version (x.y).`); - } - - const secondDot = version.indexOf('.', firstDot + 1); - - try { - if (secondDot == -1) { - return parseFloat(version); - } else { - return parseFloat(version.substring(0, secondDot)); - } - } catch (error) { - throw new Error(`Invalid Elasticsearch version: ${version}. Version string needs to start with major and minor version (x.y).`); - } - } + const elasticsearchVersionNum = parseVersion(props.version); if ( elasticsearchVersionNum <= 7.7 && @@ -1227,8 +1220,33 @@ export class Domain extends DomainBase implements IDomain { throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); } + const unsignedBasicAuthEnabled = props.useUnsignedBasicAuth ?? false; + + if (unsignedBasicAuthEnabled) { + if (props.enforceHttps == false) { + throw new Error('You cannot disable HTTPS and use unsigned basic auth'); + } + if (props.nodeToNodeEncryption == false) { + throw new Error('You cannot disable node to node encryption and use unsigned basic auth'); + } + if (props.encryptionAtRest?.enabled == false) { + throw new Error('You cannot disable encryption at rest and use unsigned basic auth'); + } + } + + const unsignedAccessPolicy = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.Anyone()], + resources: [cdk.Lazy.stringValue({ produce: () => `${this.domainArn}/*` })], + }); + const masterUserArn = props.fineGrainedAccessControl?.masterUserArn; - const masterUserName = props.fineGrainedAccessControl?.masterUserName; + const masterUserNameProps = props.fineGrainedAccessControl?.masterUserName; + // If basic auth is enabled set the user name to admin if no other user info is supplied. + const masterUserName = unsignedBasicAuthEnabled + ? (masterUserArn == null ? (masterUserNameProps ?? 'admin') : undefined) + : masterUserNameProps; if (masterUserArn != null && masterUserName != null) { throw new Error('Invalid fine grained access control settings. Only provide one of master user ARN or master user name. Not both.'); @@ -1253,12 +1271,12 @@ export class Domain extends DomainBase implements IDomain { : undefined; const encryptionAtRestEnabled = - props.encryptionAtRest?.enabled ?? props.encryptionAtRest?.kmsKey != null; - const nodeToNodeEncryptionEnabled = props.nodeToNodeEncryption ?? false; + props.encryptionAtRest?.enabled ?? (props.encryptionAtRest?.kmsKey != null || unsignedBasicAuthEnabled); + const nodeToNodeEncryptionEnabled = props.nodeToNodeEncryption ?? unsignedBasicAuthEnabled; const volumeSize = props.ebs?.volumeSize ?? 10; const volumeType = props.ebs?.volumeType ?? ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; const ebsEnabled = props.ebs?.enabled ?? true; - const enforceHttps = props.enforceHttps; + const enforceHttps = props.enforceHttps ?? unsignedBasicAuthEnabled; function isInstanceType(t: string): Boolean { return dedicatedMasterType.startsWith(t) || instanceType.startsWith(t); @@ -1301,8 +1319,11 @@ export class Domain extends DomainBase implements IDomain { } if (elasticsearchVersionNum < 6.7) { + if (unsignedBasicAuthEnabled) { + throw new Error('Using unsigned basic auth requires Elasticsearch version 6.7 or later.'); + } if (advancedSecurityEnabled) { - throw new Error('Fine-grained access logging requires Elasticsearch version 6.7 or later.'); + throw new Error('Fine-grained access control requires Elasticsearch version 6.7 or later.'); } } @@ -1398,6 +1419,9 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, + accessPolicies: unsignedBasicAuthEnabled + ? (props.accessPolicies ?? []).concat(unsignedAccessPolicy) + : props.accessPolicies, elasticsearchVersion, elasticsearchClusterConfig: { dedicatedMasterEnabled, @@ -1472,14 +1496,15 @@ export class Domain extends DomainBase implements IDomain { if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + this.domainName = this.getResourceNameAttribute(this.domain.ref); + + this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); + this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { service: 'es', resource: 'domain', resourceName: this.physicalName, }); - this.domainName = this.getResourceNameAttribute(this.domain.ref); - - this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); } } @@ -1503,3 +1528,29 @@ function extractNameFromEndpoint(domainEndpoint: string) { const suffix = '-' + domain.split('-').slice(-1)[0]; return domain.split(suffix)[0]; } + +/** + * Converts an Elasticsearch version into a into a decimal number with major and minor version i.e x.y. + * + * @param version The Elasticsearch version object + */ +function parseVersion(version: ElasticsearchVersion): number { + const versionStr = version.version; + const firstDot = versionStr.indexOf('.'); + + if (firstDot < 1) { + throw new Error(`Invalid Elasticsearch version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); + } + + const secondDot = versionStr.indexOf('.', firstDot + 1); + + try { + if (secondDot == -1) { + return parseFloat(versionStr); + } else { + return parseFloat(versionStr.substring(0, secondDot)); + } + } catch (error) { + throw new Error(`Invalid Elasticsearch version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 0f3c3d11b55af..622d1784362d3 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -856,6 +856,152 @@ test('can specify future version', () => { }); }); +describe('unsigned basic auth', () => { + test('can create a domain with unsigned basic auth', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AccessPolicies: [{ + action: ['es:ESHttp*'], + principal: { + AWS: ['*'], + }, + resource: [{ + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'Arn', + ], + }, + '/*', + ], + ], + }], + effect: 'Allow', + }], + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: 'admin', + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('does not overwrite master user ARN configuration', () => { + const masterUserArn = 'arn:aws:iam::123456789012:user/JohnDoe'; + + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + fineGrainedAccessControl: { + masterUserArn, + }, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: false, + MasterUserOptions: { + MasterUserARN: masterUserArn, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('does not overwrite master user name and password', () => { + const masterUserName = 'JohnDoe'; + const password = 'password'; + const masterUserPassword = SecretValue.plainText(password); + + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + fineGrainedAccessControl: { + masterUserName, + masterUserPassword, + }, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: password, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('fails to create a domain with unsigned basic auth when enforce HTTPS is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + enforceHttps: false, + })).toThrow(/You cannot disable HTTPS and use unsigned basic auth/); + }); + + test('fails to create a domain with unsigned basic auth when node to node encryption is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + nodeToNodeEncryption: false, + })).toThrow(/You cannot disable node to node encryption and use unsigned basic auth/); + }); + + test('fails to create a domain with unsigned basic auth when encryption at rest is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + encryptionAtRest: { enabled: false }, + })).toThrow(/You cannot disable encryption at rest and use unsigned basic auth/); + }); + + test('using unsigned basic auth throws with Elasticsearch < 6.7', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V6_5, + useUnsignedBasicAuth: true, + })).toThrow(/Using unsigned basic auth requires Elasticsearch version 6\.7 or later./); + }); +}); + function testGrant( expectedActions: string[], diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index b20da010c0e52..1178733807627 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -84,7 +84,10 @@ "InstallLatestAwsSdk": true }, "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + "DeletionPolicy": "Delete", + "DependsOn": [ + "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41" + ] }, "Domain66AC69E0": { "Type": "AWS::Elasticsearch::Domain", @@ -93,6 +96,7 @@ "Enabled": false }, "DomainEndpointOptions": { + "EnforceHTTPS": false, "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" }, "EBSOptions": { @@ -138,6 +142,7 @@ } }, "DependsOn": [ + "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41", "DomainESLogGroupPolicy5373A2E8" ] }, @@ -188,7 +193,7 @@ ] } }, - "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E": { + "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -204,7 +209,7 @@ ], "Version": "2012-10-17" }, - "PolicyName": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "PolicyName": "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" @@ -217,7 +222,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3Bucket1EBE4391" + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" }, "S3Key": { "Fn::Join": [ @@ -230,7 +235,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8" + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" } ] } @@ -243,7 +248,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8" + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" } ] } @@ -264,23 +269,22 @@ "Timeout": 120 }, "DependsOn": [ - "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" ] } }, "Parameters": { - "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3Bucket1EBE4391": { - "Type": "String", - "Description": "S3 bucket for asset \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "Type":"String", + "Description":"Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" }, - "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8": { + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { "Type": "String", - "Description": "S3 key for asset version \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" + "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" }, - "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feArtifactHashC36CC496": { + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { "Type": "String", - "Description": "Artifact hash for asset \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" + "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" } } } From 7119981598d947bbeb1ba1765c6c6106c71cb919 Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Wed, 30 Sep 2020 15:39:52 +1000 Subject: [PATCH 41/45] Use fromSdkCalls for log-group-resources-policy --- .../aws-elasticsearch/lib/log-group-resource-policy.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts index ae76c84619fcd..949f03aa61baa 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts @@ -44,13 +44,7 @@ export class LogGroupResourcePolicy extends cr.AwsCustomResource { }, ignoreErrorCodesMatching: '400', }, - policy: cr.AwsCustomResourcePolicy.fromStatements([ - new iam.PolicyStatement({ - actions: ['logs:PutResourcePolicy', 'logs:DeleteResourcePolicy'], - // Resource Policies are global in Cloudwatch Logs per-region, per-account. - resources: ['*'], - }), - ]), + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ resources: ['*'] }), }); } } From 7505d7f5f3525e623e62ad1a67d9a5038fdf43bd Mon Sep 17 00:00:00 2001 From: Stephan Hoermann Date: Wed, 30 Sep 2020 18:02:21 +1000 Subject: [PATCH 42/45] Address PR feedback * Use custom resource to updated domain access policy after the domain creation * Updated readme * Added integration test for basic unsigned auth --- packages/@aws-cdk/aws-elasticsearch/README.md | 7 +- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 27 +- .../lib/elasticsearch-access-policy.ts | 50 +++ .../aws-elasticsearch/test/domain.test.ts | 23 +- .../test/elasticsearch-access-policy.test.ts | 61 ++++ .../test/integ.elasticsearch.expected.json | 10 +- ...sticsearch.unsignedbasicauth.expected.json | 296 ++++++++++++++++++ .../integ.elasticsearch.unsignedbasicauth.ts | 17 + 8 files changed, 457 insertions(+), 34 deletions(-) create mode 100644 packages/@aws-cdk/aws-elasticsearch/lib/elasticsearch-access-policy.ts create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json create mode 100644 packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.ts diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 9aa5d4baa40ef..1fb6769935081 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -143,11 +143,16 @@ policy that allows anyonmous requests, HTTPS required, node to node encryption, encryption at rest and fine grained access control. If the above settings are not set they will be configured as part of enabling -unsigned basic auth. +unsigned basic auth. If they are set with conflicting values, an error will be +thrown. If no master user is configured a default master user is created with the username `admin`. +If no password is configured a default master user password is created and +stored in the AWS Secrets Manager as secret. The secret has the prefix +`MasterUser`. + ```ts const domain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 74fa93e9a0a79..90c68db50383e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -7,7 +7,9 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { ElasticsearchAccessPolicy } from './elasticsearch-access-policy'; import { CfnDomain } from './elasticsearch.generated'; import { LogGroupResourcePolicy } from './log-group-resource-policy'; import * as perms from './perms'; @@ -1096,7 +1098,7 @@ export class Domain extends DomainBase implements IDomain { * @param domainEndpoint The domain's endpoint. */ public static fromDomainEndpoint( - scope: cdk.Construct, + scope: Construct, id: string, domainEndpoint: string, ): IDomain { @@ -1121,7 +1123,7 @@ export class Domain extends DomainBase implements IDomain { * @param id The construct's name. * @param attrs A `DomainAttributes` object. */ - public static fromDomainAttributes(scope: cdk.Construct, id: string, attrs: DomainAttributes): IDomain { + public static fromDomainAttributes(scope: Construct, id: string, attrs: DomainAttributes): IDomain { const { domainArn, domainEndpoint } = attrs; const domainName = extractNameFromEndpoint(domainEndpoint); @@ -1167,7 +1169,7 @@ export class Domain extends DomainBase implements IDomain { private readonly domain: CfnDomain; - constructor(scope: cdk.Construct, id: string, props: DomainProps) { + constructor(scope: Construct, id: string, props: DomainProps) { super(scope, id, { physicalName: props.domainName, }); @@ -1256,7 +1258,7 @@ export class Domain extends DomainBase implements IDomain { const internalUserDatabaseEnabled = masterUserName != null; const masterUserPasswordProp = props.fineGrainedAccessControl?.masterUserPassword; const createMasterUserPassword = (): cdk.SecretValue => { - return new secretsmanager.Secret(this, `${id}Password`, { + return new secretsmanager.Secret(this, 'MasterUser', { generateSecretString: { secretStringTemplate: JSON.stringify({ username: masterUserName, @@ -1419,9 +1421,6 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, - accessPolicies: unsignedBasicAuthEnabled - ? (props.accessPolicies ?? []).concat(unsignedAccessPolicy) - : props.accessPolicies, elasticsearchVersion, elasticsearchClusterConfig: { dedicatedMasterEnabled, @@ -1505,6 +1504,20 @@ export class Domain extends DomainBase implements IDomain { resource: 'domain', resourceName: this.physicalName, }); + + const accessPolicyStatements: iam.PolicyStatement[] | undefined = unsignedBasicAuthEnabled + ? (props.accessPolicies ?? []).concat(unsignedAccessPolicy) + : props.accessPolicies; + + if (accessPolicyStatements != null) { + const accessPolicy = new ElasticsearchAccessPolicy(this, 'ESAccessPolicy', { + domainName: this.domainName, + domainArn: this.domainArn, + accessPolicies: accessPolicyStatements, + }); + + accessPolicy.node.addDependency(this.domain); + } } } diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/elasticsearch-access-policy.ts b/packages/@aws-cdk/aws-elasticsearch/lib/elasticsearch-access-policy.ts new file mode 100644 index 0000000000000..78e0e4e8c5003 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/lib/elasticsearch-access-policy.ts @@ -0,0 +1,50 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as cr from '@aws-cdk/custom-resources'; + +/** + * Construction properties for ElasticsearchAccessPolicy + */ +export interface ElasticsearchAccessPolicyProps { + /** + * The Elasticsearch Domain name + */ + readonly domainName: string; + + /** + * The Elasticsearch Domain ARN + */ + readonly domainArn: string; + + /** + * The access policy statements for the Elasticsearch cluster + */ + readonly accessPolicies: iam.PolicyStatement[]; +} + +/** + * Creates LogGroup resource policies. + */ +export class ElasticsearchAccessPolicy extends cr.AwsCustomResource { + constructor(scope: cdk.Construct, id: string, props: ElasticsearchAccessPolicyProps) { + const policyDocument = new iam.PolicyDocument({ + statements: props.accessPolicies, + }); + + super(scope, id, { + resourceType: 'Custom::ElasticsearchAccessPolicy', + onUpdate: { + action: 'updateElasticsearchDomainConfig', + service: 'ES', + parameters: { + DomainName: props.domainName, + AccessPolicies: JSON.stringify(policyDocument.toJSON()), + }, + // this is needed to limit the response body, otherwise it exceeds the CFN 4k limit + outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', + physicalResourceId: cr.PhysicalResourceId.of(`${props.domainName}AccessPolicy`), + }, + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ resources: [props.domainArn] }), + }); + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 622d1784362d3..fc5472affaf6f 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -538,7 +538,7 @@ describe('advanced security options', () => { [ '{{resolve:secretsmanager:', { - Ref: 'DomainDomainPassword65DAD325', + Ref: 'DomainMasterUserBFAFA7D9', }, ':SecretString:password::}}', ], @@ -864,27 +864,6 @@ describe('unsigned basic auth', () => { }); expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { - AccessPolicies: [{ - action: ['es:ESHttp*'], - principal: { - AWS: ['*'], - }, - resource: [{ - 'Fn::Join': [ - '', - [ - { - 'Fn::GetAtt': [ - 'Domain66AC69E0', - 'Arn', - ], - }, - '/*', - ], - ], - }], - effect: 'Allow', - }], AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts new file mode 100644 index 0000000000000..2b815c16b2048 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts @@ -0,0 +1,61 @@ +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, Stack } from '@aws-cdk/core'; +import { ElasticsearchAccessPolicy } from '../lib/elasticsearch-access-policy'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('minimal example renders correctly', () => { + new ElasticsearchAccessPolicy(stack, 'ElasticsearchAccessPolicy', { + domainName: 'TestDomain', + domainArn: 'test:arn', + accessPolicies: [new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.Anyone()], + resources: ['test:arn'], + + })], + }); + + expect(stack).toHaveResource('Custom::ElasticsearchAccessPolicy', { + ServiceToken: { + 'Fn::GetAtt': [ + 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', + 'Arn', + ], + }, + Create: { + service: 'ES', + action: 'updateElasticsearchDomainConfig', + parameters: { + DomainName: 'TestDomain', + AccessPolicies: '{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"test:arn\"}],\"Version\":\"2012-10-17\"}', + }, + outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', + physicalResourceId: { + id: 'TestDomainAccessPolicy', + }, + }, + Update: { + service: 'ES', + action: 'updateElasticsearchDomainConfig', + parameters: { + DomainName: 'TestDomain', + AccessPolicies: '{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"test:arn\"}],\"Version\":\"2012-10-17\"}', + }, + outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', + physicalResourceId: { + id: 'TestDomainAccessPolicy', + }, + }, + }); +}); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 1178733807627..1956a13379bac 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -199,10 +199,12 @@ "PolicyDocument": { "Statement": [ { - "Action": [ - "logs:PutResourcePolicy", - "logs:DeleteResourcePolicy" - ], + "Action": "logs:PutResourcePolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:DeleteResourcePolicy", "Effect": "Allow", "Resource": "*" } diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json new file mode 100644 index 0000000000000..3624721f0a079 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json @@ -0,0 +1,296 @@ +{ + "Resources": { + "DomainMasterUserBFAFA7D9": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": { + "GenerateStringKey": "password", + "SecretStringTemplate": "{\"username\":\"admin\"}" + } + } + }, + "Domain66AC69E0": { + "Type": "AWS::Elasticsearch::Domain", + "Properties": { + "AdvancedSecurityOptions": { + "Enabled": true, + "InternalUserDatabaseEnabled": true, + "MasterUserOptions": { + "MasterUserName": "admin", + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "DomainMasterUserBFAFA7D9" + }, + ":SecretString:password::}}" + ] + ] + } + } + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": true, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10, + "VolumeType": "gp2" + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterEnabled": false, + "InstanceCount": 1, + "InstanceType": "r5.large.elasticsearch", + "ZoneAwarenessEnabled": false + }, + "ElasticsearchVersion": "7.1", + "EncryptionAtRestOptions": { + "Enabled": true + }, + "LogPublishingOptions": { + "ES_APPLICATION_LOGS": { + "Enabled": false + }, + "SEARCH_SLOW_LOGS": { + "Enabled": false + }, + "INDEX_SLOW_LOGS": { + "Enabled": false + } + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + } + }, + "DomainESAccessPolicyCustomResourcePolicy9747FC42": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:UpdateElasticsearchDomainConfig", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DomainESAccessPolicyCustomResourcePolicy9747FC42", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "Domain66AC69E0" + ] + }, + "DomainESAccessPolicy89986F33": { + "Type": "Custom::ElasticsearchAccessPolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "action": "updateElasticsearchDomainConfig", + "service": "ES", + "parameters": { + "DomainName": { + "Ref": "Domain66AC69E0" + }, + "AccessPolicies": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + }, + "/*\"}],\"Version\":\"2012-10-17\"}" + ] + ] + } + }, + "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", + "physicalResourceId": { + "id": { + "Fn::Join": [ + "", + [ + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy" + ] + ] + } + } + }, + "Update": { + "action": "updateElasticsearchDomainConfig", + "service": "ES", + "parameters": { + "DomainName": { + "Ref": "Domain66AC69E0" + }, + "AccessPolicies": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + }, + "/*\"}],\"Version\":\"2012-10-17\"}" + ] + ] + } + }, + "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", + "physicalResourceId": { + "id": { + "Fn::Join": [ + "", + [ + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy" + ] + ] + } + } + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "DomainESAccessPolicyCustomResourcePolicy9747FC42", + "Domain66AC69E0" + ], + "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\"" + } + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.ts new file mode 100644 index 0000000000000..4fb2e382f9537 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.ts @@ -0,0 +1,17 @@ +import { App, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as es from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + useUnsignedBasicAuth: true, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-elasticsearch-unsignedbasicauth'); +app.synth(); From ccf2edaa8f7205023746c79e35b35bc2312e39e0 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Wed, 30 Sep 2020 20:45:35 +0300 Subject: [PATCH 43/45] Remove empty code block --- packages/@aws-cdk/aws-elasticsearch/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 1fb6769935081..b7902ed654392 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -161,5 +161,3 @@ const domain = new es.Domain(this, 'Domain', { const masterUserPassword = domain.masterUserPassword; ``` - -``` From 26f2e8da120ab09883fb499771e7681264b6f8fd Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Wed, 30 Sep 2020 20:46:24 +0300 Subject: [PATCH 44/45] Exclude a few terminal non friendly characters in password generation rule --- packages/@aws-cdk/aws-elasticsearch/lib/domain.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 90c68db50383e..406c3c6dd0314 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1264,6 +1264,7 @@ export class Domain extends DomainBase implements IDomain { username: masterUserName, }), generateStringKey: 'password', + excludeCharacters: "{}'\\*[]()`", }, }) .secretValueFromJson('password'); From de0143a983499560698e7bea2e19c93930904bf3 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Wed, 30 Sep 2020 21:34:54 +0300 Subject: [PATCH 45/45] Update expectation with new password generation property --- .../test/integ.elasticsearch.unsignedbasicauth.expected.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json index 3624721f0a079..3791b985eddbe 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json @@ -5,6 +5,7 @@ "Properties": { "GenerateSecretString": { "GenerateStringKey": "password", + "ExcludeCharacters": "{}'\\*[]()`", "SecretStringTemplate": "{\"username\":\"admin\"}" } }