AWSTemplateFormatVersion: 2010-09-09
Description:
  "Creates resources necessary to demonstrate replication from Amazon Aurora PostgreSQL to
  Amazon MemoryDB for Redis using AWS Database Migration Service"
Parameters:
  DoesDMSVPCRoleExist:
    Default: N
    Type: String
    Description: If the IAM role dms-vpc-role already exists, choose Y
    AllowedValues:
      - Y
      - N
    ConstraintDescription: Permitted values are is Y or N
  VPCCIDR:
    Type: String
    Description: VPC CIDR
    Default: "10.0.0.0/26"
  PrivateSubnetOneCIDR:
    Type: String
    Description: Subnet One CIDR
    Default: "10.0.0.0/28"
  PrivateSubnetTwoCIDR:
    Type: String
    Description: Subnet One CIDR
    Default: "10.0.0.16/28"
  PublicSubnetOneCIDR:
    Type: String
    Description: Public Subnet One CIDR
    Default: "10.0.0.32/28"
  MemoryDBRedisNodeType:
    Description: "The compute and memory capacity of the nodes in the node group"
    Type: String
    Default: db.t4g.small
    AllowedValues:
      - db.t4g.small
      - db.t4g.medium
      - db.r6g.large
      - db.r6g.xlarge
      - db.r6g.2xlarge
      - db.r6g.4xlarge
      - db.r6g.8xlarge
      - db.r6g.12xlarge
      - db.r6g.16xlarge
  DBInstanceClass:
    Description: RDS Instance Class
    Type: String
    Default: db.t3.medium
    AllowedValues:
      - db.r5.large
      - db.r5.xlarge
      - db.r5.2xlarge
      - db.r5.4xlarge
      - db.r5.8xlarge
      - db.t3.medium
      - db.t3.large
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t3.small
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.medium
      - t2.large
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - r5.large
      - r5.xlarge
      - r5.2xlarge
      - r5.4xlarge
    ConstraintDescription: must be a valid EC2 instance type.
  AutoHibernateTimeout:
    Description:  How many minutes idle before shutting down the IDE. Options, 30 minutes, 1 hour, 4 hours, 1day, 1week, Never (0)
    Type:         Number
    Default:      60
    AllowedValues:
      - 30
      - 60
      - 240
      - 1440
      - 10080
      - 0

Conditions:
  NotExistsDMSVPCRole: !Equals
    - !Ref DoesDMSVPCRoleExist
    - N

Mappings:
  RegionMap:
    us-east-1:
      AZ1: use1-az2
      AZ2: use1-az4
    us-east-2:
      AZ1: use2-az1
      AZ2: use2-az2
    us-west-1:
      AZ1: usw1-az1
      AZ2: usw1-az3
    us-west-2:
      AZ1: usw2-az1
      AZ2: usw2-az2
    ca-central-1:
      AZ1: cac1-az1
      AZ2: cac1-az2
    ap-east-1:
      AZ1: ape1-az1
      AZ2: ape1-az2
    ap-south-1:
      AZ1: aps1-az1
      AZ2: aps1-az2
    ap-northeast-1:
      AZ1: apne1-az1
      AZ2: apne1-az2
    ap-northeast-2:
      AZ1: apne2-az1
      AZ2: apne2-az2
    ap-southeast-1:
      AZ1: apse1-az1
      AZ2: apse1-az2
    ap-southeast-2:
      AZ1: apse2-az1
      AZ2: apse2-az2
    eu-central-1:
      AZ1: euc1-az1
      AZ2: euc1-az2
    eu-west-1:
      AZ1: euw1-az1
      AZ2: euw1-az2
    eu-west-2:
      AZ1: euw2-az1
      AZ2: euw2-az2
    eu-north-1:
      AZ1: eun1-az1
      AZ2: eun1-az2
    sa-east-1:
      AZ1: sae1-az1
      AZ2: sae1-az2
    cn-north-1:
      AZ1: cnn1-az1
      AZ2: cnn1-az2
    cn-northwest-1:
      AZ1: cnw1-az1
      AZ2: cnw1-az2

Resources:
  # B A S T I O N   H O S T   -----------------------------------------------------------------------------------------
  Cloud9IDE:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      AutomaticStopTimeMinutes: !Ref AutoHibernateTimeout
      Description: "Cloud9 IDE to interact with Aurora PostgreSQL and Amazon MemoryDB for Redis "
      InstanceType: !Ref InstanceType
      ImageId: amazonlinux-2-x86_64
      Name: PostgreSQLInstance
      SubnetId: !Ref PublicSubnetOne

  VPCFlowLogsRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: ""
            Effect: "Allow"
            Principal:
              Service: "vpc-flow-logs.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "vpc-flow-logs-rds"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "logs:DescribeLogGroups"
                  - "logs:DescribeLogStreams"
                Resource: !GetAtt VPCFlowLogsGroupRDS.Arn

  FlowLogsKey:
    Type: AWS::KMS::Key
    Properties:
      Description: An symmetric CMK for encrypting flow logs
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Id: keyForFlowLogs
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
            Action: kms:*
            Resource: "*"
          - Sid: Allow log encryption
            Effect: Allow
            Principal:
              Service: !Sub logs.${AWS::Region}.amazonaws.com
            Action:
              - kms:Encrypt*
              - kms:Decrypt*
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:Describe*
            Resource: "*"
            Condition:
              ArnEquals:
                kms:EncryptionContext:aws:logs:arn: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:VPCFlowLogsRDS

  # N E T W O R K I N G   ---------------------------------------------------------------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: !Ref VPCCIDR
      Tags:
        - Key: Name
          Value: "Aurora-PostgreSQL-MemoryDB-VPC"

  VPCFlowLogsGroupRDS:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: "VPCFlowLogsRDS"
      KmsKeyId: !GetAtt FlowLogsKey.Arn
      RetentionInDays: 7

  VPCFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      LogGroupName: "VPCFlowLogsRDS"
      ResourceId: !Ref VPC
      ResourceType: VPC
      TrafficType: ALL
      DeliverLogsPermissionArn: !GetAtt VPCFlowLogsRole.Arn

  PrivateSubnetOne:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZoneId: !FindInMap [RegionMap, !Ref "AWS::Region", AZ1]
      VpcId: !Ref "VPC"
      CidrBlock: !Ref PrivateSubnetOneCIDR
      Tags:
        - Key: Name
          Value: "Private Subnet One"

  PrivateSubnetTwo:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZoneId: !FindInMap [RegionMap, !Ref "AWS::Region", AZ2]
      VpcId: !Ref "VPC"
      CidrBlock: !Ref PrivateSubnetTwoCIDR
      Tags:
        - Key: Name
          Value: "Private Subnet Two"

  PublicSubnetOne:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref "VPC"
      CidrBlock: !Ref PublicSubnetOneCIDR
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref "AWS::Region"
      Tags:
        - Key: Name
          Value: "Public Subnet One"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value:
            Fn::Join:
              - "-"
              - - Ref: AWS::StackName
                - InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref "VPC"
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref "VPC"
      Tags:
        - Key: Name
          Value:
            Fn::Join:
              - "-"
              - - Ref: AWS::StackName
                - PublicRouteTable

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetOne
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: "RDS Route Table"

  PrivateSubnetOneRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnetOne
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetTwoRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnetTwo
      RouteTableId: !Ref PrivateRouteTable

  # R D B M S   A U R O R A   P O S T G R E S Q L   -------------------------------------------------------------------
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Private SG For internal communication
      VpcId: !Ref "VPC"

  RDSSecurityGroupIngress1:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Allow EC2 Instance to connect
      GroupId: !Ref RDSSecurityGroup
      IpProtocol: "tcp"
      FromPort: 5432
      ToPort: 5432
      CidrIp: !Ref VPCCIDR  

  RDSSecurityGroupEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: To communicate within the SG
      GroupId: !Ref RDSSecurityGroup
      IpProtocol: "tcp"
      FromPort: 0
      ToPort: 0
      DestinationSecurityGroupId: !GetAtt RDSSecurityGroup.GroupId

  DMSSecretsKey:
    Type: AWS::KMS::Key
    Properties:
      Description: An symmetric CMK for Secrets Manager
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Id: keyForSecrets
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
            Action: kms:*
            Resource: "*"

  DMSSecret:
    Type: "AWS::SecretsManager::Secret"
    Properties:
      Name: /dmsdemo/dbsecret
      Description: Generates random value for db password and stores in secrets manager
      KmsKeyId: !Ref DMSSecretsKey
      GenerateSecretString:
        SecretStringTemplate: '{"username": "dbadmin", "port": "5432", "host": " "}'
        GenerateStringKey: "password"
        PasswordLength: 20
        ExcludeCharacters: "\"@/\\;.:+' %"

  RDSCluster:
    Type: "AWS::RDS::DBCluster"
    Properties:
      DBClusterIdentifier: dmsdemo-aurora-cluster-aws
      DBClusterParameterGroupName: !Ref RDSDBClusterParameterGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      Engine: aurora-postgresql
      MasterUserPassword: !Sub "{{resolve:secretsmanager:${DMSSecret}::password}}"
      MasterUsername: dbadmin
      DatabaseName: "dmssource"
      Port: 5432
      StorageEncrypted: true
      VpcSecurityGroupIds: [!Ref RDSSecurityGroup]
      Tags:
        - Key: Name
          Value: dmsdemo-cluster-aws

  RDSDBClusterParameterGroup:
    Type: "AWS::RDS::DBClusterParameterGroup"
    Properties:
      Description: "Aurora Cluster Parameter Group"
      Family: aurora-postgresql14
      Parameters:
        rds.logical_replication: 1
        wal_sender_timeout: 0

  RDSDBInstance1:
    Type: "AWS::RDS::DBInstance"
    Properties:
      AvailabilityZone: !GetAtt PrivateSubnetOne.AvailabilityZone
      DBClusterIdentifier: !Ref RDSCluster
      DBInstanceClass: !Ref DBInstanceClass
      DBParameterGroupName: !Ref RDSDBParameterGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      Engine: aurora-postgresql
      PubliclyAccessible: false

  RDSDBParameterGroup:
    Type: "AWS::RDS::DBParameterGroup"
    Properties:
      Description: Aurora Parameter Group
      Family: aurora-postgresql14

  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: String
      DBSubnetGroupName: source-db-subnet-group-aws
      SubnetIds: [!Ref PrivateSubnetOne, !Ref PrivateSubnetTwo]

  SMRDSAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment
    Properties:
      SecretId: !Ref DMSSecret
      TargetId: !Ref RDSDBInstance1
      TargetType: AWS::RDS::DBInstance

  # M E M O R Y D B   F O R   R E D I S   -----------------------------------------------------------------------------
  MemoryDBRedisSecret:
    Type: "AWS::SecretsManager::Secret"
    Properties:
      Name: /dmsdemo/memorydbredissecret
      Description: Generates random value for db password and stores in secrets manager
      KmsKeyId: !Ref DMSSecretsKey
      GenerateSecretString:
        SecretStringTemplate: '{"username": "memorydbdmsuser", "port": "6379"}'
        GenerateStringKey: "password"
        PasswordLength: 20
        ExcludePunctuation: true

  MemoryDBRedisSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: MemoryDB Redis Security Group
      VpcId: !Ref "VPC"

  MemoryDBRedisSecurityGroupIngress1:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Allow EC2 Instance to connect to Redis
      GroupId: !Ref MemoryDBRedisSecurityGroup
      IpProtocol: "tcp"
      FromPort: 6379
      ToPort: 6379
      CidrIp: !Ref VPCCIDR  

  MemoryDBRedisSecurityGroupEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: To communicate within the SG
      GroupId: !Ref MemoryDBRedisSecurityGroup
      IpProtocol: "tcp"
      FromPort: 0
      ToPort: 0
      DestinationSecurityGroupId: !GetAtt MemoryDBRedisSecurityGroup.GroupId

  MemoryDBSubnetGroup:
    Type: AWS::MemoryDB::SubnetGroup
    Properties:
      Description: MemoryDB for Redis Subnet Group
      SubnetGroupName: "memory-db-redis-subnet-group"
      SubnetIds: [!Ref PrivateSubnetOne, !Ref PrivateSubnetTwo]

  MemoryDBRedisUser:
    Type: AWS::MemoryDB::User
    Properties:
      AccessString: "on ~* &* +@all"
      AuthenticationMode:
        Type: "password"
        Passwords:
          - !Sub "{{resolve:secretsmanager:${MemoryDBRedisSecret}::password}}"
      UserName: "memorydbdmsuser"

  MemoryDBRedisACL:
    Type: AWS::MemoryDB::ACL
    DependsOn: MemoryDBRedisUser
    Properties:
      ACLName: "memorydbdmsacl"
      UserNames:
        - "memorydbdmsuser"

  MemoryDBRedisCluster:
    Type: AWS::MemoryDB::Cluster
    DependsOn: MemoryDBRedisACL
    Properties:
      ACLName: "memorydbdmsacl"
      ClusterName: "memorydbdmscluster"
      Description: "Memory DB for Redis Cluster"
      NodeType: !Ref MemoryDBRedisNodeType
      NumReplicasPerShard: 1
      NumShards: 3
      Port: 6379
      SecurityGroupIds: [!Ref MemoryDBRedisSecurityGroup]
      SubnetGroupName: !Ref MemoryDBSubnetGroup
      TLSEnabled: true
      Tags:
        - Key: Name
          Value: "MemoryDBRedisCluster"

  # D A T A B A S E   M I G R A T I O N   S E R V I C E   -------------------------------------------------------------
  DMSVPCRole:
    Type: "AWS::IAM::Role"
    Condition: NotExistsDMSVPCRole
    Properties:
      RoleName: "dms-vpc-role"
      AssumeRolePolicyDocument:
        Statement:
          - Principal:
              Service: "dms.amazonaws.com"
            Action:
              - "sts:AssumeRole"
            Effect: Allow
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AmazonDMSVPCManagementRole"

  DMSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Redis Security Group
      VpcId: !Ref "VPC"

  DMSSecurityGroupEgress1:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: To communicate with Redis
      GroupId: !Ref DMSSecurityGroup
      IpProtocol: "tcp"
      FromPort: 6379
      ToPort: 6379
      DestinationSecurityGroupId: !GetAtt MemoryDBRedisSecurityGroup.GroupId

  DMSSecurityGroupEgress2:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: To communicate with RDS
      GroupId: !Ref DMSSecurityGroup
      IpProtocol: "tcp"
      FromPort: 5432
      ToPort: 5432
      DestinationSecurityGroupId: !GetAtt RDSSecurityGroup.GroupId

  DMSSubnetGroup:
    Type: AWS::DMS::ReplicationSubnetGroup
    Properties:
      ReplicationSubnetGroupDescription: "DMS Subnet Group"
      ReplicationSubnetGroupIdentifier: "dms-subnet-group"
      SubnetIds: [!Ref PrivateSubnetOne, !Ref PrivateSubnetTwo]
      Tags:
        - Key: Name
          Value: "DMS Subnet Group"

  DMSReplicationInstance:
    Type: "AWS::DMS::ReplicationInstance"
    Properties:
      ReplicationInstanceClass: dms.t3.medium
      PubliclyAccessible: false
      EngineVersion: "3.4.7"
      ReplicationInstanceIdentifier: "postgresql-memorydb-dms-instance"
      ReplicationSubnetGroupIdentifier: !Ref DMSSubnetGroup
      VpcSecurityGroupIds: [!Ref DMSSecurityGroup]
      Tags:
        - Key: Name
          Value: "DMS Replication Instance"

  DMSSourceEndpoint:
    Type: AWS::DMS::Endpoint
    Properties:
      EndpointIdentifier: "postgresql-source-endpoint"
      EndpointType: "source"
      EngineName: "aurora-postgresql"
      Password: !Sub "{{resolve:secretsmanager:${DMSSecret}::password}}"
      Port: 5432
      ServerName: !GetAtt RDSDBInstance1.Endpoint.Address
      Username: dbadmin
      DatabaseName: "dmssource"

  DMSTargetEndpoint:
    Type: AWS::DMS::Endpoint
    DependsOn: MemoryDBRedisCluster
    Properties:
      EndpointIdentifier: "memorydb-target-endpoint"
      EndpointType: "target"
      EngineName: "redis"
      SslMode: "none"
      RedisSettings:
        ServerName: !GetAtt MemoryDBRedisCluster.ClusterEndpoint.Address
        Port: 6379
        AuthType: "auth-role"
        AuthUserName: "memorydbdmsuser"
        AuthPassword: !Sub "{{resolve:secretsmanager:${MemoryDBRedisSecret}::password}}"
        SslSecurityProtocol: "ssl-encryption"

  DMSReplicationTask:
    Type: AWS::DMS::ReplicationTask
    Properties:
      MigrationType: "full-load"
      ReplicationInstanceArn: !Ref DMSReplicationInstance
      ReplicationTaskIdentifier: "replicate-products"
      ResourceIdentifier: String
      SourceEndpointArn: !Ref DMSSourceEndpoint
      TableMappings: '{
        "rules": [
        {
        "rule-type": "selection",
        "rule-id": "1",
        "rule-name": "1",
        "object-locator": {
        "schema-name": "public",
        "table-name": "%"
        },
        "rule-action": "include",
        "filters": []
        }
        ]
        }'
      TargetEndpointArn: !Ref DMSTargetEndpoint

Outputs:
  RDSEndpoint:
    Description: RDS Endpoint Amazon Aurora PostgreSQL
    Value: !GetAtt RDSCluster.Endpoint.Address
  MemoryDBRedisClusterEndpoint:
    Description: MemoryDB for Redis Cluster Endpoint
    Value: !GetAtt MemoryDBRedisCluster.ClusterEndpoint.Address
  DMSSourceEndpointArn:
    Description: Endpoint ARN of the DMS Source Endpoint
    Value: !Ref DMSSourceEndpoint
  DMSTargetEndpointArn:
      Description: Endpoint ARN of the DMS Target Endpoint
      Value: !Ref DMSTargetEndpoint
  DMSReplicationInstanceArn:
    Description: ARN of the DMS Replication Instance
    Value: !Ref DMSReplicationInstance
  SecretArn:
    Description: Secret Key ARN
    Value: !Ref DMSSecret