packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts (1,141 lines of code) (raw):

import { URL } from 'url'; import { Construct } from 'constructs'; import { LogGroupResourcePolicy } from './log-group-resource-policy'; import { OpenSearchAccessPolicy } from './opensearch-access-policy'; import { CfnDomain } from './opensearchservice.generated'; import * as perms from './perms'; import { EngineVersion } from './version'; import * as acm from '../../aws-certificatemanager'; import { Metric, MetricOptions, Statistic } from '../../aws-cloudwatch'; import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as logs from '../../aws-logs'; import * as route53 from '../../aws-route53'; import * as secretsmanager from '../../aws-secretsmanager'; import * as cdk from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import * as cxapi from '../../cx-api'; /** * Configures the capacity of the cluster such as the instance type and the * number of instances. */ export interface CapacityConfig { /** * The number of instances to use for the master node. * * @default - no dedicated master nodes */ readonly masterNodes?: number; /** * The hardware configuration of the computer that hosts the dedicated master * node, such as `m3.medium.search`. For valid values, see [Supported * Instance Types](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html) * in the Amazon OpenSearch Service Developer Guide. * * @default - r5.large.search */ readonly masterNodeInstanceType?: string; /** * The number of data nodes (instances) to use in the Amazon OpenSearch Service domain. * * @default - 1 */ readonly dataNodes?: number; /** * The instance type for your data nodes, such as * `m3.medium.search`. For valid values, see [Supported Instance * Types](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html) * in the Amazon OpenSearch Service Developer Guide. * * @default - r5.large.search */ readonly dataNodeInstanceType?: string; /** * The number of UltraWarm nodes (instances) to use in the Amazon OpenSearch Service domain. * * @default - no UltraWarm nodes */ readonly warmNodes?: number; /** * The instance type for your UltraWarm node, such as `ultrawarm1.medium.search`. * For valid values, see [UltraWarm Storage * Limits](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/limits.html#limits-ultrawarm) * in the Amazon OpenSearch Service Developer Guide. * * @default - ultrawarm1.medium.search */ readonly warmInstanceType?: string; /** * Indicates whether Multi-AZ with Standby deployment option is enabled. * For more information, see [Multi-AZ with * Standby](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-multiaz.html#managedomains-za-standby) * * @default - multi-az with standby if the feature flag `ENABLE_OPENSEARCH_MULTIAZ_WITH_STANDBY` * 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[]; } /** * Specifies zone awareness configuration options. */ export interface ZoneAwarenessConfig { /** * Indicates whether to enable zone awareness for the Amazon OpenSearch Service domain. * When you enable zone awareness, Amazon OpenSearch Service 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. For more information, * see [Configuring a Multi-AZ Domain](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-multiaz.html) * in the Amazon OpenSearch Service Developer Guide. * * @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; } /** * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that * are attached to data nodes in the Amazon OpenSearch Service domain. For more information, see * [Amazon EBS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AmazonEBS.html) * in the Amazon Elastic Compute Cloud Developer Guide. */ export interface EbsOptions { /** * Specifies whether Amazon EBS volumes are attached to data nodes in the * Amazon OpenSearch Service 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 gp3 and Provisioned IOPS (SSD) EBS * volume type. * * @default - iops are not set. */ readonly iops?: number; /** * The throughput (in MiB/s) of the EBS volumes attached to data nodes. * This property applies only to the gp3 volume type. * * @default - throughput is not set. */ readonly throughput?: 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 valid values, see * [EBS volume size limits](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/limits.html#ebsresource) * in the Amazon OpenSearch Service Developer Guide. * * @default 10 */ readonly volumeSize?: number; /** * The EBS volume type to use with the Amazon OpenSearch Service domain, such as standard, gp2, io1. * * @default gp2 */ readonly volumeType?: ec2.EbsDeviceVolumeType; } /** * Configures log settings for the domain. */ export interface LoggingOptions { /** * Specify if slow search logging should be set up. * Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. * An explicit `false` is required when disabling it from `true`. * * @default - false */ readonly slowSearchLogEnabled?: boolean; /** * Log slow searches to this log group. * * @default - a new log group is created if slow search logging is enabled */ readonly slowSearchLogGroup?: logs.ILogGroup; /** * Specify if slow index logging should be set up. * Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. * An explicit `false` is required when disabling it from `true`. * * @default - false */ readonly slowIndexLogEnabled?: boolean; /** * Log slow indices to this log group. * * @default - a new log group is created if slow index logging is enabled */ readonly slowIndexLogGroup?: logs.ILogGroup; /** * Specify if Amazon OpenSearch Service application logging should be set up. * Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. * An explicit `false` is required when disabling it from `true`. * * @default - false */ readonly appLogEnabled?: boolean; /** * Log Amazon OpenSearch Service application logs to this log group. * * @default - a new log group is created if app logging is enabled */ readonly appLogGroup?: logs.ILogGroup; /** * Specify if Amazon OpenSearch Service audit logging should be set up. * Requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later and fine grained access control to be enabled. * * @default - false */ readonly auditLogEnabled?: boolean; /** * Log Amazon OpenSearch Service audit logs to this log group. * * @default - a new log group is created if audit logging is enabled */ readonly auditLogGroup?: logs.ILogGroup; } /** * 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. Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. */ 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 OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards. * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cognito-auth.html */ export interface CognitoOptions { /** * The Amazon Cognito identity pool ID that you want Amazon OpenSearch Service to use for OpenSearch Dashboards authentication. */ readonly identityPoolId: string; /** * A role that allows Amazon OpenSearch Service to configure your user pool and identity pool. It must have the `AmazonESCognitoAccess` policy attached to it. * * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cognito-auth.html#cognito-auth-prereq */ readonly role: iam.IRole; /** * The Amazon Cognito user pool ID that you want Amazon OpenSearch Service to use for OpenSearch Dashboards authentication. */ readonly userPoolId: string; } /** * 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', /** Cipher suite TLS 1.2 to 1.3 with perfect forward secrecy (PFS) */ TLS_1_2_PFS = 'Policy-Min-TLS-1-2-PFS-2023-10', } /** * Container for information about the SAML configuration for OpenSearch Dashboards. */ export interface SAMLOptionsProperty { /** * The unique entity ID of the application in the SAML identity provider. */ readonly idpEntityId: string; /** * The metadata of the SAML application, in XML format. */ readonly idpMetadataContent: string; /** * The SAML master username, which is stored in the domain's internal user database. * This SAML user receives full permission in OpenSearch Dashboards/Kibana. * Creating a new master username does not delete any existing master usernames. * * @default - No master user name is configured */ readonly masterUserName?: string; /** * The backend role that the SAML master user is mapped to. * Any users with this backend role receives full permission in OpenSearch Dashboards/Kibana. * To use a SAML master backend role, configure the `rolesKey` property. * * @default - The master user is not mapped to a backend role */ readonly masterBackendRole?: string; /** * Element of the SAML assertion to use for backend roles. * * @default - roles */ readonly rolesKey?: string; /** * Element of the SAML assertion to use for the user name. * * @default - NameID element of the SAML assertion fot the user name */ readonly subjectKey?: string; /** * The duration, in minutes, after which a user session becomes inactive. * * @default - 60 */ readonly sessionTimeoutMinutes?: number; } /** * 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.unsafePlainText` 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 masterUserPassword?: cdk.SecretValue; /** * True to enable SAML authentication for a domain. * * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/saml.html * * @default - SAML authentication is disabled. Enabled if `samlAuthenticationOptions` is set. */ readonly samlAuthenticationEnabled?: boolean; /** * Container for information about the SAML configuration for OpenSearch Dashboards. * If set, `samlAuthenticationEnabled` will be enabled. * * @default - no SAML authentication options */ readonly samlAuthenticationOptions?: SAMLOptionsProperty; } /** * Configures a custom domain endpoint for the Amazon OpenSearch Service domain */ export interface CustomEndpointOptions { /** * The custom domain name to assign */ readonly domainName: string; /** * The certificate to use * @default - create a new one */ readonly certificate?: acm.ICertificate; /** * The hosted zone in Route53 to create the CNAME record in * @default - do not create a CNAME */ readonly hostedZone?: route53.IHostedZone; } export interface WindowStartTime { /** * The start hour of the window in Coordinated Universal Time (UTC), using 24-hour time. * For example, 17 refers to 5:00 P.M. UTC. * * @default - 22 */ readonly hours: number; /** * The start minute of the window, in UTC. * * @default - 0 */ readonly minutes: number; } /** * The IP address type for the domain. */ export enum IpAddressType { /** * IPv4 addresses only */ IPV4 = 'ipv4', /** * IPv4 and IPv6 addresses */ 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; } /** * NodeType is a string enum of the node types in OpenSearch domain * */ export enum NodeType { /** * Coordinator node type */ COORDINATOR = 'coordinator', } /** * Configuration for node options in OpenSearch domain */ export interface NodeOptions { /** * The type of node. Currently only 'coordinator' is supported. */ readonly nodeType: NodeType; /** * Configuration for the node type */ readonly nodeConfig: NodeConfig; } /** * Properties for an Amazon OpenSearch Service domain. */ export interface DomainProps { /** * Domain access policies. * * @default - No access policies. */ readonly accessPolicies?: iam.PolicyStatement[]; /** * Additional options to specify for the Amazon OpenSearch Service domain. * * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomain-configure-advanced-options * @default - no advanced options are specified */ readonly advancedOptions?: { [key: string]: (string) }; /** * Configures Amazon OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards. * * @default - Cognito not used for authentication to OpenSearch Dashboards. */ readonly cognitoDashboardsAuth?: CognitoOptions; /** * Enforces a particular physical domain name. * * @default - A name will be auto-generated. */ readonly domainName?: string; /** * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that * are attached to data nodes in the Amazon OpenSearch Service domain. * * @default - 10 GiB General Purpose (SSD) volumes per node. */ readonly ebs?: EbsOptions; /** * The cluster capacity configuration for the Amazon OpenSearch Service domain. * * @default - 1 r5.large.search data node; no dedicated master nodes. */ readonly capacity?: CapacityConfig; /** * The cluster zone awareness configuration for the Amazon OpenSearch Service domain. * * @default - no zone awareness (1 AZ) */ readonly zoneAwareness?: ZoneAwarenessConfig; /** * The Elasticsearch/OpenSearch version that your domain will leverage. */ readonly version: EngineVersion; /** * Encryption at rest options for the cluster. * * @default - No encryption at rest */ readonly encryptionAtRest?: EncryptionAtRestOptions; /** * Configuration log publishing configuration options. * * @default - No logs are published */ readonly logging?: LoggingOptions; /** * Specify true to enable node to node encryption. * Requires Elasticsearch version 6.0 or later or OpenSearch version 1.0 or later. * * @default - Node to node encryption is not enabled. */ readonly nodeToNodeEncryption?: boolean; /** * The hour in UTC during which the service takes an automated daily snapshot * of the indices in the Amazon OpenSearch Service domain. Only applies for Elasticsearch versions * below 5.3. * * @default - Hourly automated snapshots not used */ readonly automatedSnapshotStartHour?: number; /** * Place the domain inside this VPC. * * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/vpc.html * @default - Domain is not placed in a VPC. */ readonly vpc?: ec2.IVpc; /** * The list of security groups that are associated with the VPC endpoints * for the domain. * * Only used if `vpc` is specified. * * @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html * @default - One new security group is created. */ readonly securityGroups?: ec2.ISecurityGroup[]; /** * The specific vpc subnets the domain will be placed in. You must provide one subnet for each Availability Zone * that your domain uses. For example, you must specify three subnet IDs for a three Availability Zone * domain. * * Only used if `vpc` is specified. * * @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html * @default - All private subnets. */ readonly vpcSubnets?: ec2.SubnetSelection[]; /** * True to require that all traffic to the domain arrive over HTTPS. * * @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 or OpenSearch version 1.0 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; /** * 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; /** * To upgrade an Amazon OpenSearch Service domain to a new version, rather than replacing the entire * domain resource, use the EnableVersionUpgrade update policy. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-upgradeopensearchdomain * * @default - false */ readonly enableVersionUpgrade?: boolean; /** * Policy to apply when the domain is removed from the stack * * @default RemovalPolicy.RETAIN */ readonly removalPolicy?: cdk.RemovalPolicy; /** * To configure a custom domain configure these options * * If you specify a Route53 hosted zone it will create a CNAME record and use DNS validation for the certificate * * @default - no custom domain endpoint will be configured */ readonly customEndpoint?: CustomEndpointOptions; /** * Options for enabling a domain's off-peak window, during which OpenSearch Service can perform mandatory * configuration changes on the domain. * * Off-peak windows were introduced on February 16, 2023. * All domains created before this date have the off-peak window disabled by default. * You must manually enable and configure the off-peak window for these domains. * All domains created after this date will have the off-peak window enabled by default. * You can't disable the off-peak window for a domain after it's enabled. * * @see https://docs.aws.amazon.com/it_it/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-offpeakwindow.html * * @default - Disabled for domains created before February 16, 2023. Enabled for domains created after. Enabled if `offPeakWindowStart` is set. */ readonly offPeakWindowEnabled?: boolean; /** * Start time for the off-peak window, in Coordinated Universal Time (UTC). * The window length will always be 10 hours, so you can't specify an end time. * For example, if you specify 11:00 P.M. UTC as a start time, the end time will automatically be set to 9:00 A.M. * * @default - 10:00 P.M. local time */ readonly offPeakWindowStart?: WindowStartTime; /** * Specifies whether automatic service software updates are enabled for the domain. * * @see https://docs.aws.amazon.com/it_it/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-softwareupdateoptions.html * * @default - false */ readonly enableAutoSoftwareUpdate?: boolean; /** * Specify either dual stack or IPv4 as your IP address type. * Dual stack allows you to share domain resources across IPv4 and IPv6 address types, and is the recommended option. * * If you set your IP address type to dual stack, you can't change your address type later. * * @default - IpAddressType.IPV4 */ readonly ipAddressType?: IpAddressType; /** * Specify whether to create a CloudWatch Logs resource policy or not. * * When logging is enabled for the domain, a CloudWatch Logs resource policy is created by default. * However, CloudWatch Logs supports only 10 resource policies per region. * If you enable logging for several domains, it may hit the quota and cause an error. * By setting this property to true, creating a resource policy is suppressed, allowing you to avoid this problem. * * If you set this option to true, you must create a resource policy before deployment. * * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createdomain-configure-slow-logs.html * * @default - false */ readonly suppressLogsResourcePolicy?: boolean; /** * Whether to enable or disable cold storage on the domain. You must enable UltraWarm storage to enable cold storage. * * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cold-storage.html * * @default - undefined */ readonly coldStorageEnabled?: boolean; } /** * An interface that represents an Amazon OpenSearch Service domain - either created with the CDK, or an existing one. */ export interface IDomain extends cdk.IResource { /** * Arn of the Amazon OpenSearch Service domain. * * @attribute */ readonly domainArn: string; /** * Domain name of the Amazon OpenSearch Service domain. * * @attribute */ readonly domainName: string; /** * Identifier of the Amazon OpenSearch Service domain. * * @attribute */ readonly domainId: string; /** * Endpoint of the Amazon OpenSearch Service domain. * * @attribute */ readonly domainEndpoint: string; /** * Grant read permissions for this domain and its contents to an IAM * principal (Role/Group/User). * * @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). * * @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 identity The principal */ grantReadWrite(identity: iam.IGrantable): iam.Grant; /** * Grant read permissions for an index in this domain to an IAM * principal (Role/Group/User). * * @param index The index to grant permissions for * @param identity The principal */ grantIndexRead(index: string, identity: iam.IGrantable): iam.Grant; /** * Grant write permissions for an index in this domain to an IAM * principal (Role/Group/User). * * @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). * * @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). * * @param path The path to grant permissions for * @param identity The principal */ 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). * * @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). * * @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. */ metric(metricName: string, props?: MetricOptions): Metric; /** * Metric for the time the cluster status is red. * * @default maximum over 5 minutes */ metricClusterStatusRed(props?: MetricOptions): Metric; /** * Metric for the time the cluster status is yellow. * * @default maximum over 5 minutes */ metricClusterStatusYellow(props?: MetricOptions): Metric; /** * Metric for the storage space of nodes in the cluster. * * @default minimum over 5 minutes */ metricFreeStorageSpace(props?: MetricOptions): Metric; /** * Metric for the cluster blocking index writes. * * @default maximum over 1 minute */ metricClusterIndexWritesBlocked(props?: MetricOptions): Metric; /** * Metric for the number of nodes. * * @default minimum over 1 hour */ metricNodes(props?: MetricOptions): Metric; /** * Metric for automated snapshot failures. * * @default maximum over 5 minutes */ metricAutomatedSnapshotFailure(props?: MetricOptions): Metric; /** * Metric for CPU utilization. * * @default maximum over 5 minutes */ metricCPUUtilization(props?: MetricOptions): Metric; /** * Metric for JVM memory pressure. * * @default maximum over 5 minutes */ metricJVMMemoryPressure(props?: MetricOptions): Metric; /** * Metric for master CPU utilization. * * @default maximum over 5 minutes */ metricMasterCPUUtilization(props?: MetricOptions): Metric; /** * Metric for master JVM memory pressure. * * @default maximum over 5 minutes */ metricMasterJVMMemoryPressure(props?: MetricOptions): Metric; /** * Metric for KMS key errors. * * @default maximum over 5 minutes */ metricKMSKeyError(props?: MetricOptions): Metric; /** * Metric for KMS key being inaccessible. * * @default maximum over 5 minutes */ metricKMSKeyInaccessible(props?: MetricOptions): Metric; /** * Metric for number of searchable documents. * * @default maximum over 5 minutes */ metricSearchableDocuments(props?: MetricOptions): Metric; /** * Metric for search latency. * * @default p99 over 5 minutes */ metricSearchLatency(props?: MetricOptions): Metric; /** * Metric for indexing latency. * * @default p99 over 5 minutes */ metricIndexingLatency(props?: MetricOptions): Metric; } /** * A new or imported domain. */ abstract class DomainBase extends cdk.Resource implements IDomain { public abstract readonly domainArn: string; public abstract readonly domainName: string; public abstract readonly domainId: string; public abstract readonly domainEndpoint: string; /** * Grant read permissions for this domain and its contents to an IAM * principal (Role/Group/User). * * @param identity The principal */ grantRead(identity: iam.IGrantable): iam.Grant { return this.grant( identity, perms.ES_READ_ACTIONS, this.domainArn, `${this.domainArn}/*`, ); } /** * Grant write permissions for this domain and its contents to an IAM * principal (Role/Group/User). * * @param identity The principal */ grantWrite(identity: iam.IGrantable): iam.Grant { return this.grant( identity, perms.ES_WRITE_ACTIONS, this.domainArn, `${this.domainArn}/*`, ); } /** * Grant read/write permissions for this domain and its contents to an IAM * principal (Role/Group/User). * * @param identity The principal */ grantReadWrite(identity: iam.IGrantable): iam.Grant { return this.grant( identity, perms.ES_READ_WRITE_ACTIONS, this.domainArn, `${this.domainArn}/*`, ); } /** * Grant read permissions for an index in this domain to an IAM * principal (Role/Group/User). * * @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, `${this.domainArn}/${index}`, `${this.domainArn}/${index}/*`, ); } /** * Grant write permissions for an index in this domain to an IAM * principal (Role/Group/User). * * @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, `${this.domainArn}/${index}`, `${this.domainArn}/${index}/*`, ); } /** * Grant read/write permissions for an index in this domain to an IAM * principal (Role/Group/User). * * @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, `${this.domainArn}/${index}`, `${this.domainArn}/${index}/*`, ); } /** * Grant read permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * * @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, `${this.domainArn}/${path}`, ); } /** * Grant write permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * * @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, `${this.domainArn}/${path}`, ); } /** * Grant read/write permissions for a specific path in this domain to an IAM * principal (Role/Group/User). * * @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, `${this.domainArn}/${path}`, ); } /** * Return the given named metric for this domain. */ public metric(metricName: string, props?: MetricOptions): Metric { return new Metric({ namespace: 'AWS/ES', metricName, dimensionsMap: { DomainName: this.domainName, ClientId: this.env.account, }, ...props, }).attachTo(this); } /** * Metric for the time the cluster status is red. * * @default maximum over 5 minutes */ 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 5 minutes */ 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 5 minutes */ public metricFreeStorageSpace(props?: MetricOptions): Metric { return this.metric('FreeStorageSpace', { statistic: Statistic.MINIMUM, ...props, }); } /** * Metric for the cluster blocking index writes. * * @default maximum over 1 minute */ public metricClusterIndexWritesBlocked(props?: MetricOptions): Metric { return this.metric('ClusterIndexWritesBlocked', { statistic: Statistic.MAXIMUM, period: cdk.Duration.minutes(1), ...props, }); } /** * Metric for the number of nodes. * * @default minimum over 1 hour */ public metricNodes(props?: MetricOptions): Metric { return this.metric('Nodes', { statistic: Statistic.MINIMUM, period: cdk.Duration.hours(1), ...props, }); } /** * Metric for automated snapshot failures. * * @default maximum over 5 minutes */ public metricAutomatedSnapshotFailure(props?: MetricOptions): Metric { return this.metric('AutomatedSnapshotFailure', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for CPU utilization. * * @default maximum over 5 minutes */ public metricCPUUtilization(props?: MetricOptions): Metric { return this.metric('CPUUtilization', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for JVM memory pressure. * * @default maximum over 5 minutes */ public metricJVMMemoryPressure(props?: MetricOptions): Metric { return this.metric('JVMMemoryPressure', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for master CPU utilization. * * @default maximum over 5 minutes */ public metricMasterCPUUtilization(props?: MetricOptions): Metric { return this.metric('MasterCPUUtilization', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for master JVM memory pressure. * * @default maximum over 5 minutes */ public metricMasterJVMMemoryPressure(props?: MetricOptions): Metric { return this.metric('MasterJVMMemoryPressure', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for KMS key errors. * * @default maximum over 5 minutes */ public metricKMSKeyError(props?: MetricOptions): Metric { return this.metric('KMSKeyError', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for KMS key being inaccessible. * * @default maximum over 5 minutes */ public metricKMSKeyInaccessible(props?: MetricOptions): Metric { return this.metric('KMSKeyInaccessible', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for number of searchable documents. * * @default maximum over 5 minutes */ public metricSearchableDocuments(props?: MetricOptions): Metric { return this.metric('SearchableDocuments', { statistic: Statistic.MAXIMUM, ...props, }); } /** * Metric for search latency. * * @default p99 over 5 minutes */ public metricSearchLatency(props?: MetricOptions): Metric { return this.metric('SearchLatency', { statistic: 'p99', ...props }); } /** * Metric for indexing latency. * * @default p99 over 5 minutes */ public metricIndexingLatency(props?: MetricOptions): Metric { return this.metric('IndexingLatency', { statistic: 'p99', ...props }); } private grant( grantee: iam.IGrantable, domainActions: string[], resourceArn: string, ...otherResourceArns: string[] ): iam.Grant { const resourceArns = [resourceArn, ...otherResourceArns]; const grant = iam.Grant.addToPrincipal({ grantee, actions: domainActions, resourceArns, scope: this, }); return grant; } } /** * Reference to an Amazon OpenSearch Service domain. */ export interface DomainAttributes { /** * The ARN of the Amazon OpenSearch Service domain. */ readonly domainArn: string; /** * The domain endpoint of the Amazon OpenSearch Service domain. */ readonly domainEndpoint: string; } /** * Provides an Amazon OpenSearch Service domain. */ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { /** * 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 domainEndpoint The domain's endpoint. */ public static fromDomainEndpoint( scope: 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, 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: Construct, id: string, attrs: DomainAttributes): IDomain { const { domainArn, domainEndpoint } = attrs; const domainName = cdk.Stack.of(scope).splitArn(domainArn, cdk.ArnFormat.SLASH_RESOURCE_NAME).resourceName ?? extractNameFromEndpoint(domainEndpoint); return new class extends DomainBase { public readonly domainArn = domainArn; public readonly domainName = domainName; public readonly domainId = domainName; public readonly domainEndpoint = domainEndpoint.replace(/^https?:\/\//, ''); constructor() { super(scope, id); } }; } public readonly domainArn: string; public readonly domainName: string; public readonly domainId: 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; /** * Log group that audit logs are logged to. * * @attribute */ public readonly auditLogGroup?: logs.ILogGroup; /** * Master user password if fine grained access control is configured. */ public readonly masterUserPassword?: cdk.SecretValue; private readonly domain: CfnDomain; private accessPolicy?: OpenSearchAccessPolicy; private encryptionAtRestOptions?: EncryptionAtRestOptions; private readonly _connections: ec2.Connections | undefined; constructor(scope: Construct, id: string, props: DomainProps) { super(scope, id, { physicalName: props.domainName, }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); 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; const dedicatedMasterEnabled = cdk.Token.isUnresolved(dedicatedMasterCount) ? true : dedicatedMasterCount > 0; const instanceType = initializeInstanceType(defaultInstanceType, props.capacity?.dataNodeInstanceType); const instanceCount = props.capacity?.dataNodes ?? 1; const warmType = initializeInstanceType(warmDefaultInstanceType, props.capacity?.warmInstanceType); const warmCount = props.capacity?.warmNodes ?? 0; const warmEnabled = cdk.Token.isUnresolved(warmCount) ? true : warmCount > 0; const availabilityZoneCount = props.zoneAwareness?.availabilityZoneCount ?? 2; if (![2, 3].includes(availabilityZoneCount)) { throw new Error('Invalid zone awareness configuration; availabilityZoneCount must be 2 or 3'); } const zoneAwarenessEnabled = props.zoneAwareness?.enabled ?? props.zoneAwareness?.availabilityZoneCount != null; let securityGroups: ec2.ISecurityGroup[] | undefined; let subnets: ec2.ISubnet[] | undefined; let skipZoneAwarenessCheck: boolean = false; if (props.vpc) { const subnetSelections = props.vpcSubnets ?? [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }]; subnets = selectSubnets(props.vpc, subnetSelections); skipZoneAwarenessCheck = zoneAwarenessCheckShouldBeSkipped(props.vpc, subnetSelections); securityGroups = props.securityGroups ?? [new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, description: `Security group for domain ${this.node.id}`, })]; if (props.enforceHttps) { this._connections = new ec2.Connections({ securityGroups, defaultPort: ec2.Port.tcp(443) }); } else { this._connections = new ec2.Connections({ securityGroups }); } } // If VPC options are supplied ensure that the number of subnets matches the number AZ (only if the vpc is not imported from another stack) if (subnets && zoneAwarenessEnabled && !skipZoneAwarenessCheck && new Set(subnets.map((subnet) => subnet.availabilityZone)).size < availabilityZoneCount ) { throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); } if ([dedicatedMasterType, instanceType, warmType].some(t => (!cdk.Token.isUnresolved(t) && !t.endsWith('.search')))) { throw new Error('Master, data and UltraWarm node instance types must end with ".search".'); } if (!cdk.Token.isUnresolved(warmType) && !warmType.startsWith('ultrawarm')) { throw new Error('UltraWarm node instance type must start with "ultrawarm".'); } 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.AnyPrincipal()], resources: [cdk.Lazy.string({ produce: () => `${this.domainArn}/*` })], }); const masterUserArn = props.fineGrainedAccessControl?.masterUserArn; 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.'); } const advancedSecurityEnabled = (masterUserArn ?? masterUserName) != null; const internalUserDatabaseEnabled = masterUserName != null; const masterUserPasswordProp = props.fineGrainedAccessControl?.masterUserPassword; const createMasterUserPassword = (): cdk.SecretValue => { return new secretsmanager.Secret(this, 'MasterUser', { generateSecretString: { secretStringTemplate: JSON.stringify({ username: masterUserName, }), generateStringKey: 'password', excludeCharacters: "{}'\\*[]()`", }, }) .secretValueFromJson('password'); }; this.masterUserPassword = internalUserDatabaseEnabled ? (masterUserPasswordProp ?? createMasterUserPassword()) : undefined; const encryptionAtRestEnabled = 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 ?? unsignedBasicAuthEnabled; function isInstanceType(t: string): Boolean { return dedicatedMasterType.startsWith(t) || instanceType.startsWith(t); } function isSomeInstanceType(...instanceTypes: string[]): Boolean { return instanceTypes.some(isInstanceType); } function isEveryDatanodeInstanceType(...instanceTypes: string[]): Boolean { return instanceTypes.some(t => instanceType.startsWith(t)); } // Validate feature support for the given Elasticsearch/OpenSearch version, per // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/features-by-version.html const { versionNum: versionNum, isElasticsearchVersion } = parseVersion(props.version); if (isElasticsearchVersion) { if ( versionNum <= 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(versionNum) ) { throw new Error(`Unknown Elasticsearch version: ${versionNum}`); } if (versionNum < 5.1) { if (props.logging?.appLogEnabled) { throw new Error('Error logs publishing requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later.'); } if (props.encryptionAtRest?.enabled) { throw new Error('Encryption of data at rest requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later.'); } if (props.cognitoDashboardsAuth != null) { throw new Error('Cognito authentication for OpenSearch Dashboards requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 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 or OpenSearch version 1.0 or later.'); } } if (versionNum < 6.0) { if (props.nodeToNodeEncryption) { throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later or OpenSearch version 1.0 or later.'); } } if (versionNum < 6.7) { if (unsignedBasicAuthEnabled) { throw new Error('Using unsigned basic auth requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later.'); } if (advancedSecurityEnabled) { throw new Error('Fine-grained access control requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later.'); } } if (versionNum < 6.8 && warmEnabled) { throw new Error('UltraWarm requires Elasticsearch version 6.8 or later or OpenSearch version 1.0 or later.'); } } const unSupportEbsInstanceType = [ ec2.InstanceClass.I3, ec2.InstanceClass.R6GD, ec2.InstanceClass.I4G, ec2.InstanceClass.I4I, ec2.InstanceClass.IM4GN, ec2.InstanceClass.R7GD, ]; const supportInstanceStorageInstanceType = [ ec2.InstanceClass.R3, ...unSupportEbsInstanceType, ]; const unSupportEncryptionAtRestInstanceType=[ ec2.InstanceClass.M3, ec2.InstanceClass.R3, ec2.InstanceClass.T2, ]; const unSupportUltraWarmInstanceType=[ ec2.InstanceClass.T2, ec2.InstanceClass.T3, ]; // Validate against instance type restrictions, per // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html if (isSomeInstanceType(...unSupportEbsInstanceType) && ebsEnabled) { throw new Error(`${formatInstanceTypesList(unSupportEbsInstanceType, 'and')} instance types do not support EBS storage volumes.`); } if (isSomeInstanceType('m3', 'r3', 't2') && encryptionAtRestEnabled) { throw new Error(`${formatInstanceTypesList(unSupportEncryptionAtRestInstanceType, 'and')} instance types do not support encryption of data at rest.`); } if (isInstanceType('t2.micro') && !(isElasticsearchVersion && versionNum <= 2.3)) { throw new Error('The t2.micro.search instance type supports only Elasticsearch versions 1.5 and 2.3.'); } if (isSomeInstanceType('t2', 't3') && warmEnabled) { throw new Error(`${formatInstanceTypesList(unSupportUltraWarmInstanceType, 'and')} instance types do not support UltraWarm storage.`); } // Only R3, I3, R6GD, I4G, I4I, IM4GN and R7GD support instance storage, per // https://aws.amazon.com/opensearch-service/pricing/ if (!ebsEnabled && !isEveryDatanodeInstanceType(...supportInstanceStorageInstanceType)) { throw new Error(`EBS volumes are required when using instance types other than ${formatInstanceTypesList(supportInstanceStorageInstanceType, 'or')}.`); } // Only for a valid ebs volume configuration, per // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html if (ebsEnabled) { // Check if iops or throughput if general purpose is configured if (volumeType == ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD || volumeType == ec2.EbsDeviceVolumeType.STANDARD) { if (props.ebs?.iops !== undefined || props.ebs?.throughput !== undefined) { throw new Error('General Purpose EBS volumes can not be used with Iops or Throughput configuration'); } } if ( volumeType && [ ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, ].includes(volumeType) && !props.ebs?.iops ) { throw new Error( '`iops` must be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`.', ); } if (props.ebs?.iops) { if ( ![ ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, ].includes(volumeType) ) { throw new Error( '`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`, `PROVISIONED_IOPS_SSD_IO2` or `GENERAL_PURPOSE_SSD_GP3`.', ); } // Enforce maximum ratio of IOPS/GiB: // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html const maximumRatios: { [key: string]: number } = {}; maximumRatios[ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = 500; maximumRatios[ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = 50; maximumRatios[ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = 500; const maximumRatio = maximumRatios[volumeType]; if (props.ebs?.volumeSize && (props.ebs?.iops > maximumRatio * props.ebs?.volumeSize)) { throw new Error(`\`${volumeType}\` volumes iops has a maximum ratio of ${maximumRatio} IOPS/GiB.`); } const maximumThroughputRatios: { [key: string]: number } = {}; maximumThroughputRatios[ec2.EbsDeviceVolumeType.GP3] = 0.25; const maximumThroughputRatio = maximumThroughputRatios[volumeType]; if (props.ebs?.throughput && props.ebs?.iops) { const iopsRatio = (props.ebs?.throughput / props.ebs?.iops); if (iopsRatio > maximumThroughputRatio) { throw new Error(`Throughput (MiBps) to iops ratio of ${iopsRatio} is too high; maximum is ${maximumThroughputRatio} MiBps per iops.`); } } } if (props.ebs?.throughput) { const throughputRange = { Min: 125, Max: 1000 }; const { Min, Max } = throughputRange; if (volumeType != ec2.EbsDeviceVolumeType.GP3) { throw new Error( '`throughput` property requires volumeType: `EbsDeviceVolumeType.GP3`', ); } if (props.ebs?.throughput < Min || props.ebs?.throughput > Max) { throw new Error( `throughput property takes a minimum of ${Min} and a maximum of ${Max}.`, ); } } } // 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.'); } } // Validate fine grained access control enabled for audit logs, per // https://aws.amazon.com/about-aws/whats-new/2020/09/elasticsearch-audit-logs-now-available-on-amazon-elasticsearch-service/ if (props.logging?.auditLogEnabled && !advancedSecurityEnabled) { throw new Error('Fine-grained access control is required when audit logs publishing is enabled.'); } // Validate UltraWarm requirement for dedicated master nodes, per // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ultrawarm.html if (warmEnabled && !dedicatedMasterEnabled) { throw new Error('Dedicated master node is required when UltraWarm storage is enabled.'); } if (props.coldStorageEnabled && !warmEnabled) { throw new Error('You must enable UltraWarm storage to enable cold storage.'); } let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; if (securityGroups && subnets) { cfnVpcOptions = { securityGroupIds: securityGroups.map((sg) => sg.securityGroupId), subnetIds: subnets.map((subnet) => subnet.subnetId), }; } // Setup logging const logGroups: logs.ILogGroup[] = []; const logPublishing: Record<string, any> = {}; if (props.logging?.slowSearchLogEnabled) { this.slowSearchLogGroup = props.logging.slowSearchLogGroup ?? new logs.LogGroup(this, 'SlowSearchLogs', { retention: logs.RetentionDays.ONE_MONTH, }); logGroups.push(this.slowSearchLogGroup); logPublishing.SEARCH_SLOW_LOGS = { enabled: true, cloudWatchLogsLogGroupArn: this.slowSearchLogGroup.logGroupArn, }; } else if (props.logging?.slowSearchLogEnabled === false) { logPublishing.SEARCH_SLOW_LOGS = { enabled: false, }; } if (props.logging?.slowIndexLogEnabled) { this.slowIndexLogGroup = props.logging.slowIndexLogGroup ?? new logs.LogGroup(this, 'SlowIndexLogs', { retention: logs.RetentionDays.ONE_MONTH, }); logGroups.push(this.slowIndexLogGroup); logPublishing.INDEX_SLOW_LOGS = { enabled: true, cloudWatchLogsLogGroupArn: this.slowIndexLogGroup.logGroupArn, }; } else if (props.logging?.slowIndexLogEnabled === false) { logPublishing.INDEX_SLOW_LOGS = { enabled: false, }; } if (props.logging?.appLogEnabled) { this.appLogGroup = props.logging.appLogGroup ?? new logs.LogGroup(this, 'AppLogs', { retention: logs.RetentionDays.ONE_MONTH, }); logGroups.push(this.appLogGroup); logPublishing.ES_APPLICATION_LOGS = { enabled: true, cloudWatchLogsLogGroupArn: this.appLogGroup.logGroupArn, }; } else if (props.logging?.appLogEnabled === false) { logPublishing.ES_APPLICATION_LOGS = { enabled: false, }; } if (props.logging?.auditLogEnabled) { this.auditLogGroup = props.logging.auditLogGroup ?? new logs.LogGroup(this, 'AuditLogs', { retention: logs.RetentionDays.ONE_MONTH, }); logGroups.push(this.auditLogGroup); logPublishing.AUDIT_LOGS = { enabled: true, cloudWatchLogsLogGroupArn: this.auditLogGroup?.logGroupArn, }; } else if (props.logging?.auditLogEnabled === false) { logPublishing.AUDIT_LOGS = { enabled: false, }; } let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; if (logGroups.length > 0 && !props.suppressLogsResourcePolicy) { 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${this.node.addr}`, { // create a cloudwatch logs resource policy name that is unique to this domain instance policyName: `ESLogPolicy${this.node.addr}`, policyStatements: [logPolicyStatement], }); } let customEndpointCertificate: acm.ICertificate | undefined; if (props.customEndpoint) { if (props.customEndpoint.certificate) { customEndpointCertificate = props.customEndpoint.certificate; } else { customEndpointCertificate = new acm.Certificate(this, 'CustomEndpointCertificate', { domainName: props.customEndpoint.domainName, validation: props.customEndpoint.hostedZone ? acm.CertificateValidation.fromDns(props.customEndpoint.hostedZone) : undefined, }); } } let multiAzWithStandbyEnabled = props.capacity?.multiAzWithStandbyEnabled; if (multiAzWithStandbyEnabled === undefined) { if (cdk.FeatureFlags.of(this).isEnabled(cxapi.ENABLE_OPENSEARCH_MULTIAZ_WITH_STANDBY)) { multiAzWithStandbyEnabled = true; } } if (isSomeInstanceType('t3') && multiAzWithStandbyEnabled) { throw new Error('T3 instance type does not support Multi-AZ with standby feature.'); } const offPeakWindowEnabled = props.offPeakWindowEnabled ?? props.offPeakWindowStart !== undefined; if (offPeakWindowEnabled) { this.validateWindowStartTime(props.offPeakWindowStart); } const samlAuthenticationEnabled = props.fineGrainedAccessControl?.samlAuthenticationEnabled ?? props.fineGrainedAccessControl?.samlAuthenticationOptions !== undefined; if (samlAuthenticationEnabled) { if (!advancedSecurityEnabled) { throw new Error('SAML authentication requires fine-grained access control to be enabled.'); } this.validateSamlAuthenticationOptions(props.fineGrainedAccessControl?.samlAuthenticationOptions); } if (props.capacity?.nodeOptions) { // Validate coordinator node configuration const coordinatorConfig = props.capacity.nodeOptions.find(opt => opt.nodeType === 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, engineVersion: props.version.version, clusterConfig: { coldStorageOptions: props.coldStorageEnabled !== undefined ? { enabled: props.coldStorageEnabled, } : undefined, dedicatedMasterEnabled, dedicatedMasterCount: dedicatedMasterEnabled ? dedicatedMasterCount : undefined, dedicatedMasterType: dedicatedMasterEnabled ? dedicatedMasterType : undefined, instanceCount, instanceType, multiAzWithStandbyEnabled, warmEnabled: warmEnabled ? warmEnabled : undefined, warmCount: warmEnabled ? warmCount : undefined, warmType: warmEnabled ? warmType : undefined, zoneAwarenessEnabled, zoneAwarenessConfig: zoneAwarenessEnabled ? { availabilityZoneCount } : undefined, nodeOptions: props.capacity?.nodeOptions, }, ebsOptions: { ebsEnabled, volumeSize: ebsEnabled ? volumeSize : undefined, volumeType: ebsEnabled ? volumeType : undefined, iops: ebsEnabled ? props.ebs?.iops : undefined, throughput: ebsEnabled ? props.ebs?.throughput : undefined, }, encryptionAtRestOptions: { enabled: encryptionAtRestEnabled, kmsKeyId: encryptionAtRestEnabled ? props.encryptionAtRest?.kmsKey?.keyId : undefined, }, nodeToNodeEncryptionOptions: { enabled: nodeToNodeEncryptionEnabled }, logPublishingOptions: logPublishing, cognitoOptions: props.cognitoDashboardsAuth ? { enabled: true, identityPoolId: props.cognitoDashboardsAuth?.identityPoolId, roleArn: props.cognitoDashboardsAuth?.role.roleArn, userPoolId: props.cognitoDashboardsAuth?.userPoolId, } : undefined, vpcOptions: cfnVpcOptions, snapshotOptions: props.automatedSnapshotStartHour ? { automatedSnapshotStartHour: props.automatedSnapshotStartHour } : undefined, domainEndpointOptions: { enforceHttps, tlsSecurityPolicy: props.tlsSecurityPolicy ?? TLSSecurityPolicy.TLS_1_0, ...props.customEndpoint && { customEndpointEnabled: true, customEndpoint: props.customEndpoint.domainName, customEndpointCertificateArn: customEndpointCertificate!.certificateArn, }, }, advancedSecurityOptions: advancedSecurityEnabled ? { enabled: true, internalUserDatabaseEnabled, masterUserOptions: { masterUserArn: masterUserArn, masterUserName: masterUserName, masterUserPassword: this.masterUserPassword?.unsafeUnwrap(), // Safe usage }, samlOptions: samlAuthenticationEnabled ? { enabled: true, idp: props.fineGrainedAccessControl && props.fineGrainedAccessControl.samlAuthenticationOptions ? { entityId: props.fineGrainedAccessControl.samlAuthenticationOptions.idpEntityId, metadataContent: props.fineGrainedAccessControl.samlAuthenticationOptions.idpMetadataContent, } : undefined, masterUserName: props.fineGrainedAccessControl?.samlAuthenticationOptions?.masterUserName, masterBackendRole: props.fineGrainedAccessControl?.samlAuthenticationOptions?.masterBackendRole, rolesKey: props.fineGrainedAccessControl?.samlAuthenticationOptions?.rolesKey ?? 'roles', subjectKey: props.fineGrainedAccessControl?.samlAuthenticationOptions?.subjectKey, sessionTimeoutMinutes: props.fineGrainedAccessControl?.samlAuthenticationOptions?.sessionTimeoutMinutes ?? 60, } : undefined, } : undefined, advancedOptions: props.advancedOptions, offPeakWindowOptions: offPeakWindowEnabled ? { enabled: offPeakWindowEnabled, offPeakWindow: { windowStartTime: props.offPeakWindowStart ?? { hours: 22, minutes: 0, }, }, } : undefined, softwareUpdateOptions: props.enableAutoSoftwareUpdate ? { autoSoftwareUpdateEnabled: props.enableAutoSoftwareUpdate, } : undefined, ipAddressType: props.ipAddressType, }); this.domain.applyRemovalPolicy(props.removalPolicy); if (props.enableVersionUpgrade) { this.domain.cfnOptions.updatePolicy = { ...this.domain.cfnOptions.updatePolicy, enableVersionUpgrade: props.enableVersionUpgrade, }; } if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } if (props.domainName) { if (!cdk.Token.isUnresolved(props.domainName)) { // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/configuration-api.html#configuration-api-datatypes-domainname if (!props.domainName.match(/^[a-z0-9\-]+$/)) { throw new Error(`Invalid domainName '${props.domainName}'. Valid characters are a-z (lowercase only), 0-9, and – (hyphen).`); } if (props.domainName.length < 3 || props.domainName.length > 28) { throw new Error(`Invalid domainName '${props.domainName}'. It must be between 3 and 28 characters`); } if (props.domainName[0] < 'a' || props.domainName[0] > 'z') { throw new Error(`Invalid domainName '${props.domainName}'. It must start with a lowercase letter`); } } this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } this.domainName = this.getResourceNameAttribute(this.domain.ref); this.domainId = this.domain.getAtt('Id').toString(); this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { service: 'es', resource: 'domain', resourceName: this.physicalName, }); if (props.customEndpoint?.hostedZone) { new route53.CnameRecord(this, 'CnameRecord', { recordName: props.customEndpoint.domainName, zone: props.customEndpoint.hostedZone, domainName: this.domainEndpoint, }); } this.encryptionAtRestOptions = props.encryptionAtRest; if (props.accessPolicies) { this.addAccessPolicies(...props.accessPolicies); } if (unsignedBasicAuthEnabled) { this.addAccessPolicies(unsignedAccessPolicy); } } /** * Validate windowStartTime property according to * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-windowstarttime.html */ private validateWindowStartTime(windowStartTime?: WindowStartTime) { if (!windowStartTime) return; if (windowStartTime.hours < 0 || windowStartTime.hours > 23) { throw new Error(`Hours must be a value between 0 and 23, but got ${windowStartTime.hours}.`); } if (windowStartTime.minutes < 0 || windowStartTime.minutes > 59) { throw new Error(`Minutes must be a value between 0 and 59, but got ${windowStartTime.minutes}.`); } } /** * Validate SAML configuration according to * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-samloptions.html */ private validateSamlAuthenticationOptions(samlAuthenticationOptions?: SAMLOptionsProperty) { if (!samlAuthenticationOptions) { throw new Error('You need to specify at least an Entity ID and Metadata content for the SAML configuration'); } if (samlAuthenticationOptions.idpEntityId.length < 8 || samlAuthenticationOptions.idpEntityId.length > 512) { throw new Error(`SAML identity provider entity ID must be between 8 and 512 characters long, received ${samlAuthenticationOptions.idpEntityId.length}.`); } if (samlAuthenticationOptions.idpMetadataContent.length < 1 || samlAuthenticationOptions.idpMetadataContent.length > 1048576) { throw new Error(`SAML identity provider metadata content must be between 1 and 1048576 characters long, received ${samlAuthenticationOptions.idpMetadataContent.length}.`); } if ( samlAuthenticationOptions.masterUserName && (samlAuthenticationOptions.masterUserName.length < 1 || samlAuthenticationOptions.masterUserName.length > 64) ) { throw new Error(`SAML master username must be between 1 and 64 characters long, received ${samlAuthenticationOptions.masterUserName.length}.`); } if ( samlAuthenticationOptions.masterBackendRole && (samlAuthenticationOptions.masterBackendRole.length < 1 || samlAuthenticationOptions.masterBackendRole.length > 256) ) { throw new Error(`SAML backend role must be between 1 and 256 characters long, received ${samlAuthenticationOptions.masterBackendRole.length}.`); } if ( samlAuthenticationOptions.sessionTimeoutMinutes && (samlAuthenticationOptions.sessionTimeoutMinutes < 1 || samlAuthenticationOptions.sessionTimeoutMinutes > 1440) ) { throw new Error(`SAML session timeout must be a value between 1 and 1440, received ${samlAuthenticationOptions.sessionTimeoutMinutes}.`); } } /** * Manages network connections to the domain. This will throw an error in case the domain * is not placed inside a VPC. */ public get connections(): ec2.Connections { if (!this._connections) { throw new Error("Connections are only available on VPC enabled domains. Use the 'vpc' property to place a domain inside a VPC"); } return this._connections; } /** * Add policy statements to the domain access policy */ @MethodMetadata() public addAccessPolicies(...accessPolicyStatements: iam.PolicyStatement[]) { if (accessPolicyStatements.length > 0) { if (!this.accessPolicy) { // Only create the custom resource after there are statements to set. this.accessPolicy = new OpenSearchAccessPolicy(this, 'AccessPolicy', { domainName: this.domainName, domainArn: this.domainArn, accessPolicies: accessPolicyStatements, }); if (this.encryptionAtRestOptions?.kmsKey) { // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/encryption-at-rest.html // these permissions are documented as required during domain creation. // while not strictly documented for updates as well, it stands to reason that an update // operation might require these in case the cluster uses a kms key. // empircal evidence shows this is indeed required: https://github.com/aws/aws-cdk/issues/11412 this.accessPolicy.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['kms:List*', 'kms:Describe*', 'kms:CreateGrant'], resources: [this.encryptionAtRestOptions.kmsKey.keyArn], effect: iam.Effect.ALLOW, })); } } else { this.accessPolicy.addAccessPolicies(...accessPolicyStatements); } } } } /** * Given an Amazon OpenSearch Service 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://<domain-name>-<suffix>.<region>.es.amazonaws.com * * ..which means that in order to extract the domain name from the endpoint, we can * split the endpoint using "-<suffix>" and select the component in index 0. * * @param domainEndpoint The Amazon OpenSearch Service 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]; } /** * Converts an engine version into a decimal number with major and minor version i.e x.y. * * @param version The engine version object */ function parseVersion(version: EngineVersion): { versionNum: number; isElasticsearchVersion: boolean } { const elasticsearchPrefix = 'Elasticsearch_'; const openSearchPrefix = 'OpenSearch_'; const isElasticsearchVersion = version.version.startsWith(elasticsearchPrefix); const versionStr = isElasticsearchVersion ? version.version.substring(elasticsearchPrefix.length) : version.version.substring(openSearchPrefix.length); const firstDot = versionStr.indexOf('.'); if (firstDot < 1) { throw new Error(`Invalid engine 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 { versionNum: parseFloat(versionStr), isElasticsearchVersion }; } else { return { versionNum: parseFloat(versionStr.substring(0, secondDot)), isElasticsearchVersion }; } } catch { throw new Error(`Invalid engine version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); } } function selectSubnets(vpc: ec2.IVpc, vpcSubnets: ec2.SubnetSelection[]): ec2.ISubnet[] { const selected = []; for (const selection of vpcSubnets) { selected.push(...vpc.selectSubnets(selection).subnets); } return selected; } /** * Check if any of the subnets are pending lookups. If so, the zone awareness check should be skipped, otherwise it will always throw an error * * @param vpc The vpc to which the subnets apply * @param vpcSubnets The vpc subnets that should be checked * @returns true if there are pending lookups for the subnets */ function zoneAwarenessCheckShouldBeSkipped(vpc: ec2.IVpc, vpcSubnets: ec2.SubnetSelection[]): boolean { for (const selection of vpcSubnets) { if (vpc.selectSubnets(selection).isPendingLookup) { return true; } } return false; } /** * Initializes an instance type. * * @param defaultInstanceType Default instance type which is used if no instance type is provided * @param instanceType Instance type * @returns Instance type in lowercase (if provided) or default instance type */ function initializeInstanceType(defaultInstanceType: string, instanceType?: string): string { if (instanceType) { return cdk.Token.isUnresolved(instanceType) ? instanceType : instanceType.toLowerCase(); } else { return defaultInstanceType; } } /** * Format instance types list for error messages. * * @param instanceTypes List of instance types to format * @param conjunction Word to use as the conjunction (e.g., 'and', 'or') * @returns Formatted instance types list for error messages */ function formatInstanceTypesList(instanceTypes: string[], conjunction: string): string { return instanceTypes.map((type) => type.toUpperCase()).join(', ').replace(/, ([^,]*)$/, ` ${conjunction} $1`); }