AWSTemplateFormatVersion: 2010-09-09
Description: AWS ParallelCluster Slurm Accounting Database
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Database Cluster Configuration"
        Parameters:
          - ClusterName
          - ClusterAdmin
          - AdminPasswordSecretString
          - MinCapacity
          - MaxCapacity
      - Label:
          default: "Network Configuration"
        Parameters:
          - Vpc
          - DatabaseClusterSubnetOne
          - DatabaseClusterSubnetTwo
          - Subnet1CidrBlock
          - Subnet2CidrBlock
    ParameterLabels:
      Vpc:
        default: "The VPC to use for the database cluster."
      ClusterName:
        default: "The name of the database cluster"
      ClusterAdmin:
        default: "The database administrator user name."
      AdminPasswordSecretString:
        default: "The administrator password."
      MinCapacity:
        default: "The minimum scaling capacity of the database cluster."
      MaxCapacity:
        default: "The maximum scaling capacity of the database cluster."
      DatabaseClusterSubnetOne:
        default: "The first subnet to use for the database cluster."
      DatabaseClusterSubnetTwo:
        default: "The second subnet to use for the database cluster."
      Subnet1CidrBlock:
        default: "The CIDR block to be used for the first Subnet if its creation is requested."
      Subnet2CidrBlock:
        default: "The CIDR block to be used for the second Subnet if its creation is requested."
  cfn-lint:
    config:
      ignore_checks:
        - E3690
Parameters:
  ClusterName:
    Description: Database Cluster Name
    Type: String
    Default: "slurm-accounting-cluster"
    MinLength: 1
    MaxLength: 63
    AllowedPattern: ^[a-z][-a-z0-9]{0,62}$
    ConstraintDescription: >-
      Cluster name must be between 1 and 63 characters, start with a lower case character, and be followed by a mix of
      lower case characters, digits, and - (hyphens).
  AdminPasswordSecretString:
    Description: >-
      Password must be at least 8 characters long and contain at least 1 upper case, 1 lower case, 1 digit, and 1
      non-alphanumeric character. It must not contain any of the following: # (hash), / (slash), ' (single quote), " (double quote)
      or @ (at sign).
    Type: String
    NoEcho: true
    MinLength: 8
    # Only allow 'Medium' or better strength passwords according to https://dev.mysql.com/doc/refman/8.0/en/validate-password.html
    AllowedPattern: (?=^.{8,}$)(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\s])(?!.*[@/'"#])^.*
    ConstraintDescription: >-
      Password must be at least 8 characters with at least one uppercase letter, one lower case letter, one numeric
      digit, and one special (non-alphanumeric) character. It can not contain any of the following: # (hash), / (slash), ' (single quote), " (double quote) and @ (at sign).
  ClusterAdmin:
    Description: Administrator user name.
    Type: String
    Default: clusteradmin
    MinLength: 3
    MaxLength: 64
  Vpc:
    Description: VPC ID.
    Type: AWS::EC2::VPC::Id
  DatabaseClusterSubnetOne:
    Description: First Subnet ID (leave BLANK to create a new subnet).
    Type: String
    # Type: AWS::EC2::Subnet::Id
    Default: ''
  DatabaseClusterSubnetTwo:
    Description: Second subnet ID (leave BLANK to create a new subnet). This subnet must be in different availability zone from the first subnet.
    Type: String
    # Type: AWS::EC2::Subnet::Id
    Default: ''
  Subnet1CidrBlock:
    Description: CIDR block for first Subnet (used only if creation is requested - IN THIS CASE THE DEFAULT VALUE MUST BE CHANGED).
    Type: String
    Default: '0.0.0.0/24'
    AllowedPattern: >-
      ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: >-
      The CIDR block must be formatted as W.X.Y.Z/prefix , where prefix must be between 16 and 28.
  Subnet2CidrBlock:
    Description: CIDR block for the second Subnet (used only if creation is requested - IN THIS CASE THE DEFAULT VALUE MUST BE CHANGED).
    Type: String
    Default: '0.0.0.0/24'
    AllowedPattern: >-
      ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: >-
      The CIDR block must be formatted as W.X.Y.Z/prefix , where prefix must be between 16 and 28.
  MinCapacity:
    Description: Must be less than the maximum capacity.
    Type: Number
    Default: 1
    MinValue: .5
    MaxValue: 127.5
  MaxCapacity:
    Description: Must be greater than or equal to the minimum capacity.
    Type: Number
    Default: 4
    MinValue: 1
    MaxValue: 128
Transform: AWS::Serverless-2016-10-31
Conditions:
  CreateSubnets: !Or [!Equals [!Ref DatabaseClusterSubnetOne, ''], !Equals [!Ref DatabaseClusterSubnetTwo, '']]
  InUsIsobEast1: !Equals [ !Ref AWS::Region, 'us-isob-east-1' ]
  InUsIsoEast1: !Equals [ !Ref AWS::Region, 'us-iso-east-1' ]
  UseServerlessDatabase: !Not [!Or [Condition: InUsIsobEast1, Condition: InUsIsoEast1]]
Rules:
  BadSubnetsCidrsAssertion:
    RuleCondition: !Or
      - !Equals
        - !Ref DatabaseClusterSubnetOne
        - ''
      - !Equals
        - !Ref DatabaseClusterSubnetTwo
        - ''
    Assertions:
      - Assert: !Not
          - !Equals
            - !Ref Subnet1CidrBlock
            - '0.0.0.0/24'
        AssertDescription: The default CIDR block for the first subnet must be changed if the subnet creation is requested.
      - Assert: !Not
          - !Equals
            - !Ref Subnet2CidrBlock
            - '0.0.0.0/24'
        AssertDescription: The default CIDR block for the second subnet must be changed if the subnet creation is requested.
Resources:
  #
  # Optional Networking
  #
  AccountingClusterSubnet1:
    Type: 'AWS::EC2::Subnet'
    Condition: CreateSubnets
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: !Sub
        - "${AWS::Region}${AzLetter}"
        - { AzLetter: !If [ InUsIsobEast1, 'c', 'a'] }
      CidrBlock: !Ref Subnet1CidrBlock
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: AccountingClusterSubnet1
        - Key: 'parallelcluster:infrastructure'
          Value: 'slurm accounting'
  AccountingClusterSubnet1RouteTable:
    Type: 'AWS::EC2::RouteTable'
    Condition: CreateSubnets
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: AccountingClusterSubnet1
        - Key: 'parallelcluster:infrastructure'
          Value: 'slurm accounting'
  AccountingClusterSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Condition: CreateSubnets
    Properties:
      RouteTableId: !Ref AccountingClusterSubnet1RouteTable
      SubnetId: !Ref AccountingClusterSubnet1
  AccountingClusterSubnet2:
    Type: 'AWS::EC2::Subnet'
    Condition: CreateSubnets
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: !Sub '${AWS::Region}b'
      CidrBlock:  !Ref Subnet2CidrBlock
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: AccountingClusterSubnet2
        - Key: 'parallelcluster:infrastructure'
          Value: 'slurm accounting'
  AccountingClusterSubnet2RouteTable:
    Type: 'AWS::EC2::RouteTable'
    Condition: CreateSubnets
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: AccountingClusterSubnet2
        - Key: 'parallelcluster:infrastructure'
          Value: 'slurm accounting'
  AccountingClusterSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Condition: CreateSubnets
    Properties:
      RouteTableId: !Ref AccountingClusterSubnet2RouteTable
      SubnetId: !Ref AccountingClusterSubnet2

  #
  # Database Cluster
  #
  AccountingClusterParameterGroup:
    Type: 'AWS::RDS::DBClusterParameterGroup'
    Properties:
      Description: Cluster parameter group for aurora-mysql
      Family: aurora-mysql8.0
      Parameters:
        require_secure_transport: 'ON'
        innodb_lock_wait_timeout: '900'
      Tags:
        - Key: 'parallelcluster:usecase'
          Value: 'slurm accounting'
  AccountingClusterSubnetGroup:
    Type: 'AWS::RDS::DBSubnetGroup'
    Properties:
      DBSubnetGroupDescription: !Sub 'Subnets for AccountingCluster-${AWS::Region} database'
      SubnetIds:
        - !If [CreateSubnets, !Ref AccountingClusterSubnet1, !Ref DatabaseClusterSubnetOne]
        - !If [CreateSubnets, !Ref AccountingClusterSubnet2, !Ref DatabaseClusterSubnetTwo]
      Tags:
        - Key: 'parallelcluster:usecase'
          Value: 'slurm accounting'
  AccountingClusterSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: RDS security group
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: '-1'
      Tags:
        - Key: 'parallelcluster:usecase'
          Value: 'slurm accounting'
      VpcId: !Ref Vpc
  AccountingClusterSecurityGroupInboundRule:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      IpProtocol: tcp
      Description: Allow incoming connections from client security group
      FromPort: !GetAtt
        - AccountingCluster
        - Endpoint.Port
      GroupId: !GetAtt
        - AccountingClusterSecurityGroup
        - GroupId
      SourceSecurityGroupId: !GetAtt
        - AccountingClusterClientSecurityGroup
        - GroupId
      ToPort: !GetAtt
        - AccountingCluster
        - Endpoint.Port
  AccountingClusterAdminSecret:
    Type: 'AWS::SecretsManager::Secret'
    Properties:
      Description: 'Serverless Database Cluster Administrator Password'
      SecretString: !Ref AdminPasswordSecretString
      Tags:
        - Key: 'parallelcluster:usecase'
          Value: 'slurm accounting'
  AccountingCluster:
    Type: 'AWS::RDS::DBCluster'
    Properties:
      DBClusterIdentifier: !Ref ClusterName
      Engine: "aurora-mysql"
      EngineVersion: "8.0"
      CopyTagsToSnapshot: true
      DBClusterParameterGroupName: !Ref AccountingClusterParameterGroup
      DBSubnetGroupName: !Ref AccountingClusterSubnetGroup
      EnableHttpEndpoint: false
      MasterUsername: !Ref ClusterAdmin
      MasterUserPassword: !Ref AdminPasswordSecretString
      ServerlessV2ScalingConfiguration: !If
        - UseServerlessDatabase
        - MaxCapacity: !Ref MaxCapacity
          MinCapacity: !Ref MinCapacity
        - !Ref AWS::NoValue
      StorageEncrypted: true
      Tags:
        - Key: 'parallelcluster:usecase'
          Value: 'slurm accounting'
      VpcSecurityGroupIds:
        - !GetAtt
          - AccountingClusterSecurityGroup
          - GroupId
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  AccountingClusterInstance1:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      DBInstanceClass: !If [UseServerlessDatabase, 'db.serverless', 'db.r5.large']
      DBClusterIdentifier: !Ref AccountingCluster
      DBInstanceIdentifier: !Sub '${ClusterName}-instance-1'
      Engine: "aurora-mysql"
      PubliclyAccessible: false
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  AccountingClusterInstance2:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      DBInstanceClass: !If [UseServerlessDatabase, 'db.serverless', 'db.r5.large']
      DBClusterIdentifier: !Ref AccountingCluster
      DBInstanceIdentifier: !Sub '${ClusterName}-instance-2'
      Engine: "aurora-mysql"
      PubliclyAccessible: false
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  AccountingClusterClientSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Security Group to allow connection to Serverless DB Cluster
      Tags:
        - Key: 'parallel-cluster:usecase'
          Value: 'slurm accounting'
      VpcId: !Ref Vpc
  AccountingClusterClientSecurityGroupOutboundRule:
    Type: 'AWS::EC2::SecurityGroupEgress'
    Properties:
      GroupId: !GetAtt
        - AccountingClusterClientSecurityGroup
        - GroupId
      IpProtocol: tcp
      Description: Allow incoming connections from PCluster
      DestinationSecurityGroupId: !GetAtt
        - AccountingClusterSecurityGroup
        - GroupId
      FromPort: !GetAtt
        - AccountingCluster
        - Endpoint.Port
      ToPort: !GetAtt
        - AccountingCluster
        - Endpoint.Port
Outputs:
  ClusterName:
    Value: !Ref ClusterName
  DatabaseHost:
    Value: !GetAtt
      - AccountingCluster
      - Endpoint.Address
    Export:
      Name: !Sub ${AWS::StackName}-DatabaseHost
  DatabasePort:
    Value: !GetAtt
      - AccountingCluster
      - Endpoint.Port
    Export:
      Name: !Sub ${AWS::StackName}-DatabasePort
  DatabaseAdminUser:
    Value: !Ref ClusterAdmin
    Export:
      Name: !Sub ${AWS::StackName}-DatabaseAdminUser
  DatabaseSecretArn:
    Value: !Ref AccountingClusterAdminSecret
    Export:
      Name: !Sub ${AWS::StackName}-DatabaseSecretArn
  DatabaseVpcId:
    Value: !Ref Vpc
  DatabaseClusterSecurityGroup:
    Value: !GetAtt
      - AccountingClusterSecurityGroup
      - GroupId
    Export:
      Name: !Sub ${AWS::StackName}-DatabaseClusterSecurityGroup
  DatabaseClusterSubnet1:
    Value: !If [CreateSubnets, !Ref AccountingClusterSubnet1, !Ref DatabaseClusterSubnetOne]
  DatabaseClusterSubnet2:
    Value: !If [CreateSubnets, !Ref AccountingClusterSubnet2, !Ref DatabaseClusterSubnetTwo]
  DatabaseClientSecurityGroup:
    Value: !GetAtt
      - AccountingClusterClientSecurityGroup
      - GroupId
