From e5bb801d32e073ba305c597dd37de535cfcb8796 Mon Sep 17 00:00:00 2001 From: Sarvesh Dubey Date: Wed, 15 Jan 2025 15:29:16 +0530 Subject: [PATCH] feat(opensearch): add node options configuration for coordinator nodes ### Reason for this change Introduced new interfaces and validation for configuring coordinator nodes in OpenSearch domains, allowing users to specify node options such as instance type and count. ### Description of changes - Added `NodeOptions` and `NodeConfig` interfaces to define configurations for coordinator nodes. - Updated the `Domain` class to handle `nodeOptions` in the capacity configuration, including validation for instance type and count. - Enhanced unit tests to cover new configurations and validation scenarios for coordinator nodes. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-opensearchservice/lib/domain.ts | 66 ++++++++++++- .../aws-opensearchservice/test/domain.test.ts | 93 ++++++++++++++++++- 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts b/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts index fe00ce2a39f6e..a8d9352d62fd4 100644 --- a/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts +++ b/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts @@ -82,6 +82,13 @@ export interface CapacityConfig { * is true, no multi-az with standby otherwise */ readonly multiAzWithStandbyEnabled?: boolean; + + /** + * Additional node options for the domain + * + * @default - no additional node options + */ + readonly nodeOptions?: NodeOptions[]; } /** @@ -380,7 +387,7 @@ export interface AdvancedSecurityOptions { /** * Container for information about the SAML configuration for OpenSearch Dashboards. - * If set, `samlAuthenticationEnabled` will be enabled. + * If set, `samlAuthenticationEnabled` will be enabled. * * @default - no SAML authentication options */ @@ -439,6 +446,47 @@ export enum IpAddressType { DUAL_STACK = 'dualstack', } +/** + * Configuration for a specific node type in OpenSearch domain + */ +export interface NodeConfig { + /** + * Whether this node type is enabled + * + * @default - false + */ + readonly enabled?: boolean; + + /** + * The instance type for the nodes + * + * @default - m5.large.search + */ + readonly type?: string; + + /** + * The number of nodes of this type + * + * @default - 1 + */ + readonly count?: number; +} + +/** + * Configuration for node options in OpenSearch domain + */ +export interface NodeOptions { + /** + * The type of node. Currently only 'coordinator' is supported. + */ + readonly nodeType: 'coordinator'; + + /** + * Configuration for the node type + */ + readonly nodeConfig: NodeConfig; +} + /** * Properties for an Amazon OpenSearch Service domain. */ @@ -1393,6 +1441,7 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { const defaultInstanceType = 'r5.large.search'; const warmDefaultInstanceType = 'ultrawarm1.medium.search'; + const defaultCoordinatorInstanceType = 'm5.large.search'; const dedicatedMasterType = initializeInstanceType(defaultInstanceType, props.capacity?.masterNodeInstanceType); const dedicatedMasterCount = props.capacity?.masterNodes ?? 0; @@ -1871,6 +1920,20 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { this.validateSamlAuthenticationOptions(props.fineGrainedAccessControl?.samlAuthenticationOptions); } + if (props.capacity?.nodeOptions) { + // Validate coordinator node configuration + const coordinatorConfig = props.capacity.nodeOptions.find(opt => opt.nodeType === 'coordinator')?.nodeConfig; + if (coordinatorConfig?.enabled) { + const coordinatorType = initializeInstanceType(defaultCoordinatorInstanceType, coordinatorConfig.type); + if (!cdk.Token.isUnresolved(coordinatorType) && !coordinatorType.endsWith('.search')) { + throw new Error('Coordinator node instance type must end with ".search".'); + } + if (coordinatorConfig.count !== undefined && coordinatorConfig.count < 1) { + throw new Error('Coordinator node count must be at least 1.'); + } + } + } + // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, @@ -1902,6 +1965,7 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { zoneAwarenessConfig: zoneAwarenessEnabled ? { availabilityZoneCount } : undefined, + nodeOptions: props.capacity?.nodeOptions, }, ebsOptions: { ebsEnabled, diff --git a/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts b/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts index 78b6be1583eee..438edebeb4bac 100644 --- a/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts +++ b/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts @@ -10,7 +10,7 @@ import * as logs from '../../aws-logs'; import * as route53 from '../../aws-route53'; import { App, Stack, Duration, SecretValue, CfnParameter, Token } from '../../core'; import * as cxapi from '../../cx-api'; -import { Domain, DomainProps, EngineVersion, IpAddressType } from '../lib'; +import { Domain, DomainProps, EngineVersion, IpAddressType, NodeOptions } from '../lib'; let app: App; let stack: Stack; @@ -2747,3 +2747,94 @@ function testMetric( }); expect(metric.dimensions).toHaveProperty('DomainName'); } + +each(testedOpenSearchVersions).test('can configure coordinator nodes with nodeOptions', (engineVersion) => { + const coordinatorConfig: NodeOptions = { + nodeType: 'coordinator', + nodeConfig: { + enabled: true, + type: 'm5.large.search', + count: 2, + }, + }; + + const domain = new Domain(stack, 'Domain', { + version: engineVersion, + capacity: { + nodeOptions: [coordinatorConfig], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { + ClusterConfig: { + NodeOptions: [{ + NodeType: 'coordinator', + NodeConfig: { + Enabled: true, + Type: 'm5.large.search', + Count: 2, + }, + }], + }, + }); +}); + +each(testedOpenSearchVersions).test('throws when coordinator node instance type does not end with .search', (engineVersion) => { + expect(() => { + new Domain(stack, 'Domain', { + version: engineVersion, + capacity: { + nodeOptions: [{ + nodeType: 'coordinator' as const, + nodeConfig: { + enabled: true, + type: 'm5.large', + }, + }], + }, + }); + }).toThrow('Coordinator node instance type must end with ".search".'); +}); + +each(testedOpenSearchVersions).test('throws when coordinator node count is less than 1', (engineVersion) => { + expect(() => { + new Domain(stack, 'Domain', { + version: engineVersion, + capacity: { + nodeOptions: [{ + nodeType: 'coordinator' as const, + nodeConfig: { + enabled: true, + count: 0, + type: 'm5.large.search', + }, + }], + }, + }); + }).toThrow('Coordinator node count must be at least 1.'); +}); + +each(testedOpenSearchVersions).test('can disable coordinator nodes', (engineVersion) => { + const domain = new Domain(stack, 'Domain', { + version: engineVersion, + capacity: { + nodeOptions: [{ + nodeType: 'coordinator' as const, + nodeConfig: { + enabled: false, + }, + }], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { + ClusterConfig: { + NodeOptions: [{ + NodeType: 'coordinator', + NodeConfig: { + Enabled: false, + }, + }], + }, + }); +});