AWSTemplateFormatVersion: "2010-09-09"
Description: Serverless Application that detects objects on input images.

Parameters:
  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16

  PublicSubnetCIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet 
    Type: String
    Default: 10.192.10.0/20

  PrivateSubnetCIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet
    Type: String
    Default: 10.192.20.0/20
  
  InstanceType:
    Description: WebServer EC2 instance type
    Type: String
    Default: t2.large
    AllowedValues: [t2.large, t2.xlarge, t2.2xlarge,
      t3.large, t3.xlarge, t3.2xlarge,
      m4.large, m4.xlarge, m4.2xlarge, m4.4xlarge, m4.10xlarge,
      m5.large, m5.xlarge, m5.2xlarge, m5.4xlarge,
      c5.large, c5.xlarge, c5.2xlarge, c5.4xlarge, c5.9xlarge,
      g3.8xlarge,
      r5.large, r5.xlarge, r5.2xlarge, r5.4xlarge, r3.12xlarge,
      i3.xlarge, i3.2xlarge, i3.4xlarge, i3.8xlarge,
      d2.xlarge, d2.2xlarge, d2.4xlarge, d2.8xlarge]
    ConstraintDescription: Must be a sufficiently large EC2 instance.

  LatestAmiId:
    Type:  'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
    
Resources:

# VPC environment 

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref AWS::StackName

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref AWS::StackName

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

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [2, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnetCIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName} Public Subnet

  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [3, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnetCIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName} Private Subnet

  NatGatewayEIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName} Public Routes

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

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

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName} Private Routes

  DefaultPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet
  
# Elastic File System

  FileSystemResource:
    Type: AWS::EFS::FileSystem
    Properties: 
      PerformanceMode: generalPurpose
      FileSystemTags:
        - Key: Name
          Value: !Ref AWS::StackName
  
  MountTargetPublic:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref FileSystemResource
      SubnetId: !Ref PublicSubnet
      SecurityGroups:
      - !GetAtt VPC.DefaultSecurityGroup
  
  MountTargetPrivate:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref FileSystemResource
      SubnetId: !Ref PrivateSubnet
      SecurityGroups:
      - !GetAtt VPC.DefaultSecurityGroup

  AccessPointResource:
      Type: AWS::EFS::AccessPoint
      Properties:
        FileSystemId: !Ref FileSystemResource
        PosixUser:
          Uid: 1001
          Gid: 1001
        RootDirectory:
          CreationInfo:
            OwnerGid: 1001
            OwnerUid: 1001
            Permissions: 750
          Path: /ml


# S3 bucket for static website hosting

  BucketPolicy: 
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref HostingBucket
      PolicyDocument: 
        Statement: 
          - 
            Action: 
              - "s3:GetObject"
            Effect: "Allow"
            Resource: 
              Fn::Join: 
                - ""
                - 
                  - "arn:aws:s3:::"
                  - 
                    !Ref HostingBucket
                  - "/*"
            Principal: "*"
             

  HostingBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      AccessControl: PublicRead
      BucketName:  !Join
        - "-"
        - - "object-detection-app"
          - !Select
            - 0
            - !Split
              - "-"
              - !Select
                - 2
                - !Split
                  - "/"
                  - !Ref AWS::StackId
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: index.html
      CorsConfiguration:
        CorsRules:
          -
            AllowedOrigins:
              - '*'
            AllowedHeaders:
              - '*'
            AllowedMethods:
              - GET
              - HEAD
              - POST
              - PUT
            MaxAge: 3000

# API Gateway 

  InferenceApi :
      Type: AWS::ApiGateway::RestApi
      Description: API used for ML inference
      Properties:
        Name: !Ref AWS::StackName
        EndpointConfiguration:
          Types:
            - REGIONAL

  RoleForApiGatewayCloudWatchLogs:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: ApiGatewayLogsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:DescribeLogGroups
                  - logs:DescribeLogStreams
                  - logs:PutLogEvents
                  - logs:GetLogEvents
                  - logs:FilterLogEvents
                Resource: "*"

  ApiGatewayAccount:
    DependsOn: 
      - RoleForApiGatewayCloudWatchLogs
    Type: AWS::ApiGateway::Account
    Properties:
      CloudWatchRoleArn: !GetAtt RoleForApiGatewayCloudWatchLogs.Arn

  LambdaPermissionForApiGateway:
    DependsOn: 
      - InferenceApi
      - InferenceFunction
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      FunctionName: !Ref InferenceFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub 'arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${InferenceApi}/*/*/*'

  InferenceApiStage:
    DependsOn:
      - ApiGatewayAccount
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId: !Ref Deployment
      MethodSettings:
        - ResourcePath: /inference
          HttpMethod: POST
        - ResourcePath: /inference
          HttpMethod: OPTIONS
      RestApiId: !Ref InferenceApi
      StageName: LATEST
      
  Deployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - InferenceRequestPOST
      - InferenceRequestOPTIONS
    Properties:
      RestApiId: !Ref InferenceApi
      StageName: Production

  InferenceResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref InferenceApi
      ParentId: !GetAtt InferenceApi.RootResourceId
      PathPart: inference

  InferenceRequestPOST:
    DependsOn: 
      - LambdaPermissionForApiGateway
    Type: AWS::ApiGateway::Method
    Properties:
      ResourceId: !Ref InferenceResource
      RestApiId: !Ref InferenceApi
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${InferenceFunction.Arn}/invocations'
      MethodResponses:
        - StatusCode: 200

  InferenceRequestOPTIONS:
    DependsOn: 
      - LambdaPermissionForApiGateway
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      ResourceId: !Ref InferenceResource
      RestApiId: !Ref InferenceApi
      Integration:
        IntegrationResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
            method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
            method.response.header.Access-Control-Allow-Origin: "'*'"
          ResponseTemplates:
            application/json: ''
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
        Type: MOCK
      MethodResponses:
      - StatusCode: 200
        ResponseModels:
          application/json: 'Empty'
        ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: false
            method.response.header.Access-Control-Allow-Methods: false
            method.response.header.Access-Control-Allow-Origin: false

  GatewayResponseDefault4XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
         gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
         gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
      ResponseType: DEFAULT_4XX
      RestApiId:
        Ref: InferenceApi

  GatewayResponseDefault5XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
         gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
         gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
      ResponseType: DEFAULT_5XX
      RestApiId:
        Ref: InferenceApi

# Configure EFS 

  OpenSSH:
    Type: AWS::EC2::SecurityGroup
    Properties:
        GroupDescription: Allow http to client host
        VpcId:
          Ref: VPC
        SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

  EC2Instance: 
    DependsOn: 
      - MountTargetPublic
      - AccessPointResource
      - FileSystemResource
      - HostingBucket
      - InferenceApi
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      KeyName: lambda-ml
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          DeleteOnTermination: "true"
          SubnetId: !Ref PublicSubnet
          GroupSet: 
            - !GetAtt VPC.DefaultSecurityGroup
            - !Ref OpenSSH
      IamInstanceProfile: !Ref InstanceProfile
      InstanceInitiatedShutdownBehavior: terminate
      UserData: 
        Fn::Base64: 
          !Sub |
            #!/bin/bash
            sudo yum update -y

            # Donwload the repo
            curl -L -o repo.zip https://github.com/molly-moon/app-object-detection/zipball/master
            unzip repo.zip 
          
            # Upload Lambda handler to S3
            aws s3 cp molly*/lambda_handler/inference.zip s3://${HostingBucket}

            # Update env variables
            find molly*/build/static/js -type f -exec sed -i 's/REPLACE_REGION/${AWS::Region}/g' {} \;
            find molly*/build/static/js -type f -exec sed -i 's/REPLACE_APIID/${InferenceApi}/g' {} \;

            # Upload website files to S3
            aws s3 sync molly*/build s3://${HostingBucket}

            # Mount file system
            sudo mkdir -p /mnt/efs/fs1
            sudo chown ec2-user:ec2-user /mnt/efs/fs1
            sudo yum install -y amazon-efs-utils
            sleep 120
            sudo mount -t efs -o tls ${FileSystemResource}:/ /mnt/efs/fs1

            # Install Python 3.6  
            sudo yum -y groupinstall development
            sudo yum -y install zlib-devel
            sudo yum -y install openssl-devel bzip2-devel
            sudo curl -O https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz
            sudo tar xzvf Python-3.6.8.tgz
            cd Python-3.6.8
            sudo ./configure --enable-optimizations
            sudo make install
            cd ~
            
            # Lambda dependencies setup
            sudo mkdir -p /mnt/efs/fs1/ml
            sudo chown ec2-user:ec2-user /mnt/efs/fs1/ml
            wget -P /mnt/efs/fs1/ml/model_data https://tutorial-upload.s3.amazonaws.com/yolov2.h5
            sudo cp /molly*/model/* /mnt/efs/fs1/ml/model_data
            python3 -m pip install -t /mnt/efs/fs1/ml/lib keras==2.0 imageio
            
            # Send signal to CloudFormation
            /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}

            # Terminate instance
            #sudo shutdown -h now

    CreationPolicy:
      ResourceSignal:
        Count: 1
        Timeout: "PT60M"

  EC2Role:
    Type: "AWS::IAM::Role"
    Properties:
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "ec2.amazonaws.com"
            Action:
              - "sts:AssumeRole"

  InstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Roles:
        - !Ref EC2Role


# Lambda inference function 

  RoleLambda:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: lambdaEfsVpcPermissions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: [
                  'logs:CreateLogGroup',
                  'logs:CreateLogStream',
                  'logs:PutLogEvents',
                  'ec2:CreateNetworkInterface',
                  'ec2:DescribeNetworkInterfaces',
                  'ec2:DeleteNetworkInterface']
                Resource: '*'
              - Effect: Allow
                Action: [
                  'elasticfilesystem:ClientMount',
                  'elasticfilesystem:ClientWrite',
                  'elasticfilesystem:DescribeMountTargets']
                Resource: '*'

  InferenceFunction: 
    DependsOn: 
      - EC2Instance
      - MountTargetPrivate
    Type: AWS::Lambda::Function
    Properties: 
      FunctionName: !Ref AWS::StackName
      Runtime: python3.6
      Handler: inference.handler
      Role: !GetAtt RoleLambda.Arn
      Code: 
        S3Bucket: !Ref HostingBucket
        S3Key: inference.zip
      Timeout: 300
      MemorySize: 3008
      Environment:
        Variables:
          PYTHONPATH: /mnt/inference/lib
          MODEL_DATA: /mnt/inference/model_data
      VpcConfig: 
        SecurityGroupIds: 
          - !GetAtt VPC.DefaultSecurityGroup
        SubnetIds: 
          - !Ref PrivateSubnet
          - !Ref PublicSubnet
      FileSystemConfigs:
        - Arn: !GetAtt AccessPointResource.Arn
          LocalMountPath: /mnt/inference

Outputs:
  WebsiteURL:
    Value: !GetAtt [HostingBucket, WebsiteURL]
    Description: URL for website hosted on S3

  S3BucketSecureURL:
    Value: !Join ['', ['https://', !GetAtt [HostingBucket, DomainName]]]
    Description: Name of S3 bucket to hold website content

  RootUrl:
    Description: Root URL of the API gateway
    Value: !Sub 'https://${InferenceApi}.execute-api.${AWS::Region}.amazonaws.com'