--- AWSTemplateFormatVersion: 2010-09-09 Description: OpenVidu Pro CloudFormation template 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 Recording: Description: | If 'disabled', recordings will not be active. If 'local' recordings will be saved in EC2 instance locally. If 's3', recordings will be stored in a S3 bucket" Type: String AllowedValues: - disabled - local - s3 Default: local S3RecordingsBucketName: Description: "S3 Bucket Name" Type: String # OpenVidu Configuration OpenViduLicense: Description: "Visit https://openvidu.io/account" Type: String AllowedPattern: ^(?!\s*$).+$ NoEcho: true ConstraintDescription: OpenVidu Pro License is mandatory OpenViduEdition: Description: "Visit https://docs.openvidu.io/en/stable/deployment/#openvidu-editions" Type: String AllowedValues: - pro - enterprise Default: pro 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 ('_')" MediaNodesStartNumber: Description: "How many Media Nodes do you want on startup (EC2 instances will be launched)" Type: Number Default: 1 # Enable Elasticsearch and Kibana ElasticsearchEnabled: Description: "Choose if you want OpenVidu to use Elasticsearch." Type: String AllowedValues: - true - false Default: true # Elasticsearch configuration ElasticsearchUser: Description: "Username for Elasticsearch and Kibana. ('ElasticSearch Enabled' must be true)" Type: String AllowedPattern: ^$|^[^" ]+$ ConstraintDescription: Elasticsearch user is mandatory (no whitespaces or quotations allowed) Default: elasticadmin ElasticsearchPassword: Description: "Password for Elasticsearch and Kibana ('ElasticSearch Enabled' must be true)" Type: String AllowedPattern: ^$|^[^" ]+$ NoEcho: true ConstraintDescription: Elasticsearch password is mandatory and it should have at least 6 characters (no whitespaces or quotations allowed) # Elasticsearch configuration ElasticsearchUrl: Description: "If you have an external Elasticsearch service running, put here the url to the service. If empty, an Elasticsearch service will be deployed next to OpenVidu. ('ElasticSearch Enabled' must be true)" Type: String AllowedPattern: (^(https?:\/\/)?([^:\/]+)(:([0-9]+))?(\/.*)?$|^$) ConstraintDescription: "It is very important to specify the Elasticsearch URL with the port used by this service. For example: https://es-example" KibanaUrl: Description: "If you have an external Kibana service running, put here the url to the service. If empty, a Kibana service will be deployed next to OpenVidu. ('ElasticSearch Enabled' must be true)" Type: String AllowedPattern: (^(https?:\/\/)?([^:\/]+)(:([0-9]+))?(\/.*)?$|^$) ConstraintDescription: "It is very important to specify the url with port used by this service. For example: https://kibana-example" # EC2 Instance configuration AwsInstanceTypeOV: Description: "Specifies the EC2 instance type for your OpenVidu Server Pro Node" 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 - c6a.large - c6a.xlarge - c6a.2xlarge - c6a.4xlarge - c6a.8xlarge - c6a.12xlarge - c6a.16xlarge - c6a.24xlarge - c6a.32xlarge - c6a.48xlarge - c6a.metal ConstraintDescription: "Must be a valid EC2 instance type" AwsInstanceTypeKMS: Description: "Specifies the EC2 instance type for your Media Nodes" 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 - c6a.large - c6a.xlarge - c6a.2xlarge - c6a.4xlarge - c6a.8xlarge - c6a.12xlarge - c6a.16xlarge - c6a.24xlarge - c6a.32xlarge - c6a.48xlarge - c6a.metal 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" # Networking configuration OpenViduVPC: Description: "Dedicated VPC for OpenVidu cluster" Type: AWS::EC2::VPC::Id AllowedPattern: ^.+$ ConstraintDescription: You must specify a VPC ID OpenViduSubnet: Description: "Subnet for OpenVidu cluster" Type: AWS::EC2::Subnet::Id AllowedPattern: ^.+$ ConstraintDescription: You must specify a subnet ID # Other configuration WantToDeployDemos: Description: "Choose if you want to deploy OpenVidu Call application alongside OpenVidu platform." Type: String AllowedValues: - true - false Default: true CoturnInMediaNodes: Description: "If true, Coturn will be deployed on media nodes. Otherwise it will be deployed in master nodes." Type: String AllowedValues: - true - false Default: false #start_mappings Mappings: OVAMIMAP: eu-west-1: AMI: OV_AMI_ID KMSAMIMAP: eu-west-1: AMI: KMS_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: - OpenViduLicense - OpenViduEdition - OpenViduSecret - MediaNodesStartNumber - Label: default: OpenVidu Recording Configuration Parameters: - Recording - S3RecordingsBucketName - Label: default: Elasticsearch and Kibana configuration Parameters: - ElasticsearchEnabled - ElasticsearchUrl - KibanaUrl - ElasticsearchUser - ElasticsearchPassword - Label: default: EC2 Instance configuration Parameters: - AwsInstanceTypeOV - AwsInstanceTypeKMS - KeyName - Label: default: Networking configuration Parameters: - OpenViduVPC - OpenViduSubnet - Label: default: Other configuration Parameters: - WantToDeployDemos - CoturnInMediaNodes 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)" Recording: default: "OpenVidu Recording" S3RecordingsBucketName: default: "S3 Bucket where recordings will be stored" # OpenVidu configuration OpenViduLicense: default: "OpenVidu Pro License key" OpenViduEdition: default: "Which OpenVidu Edition you want to deploy" MediaNodesStartNumber: default: "Initial number of Media Node in your cluster" OpenViduSecret: default: "Openvidu Secret" # Kibana configuration ElasticsearchEnabled: default: "Enable Elasticsearch and Kibana" ElasticsearchUrl: default: "Elasticsearch URL" KibanaUrl: default: "Kibana URL" ElasticsearchUser: default: "Elasticsearch and Kibana username" ElasticsearchPassword: default: "Elasticsearch and Kibana password" # EC2 instance configuration AwsInstanceTypeOV: default: "Instance type for Openvidu Server Pro Node" AwsInstanceTypeKMS: default: "Instance type for Media Nodes" KeyName: default: "SSH Key" # Networking configuration OpenViduVPC: default: "OpenVidu VPC" OpenViduSubnet: default: "OpenVidu Subnet" # Other configuration WantToDeployDemos: default: "Deploy OpenVidu Call application" CoturnInMediaNodes: default: "Deploy Coturn in Media Nodes. (Experimental)" Conditions: WhichCertPresent: !Not [ !Equals [!Ref WhichCert, ''] ] PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ''] ] RecordingStorageIsS3: !Equals [ !Ref Recording, 's3' ] CreateS3Bucket: !And - !Equals [!Ref Recording, 's3' ] - !Equals [!Ref S3RecordingsBucketName, ''] Rules: # Check recording RecordingValidation: RuleCondition: !Or [ !Equals [!Ref Recording, 'disabled' ], !Equals [!Ref Recording, 'local' ] ] Assertions: - AssertDescription: Parameter 'S3 Bucket where recordings will be stored' (S3RecordingsBucketName) is not needed when 'Recording' is 'disabled' or 'local'. Assert: !Equals [ !Ref S3RecordingsBucketName, '' ] # Check when Elasticsearch is enabled that all the parameters are present ElasticsearchValidation: RuleCondition: !Equals [ !Ref ElasticsearchEnabled, 'true' ] Assertions: - AssertDescription: Paramter 'Elasticsearch and Kibana username' (ElasticsearchUser) is needed when 'Enable Elasticsearch and Kibana' (ElasticsearchEnabled) is 'true'. Assert: !Not [ !Equals [!Ref ElasticsearchUser, ''] ] - AssertDescription: Parameter 'Elasticsearch and Kibana password' (ElasticsearchPassword) is needed when 'Enable Elasticsearch and Kibana' (ElasticsearchEnabled) is 'true'. Assert: !Not [ !Equals [!Ref ElasticsearchPassword, ''] ] # Check when Elasticsearch is disabled that any parameter of elasticsearch is not present ElasticsearchDisabledValidation: RuleCondition: !Equals [ !Ref ElasticsearchEnabled, 'false' ] Assertions: - AssertDescription: Parameter 'Elasticsearch URL' (ElasticsearchUrl) is not needed when 'Enable Elasticsearch and Kibana' (ElasticsearchEnabled) is 'false'. Assert: !Equals [ !Ref ElasticsearchUrl, "" ] - AssertDescription: Parameter 'Kibana URL' (KibanaUrl) is not needed when 'Enable Elasticsearch and Kibana' (ElasticsearchEnabled) is 'false'. Assert: !Equals [ !Ref KibanaUrl, "" ] - AssertDescription: Parameter 'Elasticsearch and Kibana username' (ElasticsearchUser) is not needed when 'Enable Elasticsearch and Kibana' (ElasticsearchEnabled) is 'false'. Assert: !Equals [ !Ref ElasticsearchUser, "" ] - AssertDescription: Parameter 'Elasticsearch and Kibana password' (ElasticsearchPassword) is not needed when 'Enable Elasticsearch and Kibana' (ElasticsearchEnabled) is 'false'. Assert: !Equals [ !Ref ElasticsearchPassword, "" ] # Check selfsigend parameters SelfSignedValidation: RuleCondition: !Equals [!Ref WhichCert, 'selfsigned' ] Assertions: - AssertDescription: Parameter 'URL to the CRT file' (OwnCertCRT) is not necessary when using 'selfsigned' as 'Certificate Type' (WhichCert). Assert: !Equals [ !Ref OwnCertCRT, '' ] - AssertDescription: Parameter 'URL to the key file' (OwnCertKEY) is not necessary when using 'selfsigned' as 'Certificate Type' (WhichCert). Assert: !Equals [!Ref OwnCertKEY, ''] - AssertDescription: Parameter 'Email for Let's Encrypt' (LetsEncryptEmail) is not necessary when using 'selfsigned' as 'Certificate Type' (WhichCert). Assert: !Equals [!Ref LetsEncryptEmail, ''] # Check Letsencrypt parameters LetsEncryptValidation: RuleCondition: !Equals [!Ref WhichCert, 'letsencrypt' ] Assertions: - AssertDescription: Parameter 'AWS Elastic IP' (PublicElasticIP) is needed when using 'letsencrypt' as 'Certificate Type' (WhichCert). Assert: !Not [ !Equals [ !Ref PublicElasticIP, '' ] ] - AssertDescription: Parameter 'Email for Let's Encrypt' (LetsEncryptEmail) is needed when using 'letsencrypt' as 'Certificate Type' (WhichCert). Assert: !Not [ !Equals [!Ref LetsEncryptEmail, ''] ] # Check OwnCertCRT and OwnCertKEY are defined if owncert is selected OwnCertValidation: RuleCondition: !Equals [ !Ref WhichCert, 'owncert' ] Assertions: - AssertDescription: Parameter 'AWS Elastic IP' (PublicElasticIP) is needed when using 'owncert' as 'Certificate Type' (WhichCert). Assert: !Not [ !Equals [ !Ref PublicElasticIP, '' ] ] - AssertDescription: Parameter 'URL to the CRT file' (OwnCertCRT) is needed when using 'owncert' as 'Certificate Type' (WhichCert). Assert: !Not [ !Equals [!Ref OwnCertCRT, ''] ] - AssertDescription: Parameter 'URL to the key file' (OwnCertKEY) is needed when using 'owncert' as 'Certificate Type' (WhichCert). Assert: !Not [ !Equals [!Ref OwnCertKEY, ''] ] Resources: OpenViduManageEC2Role: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: OpenViduManageEC2Policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'ec2:DescribeInstances' - 'ec2:RunInstances' - 'ec2:TerminateInstances' - 'ec2:CreateTags' - 'ec2:DescribeSecurityGroups' - 'ec2:DescribeSubnets' - 'iam:PassRole' - 'route53:ChangeResourceRecordSets' - 'route53:ListHostedZones' Resource: '*' - Fn::If: # Only apply this policy if S3 is configured - RecordingStorageIsS3 - Effect: Allow Action: - 's3:DeleteObject' - 's3:GetObject' - 's3:PutObject' Resource: - Fn::If: # Get bucket name depending if the user defines a bucket name or not - CreateS3Bucket ### Unique bucket name using Stack ID - !Join [ "", [ 'arn:aws:s3:::', 'openvidu-recordings-', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]], "/*" ]] - !Join [ "", [ 'arn:aws:s3:::', !Ref S3RecordingsBucketName, '/*'] ] - Ref: AWS::NoValue - Fn::If: # Only apply this policy if S3 is configured - RecordingStorageIsS3 - Effect: Allow Action: - 's3:ListBucket' - 's3:GetBucketLocation' Resource: - Fn::If: # Get bucket name depending if the user defines a bucket name or not - CreateS3Bucket ### Unique bucket name using Stack ID - !Join [ "", [ 'arn:aws:s3:::', 'openvidu-recordings-', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] - !Join [ "", [ 'arn:aws:s3:::', !Ref S3RecordingsBucketName ] ] - Ref: AWS::NoValue - Fn::If: # Only apply this policy if S3 is configured - RecordingStorageIsS3 - Effect: Allow Action: - s3:ListAllMyBuckets Resource: 'arn:aws:s3:::' - Ref: AWS::NoValue RoleName: !Join [ "-", [ OpenViduManageEC2Role, !Ref 'AWS::StackName', !Ref 'AWS::Region'] ] OpenviduInstancesProfile: Type: 'AWS::IAM::InstanceProfile' Properties: InstanceProfileName: !Join [ "-", [ OpenViduInstanceProfile, !Ref 'AWS::StackName', !Ref 'AWS::Region'] ] Path: / Roles: - !Join [ "-", [ OpenViduManageEC2Role, !Ref 'AWS::StackName', !Ref 'AWS::Region'] ] DependsOn: - OpenViduManageEC2Role S3bucket: Type: 'AWS::S3::Bucket' Properties: ### Unique bucket name using Stack ID BucketName: !Join ["-" , [ 'openvidu-recordings', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls : true RestrictPublicBuckets: true DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateS3Bucket LaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateName: IMDSV2 LaunchTemplateData: MetadataOptions: HttpEndpoint: enabled HttpPutResponseHopLimit: 1 HttpTokens: required OpenViduServer: Type: AWS::EC2::Instance Metadata: Comment: OpenVidu Pro AWS::CloudFormation::Init: config: files: '/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 -xe WORKINGDIR=/opt/openvidu TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") # Pro License sed -i "s/OPENVIDU_PRO_LICENSE=/OPENVIDU_PRO_LICENSE=${OpenViduLicense}/" $WORKINGDIR/.env # OpenVidu Edition sed -i "s/OPENVIDU_EDITION=pro/OPENVIDU_EDITION=${OpenViduEdition}/" $WORKINGDIR/.env # 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 -H "X-aws-ec2-metadata-token: $TOKEN" 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 # OpenVidu Pro mode sed -i "s/OPENVIDU_PRO_CLUSTER_MODE=manual/OPENVIDU_PRO_CLUSTER_MODE=auto/" $WORKINGDIR/.env # OpenVidu Pro Media Nodes sed -i "s/#OPENVIDU_PRO_CLUSTER_MEDIA_NODES=/OPENVIDU_PRO_CLUSTER_MEDIA_NODES=${MediaNodesStartNumber}/" $WORKINGDIR/.env # OpenVidu Pro enviroment sed -i "s/OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise/OPENVIDU_PRO_CLUSTER_ENVIRONMENT=aws/" $WORKINGDIR/.env # 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 # Replace Elastic Search Conf if [[ "${ElasticsearchEnabled}" == "true" ]]; then if [[ ! -z "${ElasticsearchUrl}" ]]; then sed -i "s,#OPENVIDU_PRO_ELASTICSEARCH_HOST=,OPENVIDU_PRO_ELASTICSEARCH_HOST=${ElasticsearchUrl}," $WORKINGDIR/.env fi if [[ ! -z "${KibanaUrl}" ]]; then sed -i "s,#OPENVIDU_PRO_KIBANA_HOST=,OPENVIDU_PRO_KIBANA_HOST=${KibanaUrl}," $WORKINGDIR/.env fi sed -i "s/ELASTICSEARCH_USERNAME=elasticadmin/ELASTICSEARCH_USERNAME=${ElasticsearchUser}/" $WORKINGDIR/.env sed -i "s/ELASTICSEARCH_PASSWORD=/ELASTICSEARCH_PASSWORD=${ElasticsearchPassword}/" $WORKINGDIR/.env else sed -i "s/OPENVIDU_PRO_ELASTICSEARCH=true/OPENVIDU_PRO_ELASTICSEARCH=false/" $WORKINGDIR/.env fi # Replace vars AWS INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) sed -i "s/#AWS_DEFAULT_REGION=/AWS_DEFAULT_REGION=${AWS::Region}/" $WORKINGDIR/.env sed -i "s/#AWS_IMAGE_ID=/AWS_IMAGE_ID=${kmsAmi}/" $WORKINGDIR/.env sed -i "s/#AWS_INSTANCE_TYPE=/AWS_INSTANCE_TYPE=${AwsInstanceTypeKMS}/" $WORKINGDIR/.env sed -i "s/#AWS_INSTANCE_ID=/AWS_INSTANCE_ID=$INSTANCE_ID/" $WORKINGDIR/.env sed -i "s/#AWS_KEY_NAME=/AWS_KEY_NAME=${KeyName}/" $WORKINGDIR/.env sed -i "s/#AWS_SUBNET_ID=/AWS_SUBNET_ID=${OpenViduSubnet}/" $WORKINGDIR/.env sed -i "s/#AWS_STACK_ID=/AWS_STACK_ID=$(echo ${AWS::StackId} | sed 's#/#\\/#g')/" $WORKINGDIR/.env sed -i "s/#AWS_STACK_NAME=/AWS_STACK_NAME=${AWS::StackName}/" $WORKINGDIR/.env sed -i "s/#AWS_CLI_DOCKER_TAG=/AWS_CLI_DOCKER_TAG=_AWS_CLI_DOCKER_TAG_/" $WORKINGDIR/.env sed -i "s/#AWS_VOLUME_SIZE=/AWS_VOLUME_SIZE=50/" $WORKINGDIR/.env sed -i "s/#OPENVIDU_PRO_AWS_REGION=/OPENVIDU_PRO_AWS_REGION=${AWS::Region}/" $WORKINGDIR/.env # Get security group id of kms and use it as env variable SECGRPIDKMS=$(/usr/local/bin/getSecurityGroupKms.sh) sed -i "s/#AWS_SECURITY_GROUP=/AWS_SECURITY_GROUP=$SECGRPIDKMS/" $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 # Deploy Coturn in media nodes if [ "${CoturnInMediaNodes}" == "true" ]; then sed -i "s/OPENVIDU_PRO_COTURN_IN_MEDIA_NODES=false/OPENVIDU_PRO_COTURN_IN_MEDIA_NODES=true/" $WORKINGDIR/.env fi # Recording Configuration if [ "${Recording}" != "disabled" ]; then sed -i "s/OPENVIDU_RECORDING=false/OPENVIDU_RECORDING=true/" $WORKINGDIR/.env sed -i "s/#OPENVIDU_PRO_RECORDING_STORAGE=/OPENVIDU_PRO_RECORDING_STORAGE=${Recording}/" $WORKINGDIR/.env if [ ! -z "${S3RecordingsBucketName}" ]; then sed -i "s/#OPENVIDU_PRO_AWS_S3_BUCKET=/OPENVIDU_PRO_AWS_S3_BUCKET=${S3RecordingsBucketName}/" $WORKINGDIR/.env elif [ "${Recording}" == "s3" ]; then sed -i "s/#OPENVIDU_PRO_AWS_S3_BUCKET=/OPENVIDU_PRO_AWS_S3_BUCKET=${s3BucketName}/" $WORKINGDIR/.env fi fi - kmsAmi: !GetAtt CloudformationLambdaInvoke.MediaNodeImageId ### Unique bucket name using Stack ID s3BucketName: !Join ["" , [ 'openvidu-recordings-', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] 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/getSecurityGroupKms.sh': content: !Sub | #!/bin/bash -x docker run --rm amazon/aws-cli:_AWS_CLI_DOCKER_TAG_ ec2 describe-security-groups \ --region ${AWS::Region} \ --output text \ --filters "Name=tag:aws:cloudformation:logical-id,Values=KMSSecurityGroup" \ "Name=tag:aws:cloudformation:stack-id,Values=${AWS::StackId}" \ --query 'SecurityGroups[].GroupId[]' mode: "000755" owner: "root" group: "root" '/usr/local/bin/restartPRO.sh': content: | #!/bin/bash -x WORKINGDIR=/opt/openvidu # Get new amazon URL OldPublicHostname=$(cat /usr/share/openvidu/old-host-name) TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") PublicHostname=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" 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 export FOLLOW_OPENVIDU_LOGS=false ./openvidu stop ./openvidu start popd mode: "000755" owner: "root" group: "root" Properties: ImageId: !GetAtt CloudformationLambdaInvoke.MasterNodeImageId InstanceType: !Ref AwsInstanceTypeOV KeyName: !Ref KeyName IamInstanceProfile: !Ref OpenviduInstancesProfile SubnetId: !Ref OpenViduSubnet SecurityGroupIds: - !GetAtt 'OpenViduSecurityGroup.GroupId' Tags: - Key: Name Value: 'OpenVidu Pro Master Node' - Key: 'ov-cluster-member' Value: 'server' UserData: "Fn::Base64": !Sub | #!/bin/bash -xe 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/restartPRO.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 export FOLLOW_OPENVIDU_LOGS=false ./openvidu start popd # 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: gp3 DeleteOnTermination: true VolumeSize: 200 ########## # Security groups ########## KMSSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: VpcId: !Ref OpenViduVPC GroupDescription: SSH, Proxy and KMS WebRTC Ports GroupName: !Join [ "-", [ !Ref 'AWS::StackName', 'KMSSecurityGroup'] ] SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIpv6: ::/0 - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIpv6: ::/0 - IpProtocol: udp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 443 ToPort: 443 CidrIpv6: ::/0 - IpProtocol: tcp FromPort: 3000 ToPort: 3000 SourceSecurityGroupId: !Ref OpenViduSecurityGroup - IpProtocol: tcp FromPort: 4000 ToPort: 4000 SourceSecurityGroupId: !Ref OpenViduSecurityGroup - IpProtocol: tcp FromPort: 8888 ToPort: 8888 SourceSecurityGroupId: !Ref OpenViduSecurityGroup - IpProtocol: udp FromPort: 40000 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 40000 ToPort: 65535 CidrIpv6: ::/0 - IpProtocol: tcp FromPort: 40000 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 40000 ToPort: 65535 CidrIpv6: ::/0 SecurityGroupEgress: - IpProtocol: tcp FromPort: 1 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 1 ToPort: 65535 CidrIpv6: ::/0 - IpProtocol: udp FromPort: 1 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 1 ToPort: 65535 CidrIpv6: ::/0 OpenViduSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: SSH, Proxy and OpenVidu WebRTC Ports GroupName: !Join [ "-", [ !Ref 'AWS::StackName', 'OpenViduSecurityGroup'] ] VpcId: !Ref OpenViduVPC 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: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 40000 ToPort: 65535 CidrIpv6: ::/0 - IpProtocol: tcp FromPort: 40000 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 40000 ToPort: 65535 CidrIpv6: ::/0 SecurityGroupEgress: - IpProtocol: tcp FromPort: 1 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 1 ToPort: 65535 CidrIpv6: ::/0 - IpProtocol: udp FromPort: 1 ToPort: 65535 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 1 ToPort: 65535 CidrIpv6: ::/0 OpenViduSecurityGroupIngressELK: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduSecurityGroup IpProtocol: tcp FromPort: 9200 ToPort: 9200 SourceSecurityGroupId: !Ref KMSSecurityGroup WaitCondition: Type: AWS::CloudFormation::WaitCondition CreationPolicy: ResourceSignal: Timeout: PT25M Count: 1 MyEIP: Type: AWS::EC2::EIPAssociation Condition: PublicElasticIPPresent Properties: InstanceId: !Ref OpenViduServer EIP: !Ref PublicElasticIP ########## # Lambda which complements Cloudformation to: # 1. On create: Copy original AMIs from eu-west-1 to the deployment region # 2. On Delete: Removes media nodes created by OpenVidu Server PRO ########## 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: # Permissions to copy original Lambda to the region where it is being deployed - Effect: Allow Action: - 'ec2:DescribeImages' - 'ec2:CopyImage' Resource: '*' # Describe instances to get instances which OpenVidu PRO creates - Effect: Allow Action: - 'ec2:DescribeInstances' Resource: '*' # Permissions to remove media nodes while destroying the Cloudformation # Only those created by OpenVidu PRO can be deleted - Effect: Allow Action: - 'ec2:TerminateInstances' Resource: '*' Condition: StringEquals: 'aws:ResourceTag/ov-cluster-member': 'kms' 'aws:ResourceTag/ov-stack-name': !Ref AWS::StackName 'aws:ResourceTag/ov-stack-region': !Ref AWS::Region 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) elif (event['RequestType'] == 'Delete'): removeMediaNodes(event, context) else: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except Exception: cfnresponse.send(event, context, cfnresponse.FAILED, {}) def copy_ami_operation(source_image_id, source_region, new_ami_name, ec2_client): own_ami_filter = [{ 'Name': 'name', 'Values': [new_ami_name] }] amis_response = ec2_client.describe_images(Filters=own_ami_filter) if (len(amis_response['Images']) == 1): # If AMI exists, don't copy return amis_response['Images'][0]['ImageId'] else: # If AMI does not exist, copy new_amis_response = ec2_client.copy_image( SourceImageId=source_image_id, SourceRegion=source_region, Name=new_ami_name ) return new_amis_response['ImageId'] def copy_ami(event, context): new_images=[] cfn_output = {} source_image_id_master_node = event['ResourceProperties']['MasterNodeAmiSourceId'] source_image_id_media_node = event['ResourceProperties']['MediaNodeAmiSourceId'] 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_master_node_filter = [{ 'Name': 'image-id', 'Values': [ source_image_id_master_node ] }] public_ami_media_node_filter = [{ 'Name': 'image-id', 'Values': [ source_image_id_media_node ] }] response = ec2_client_ov.describe_images(Filters=public_ami_master_node_filter) new_ami_name_master_node = "[ OpenVidu PRO Master Node AMI Copy ] - " + response['Images'][0]['Name'] response = ec2_client_ov.describe_images(Filters=public_ami_media_node_filter) new_ami_name_media_node = "[ OpenVidu PRO/ENTERPRISE Media Node AMI Copy ] - " + response['Images'][0]['Name'] # Copy master node AMI and media node AMI master_node_ami_id = copy_ami_operation(source_image_id_master_node, source_region, new_ami_name_master_node, ec2_client) new_images.append(master_node_ami_id) cfn_output['MasterNodeImageId'] = master_node_ami_id media_node_ami_id = copy_ami_operation(source_image_id_media_node, source_region, new_ami_name_media_node, ec2_client) new_images.append(media_node_ami_id) cfn_output['MediaNodeImageId'] = media_node_ami_id # Wait images to be available waiter_config = {'Delay': 15, 'MaxAttempts': 59 } response = img_exists_waiter.wait(ImageIds=new_images, WaiterConfig=waiter_config) response = img_avail_waiter.wait(ImageIds=new_images, WaiterConfig=waiter_config) # Return AMIs cfnresponse.send(event, context, cfnresponse.SUCCESS, cfn_output) def removeMediaNodes(event, context): cluster_stack_name = event['ResourceProperties']['StackName'] deployment_region = event['ResourceProperties']['DeploymentRegion'] # Clients init ec2_client = boto3.client('ec2', config = Config(region_name=deployment_region)) ec2_media_node_filter = [ { 'Name': 'tag:ov-cluster-member', 'Values': [ 'kms' ] }, { 'Name': 'tag:ov-stack-region', 'Values': [ deployment_region ] }, { 'Name': 'tag:ov-stack-name', 'Values': [ cluster_stack_name ] } ] # Get instances to remove response_media_nodes = ec2_client.describe_instances(Filters=ec2_media_node_filter, MaxResults=1000) # Remove instances instance_ids_to_remove = [] for reservation in response_media_nodes['Reservations']: for instance in reservation['Instances']: instance_ids_to_remove.append(instance['InstanceId']) print(instance_ids_to_remove) ec2_client.terminate_instances(InstanceIds=instance_ids_to_remove) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) Handler: index.handler Role: !GetAtt CloudformationLambdaRole.Arn Runtime: python3.11 Timeout: 900 CloudformationLambdaInvoke: Type: AWS::CloudFormation::CustomResource DeletionPolicy: Delete Version: "1.0" Properties: ServiceToken: !GetAtt CloudformationLambda.Arn AmiSourceRegion: 'eu-west-1' MasterNodeAmiSourceId: !FindInMap [OVAMIMAP, 'eu-west-1', AMI] MediaNodeAmiSourceId: !FindInMap [KMSAMIMAP, 'eu-west-1', AMI] StackName: !Ref AWS::StackName DeploymentRegion: !Ref AWS::Region Outputs: OpenViduInspector: Description: "Use this URL to connect OpenVidu with user and password" Value: !Join - '' - - 'https://' - !GetAtt OpenViduServer.PublicDnsName - '/inspector' OpenViduInspectorLE: Description: "Use this URL to connect to OpenVidu with user and password if you're using Let's Encrypt" Value: !Join - '' - - 'https://' - !Ref MyDomainName - '/inspector' Condition: WhichCertPresent Kibana: Description: "Check out graph and performance of your OpenVidu installation" Value: !Join - '' - - 'https://' - !GetAtt OpenViduServer.PublicDnsName - '/kibana' KibanaLE: Description: "Check out graph and performance of your OpenVidu installation" Value: !Join - '' - - 'https://' - !Ref MyDomainName - '/kibana' Condition: WhichCertPresent