---
AWSTemplateFormatVersion: "2010-09-09"
Description: This stack deploys the baseline infrastructure to be used for AmazonDocumentDB

Parameters:

  DocDBIdentifier:
    Type: String
    Description: The cluster identifier. This parameter is stored as a lowercase string. (the first character must be a letter. Cannot end with a hyphen or contain two consecutive hyphens.)
    Default: transactcluster

Mappings:

  SubnetConfig:
    VPC:
      CIDR: '10.0.0.0/16'
    PublicOne:
      CIDR: '10.0.0.0/24'
    PublicTwo:
      CIDR: '10.0.1.0/24'
    PublicThree:
      CIDR: '10.0.2.0/24'
    PrivateOne:
      CIDR: '10.0.100.0/24'
    PrivateTwo:
      CIDR: '10.0.101.0/24'
    PrivateThree:
      CIDR: '10.0.102.0/24'
  Clusterparameters:
    DocumentDB: 
      DatabaseName: "dev"
    Redshift:
      BaseRPU: 32
      PortNumber: 5439

Resources:

  RedshiftRole:
    Type: AWS::IAM::Role
    Properties :
      RoleName: !Join ['-', ['RedshiftImmersionRole', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]]
      AssumeRolePolicyDocument:
        Version : 2012-10-17
        Statement :
          -
            Effect : Allow
            Principal :
              Service :
                - redshift.amazonaws.com
            Action :
              - sts:AssumeRole
          -
            Effect : Allow
            Principal :
              Service :
                - redshift-serverless.amazonaws.com
            Action :
              - sts:AssumeRole
          -
            Effect : Allow
            Principal :
              Service :
                - sagemaker.amazonaws.com
            Action :
              - sts:AssumeRole
          -
            Effect : Allow
            Principal :
              Service :
                - events.amazonaws.com
            Action :
              - sts:AssumeRole
          -
            Effect : Allow
            Principal :
              Service :
                - scheduler.redshift.amazonaws.com
            Action :
              - sts:AssumeRole
      Path : /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/AWSGlueConsoleFullAccess
        - arn:aws:iam::aws:policy/AmazonRedshiftFullAccess
        - arn:aws:iam::aws:policy/AmazonSageMakerFullAccess
        - arn:aws:iam::aws:policy/AmazonKinesisFullAccess

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']
      Tags:
        - Key: Name
          Value: !Sub VPC-${AWS::StackName}

  PublicSubnetOne:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 0
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub PublicOne-${AWS::StackName}

  PublicSubnetTwo:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 1
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub PublicTwo-${AWS::StackName}

  PublicSubnetThree:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 2
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicThree', 'CIDR']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub PublicThree-${AWS::StackName}

  PrivateSubnetOne:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 0
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR']
      Tags:
        - Key: Name
          Value: !Sub PrivateOne-${AWS::StackName}

  PrivateSubnetTwo:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 1
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PrivateTwo', 'CIDR']
      Tags:
        - Key: Name
          Value: !Sub PrivateTwo-${AWS::StackName}

  PrivateSubnetThree:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 2
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PrivateThree', 'CIDR']
      Tags:
        - Key: Name
          Value: !Sub PrivateThree-${AWS::StackName}

  InternetGateway:
    Type: AWS::EC2::InternetGateway
  GatewayAttachement:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: GatewayAttachement
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnetOneRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetOne
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetTwoRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetTwo
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetThreeRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetThree
      RouteTableId: !Ref PublicRouteTable

  NatGatewayOneAttachment:
    Type: AWS::EC2::EIP
    DependsOn: GatewayAttachement
    Properties:
        Domain: vpc
  NatGatewayTwoAttachment:
    Type: AWS::EC2::EIP
    DependsOn: GatewayAttachement
    Properties:
        Domain: vpc
  NatGatewayThreeAttachment:
    Type: AWS::EC2::EIP
    DependsOn: GatewayAttachement
    Properties:
        Domain: vpc
  NatGatewayOne:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayOneAttachment.AllocationId
      SubnetId: !Ref PublicSubnetOne
  NatGatewayTwo:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayTwoAttachment.AllocationId
      SubnetId: !Ref PublicSubnetTwo
  NatGatewayThree:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayThreeAttachment.AllocationId
      SubnetId: !Ref PublicSubnetThree
  PrivateRouteTableOne:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PrivateRouteOne:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTableOne
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayOne
  PrivateRouteTableOneAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTableOne
      SubnetId: !Ref PrivateSubnetOne
  PrivateRouteTableTwo:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PrivateRouteTwo:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTableTwo
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayTwo
  PrivateRouteTableTwoAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTableTwo
      SubnetId: !Ref PrivateSubnetTwo
  PrivateRouteTableThree:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  PrivateRouteThree:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTableThree
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayThree
  PrivateRouteTableThreeAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTableThree
      SubnetId: !Ref PrivateSubnetThree

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group
      SecurityGroupIngress:
      - CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']
        FromPort: !FindInMap [Clusterparameters, "Redshift", "PortNumber"]
        ToPort: !FindInMap [Clusterparameters, "Redshift", "PortNumber"]
        IpProtocol: tcp
      VpcId:
        Ref: VPC
  SecurityGroupSelfReference:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Self Referencing Rule
      FromPort: -1
      IpProtocol: -1
      GroupId: !GetAtt [SecurityGroup, GroupId]
      SourceSecurityGroupId: !GetAtt [SecurityGroup, GroupId]
      ToPort: -1
  secretDocDBAdminUser:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: "Administrator user credentials for DocDB cluster"
      GenerateSecretString:
        SecretStringTemplate: '{"username": "docdbuser"}'
        GenerateStringKey: 'password'
        PasswordLength: 10
        ExcludeCharacters: '="@/\$`&:{}()[];'
  secretsRedshiftAdminUser:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: "Administrator user credentials for Redshift cluster"
      GenerateSecretString:
        SecretStringTemplate: '{"username": "redshiftuser"}'
        GenerateStringKey: 'password'
        PasswordLength: 10
        ExcludePunctuation: true
  RedshiftServerlessNamespace:
    Type: 'AWS::RedshiftServerless::Namespace'
    Properties:
      AdminUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref secretsRedshiftAdminUser, ':SecretString:username}}' ]]
      AdminUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref secretsRedshiftAdminUser, ':SecretString:password}}' ]]
      DbName: !FindInMap [Clusterparameters, "DocumentDB", "DatabaseName"]
      NamespaceName:  !Join ['-', ['namespace-', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]]
      IamRoles: 
        - !GetAtt 'RedshiftRole.Arn'
      DefaultIamRoleArn: !GetAtt 'RedshiftRole.Arn'
  RedshiftServerlessWorkgroup:
    Type: 'AWS::RedshiftServerless::Workgroup'
    Properties:
      WorkgroupName: !Join ['-', ['docdb-rs-', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]]
      NamespaceName:  !Join ['-', ['namespace-', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]]
      BaseCapacity: !FindInMap [Clusterparameters, "Redshift", "BaseRPU"]
      PubliclyAccessible: 'true'
      SubnetIds:
        - Ref: PublicSubnetOne
        - Ref: PublicSubnetTwo
        - Ref: PublicSubnetThree
      SecurityGroupIds:
        - Ref: SecurityGroup
    DependsOn:
      - RedshiftServerlessNamespace  


  SecretDocDBClusterAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment
    Properties:
      SecretId: !Ref secretDocDBAdminUser
      TargetId: !Ref DocumentDBCluster
      TargetType: AWS::DocDB::DBCluster

  LambdaChangeStreamsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      Policies:
        - PolicyName: !Sub '${AWS::StackName}-SecretsManager-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref secretDocDBAdminUser
      Path: /

  AWSCloud9SSMAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
                - cloud9.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCloud9SSMInstanceProfile
      RoleName: !Join ['-', ['AWSCloud9SSMAccessRole', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]]
      Path: /service-role/

  AWSCloud9RolePolicyNew:
    Type: AWS::IAM::ManagedPolicy
    DependsOn: AWSCloud9SSMAccessRole
    Properties:
      Description: Policy for Cloud9 Environments used in Change Stream
      Path: /service-role/
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - ec2:DescribeSubnets
              - ec2:DescribeVpcs
              - ec2:DescribeNetworkInterfaces
              - ec2:DescribeSecurityGroups
              - lambda:GetFunctionConfiguration
              - lambda:GetFunction
              - cloudformation:DescribeChangeSet
              - cloudformation:DescribeStacks
              - cloudformation:ListChangeSets
              - secretsmanager:GetRandomPassword
              - secretsmanager:ListSecrets
              - secretsmanager:GetResourcePolicy
              - secretsmanager:GetSecretValue
              - secretsmanager:DescribeSecret
              - secretsmanager:ListSecretVersionIds
              - secretsmanager:BatchGetSecretValue
            Resource: '*'
          - Effect: Allow
            Action:
              - cloudformation:CreateStack
              - cloudformation:CreateChangeSet
              - cloudformation:ExecuteChangeSet
            Resource:
              !Join
                - ''
                - - 'arn:aws:cloudformation:'
                  - !Ref "AWS::Region"
                  - ':'
                  - !Ref "AWS::AccountId"
                  - ':stack/change-streams-function/*'
          - Effect: Allow
            Action:
              - lambda:CreateFunction
              - lambda:AddPermission
              - lambda:DeleteFunction
              - lambda:RemovePermission
            Resource:
              !Join
                - ''
                - - 'arn:aws:lambda:'
                  - !Ref "AWS::Region"
                  - ':'
                  - !Ref "AWS::AccountId"
                  - ':function:docdb-elasticsearch-replicator'
          - Effect: Allow
            Action: iam:PassRole
            Resource: !GetAtt LambdaChangeStreamsRole.Arn
      Roles:
        - Ref: AWSCloud9SSMAccessRole

  DocumentDBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Amazon DocumentDB Security Group
      VpcId: !Ref VPC
      SecurityGroupIngress:
         - IpProtocol: tcp
           CidrIp: 10.0.0.0/16
           FromPort: 27017
           ToPort: 27017

  DocumentDBSubnetGroup:
    Type: AWS::DocDB::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnet Group for DocumentDB
      SubnetIds:
         - !Ref PrivateSubnetOne
         - !Ref PrivateSubnetTwo
         - !Ref PrivateSubnetThree

  DocumentDBCluster:
    Type: AWS::DocDB::DBCluster
    Properties:
      DBClusterIdentifier: !Ref DocDBIdentifier
      MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref secretDocDBAdminUser, ':SecretString:username}}' ]]
      MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref secretDocDBAdminUser, ':SecretString:password}}' ]]
      DBSubnetGroupName : !Ref DocumentDBSubnetGroup
      StorageEncrypted: true
      EngineVersion: "4.0.0"
      DeletionProtection: false
      Tags:
        - Key: Name
          Value: !Sub DocumentDB-${AWS::StackName}
      VpcSecurityGroupIds:
        - !Ref DocumentDBSecurityGroup
    DependsOn: VPC

  DocumentDBInstanceWriter:
    Type: AWS::DocDB::DBInstance
    Properties:
      DBClusterIdentifier: !Ref DocumentDBCluster
      DBInstanceClass: db.r6g.large
      Tags:
        - Key: Name
          Value: !Sub DocumentDBInstance-${AWS::StackName}

  Cloud9envNew:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      ConnectionType: CONNECT_SSM
      Description: Cloud9 instance to access Cloud9
      InstanceType: t2.micro
      ImageId: amazonlinux-2-x86_64
      Name: DocDBStreaming
      SubnetId: !Ref PrivateSubnetOne


  ElasticSearchSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Amazon DocumentDB Security Group
      VpcId: !Ref VPC
      SecurityGroupIngress:
         - IpProtocol: tcp
           CidrIp: 10.0.0.0/16
           FromPort: 443
           ToPort: 443


  KinesisStreamChange:
    Type: AWS::Kinesis::Stream
    Properties:
      Name: docdbstream
      StreamModeDetails: 
        StreamMode: ON_DEMAND


  DocumentDBLambdaESMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-DocumentDBLambdaESMRole-${AWS::Region}' 
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
        - arn:aws:iam::aws:policy/AmazonKinesisFullAccess
      Policies:
        - PolicyName:  !Sub '${AWS::StackName}-LambdaESMRDS-${AWS::Region}' 
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - rds:DescribeDBClusters
                  - rds:DescribeDBClusterParameters
                  - rds:DescribeDBSubnetGroups
                Resource: '*'
        - PolicyName:  !Sub '${AWS::StackName}-LambdaESMEC2-${AWS::Region}' 
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateNetworkInterface
                  - ec2:DescribeNetworkInterfaces
                  - ec2:DescribeVpcs
                  - ec2:DeleteNetworkInterface
                  - ec2:DescribeSubnets
                  - ec2:DescribeSecurityGroups
                Resource: '*'
        - PolicyName:  !Sub '${AWS::StackName}-LambdaESMKMS-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - kms:Decrypt
                Resource: '*'               
        - PolicyName:  !Sub '${AWS::StackName}-LambdaESMSecretsManager-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref secretDocDBAdminUser
      Path: /

  DocumentDBLambdaESM:
    DependsOn: DocumentDBLambdaESMRole
    Type: AWS::Lambda::Function
    Properties:
      FunctionName:  !Sub '${AWS::StackName}-DocumentDBLambdaESM-${AWS::Region}'
      Role: !GetAtt DocumentDBLambdaESMRole.Arn
      Handler: index.lambda_handler
      Code:
        ZipFile: |
            import json
            import boto3

            def lambda_handler(event, context):
              print("Received event:", json.dumps(event))

              kinesis_client = boto3.client('kinesis')

              for change_event in event['events']:
                  if 'fullDocument' in change_event['event']:
                      change_data = json.dumps(change_event['event']['fullDocument']).encode('utf-8')

                      # Specify the Kinesis Data Stream name
                      stream_name = 'docdbstream'

                      # Put the change data into the Kinesis Data Stream
                      response = kinesis_client.put_record(
                          StreamName=stream_name,
                          Data=change_data,
                          PartitionKey='member_id'
                      )

                      print(f"Successfully loaded change data to Kinesis Data Stream. Shard ID: {response['ShardId']}")
                  else:
                      print("No 'fullDocument' found in the change event. Skipping the processing.")

      MemorySize: 128
      Runtime: python3.7
      Timeout: 120
      VpcConfig:
        SecurityGroupIds:
          - !Ref DocumentDBSecurityGroup
          - !Ref ElasticSearchSecurityGroup
        SubnetIds:
          - !Ref PrivateSubnetOne
          - !Ref PrivateSubnetTwo
          - !Ref PrivateSubnetThree

Outputs:
  VpcId:
    Value: !Ref VPC
  PrivateSubnetOne:
    Value: !Ref PrivateSubnetOne
  PrivateSubnetTwo:
    Value: !Ref PrivateSubnetOne
  PrivateSubnetThree:
    Value: !Ref PrivateSubnetThree
  StackName:
    Value: !Sub ${AWS::StackName}
  DocumentDBIdentifier:
    Value: !Ref DocumentDBCluster
  DocumentDBEndpoint:
    Value: !GetAtt DocumentDBCluster.Endpoint
  DocumentDBSecurityGroup:
    Value: !Ref DocumentDBSecurityGroup
  DocDBSecretArn:
    Value: !Ref secretDocDBAdminUser
  RoleArn:
    Value: !GetAtt LambdaChangeStreamsRole.Arn
