constructor()

in packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts [1449:2099]


  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);
    }
  }