diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java index 50734731..5c40974f 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java @@ -1124,7 +1124,7 @@ protected void setupAwsServiceRoles() { List serviceRoles = new ArrayList<>(Arrays.asList("elasticloadbalancing.amazonaws.com", "ecs.amazonaws.com", "ecs.application-autoscaling.amazonaws.com", "rds.amazonaws.com", - "fsx.amazonaws.com", "autoscaling.amazonaws.com") + "fsx.amazonaws.com", "autoscaling.amazonaws.com", "es.amazonaws.com") ); for (String serviceRole : serviceRoles) { if (existingRoles.contains("/aws-service-role/" + serviceRole + "/")) { diff --git a/resources/saas-boost-svc-onboarding.yaml b/resources/saas-boost-svc-onboarding.yaml index 7868fac4..7a9fdff6 100644 --- a/resources/saas-boost-svc-onboarding.yaml +++ b/resources/saas-boost-svc-onboarding.yaml @@ -568,6 +568,15 @@ Resources: - ec2:AuthorizeSecurityGroupEgress Resource: - !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:security-group/* + # Elasticsearch onboarding policy + - Effect: Allow + Action: + - es:AddTags + - es:CreateElasticsearchDomain + - es:DescribeElasticsearchDomain + - es:CreateElasticsearchServiceRole + Resource: + - !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/tenant-* # This is for the AD stack -- need to figure out why update DNS wants to delete the directory - Effect: Allow Action: @@ -887,6 +896,15 @@ Resources: - ec2:RevokeSecurityGroupEgress Resource: - !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:security-group/* + # Elasticsearch onboarding delete policy + - Effect: Allow + Action: + - es:DeleteElasticsearchDomain + - es:DeleteElasticsearchServiceRole + - es:DescribeElasticsearchDomain + - es:RemoveTags + Resource: + - !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/tenant-* OnboardingServiceGetAllLogs: Type: AWS::Logs::LogGroup Properties: diff --git a/resources/tenant-onboarding-es.yaml b/resources/tenant-onboarding-es.yaml new file mode 100644 index 00000000..b2c4ba71 --- /dev/null +++ b/resources/tenant-onboarding-es.yaml @@ -0,0 +1,157 @@ +# Copyright 2020 Daniel Cortez Stevenson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Original: https://gist.github.com/daniel-cortez-stevenson/ +# modified: https://github.com/AffiTheCreator +# +--- +AWSTemplateFormatVersion: 2010-09-09 +Description: SaaS Boost Tenant Onboarding AWS Elasticsearch Service (ES) +Parameters: + TenantId: + Description: The GUID for the tenant + Type: String + TenantSubDomain: + Description: The subdomain for this tenant + Type: String + DomainName: + Description: The hosted zone domain name + Type: String + ECSSecurityGroup: + Description: Source security group for ECS to access EFS + Type: AWS::EC2::SecurityGroup::Id + CustomEndpointEnabled: + Type: String + AllowedValues: ['true', 'false'] + Default: 'false' + EnforceHTTPS: + Type: String + AllowedValues: ['true', 'false'] + Default: 'false' + ESPortHTTP: + Type: String + Default: '80' + ESPortHTTPS: + Type: String + Default: '443' + TLSSecurityPolicy: + Type: String + AllowedValues: ['true', 'false'] + Default: 'false' + ElasticsearchName: + Description: The name of the AWS Elasticsearch Service deployment. + Type: String + ConstraintDescription: Must be a valid AWS ES domain name prefix. The name must start with a + lowercase letter and must be between 3 and 28 characters. Valid characters + are a-z (lowercase only), 0-9, and - (hyphen). + AllowedPattern: '[a-z][a-z0-9\\-]+' + ElasticsearchVersion: + Default: '6.2' + Type: String + ConstraintDescription: Must be an allowed AWS ES version (Major.Minor) + AllowedValues: ['7.10', '7.9', '7.8', '7.7', '7.4', '7.1', '6.8', '6.7', '6.5', '6.4', '6.3', '6.2', '6.0', '5.6', '5.5'] # aws es list-elasticsearch-versions --query "ElasticsearchVersions[]" + ElasticsearchDataInstanceType: + Description: Instance type for data nodes. + Type: String + Default: 'r5.2xlarge.elasticsearch' + NumberOfMasterNodes: + Description: How many dedicated master nodes you want to have. 3 is recommended. + Type: Number + Default: 3 + NumberOfDataNodes: + Description: How many data nodes you want to have. Multiples of your number of availability zones (2) is recommended. + Type: Number + Default: '2' + MinValue: '1' + MaxValue: '20' + EBSEnabled: + Description: 'Specifies whether Amazon EBS volumes are attached to data nodes in the Amazon ES domain (some instance types come with instance store that you can use instead).' + Type: String + AllowedValues: ['true', 'false'] + Default: 'true' + EBSVolumeSize: + Description: 'The size 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.' + Type: Number + Default: '10' + Username: + Description: Master username for ElasticSearch + Type: String + Password: + Description: Master user password for ElasticSearch + Type: String +Conditions: + ShouldEnforceHTTPS: !Equals [ !Ref "EnforceHTTPS", "true" ] +Resources: + ElasticsearchDomain: + Type: AWS::Elasticsearch::Domain + Properties: + ElasticsearchVersion: !Ref ElasticsearchVersion + DomainName: !Ref ElasticsearchName + DomainEndpointOptions: + EnforceHTTPS: !Ref EnforceHTTPS + EBSOptions: + EBSEnabled: !Ref EBSEnabled + Iops: 0 + VolumeSize: !Ref EBSVolumeSize + VolumeType: standard + ElasticsearchClusterConfig: + InstanceCount: !Ref NumberOfDataNodes + InstanceType: !Ref ElasticsearchDataInstanceType + ZoneAwarenessEnabled: false + AdvancedOptions: + rest.action.multi.allow_explicit_index: "true" + AdvancedSecurityOptions: + Enabled: true + InternalUserDatabaseEnabled: true + MasterUserOptions: + MasterUserName: !Ref Username + MasterUserPassword: !Ref Password + EncryptionAtRestOptions: + Enabled: true + NodeToNodeEncryptionOptions: + Enabled: true + SnapshotOptions: + AutomatedSnapshotStartHour: 0 + AccessPolicies: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: '*' + Action: "es:*" + Resource: !Sub "arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticsearchName}/*" + Tags: + - Key: Tenant + Value: !Ref TenantId +Outputs: + Arn: + Value: !GetAtt ElasticsearchDomain.Arn + ESDomainArn: + Value: !GetAtt ElasticsearchDomain.DomainArn + ESDomainEndpoint: + Description: Elasticsearch endpoint + Value: !GetAtt ElasticsearchDomain.DomainEndpoint + ESClusterName: + Description: The Elasticsearch Cluster + Value: !Ref ElasticsearchName + ESClusterPort: + Description: Listening Port of Elasticsearch cluster + Value: !Ref ESPortHTTPS + ESUsername: + Description: Elasticsearch login username + Value: !Ref Username + ESPassword: + Description: Elasticsearch login password + Value: !Ref Password +... diff --git a/resources/tenant-onboarding.yaml b/resources/tenant-onboarding.yaml index c90f7117..389e1899 100644 --- a/resources/tenant-onboarding.yaml +++ b/resources/tenant-onboarding.yaml @@ -117,6 +117,122 @@ Parameters: - AFTER_60_DAYS - AFTER_90_DAYS Default: NEVER + UseES: + Description: Deploy the Elasticsearch nested stack? + Type: String + AllowedValues: [ 'true', 'false' ] + Default: 'true' + ElasticsearchVersion: + Default: '7.10' + Type: String + ConstraintDescription: Must be an allowed AWS ES version (Major.Minor) + AllowedValues: ['7.10', '7.9', '7.8', '7.7', '7.4', '7.1', '6.8', '6.7', '6.5', '6.4', '6.3', '6.2', '6.0', '5.6', '5.5' ] + ElasticsearchMasterInstanceType: + Description: Instance type for master nodes. + Type: String + Default: t2.small.elasticsearch + AllowedValues: + - r5.large.elasticsearch + - r5.xlarge.elasticsearch + - r5.2xlarge.elasticsearch + - r5.4xlarge.elasticsearch + - r5.12xlarge.elasticsearch + - t2.small.elasticsearch + - t2.medium.elasticsearch + - c4.large.elasticsearch + - c4.xlarge.elasticsearch + - c4.2xlarge.elasticsearch + - c4.4xlarge.elasticsearch + - c4.8xlarge.elasticsearch + - i2.xlarge.elasticsearch + - i2.2xlarge.elasticsearch + - m4.large.elasticsearch + - m4.xlarge.elasticsearch + - m4.2xlarge.elasticsearch + - m4.4xlarge.elasticsearch + - m4.10xlarge.elasticsearch + - r4.large.elasticsearch + - r4.xlarge.elasticsearch + - r4.2xlarge.elasticsearch + - r4.4xlarge.elasticsearch + - r4.8xlarge.elasticsearch + - r4.16xlarge.elasticsearch + - m3.medium.elasticsearch + - m3.large.elasticsearch + - m3.xlarge.elasticsearch + - m3.2xlarge.elasticsearch + - r3.large.elasticsearch + - r3.xlarge.elasticsearch + - r3.2xlarge.elasticsearch + - r3.4xlarge.elasticsearch + - r3.8xlarge.elasticsearch + ElasticsearchDataInstanceType: + Description: Instance type for data nodes. + Type: String + Default: r5.2xlarge.elasticsearch + AllowedValues: + - r5.large.elasticsearch + - r5.xlarge.elasticsearch + - r5.2xlarge.elasticsearch + - r5.4xlarge.elasticsearch + - r5.12xlarge.elasticsearch + - t2.small.elasticsearch + - t2.medium.elasticsearch + - c4.large.elasticsearch + - c4.xlarge.elasticsearch + - c4.2xlarge.elasticsearch + - c4.4xlarge.elasticsearch + - c4.8xlarge.elasticsearch + - i2.xlarge.elasticsearch + - i2.2xlarge.elasticsearch + - m4.large.elasticsearch + - m4.xlarge.elasticsearch + - m4.2xlarge.elasticsearch + - m4.4xlarge.elasticsearch + - m4.10xlarge.elasticsearch + - r4.large.elasticsearch + - r4.xlarge.elasticsearch + - r4.2xlarge.elasticsearch + - r4.4xlarge.elasticsearch + - r4.8xlarge.elasticsearch + - r4.16xlarge.elasticsearch + - m3.medium.elasticsearch + - m3.large.elasticsearch + - m3.xlarge.elasticsearch + - m3.2xlarge.elasticsearch + - r3.large.elasticsearch + - r3.xlarge.elasticsearch + - r3.2xlarge.elasticsearch + - r3.4xlarge.elasticsearch + - r3.8xlarge.elasticsearch + NumberOfMasterNodes: + Description: How many dedicated master nodes you want to have. 3 is recommended. + Type: Number + Default: 3 + AllowedValues: + - 3 + - 5 + NumberOfDataNodes: + Description: How many data nodes you want to have. Multiples of your number of availability zones (2) is recommended. + Type: Number + Default: 2 + MinValue: 1 + MaxValue: 20 + CustomEndpointEnabled: + Type: String + AllowedValues: [ 'true', 'false' ] + Default: 'true' + EnforceHTTPS: + Type: String + AllowedValues: [ 'true', 'false' ] + Default: 'true' + ESPortHTTP: + Type: String + Default: '80' + ESPortHTTPS: + Type: String + Default: '443' + # RDS config UseRDS: Description: Deploy the RDS nested stack? Type: String @@ -215,6 +331,7 @@ Conditions: HasDomainName: !Not [!Equals [!Ref DomainName, '']] ProvisionEFS: !Equals [!Ref UseEFS, 'true'] ProvisionRDS: !Equals [!Ref UseRDS, 'true'] + ProvisionES: !Equals [!Ref UseES, 'true'] WindowsOS: !Not [!Equals [!Ref DockerHostOS, 'LINUX']] HasCertificate: !Not [!Equals [!Ref SSLCertArnParam, '']] NoCertificate: !Not [Condition: HasCertificate] @@ -587,6 +704,11 @@ Resources: - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/DB_HOST - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/DB_NAME - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/METRICS_STREAM + # Makes more sense to add parameter to tenant rather than in saas boost environment + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/tenant/${TenantId}/ES_CLUSTER_NAME + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/tenant/${TenantId}/ES_CLUSTER_ENDPOINT + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/tenant/${TenantId}/ES_CLUSTER_PORT + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/tenant/${TenantId}/ES_CLUSTER_PORT_DEFAULT - Effect: Allow Action: - fsx:DescribeFileSystems @@ -692,8 +814,24 @@ Resources: Value: !GetAtt rds.Outputs.RdsEndpoint - Name: DB_NAME Value: !Ref RDSDatabase + - Name: ES_CLUSTER_ENDPOINT + Value: !GetAtt es.Outputs.ESDomainEndpoint + - Name: ES_CLUSTER_PORT + Value: !GetAtt es.Outputs.ESClusterPort + - Name: ES_USERNAME + Value: !GetAtt es.Outputs.ESUsername + - Name: ES_PASSWORD + Value: !GetAtt es.Outputs.ESPassword - - Name: SAAS_BOOST_BUCKET Value: !Ref SaaSBoostBucket + - Name: ES_CLUSTER_ENDPOINT + Value: !GetAtt es.Outputs.ESDomainEndpoint + - Name: ES_CLUSTER_PORT + Value: !GetAtt es.Outputs.ESClusterPort + - Name: ES_USERNAME + Value: !GetAtt es.Outputs.ESUsername + - Name: ES_PASSWORD + Value: !GetAtt es.Outputs.ESPassword MountPoints: !If - ProvisionEFS @@ -772,7 +910,7 @@ Resources: Scheme: internet-facing LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds - Value: '30' + Value: '4000' # set to max time to prevent gRPC channels from closing - Key: access_logs.s3.enabled Value: 'true' - Key: access_logs.s3.bucket @@ -1417,6 +1555,29 @@ Resources: ECSSecurityGroup: !Ref ECSSecurityGroup EncryptEFS: !Ref EncryptEFS EFSLifecyclePolicy: !Ref EFSLifecyclePolicy + # ElasticSearch Extension + es: + Type: AWS::CloudFormation::Stack + Condition: ProvisionES + Properties: + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.amazonaws.com/tenant-onboarding-es.yaml + TimeoutInMinutes: 30 + Parameters: + CustomEndpointEnabled: !Ref CustomEndpointEnabled + TenantId: !Ref TenantId + TenantSubDomain: !Ref TenantSubDomain + DomainName: !Ref DomainName + ElasticsearchVersion: !Ref ElasticsearchVersion + ECSSecurityGroup: !Ref ECSSecurityGroup + ElasticsearchDataInstanceType: !Ref ElasticsearchDataInstanceType + NumberOfDataNodes: !Ref NumberOfDataNodes + EnforceHTTPS: !Ref EnforceHTTPS + ElasticsearchName: + Fn::Join: ['', ['tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-es-cluster']] + Username: + !Join ['', ['esuser', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + Password: + !Join ['-', ['E0', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] rds: Type: AWS::CloudFormation::Stack Condition: ProvisionRDS