AWSTemplateFormatVersion: 2010-09-09
Description: Launch Standalone FortiManager-VM in a new VPC
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: VPC Configuration
        Parameters:
          - VPCCIDR
          - PublicSubnet
          - PublicSubnetRouterIP
      - Label:
          default: FortiManager Instance Configuration
        Parameters:
          - AZForFMG
          - FMGInstanceType
          - FortiManagerVersion
          - LicenseType
          - CIDRForFMGccess
          - KeyPair
          - EncryptVolumes
      - Label:
          default: Contact
        Parameters:
          - EmailNotification
    ParameterLabels:
      FortiManagerVersion:
        default: FortiManager Version
Parameters:
  VPCCIDR:
    Type: String
    Default: 10.0.0.0/16
    Description: Enter the VPC CIDR that you are using
  AZForFMG:
    Type: 'AWS::EC2::AvailabilityZone::Name'
    Description: Select the AZ for FortiManager
  PublicSubnet:
    Type: String
    Default: 10.0.1.0/24
    Description: Provide a network CIDR for the Public Subnet
  PublicSubnetRouterIP:
    Type: String
    Default: 10.0.1.1
    Description: Provide the IP of the gateway for the public subnet
  FMGInstanceType:
    Type: String
    Default: m5.xlarge
    Description: Select the instance type for the FortiManager
    AllowedValues:
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
  FortiManagerVersion:
    Type: String
    Default: 7.6.x
    Description: Select the verion of FortiManager to use (latest GA AMI will be used)
    AllowedValues:
      - 7.0.x
      - 7.2.x
      - 7.4.x
      - 7.6.x
  LicenseType:
    Type: String
    Description: Select the license type for the FortiManager
    AllowedValues:
      - BYOL
      - PAYG2Devices
      - PAYG10Devices
      - PAYG30Devices
      - PAYG100Devices
      - PAYG500Devices
  CIDRForFMGccess:
    Type: String
    Default: 0.0.0.0/0
    Description: Enter the CIDR from which FortiManager instance need to be accessed from
  KeyPair:
    Type: 'AWS::EC2::KeyPair::KeyName'
    Description: Select the keypair for the FortiManager
  EncryptVolumes:
    Type: String
    Description: Select 'true' to encrypt the FortiManager instances OS and Log volumes with your account's KMS default master key for EBS.  Otherwise select false to leave unencrypted
    AllowedValues:
      - 'true'
      - 'false'
  EmailNotification:
    Type: String
    Default: admin@example.com
    Description: >-
      Provide an email address to receive notifications when the CloudWatch Failover Alarm is triggered
Mappings:
  FortiManagerAMISearchString:
    7.0.x:
      BYOL: FortiManager-VM64-AWS *(7.0.*)*|os40mk0fw6472m8ak2lnzp4s
      PAYG2Devices: FortiManager-VM64-AWSONDEMAND *(7.0.*)*|5j3m869sh84tixvmg9br26hkn
      PAYG10Devices: FortiManager-VM64-AWSONDEMAND *(7.0.*)*|691k7ni0x5sbcj3reejxw3cy1
      PAYG30Devices: FortiManager-VM64-AWSONDEMAND *(7.0.*)*|8gmpxt177zhq1vhh32l7o69wf
      PAYG100Devices: FortiManager-VM64-AWSONDEMAND *(7.0.*)*|d3o35cw8hzusqeiv83jl1xuwb
      PAYG500Devices: FortiManager-VM64-AWSONDEMAND *(7.0.*)*|e5s6s9r3hgizwpe9tavoxwkzv
    7.2.x:
      BYOL: FortiManager-VM64-AWS *(7.2.*)*|os40mk0fw6472m8ak2lnzp4s
      PAYG2Devices: FortiManager-VM64-AWSONDEMAND *(7.2.*)*|5j3m869sh84tixvmg9br26hkn
      PAYG10Devices: FortiManager-VM64-AWSONDEMAND *(7.2.*)*|691k7ni0x5sbcj3reejxw3cy1
      PAYG30Devices: FortiManager-VM64-AWSONDEMAND *(7.2.*)*|8gmpxt177zhq1vhh32l7o69wf
      PAYG100Devices: FortiManager-VM64-AWSONDEMAND *(7.2.*)*|d3o35cw8hzusqeiv83jl1xuwb
      PAYG500Devices: FortiManager-VM64-AWSONDEMAND *(7.2.*)*|e5s6s9r3hgizwpe9tavoxwkzv
    7.4.x:
      BYOL: FortiManager-VM64-AWS *(7.4.*)*|os40mk0fw6472m8ak2lnzp4s
      PAYG2Devices: FortiManager-VM64-AWSONDEMAND *(7.4.*)*|5j3m869sh84tixvmg9br26hkn
      PAYG10Devices: FortiManager-VM64-AWSONDEMAND *(7.4.*)*|691k7ni0x5sbcj3reejxw3cy1
      PAYG30Devices: FortiManager-VM64-AWSONDEMAND *(7.4.*)*|8gmpxt177zhq1vhh32l7o69wf
      PAYG100Devices: FortiManager-VM64-AWSONDEMAND *(7.4.*)*|d3o35cw8hzusqeiv83jl1xuwb
      PAYG500Devices: FortiManager-VM64-AWSONDEMAND *(7.4.*)*|e5s6s9r3hgizwpe9tavoxwkzv
    7.6.x:
      BYOL: FortiManager-VM64-AWS *(7.6.*)*|os40mk0fw6472m8ak2lnzp4s
      PAYG2Devices: FortiManager-VM64-AWSONDEMAND *(7.6.*)*|5j3m869sh84tixvmg9br26hkn
      PAYG10Devices: FortiManager-VM64-AWSONDEMAND *(7.6.*)*|691k7ni0x5sbcj3reejxw3cy1
      PAYG30Devices: FortiManager-VM64-AWSONDEMAND *(7.6.*)*|8gmpxt177zhq1vhh32l7o69wf
      PAYG100Devices: FortiManager-VM64-AWSONDEMAND *(7.6.*)*|d3o35cw8hzusqeiv83jl1xuwb
      PAYG500Devices: FortiManager-VM64-AWSONDEMAND *(7.6.*)*|e5s6s9r3hgizwpe9tavoxwkzv
Conditions: {}
Resources:
  VPCID:
    Properties:
      CidrBlock: !Ref VPCCIDR
      Tags:
        - Key: Name
          Value: !Join 
            - ''
            - - !Ref 'AWS::StackName'
              - VPCID
    Type: 'AWS::EC2::VPC'
  InstanceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: FMGPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:AssignPrivateIpAddresses
                  - ec2:DescribeSubnets
                  - ec2:DescribeNetworkInterfaces
                  - ec2:DescribeAddresses
                  - ec2:AssociateAddress
                  - ec2:CreateTags
                Resource: '*'

  InstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      Roles:
        - !Ref InstanceRole
  PublicSubnetA:
    Properties:
      AvailabilityZone: !Ref AZForFMG
      CidrBlock: !Ref PublicSubnet
      Tags:
        - Key: Name
          Value: !Join 
            - ''
            - - !Ref 'AWS::StackName'
              - PublicSubnet
      VpcId: !Ref VPCID
    Type: 'AWS::EC2::Subnet'
  AttachGateway:
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPCID
    Type: 'AWS::EC2::VPCGatewayAttachment'
  InternetGateway:
    Properties:
      Tags:
        - Key: Name
          Value: !Join 
            - ''
            - - !Ref 'AWS::StackName'
              - IGW
    Type: 'AWS::EC2::InternetGateway'
  PublicDefaultRoute:
    DependsOn: AttachGateway
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref RouteTable
    Type: 'AWS::EC2::Route'
  RouteTable:
    Properties:
      Tags:
        - Key: Name
          Value: !Join 
            - ''
            - - !Ref 'AWS::StackName'
              - PublicRoute
      VpcId: !Ref VPCID
    Type: 'AWS::EC2::RouteTable'
  FMGSecGrpBase:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      VpcId: !Ref VPCID
      GroupDescription: FMGSecGrp
      SecurityGroupIngress:
        - Description: Allow remote access to FMG
          IpProtocol: '-1'
          FromPort: 0
          ToPort: 65535
          CidrIp: !Ref CIDRForFMGccess
        - Description: Allow local VPC access to FMG
          IpProtocol: '-1'
          FromPort: 0
          ToPort: 65535
          CidrIp: !Ref VPCCIDR
  FMGSecGrpFMGRule:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      GroupId: !Ref FMGSecGrpBase
      Description: Allow FMGs to access each other
      IpProtocol: '-1'
      FromPort: 0
      ToPort: 65535
      SourceSecurityGroupId: !Ref FMGSecGrpBase
  FMG:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: !GetAtt RunImageFunction.ami
      InstanceType: !Ref FMGInstanceType
      IamInstanceProfile: !Ref InstanceProfile
      KeyName: !Ref KeyPair
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp2
            VolumeSize: '5'
            DeleteOnTermination: 'true'
            Encrypted: !Ref EncryptVolumes
        - DeviceName: /dev/sdb
          Ebs:
            VolumeType: gp2
            VolumeSize: '80'
            DeleteOnTermination: 'true'
            Encrypted: !Ref EncryptVolumes
      NetworkInterfaces:
        - NetworkInterfaceId: !Ref FMGENI0
          DeviceIndex: '0'
      Tags:
        - Key: Name
          Value: !Join 
            - ''
            - - !Ref 'AWS::StackName'
              - '-FMG'
      UserData: !Base64 
        'Fn::Join':
          - |+

          - - config system vdom-exception
            - edit 0
            - set object router.static
            - next
            - edit 0
            - set object system.interface
            - end
            - config system global
            - set hostname FMG
            - end
            - config system interface
            - edit port1
            - set alias public
            - set mode static
            - !Join 
              - ''
              - - 'set ip '
                - !GetAtt 
                  - FMGENI0
                  - PrimaryPrivateIpAddress
                - /
                - !Select 
                  - '1'
                  - !Split 
                    - /
                    - !Ref PublicSubnet
            - set allowaccess ping https ssh
            - set mtu-override enable
            - set mtu 9001
            - next
            - end
            - config router static
            - edit 1
            - set device port1
            - !Join 
              - ''
              - - 'set gateway '
                - !Ref PublicSubnetRouterIP
            - next
            - end
            - config firewall policy
            - edit 1
            - set name outbound-all
            - set srcintf port2
            - set dstintf port1
            - set srcaddr all
            - set dstaddr all
            - set action accept
            - set schedule always
            - set service ALL
            - set logtraffic all
            - set nat enable
            - next
            - end
  FMGENI0:
    Type: 'AWS::EC2::NetworkInterface'
    Properties:
      Description: port1
      GroupSet:
        - !Ref FMGSecGrpBase
      SourceDestCheck: false
      SubnetId: !Ref PublicSubnetA
      Tags:
        - Key: Name
          Value: !Join 
            - ''
            - - !Ref 'AWS::StackName'
              - FMGENI0
        - Key: Interface
          Value: eth0
  ClusterEIP:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
    DependsOn: FMG
  FMGMgmtEIP:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
    DependsOn: FMG
  ClusterAEIPASSOCIATION:
    Type: 'AWS::EC2::EIPAssociation'
    Properties:
      AllocationId: !GetAtt 
        - ClusterEIP
        - AllocationId
      NetworkInterfaceId: !Ref FMGENI0
  SubnetRouteTableAssociation1:
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref PublicSubnetA
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
  CloudwatchAlarmFMG:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      ActionsEnabled: true
      AlarmActions:
        - !Join 
          - ''
          - - 'arn:aws:automate:'
            - !Ref 'AWS::Region'
            - ':ec2:recover'
      AlarmDescription: Cloudwatch alarm to monitor instances status
      AlarmName: !Join 
        - ''
        - - !Ref 'AWS::StackName'
          - '-CWAlarmFMG'
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: InstanceId
          Value: !Ref FMG
      EvaluationPeriods: 3
      MetricName: StatusCheckFailed_System
      Namespace: AWS/EC2
      Period: 300
      Statistic: Minimum
      Threshold: 0
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: S3AccessRole
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeImages
                  - s3:PutObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - logs:*
                Resource: '*'
  ImageFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: !Join
          - |+

          - - import boto3
            - import cfnresponse
            - import logging
            - import json
            - logger = logging.getLogger()
            - logger.setLevel(logging.INFO)
            - client = boto3.client('ec2')
            - ''
            - 'def handler(event, context):'
            - '    if event[''RequestType''] == ''Create'':'
            - '        logger.info(''<-- event received: {}''.format(json.dumps(event)))'
            - '        searchString = event[''ResourceProperties''][''SearchString'']'
            - '        productCode =  event[''ResourceProperties''][''ProductCode'']'
            - '    else:'
            - '        responseData = {''msg'':''200''}'
            - '        cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)'
            - '        return None'
            - ''
            - '    try:'
            - '        resp = client.describe_images('
            - '            Filters=[{''Name'': ''name'', ''Values'': [searchString]}, {''Name'': ''product-code'', ''Values'': [productCode]}],'
            - '            Owners=[''aws-marketplace'']'
            - '        )'
            - '    except Exception as error:'
            - '        logger.error(''<--!! Exception: {}''.format(error))'
            - '        responseData = {''msg'':''Exception: {}''.format(error)}'
            - '        cfnresponse.send(event, context, cfnresponse.FAILED, responseData)'
            - ''
            - '    if resp[''ResponseMetadata''][''HTTPStatusCode''] == 200 and resp[''Images''] != []:'
            - '        ami_dict = {}'
            - '        ami_list = []'
            - '        for entry in resp[''Images'']:'
            - '            key = entry[''CreationDate'']'
            - '            ami_dict[key] = entry[''ImageId'']'
            - '        ami_list = sorted(ami_dict, reverse = True)'
            - '        logger.info(''--> found latest AMI: {}, {}, {} {}''.format(ami_dict[ami_list[0]], ami_list[0], searchString, productCode))'
            - '        responseData = {''ami'': ami_dict[ami_list[0]]}'
            - '        cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)'
            - '    else:'
            - '        logger.error(''!!--> Unable to find AMI {} {} in describe_images response! {}''.format(searchString, productCode, resp))'
            - '        responseData = {''msg'':''Unable to find AMI {} {} in describe_images response! {}''.format(searchString, productCode,resp)}'
            - '        cfnresponse.send(event, context, cfnresponse.FAILED, responseData)'
            - ''
      Role: !GetAtt LambdaRole.Arn
      Timeout: 120
      Handler: index.handler
      Runtime: python3.9
      MemorySize: 128
  RunImageFunction:
    Type: Custom::ImageFunction
    Properties:
      ServiceToken: !GetAtt ImageFunction.Arn
      SearchString: !Select
        - '0'
        - !Split
          - '|'
          - !FindInMap
            - FortiManagerAMISearchString
            - !Ref FortiManagerVersion
            - !Ref LicenseType
      ProductCode: !Select
        - '1'
        - !Split
          - '|'
          - !FindInMap
            - FortiManagerAMISearchString
            - !Ref FortiManagerVersion
            - !Ref LicenseType
Outputs:
  FMGURL:
    Value: !Join 
      - ''
      - - 'https://'
        - !Ref FMGMgmtEIP
    Description: URL to access FortiManager
  FMGUsername:
    Value: admin
    Description: Username to access FortiManager
  FMGPassword:
    Value: !Ref FMG
    Description: Password to access FortiManager