packages/constructs/L3/analytics/opensearch-l3-construct/lib/opensearch-l3-construct.ts (227 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaLogGroup } from '@aws-mdaa/cloudwatch-constructs'; import { MdaaSecurityGroup, MdaaSecurityGroupProps, MdaaSecurityGroupRuleProps } from '@aws-mdaa/ec2-constructs'; import { EventBridgeHelper, EventBridgeRuleProps } from '@aws-mdaa/eventbridge-helper'; import { MdaaResolvableRole, MdaaRoleRef } from '@aws-mdaa/iam-role-helper'; import { MdaaKmsKey } from '@aws-mdaa/kms-constructs'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { IMdaaResourceNaming } from '@aws-mdaa/naming'; import { MdaaOpensearchDomain, MdaaOpensearchDomainProps } from '@aws-mdaa/opensearch-constructs'; import { MdaaSnsTopic } from '@aws-mdaa/sns-constructs'; import { aws_events_targets } from 'aws-cdk-lib'; import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Protocol, Subnet, Vpc } from 'aws-cdk-lib/aws-ec2'; import { Effect, PolicyStatement, PolicyStatementProps, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { IKey } from 'aws-cdk-lib/aws-kms'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import { CapacityConfig, EbsOptions, EngineVersion, ZoneAwarenessConfig } from 'aws-cdk-lib/aws-opensearchservice'; import { HostedZone } from 'aws-cdk-lib/aws-route53'; import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'; import { Construct } from 'constructs'; export interface SecurityGroupIngressProps { /** * CIDR range of the ingres definition */ readonly ipv4?: string[]; /** * Security Group ID of the ingres definition */ readonly sg?: string[]; } export interface SubnetConfig { readonly subnetId: string; readonly availabilityZone: string; } export interface CustomEndpointConfig { /** * Required if customeEndpoint section is specified. * Fully Qualified Domain Name */ readonly domainName: string; /** * Optional. A certificate will be created in ACM if not specified. */ readonly acmCertificateArn?: string; /** * Optional. Private hosted Zone configuration will not be setup (CName record). */ readonly route53HostedZoneEnabled?: boolean; /** * Optional. Domain Name used in the hosted zone. */ readonly route53HostedZoneDomainName?: string; } export interface OpensearchDomainProps { /** * Required. ARN of Data Admin role. This role will be granted admin access to Opensearch Dashboard to update SAML configurations via web interface */ readonly dataAdminRole: MdaaRoleRef; /** * Required. Functional Name of Opensearch Domain. * This will be prefixed as per MDAA naming convention. * If resultant name is longer than 28 characters, a randomly generated ID will be suffixed to truncated name. */ readonly opensearchDomainName: string; /** * Optional. Custom endpoint configuration. */ readonly customEndpoint?: CustomEndpointConfig; /** * Required. ID of VPC in which Opensearch domain will be created. */ readonly vpcId: string; /** * Required. ID(s) of subnets in which Opensearch domain will be created. * Make sure the number of subnets specified is same as or more than the number of AZs speceified in zoneAwareness configuration and span across as many AZs. */ readonly subnets: SubnetConfig[]; /** * List of security group ingress properties */ readonly securityGroupIngress: SecurityGroupIngressProps; /** * Optional. Opensearch cluster will enable shard distribution across 2 or 3 zones as specified. */ readonly zoneAwareness?: ZoneAwarenessConfig; /** * Required. Opensearch cluster node configurations. */ readonly capacity: CapacityConfig; /** * Required. EBS storage configuration for cluster nodes. */ readonly ebs: EbsOptions; /** * Required. Hour of day when automated snapshot creation will start */ readonly automatedSnapshotStartHour: number; /** * Required. version of Opensearch engine to provision in format x.y where x= major version, y=minor version. https://docs.aws.amazon.com/opensearch-service/latest/developerguide/what-is.html#choosing-version */ readonly opensearchEngineVersion: string; /** * Required. Allow/Disallow automatic version upgrades. */ readonly enableVersionUpgrade: boolean; /** * Optional. Domain access policies. */ readonly accessPolicies: PolicyStatementProps[]; /** * Event notification configuration */ readonly eventNotifications?: EventNotificationsProps; } export interface EventNotificationsProps { readonly email?: string[]; } export interface SuppressionProps { readonly id: string; readonly reason: string; } export interface OpensearchL3ConstructProps extends MdaaL3ConstructProps { readonly domain: OpensearchDomainProps; } //This stack creates all of the resources required for a Data Warehouse export class OpensearchL3Construct extends MdaaL3Construct { protected readonly props: OpensearchL3ConstructProps; private dataAdminRole: MdaaResolvableRole; private opensearchDomainKmsKey: MdaaKmsKey; private logGroup: MdaaLogGroup; constructor(scope: Construct, id: string, props: OpensearchL3ConstructProps) { super(scope, id, props); this.props = props; const azIds = this.props.domain.subnets.map(s => s.availabilityZone); const subnetIds = this.props.domain.subnets.map(s => s.subnetId); const subnets = this.props.domain.subnets.map(s => Subnet.fromSubnetAttributes(this, 'subnet-'.concat(s.subnetId), s), ); const vpc = Vpc.fromVpcAttributes(this.scope, `domain-vpc`, { vpcId: this.props.domain.vpcId, availabilityZones: azIds, privateSubnetIds: subnetIds, }); const securityGroupIngress: MdaaSecurityGroupRuleProps = { ipv4: this.props.domain.securityGroupIngress.ipv4?.map(x => { return { cidr: x, port: 443, protocol: Protocol.TCP, description: `https Ingress for IPV4 CIDR ${x}` }; }), sg: this.props.domain.securityGroupIngress.sg?.map(x => { return { sgId: x, port: 443, protocol: Protocol.TCP, description: `https Ingress for SG ${x}` }; }), }; const securityGroupProps: MdaaSecurityGroupProps = { vpc: vpc, naming: this.props.naming, ingressRules: securityGroupIngress, }; const securityGroup = new MdaaSecurityGroup(this, 'domain-sg', securityGroupProps); this.dataAdminRole = this.props.roleHelper.resolveRoleRefWithRefId(this.props.domain.dataAdminRole, 'DataAdmin'); this.opensearchDomainKmsKey = this.createOpensearchDomainKMSKey(); this.logGroup = this.createLogGroup(this.opensearchDomainKmsKey, props.domain.opensearchDomainName, props.naming); const certificate = this.props.domain.customEndpoint != undefined && this.props.domain.customEndpoint.acmCertificateArn != undefined ? Certificate.fromCertificateArn( this.scope, `opensearch-custom-endpoint-certificate-${this.props.domain.opensearchDomainName}`, this.props.domain.customEndpoint?.acmCertificateArn, ) : undefined; const hostedZoneProviderProps = this.props.domain.customEndpoint != undefined && this.props.domain.customEndpoint.route53HostedZoneDomainName != undefined ? { domainName: this.props.domain.customEndpoint.route53HostedZoneDomainName, privateZone: true, vpcId: this.props.domain.vpcId, } : undefined; const hostedZone = hostedZoneProviderProps != undefined ? HostedZone.fromLookup( this.scope, `opensearch-custom-endpoint-hosted-zone-${this.props.domain.opensearchDomainName}`, hostedZoneProviderProps, ) : undefined; const domainL2Props: MdaaOpensearchDomainProps = { masterUserRoleArn: this.dataAdminRole.arn(), version: EngineVersion.openSearch(this.props.domain.opensearchEngineVersion), opensearchDomainName: this.props.naming.props.moduleName, enableVersionUpgrade: this.props.domain.enableVersionUpgrade, encryptionKey: this.opensearchDomainKmsKey, vpc: vpc, vpcSubnets: [{ availabilityZones: azIds, subnets: subnets }], securityGroups: [securityGroup], zoneAwareness: this.props.domain.zoneAwareness ? this.props.domain.zoneAwareness : {}, capacity: this.props.domain.capacity, ebs: this.props.domain.ebs ? this.props.domain.ebs : {}, customEndpoint: this.props.domain.customEndpoint ? { domainName: this.props.domain.customEndpoint.domainName, certificate: certificate, hostedZone: hostedZone } : undefined, automatedSnapshotStartHour: this.props.domain.automatedSnapshotStartHour, accessPolicies: this.props.domain.accessPolicies.map(x => new PolicyStatement(x)), naming: this.props.naming, logGroup: this.logGroup, }; //Create the domain const domain = new MdaaOpensearchDomain( this.scope, `opensearch-domain-${props.domain.opensearchDomainName}`, domainL2Props, ); if (props.domain.eventNotifications) { this.createEventNotifications( this.props.domain.opensearchDomainName, domain, this.opensearchDomainKmsKey, props.domain.eventNotifications, ); } } private createEventNotifications( domainName: string, domain: MdaaOpensearchDomain, domainKmsKey: IKey, eventNotifications: EventNotificationsProps, ) { //Create Rule const ruleProps: EventBridgeRuleProps = { description: `Matches OpenSearch events for domain ${domainName}`, eventPattern: { source: ['aws.es'], resources: [domain.domainArn], }, }; const rule = EventBridgeHelper.createEventRule( this.scope, this.props.naming, `${domainName}-opensearch-events`, ruleProps, ); //Create Topic const topic = new MdaaSnsTopic(this.scope, `domain-events-topic`, { naming: this.props.naming, topicName: `${domainName}-opensearch-events`, masterKey: domainKmsKey, }); //Add email subs eventNotifications?.email?.forEach(email => { topic.addSubscription(new EmailSubscription(email.trim())); }); //Create DLQ const dlq = EventBridgeHelper.createDlq( this.scope, this.props.naming, `${domainName}-opensearch-events`, domainKmsKey, ); //Create Target const target = new aws_events_targets.SnsTopic(topic, { deadLetterQueue: dlq, }); //Add Target rule.addTarget(target); } private createOpensearchDomainKMSKey(): MdaaKmsKey { const kmsKey = new MdaaKmsKey(this.scope, 'opensearch-domain-key', { alias: 'opensearch-domain', naming: this.props.naming, keyAdminRoleIds: [this.dataAdminRole.id()], }); const AllowOpensearchLogGroupEncryption = new PolicyStatement({ sid: 'AllowOpensearchLogGroupEncryption', effect: Effect.ALLOW, resources: ['*'], actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], principals: [new ServicePrincipal(`logs.${this.region}.amazonaws.com`)], conditions: { ArnLike: { 'kms:EncryptionContext:aws:logs:arn': `arn:${this.partition}:logs:${this.region}:${this.account}:*`, }, }, }); kmsKey.addToResourcePolicy(AllowOpensearchLogGroupEncryption); return kmsKey; } private createLogGroup( encryptionKey: MdaaKmsKey, opensearchDomainName: string, naming: IMdaaResourceNaming, ): MdaaLogGroup { const logGroupProps = { encryptionKey: encryptionKey, logGroupNamePathPrefix: '/aws/opensearch-logs/', logGroupName: opensearchDomainName, retention: RetentionDays.INFINITE, naming: naming, }; return new MdaaLogGroup(this.scope, `cloudwatch-log-group-${opensearchDomainName}`, logGroupProps); } }