constructor()

in packages/aws-cdk-lib/aws-elasticsearch/lib/domain.ts [1465:1927]


  constructor(scope: Construct, id: string, props: DomainProps) {
    super(scope, id, {
      physicalName: props.domainName,
    });
    // Enhanced CDK Analytics Telemetry
    addConstructMetadata(this, props);

    const defaultInstanceType = 'r5.large.elasticsearch';
    const warmDefaultInstanceType = 'ultrawarm1.medium.elasticsearch';

    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;

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

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

    if ([dedicatedMasterType, instanceType, warmType].some(t => (!cdk.Token.isUnresolved(t) && !t.endsWith('.elasticsearch')))) {
      throw new Error('Master, data and UltraWarm node instance types must end with ".elasticsearch".');
    }

    if (!cdk.Token.isUnresolved(warmType) && !warmType.startsWith('ultrawarm')) {
      throw new Error('UltraWarm node instance type must start with "ultrawarm".');
    }

    const elasticsearchVersion = props.version.version;
    const elasticsearchVersionNum = parseVersion(props.version);

    if (
      elasticsearchVersionNum <= 7.7 &&
      ![
        1.5, 2.3, 5.1, 5.3, 5.5, 5.6, 6.0,
        6.2, 6.3, 6.4, 6.5, 6.7, 6.8, 7.1, 7.4,
        7.7,
      ].includes(elasticsearchVersionNum)
    ) {
      throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`);
    }

    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 version, per
    // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-features-by-version.html
    if (elasticsearchVersionNum < 5.1) {
      if (props.logging?.appLogEnabled) {
        throw new Error('Error logs publishing requires Elasticsearch version 5.1 or later.');
      }
      if (props.encryptionAtRest?.enabled) {
        throw new Error('Encryption of data at rest requires Elasticsearch version 5.1 or later.');
      }
      if (props.cognitoKibanaAuth != null) {
        throw new Error('Cognito authentication for Kibana requires Elasticsearch version 5.1 or later.');
      }
      if (isSomeInstanceType('c5', 'i3', 'm5', 'r5')) {
        throw new Error('C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later.');
      }
    }

    if (elasticsearchVersionNum < 6.0) {
      if (props.nodeToNodeEncryption) {
        throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later.');
      }
    }

    if (elasticsearchVersionNum < 6.7) {
      if (unsignedBasicAuthEnabled) {
        throw new Error('Using unsigned basic auth requires Elasticsearch version 6.7 or later.');
      }
      if (advancedSecurityEnabled) {
        throw new Error('Fine-grained access control requires Elasticsearch version 6.7 or later.');
      }
    }

    if (elasticsearchVersionNum < 6.8 && warmEnabled) {
      throw new Error('UltraWarm requires Elasticsearch 6.8 or later.');
    }

    // Validate against instance type restrictions, per
    // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html
    if (isSomeInstanceType('i3', 'r6gd') && ebsEnabled) {
      throw new Error('I3 and R6GD instance types do not support EBS storage volumes.');
    }

    if (isSomeInstanceType('m3', 'r3', 't2') && encryptionAtRestEnabled) {
      throw new Error('M3, R3, and T2 instance types do not support encryption of data at rest.');
    }

    if (isInstanceType('t2.micro') && elasticsearchVersionNum > 2.3) {
      throw new Error('The t2.micro.elasticsearch instance type supports only Elasticsearch 1.5 and 2.3.');
    }

    if (isSomeInstanceType('t2', 't3') && warmEnabled) {
      throw new Error('T2 and T3 instance types do not support UltraWarm storage.');
    }

    // Only R3, I3 and r6gd support instance storage, per
    // https://aws.amazon.com/elasticsearch-service/pricing/
    if (!ebsEnabled && !isEveryDatanodeInstanceType('r3', 'i3', 'r6gd')) {
      throw new Error('EBS volumes are required when using instance types other than r3, i3 or r6gd.');
    }

    // 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/elasticsearch-service/latest/developerguide/ultrawarm.html
    if (warmEnabled && !dedicatedMasterEnabled) {
      throw new Error('Dedicated master node is required when UltraWarm storage is enabled.');
    }

    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[] = [];

    if (props.logging?.slowSearchLogEnabled) {
      this.slowSearchLogGroup = props.logging.slowSearchLogGroup ??
        new logs.LogGroup(this, 'SlowSearchLogs', {
          retention: logs.RetentionDays.ONE_MONTH,
        });

      logGroups.push(this.slowSearchLogGroup);
    }

    if (props.logging?.slowIndexLogEnabled) {
      this.slowIndexLogGroup = props.logging.slowIndexLogGroup ??
        new logs.LogGroup(this, 'SlowIndexLogs', {
          retention: logs.RetentionDays.ONE_MONTH,
        });

      logGroups.push(this.slowIndexLogGroup);
    }

    if (props.logging?.appLogEnabled) {
      this.appLogGroup = props.logging.appLogGroup ??
        new logs.LogGroup(this, 'AppLogs', {
          retention: logs.RetentionDays.ONE_MONTH,
        });

      logGroups.push(this.appLogGroup);
    }

    if (props.logging?.auditLogEnabled) {
      this.auditLogGroup = props.logging.auditLogGroup ??
        new logs.LogGroup(this, 'AuditLogs', {
          retention: logs.RetentionDays.ONE_MONTH,
        });

      logGroups.push(this.auditLogGroup);
    }

    let logGroupResourcePolicy: LogGroupResourcePolicy | null = null;
    if (logGroups.length > 0) {
      const logPolicyStatement = new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ['logs:PutLogEvents', 'logs:CreateLogStream'],
        resources: logGroups.map((lg) => lg.logGroupArn),
        principals: [new iam.ServicePrincipal('es.amazonaws.com')],
      });

      // Use a custom resource to set the log group resource policy since it is not supported by CDK and cfn.
      // https://github.com/aws/aws-cdk/issues/5343
      logGroupResourcePolicy = new LogGroupResourcePolicy(this, `ESLogGroupPolicy${this.node.addr}`, {
        // create a cloudwatch logs resource policy name that is unique to this domain instance
        policyName: `ESLogPolicy${this.node.addr}`,
        policyStatements: [logPolicyStatement],
      });
    }

    const logPublishing: Record<string, any> = {};

    if (this.appLogGroup) {
      logPublishing.ES_APPLICATION_LOGS = {
        enabled: true,
        cloudWatchLogsLogGroupArn: this.appLogGroup.logGroupArn,
      };
    }

    if (this.slowSearchLogGroup) {
      logPublishing.SEARCH_SLOW_LOGS = {
        enabled: true,
        cloudWatchLogsLogGroupArn: this.slowSearchLogGroup.logGroupArn,
      };
    }

    if (this.slowIndexLogGroup) {
      logPublishing.INDEX_SLOW_LOGS = {
        enabled: true,
        cloudWatchLogsLogGroupArn: this.slowIndexLogGroup.logGroupArn,
      };
    }

    if (this.auditLogGroup) {
      logPublishing.AUDIT_LOGS = {
        enabled: this.auditLogGroup != null,
        cloudWatchLogsLogGroupArn: this.auditLogGroup?.logGroupArn,
      };
    }

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

    // Create the domain
    this.domain = new CfnDomain(this, 'Resource', {
      domainName: this.physicalName,
      elasticsearchVersion,
      elasticsearchClusterConfig: {
        dedicatedMasterEnabled,
        dedicatedMasterCount: dedicatedMasterEnabled
          ? dedicatedMasterCount
          : undefined,
        dedicatedMasterType: dedicatedMasterEnabled
          ? dedicatedMasterType
          : undefined,
        instanceCount,
        instanceType,
        warmEnabled: warmEnabled
          ? warmEnabled
          : undefined,
        warmCount: warmEnabled
          ? warmCount
          : undefined,
        warmType: warmEnabled
          ? warmType
          : undefined,
        zoneAwarenessEnabled,
        zoneAwarenessConfig: zoneAwarenessEnabled
          ? { availabilityZoneCount }
          : undefined,
      },
      ebsOptions: {
        ebsEnabled,
        volumeSize: ebsEnabled ? volumeSize : undefined,
        volumeType: ebsEnabled ? volumeType : undefined,
        iops: ebsEnabled ? props.ebs?.iops : undefined,
      },
      encryptionAtRestOptions: {
        enabled: encryptionAtRestEnabled,
        kmsKeyId: encryptionAtRestEnabled
          ? props.encryptionAtRest?.kmsKey?.keyId
          : undefined,
      },
      nodeToNodeEncryptionOptions: { enabled: nodeToNodeEncryptionEnabled },
      logPublishingOptions: logPublishing,
      cognitoOptions: {
        enabled: props.cognitoKibanaAuth != null,
        identityPoolId: props.cognitoKibanaAuth?.identityPoolId,
        roleArn: props.cognitoKibanaAuth?.role.roleArn,
        userPoolId: props.cognitoKibanaAuth?.userPoolId,
      },
      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
          },
        }
        : undefined,
      advancedOptions: props.advancedOptions,
    });
    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.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);
    }
  }