AWSTemplateFormatVersion: 2010-09-09
Description: OpenVidu Platform

Parameters:

  # Domain and SSL certificate configuration

  WhichCert:
    Description: >
      [selfsigned] Self signed certificate. Not recommended for production use.
      [owncert] Valid certificate purchased in a Internet services company.
      [letsencrypt] Generate a new certificate using Let's Encrypt.
    Type: String
    AllowedValues:
      - selfsigned
      - owncert
      - letsencrypt
    Default: selfsigned

  PublicElasticIP:
    Description: "Previously created AWS Elastic IP to associate it to the OpenVidu EC2 instance. If certificate type is 'selfsigned' this value is optional. If certificate type is 'owncert' or 'letsencrypt' this value is mandatory. Example 13.33.145.23."
    Type: String
    AllowedPattern: ^$|^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$
    ConstraintDescription: The public Elastic IP does not have a valid IPv4 format

  MyDomainName:
    Description: "Valid domain name pointing to previous IP. If certificate type is 'selfsigned' this value is optional. If certificate type is 'owncert' or 'letsencrypt' this value is mandatory. Example: openvidu.company.com"
    Type: String
    AllowedPattern: ^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
    ConstraintDescription: The domain name does not have a valid domain name format

  OwnCertCRT:
    Description: "If certificate type is 'owncert' this is the URL where CRT file will be downloaded"
    Type: String

  OwnCertKEY:
    Description: "If certificate type is 'owncert' this is the URL where KEY file will be downloaded"
    Type: String

  LetsEncryptEmail:
    Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
    Type: String

  # OpenVidu configuration

  OpenViduSecret:
    Description: "Secret to connect to this OpenVidu Platform. Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens ('-') and underscores ('_')"
    Type: String
    AllowedPattern: ^[a-zA-Z0-9_-]+$
    NoEcho: true
    ConstraintDescription: "Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens ('-') and underscores ('_')"

  # EC2 Instance configuration

  InstanceType:
    Description: "Specifies the EC2 instance type for your OpenVidu instance"
    Type: String
    Default: c5.xlarge
    AllowedValues:
      - t2.large
      - t2.xlarge
      - t2.2xlarge
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - m4.16xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.8xlarge
      - m5.12xlarge
      - m5.16xlarge
      - m5.24xlarge
      - c4.large
      - c4.xlarge
      - c4.2xlarge
      - c4.4xlarge
      - c4.8xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.12xlarge
      - c5.18xlarge
      - c5.24xlarge
    ConstraintDescription: "Must be a valid EC2 instance type"

  KeyName:
    Description: "Name of an existing EC2 KeyPair to enable SSH access to the instance. It is mandatory to perform some administrative tasks of OpenVidu."
    Type: 'AWS::EC2::KeyPair::KeyName'
    ConstraintDescription: "must be the name of an existing EC2 KeyPair"

  # Other configuration

  WantToDeployDemos:
    Description: "Choose if you want to deploy OpenVidu Call application alongside OpenVidu platform."
    Type: String
    AllowedValues:
      - true
      - false
    Default: true

  WantToSendInfo:
    Description: "Choose if you want to send to OpenVidu team the version deployed and AWS region."
    Type: String
    AllowedValues:
      - true
      - false
    Default: true

#start_mappings
Mappings:
  OVAMIMAP:
    eu-west-1:
      AMI: OV_AMI_ID
#end_mappings

Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: Domain and SSL certificate configuration
        Parameters:
          - WhichCert
          - PublicElasticIP
          - MyDomainName
          - OwnCertCRT
          - OwnCertKEY
          - LetsEncryptEmail
      - Label:
          default: OpenVidu configuration
        Parameters:
          - OpenViduSecret
      - Label:
          default: EC2 Instance configuration
        Parameters:
          - InstanceType
          - KeyName
      - Label:
          default: Other configuration
        Parameters:
          - WantToDeployDemos
          - WantToSendInfo

    ParameterLabels:
      # SSL certificate configuration
      WhichCert:
        default: "Certificate Type"
      PublicElasticIP:
        default: "AWS Elastic IP (EIP)"
      MyDomainName:
        default: "Domain Name pointing to Elastic IP"
      OwnCertCRT:
        default: "URL to the CRT file (owncert)"
      OwnCertKEY:
        default: "URL to the key file (owncert)"
      LetsEncryptEmail:
        default: "Email for Let's Encrypt (letsencrypt)"
      # OpenVidu configuration
      OpenViduSecret:
        default: "Openvidu Secret"
      # EC2 Instance configuration
      InstanceType:
        default: "Instance type"
      KeyName:
        default: "SSH Key"
      # Other configuration
      WantToDeployDemos:
        default: "Deploy OpenVidu Call application"
      WantToSendInfo:
        default: "Send deployment info to OpenVidu team"

Conditions:
    WhichCertPresent: !Not [ !Equals [!Ref WhichCert, ""] ]
    PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]

Resources:

  OpenviduServer:
    Type: 'AWS::EC2::Instance'
    Metadata:
      Comment: 'Install and configure OpenVidu Server and Demos'
      AWS::CloudFormation::Init:
        config:
          files:
            '/usr/local/bin/ping.sh':
              content: |
                #!/bin/bash

                INXDB_URL=193.147.51.51
                INXDB_DB=ov_server
                INXDB_MEASUREMENT=server

                OV_VERSION=OPENVIDU_VERSION
                EC2_AVAIL_ZONE=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)
                EC2_REGION=$(echo "$EC2_AVAIL_ZONE" | sed 's/[a-z]$//')

                curl -i -XPOST "http://$INXDB_URL:8086/write?db=$INXDB_DB" \
                  --data-binary "$INXDB_MEASUREMENT,region=$EC2_REGION ov_version=\"$OV_VERSION\" "
              mode: "000755"
              owner: "root"
              group: "root"
            '/usr/local/bin/check_app_ready.sh':
              content: |
                #!/bin/bash
                while true; do
                  HTTP_STATUS=$(curl -Ik http://localhost:5443 | head -n1 | awk '{print $2}')
                  if [ $HTTP_STATUS == 200 ]; then
                    break
                  fi
                  sleep 5
                done
              mode: "000755"
              owner: "root"
              group: "root"
            '/usr/local/bin/feedGroupVars.sh':
              content: !Sub |
                #!/bin/bash -x
                WORKINGDIR=/opt/openvidu

                # Replace secret
                sed -i "s/OPENVIDU_SECRET=/OPENVIDU_SECRET=${OpenViduSecret}/" $WORKINGDIR/.env

                # Replace domain name
                if [[ "${MyDomainName}" != ''  && "${PublicElasticIP}" != '' ]]; then
                  sed -i "s/DOMAIN_OR_PUBLIC_IP=/DOMAIN_OR_PUBLIC_IP=${MyDomainName}/" $WORKINGDIR/.env
                elif [[ "${MyDomainName}" == '' && "${PublicElasticIP}" != '' ]]; then
                  sed -i "s/DOMAIN_OR_PUBLIC_IP=/DOMAIN_OR_PUBLIC_IP=${PublicElasticIP}/" $WORKINGDIR/.env
                else
                  [ ! -d "/usr/share/openvidu" ] && mkdir -p /usr/share/openvidu
                  PublicHostname=$(curl http://169.254.169.254/latest/meta-data/public-hostname)
                  sed -i "s/DOMAIN_OR_PUBLIC_IP=/DOMAIN_OR_PUBLIC_IP=$PublicHostname/" $WORKINGDIR/.env
                  echo $PublicHostname > /usr/share/openvidu/old-host-name
                fi

                # Replace certificated type
                sed -i "s/CERTIFICATE_TYPE=selfsigned/CERTIFICATE_TYPE=${WhichCert}/" $WORKINGDIR/.env
                sed -i "s/LETSENCRYPT_EMAIL=user@example.com/LETSENCRYPT_EMAIL=${LetsEncryptEmail}/" $WORKINGDIR/.env

                # Without Application
                if [ "${WantToDeployDemos}" == "false" ]; then
                  sed -i "s/WITH_APP=true/WITH_APP=false/" $WORKINGDIR/docker-compose.yml
                  rm $WORKINGDIR/docker-compose.override.yml
                fi
              mode: "000755"
              owner: "root"
              group: "root"
            '/usr/local/bin/buildCerts.sh':
              content: !Sub |
                #!/bin/bash -x
                WORKINGDIR=/opt/openvidu
                wget --no-check-certificate -O $WORKINGDIR/owncert/certificate.cert ${OwnCertCRT}
                wget --no-check-certificate -O $WORKINGDIR/owncert/certificate.key ${OwnCertKEY}
              mode: "000755"
              owner: "root"
              group: "root"
            '/usr/local/bin/restartCE.sh':
              content: !Sub |
                #!/bin/bash -x
                WORKINGDIR=/opt/openvidu

                # Get new amazon URL
                OldPublicHostname=$(cat /usr/share/openvidu/old-host-name)
                PublicHostname=$(curl http://169.254.169.254/latest/meta-data/public-hostname)
                sed -i "s/$OldPublicHostname/$PublicHostname/" $WORKINGDIR/.env
                echo $PublicHostname > /usr/share/openvidu/old-host-name

                # Restart all services
                pushd /opt/openvidu
                docker-compose up -d kms
                until docker-compose ps | grep kms | grep healthy; do
                    echo "Waiting kms..."
                done
                docker-compose up -d
                popd
              mode: "000755"
              owner: "root"
              group: "root"
    Properties:
      ImageId: !GetAtt CloudformationLambdaInvoke.ImageId
      InstanceType: !Ref InstanceType
      SecurityGroups:
        - !Ref WebServerSecurityGroup
      KeyName: !Ref KeyName
      Tags:
        - Key: Name
          Value: !Ref 'AWS::StackName'
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -x
          set -eu -o pipefail

          cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenviduServer

          export HOME="/root"

          # Replace .env variables
          /usr/local/bin/feedGroupVars.sh || { echo "[OpenVidu] Parameters incorrect/insufficient"; exit 1; }

          # Launch on reboot
          echo "@reboot /usr/local/bin/restartCE.sh" | crontab

          # Download certs if "WichCert" mode
          if [ "${WhichCert}" == "owncert" ]; then
            /usr/local/bin/buildCerts.sh || { echo "[OpenVidu] error with the certificate files"; exit 1; }
          fi

          # Start openvidu application
          pushd /opt/openvidu
          docker-compose up -d kms
          until docker-compose ps | grep kms | grep healthy; do
              echo "Waiting kms..."
          done
          docker-compose up -d
          popd

          # Send info to openvidu
          if [ "${WantToSendInfo}" == "true" ]; then
            /usr/local/bin/ping.sh || true
          fi
          rm /usr/local/bin/ping.sh

          # Wait for the app
          /usr/local/bin/check_app_ready.sh

          # sending the finish call
          /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region}

      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp2
            DeleteOnTermination: true
            VolumeSize: 200

  MyEIP:
    Type: 'AWS::EC2::EIPAssociation'
    Condition: PublicElasticIPPresent
    Properties:
      InstanceId: !Ref OpenviduServer
      EIP: !Ref PublicElasticIP

  WaitCondition:
    Type: 'AWS::CloudFormation::WaitCondition'
    CreationPolicy:
      ResourceSignal:
        Timeout: PT30M
        Count: '1'

  WebServerSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: SSH, Proxy and OpenVidu WebRTC Ports
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIpv6: ::/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIpv6: ::/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIpv6: ::/0
        - IpProtocol: tcp
          FromPort: 3478
          ToPort: 3478
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 3478
          ToPort: 3478
          CidrIpv6: ::/0
        - IpProtocol: udp
          FromPort: 3478
          ToPort: 3478
          CidrIp: 0.0.0.0/0
        - IpProtocol: udp
          FromPort: 3478
          ToPort: 3478
          CidrIpv6: ::/0
        - IpProtocol: udp
          FromPort: 40000
          ToPort: 57000
          CidrIp: 0.0.0.0/0
        - IpProtocol: udp
          FromPort: 40000
          ToPort: 57000
          CidrIpv6: ::/0
        - IpProtocol: tcp
          FromPort: 40000
          ToPort: 57000
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 40000
          ToPort: 57000
          CidrIpv6: ::/0

  ##########
  # Lambda to Copy original AMI to the deployment region
  ##########
  CloudformationLambdaRole:
    Type: 'AWS::IAM::Role'
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: !Join ['', [ !Ref AWS::StackName, '-cf-lambda-policy'] ]
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'ec2:DescribeImages'
                  - 'ec2:CopyImage'
                Resource: '*'
      RoleName: !Join ['', [ !Ref AWS::StackName, '-cf-lambda-role'] ]

  CloudformationLambda:
    Type: AWS::Lambda::Function
    DeletionPolicy: Delete
    Properties:
      FunctionName: !Join ['', [ !Ref AWS::StackName, '-cf-lambda'] ]
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          from botocore.config import Config

          def handler(event, context):
              try:
                  if (event['RequestType'] == 'Create'):
                      copy_ami(event, context)
                      return
                  else:
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              except Exception:
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})

          def copy_ami(event, context):
              new_images=[]
              cfn_output = {}
              source_image_id = event['ResourceProperties']['AmiSourceId']
              source_region = event['ResourceProperties']['AmiSourceRegion']
              deployment_region = event['ResourceProperties']['DeploymentRegion']

              # Clients init
              ec2_client = boto3.client('ec2', config = Config(region_name=deployment_region))
              ec2_client_ov = boto3.client('ec2', config = Config(region_name=source_region))
              img_exists_waiter= ec2_client.get_waiter('image_exists')
              img_avail_waiter = ec2_client.get_waiter('image_available')

              # Get original ami name
              public_ami_filter = [{ 'Name': 'image-id', 'Values': [ source_image_id ] }]

              response = ec2_client_ov.describe_images(Filters=public_ami_filter)
              new_ami_name= "[ OpenVidu CE AMI Copy ] - " + response['Images'][0]['Name']

              own_ami_filter = [{ 'Name': 'name', 'Values': [new_ami_name] }]
              response = ec2_client.describe_images(Filters=own_ami_filter)
              if (len(response['Images']) == 1):
                  # If AMI exists, don't copy
                  new_images.append(response['Images'][0]['ImageId'])
                  cfn_output['ImageId'] = response['Images'][0]['ImageId']
              else:
                  # If AMI does not exist, copy
                  response = ec2_client.copy_image(
                      SourceImageId=source_image_id,
                      SourceRegion=source_region,
                      Name=new_ami_name
                  )
                  new_images.append(response['ImageId'])
                  cfn_output['ImageId'] = response['ImageId']

              # Wait images to be available
              waiter_config = {'Delay': 15, 'MaxAttempts': 59
              }
              # Wait image to exist
              response = img_exists_waiter.wait(ImageIds=new_images, WaiterConfig=waiter_config
              )
              # Wait image to be available
              response = img_avail_waiter.wait(ImageIds=new_images, WaiterConfig=waiter_config)

              # Return AMI
              cfnresponse.send(event, context, cfnresponse.SUCCESS, cfn_output)

      Handler: index.handler
      Role:
        !GetAtt CloudformationLambdaRole.Arn
      Runtime: python3.7
      Timeout: 900

  CloudformationLambdaInvoke:
    Type: AWS::CloudFormation::CustomResource
    DeletionPolicy: Delete
    Version: "1.0"
    Properties:
      ServiceToken: !GetAtt CloudformationLambda.Arn
      AmiSourceRegion: 'eu-west-1'
      AmiSourceId: !FindInMap [OVAMIMAP, 'eu-west-1', AMI]
      DeploymentRegion: !Ref AWS::Region

Outputs:
  OpenViduServerURL:
    Description: Use this URL to connect OpenVidu Server
    Value: !Join
      - ''
      - - 'https://'
        - !GetAtt
          - OpenviduServer
          - PublicDnsName
  OpenViduServerURLLE:
    Description: Use this URL to connect OpenVidu Server
    Value: !Join
      - ''
      - - 'https://'
        - !Ref MyDomainName
    Condition: WhichCertPresent
  OpenViduCallURL:
    Description: If you choose to deploy OpenVidu Call application, use this URL
    Value: !Join
      - ''
      - - 'https://'
        - !GetAtt
          - OpenviduServer
          - PublicDnsName
  OpenViduCallURLLE:
    Description: If you choose to deploy OpenVidu Call application, use this URL
    Value: !Join
      - ''
      - - 'https://'
        - !Ref MyDomainName
    Condition: WhichCertPresent
