AWSTemplateFormatVersion: 2010-09-09 Description: External TURN server for OpenVidu Server. Parameters: PublicElasticIP: Description: "Previously created AWS Elastic IP to associate it to the EC2 instance. 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 and it is mandatory MyDomainName: Description: "Valid domain name pointing to previous IP. 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 LetsEncryptEmail: Description: "This email will be used for Let's Encrypt notifications" AllowedPattern: ^(.+)@(.+)$ Type: String TurnStaticAuthSecret: Description: "Shared secret for the TURN server to generate credentials for clients" Type: String NoEcho: true AllowedPattern: .+$ ConstraintDescription: Turn secret is mandatory. InstanceType: Description: "Specifies the EC2 instance type for your TURN 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 - 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 to the TURN server." Type: 'AWS::EC2::KeyPair::KeyName' ConstraintDescription: "must be the name of an existing EC2 KeyPair" Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: Domain and SSL certificate configuration Parameters: - PublicElasticIP - MyDomainName - LetsEncryptEmail - Label: default: Turn server configuration Parameters: - TurnStaticAuthSecret - Label: default: EC2 Instance configuration Parameters: - InstanceType - KeyName ParameterLabels: # SSL certificate configuration PublicElasticIP: default: "AWS Elastic IP (EIP)" MyDomainName: default: "Domain Name pointing to Elastic IP" LetsEncryptEmail: default: "Email for Let's Encrypt (letsencrypt)" # OpenVidu configuration TurnStaticAuthSecret: default: > Turn shared secret. This is used to generate credentials and should be in OpenVidu Server configuration. # EC2 Instance configuration InstanceType: default: "Instance type" KeyName: default: "SSH Key" # Other configuration Resources: DescribeImagesRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: DescribeImages PolicyDocument: Version: '2012-10-17' Statement: - Action: ec2:DescribeImages Effect: Allow Resource: "*" GetLatestUbuntuAmi: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !Sub ${DescribeImagesRole.Arn} Timeout: 60 Runtime: python3.11 Code: ZipFile: | import boto3 import cfnresponse import json import traceback def handler(event, context): try: response = boto3.client('ec2').describe_images(Filters=[ {'Name': 'name', 'Values': [event['ResourceProperties']['Name']]}, {'Name': 'owner-alias', 'Values': ['amazon']} ]) amis = sorted(response['Images'], key=lambda x: x['CreationDate'], reverse=True) id = amis[0]['ImageId'] cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, id) except: traceback.print_last() cfnresponse.send(event, context, cfnresponse.FAIL, {}, "ok") UbuntuAmi: Type: Custom::FindAMI Properties: ServiceToken: !Sub ${GetLatestUbuntuAmi.Arn} Name: "*ubuntu/images/hvm-ssd/ubuntu-noble-24.04-amd64-server-*" TurnServerInstance: Type: 'AWS::EC2::Instance' Metadata: Comment: 'Install External TURN server for OpenVidu Server' AWS::CloudFormation::Init: config: files: '/usr/local/bin/install_docker.sh': content: | #!/bin/bash # Check if docker is already installed if ! command -v docker &> /dev/null; then # Install docker apt-get -y update apt-get -y install \ ca-certificates \ curl \ gnupg mkdir -m 0755 -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get -y update apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin fi mode: "000755" owner: "root" group: "root" '/usr/local/bin/coturn.sh': content: !Sub | #!/bin/bash -x WORKIND_DIR=/opt/coturn cd /opt # Check if directory /opt/coturn exists # If it does not exist, it means it is the first time we run this script # and we need to install coturn and fill the .env file if [ ! -d "$WORKIND_DIR" ]; then # This means it is the first time we run this script curl https://s3.eu-west-1.amazonaws.com/aws.openvidu.io/external-turn/4.6.2/install_openvidu_external_coturn.sh | bash cd "$WORKIND_DIR" # Replace environment variables sed -i "s|TURN_DOMAIN_NAME=.*|TURN_DOMAIN_NAME=${MyDomainName}|" .env sed -i "s|LETSENCRYPT_EMAIL=.*|LETSENCRYPT_EMAIL=${LetsEncryptEmail}|" .env sed -i "s|TURN_STATIC_AUTH_SECRET=.*|TURN_STATIC_AUTH_SECRET=${TurnStaticAuthSecret}|" .env fi cd "$WORKIND_DIR" docker compose down docker compose up -d mode: "000755" owner: "root" group: "root" '/usr/local/bin/wait_for_coturn.sh': content: !Sub | #!/bin/bash -x # Configuration SERVER="${MyDomainName}" PORT="443" TIMEOUT=600 # 10 minutes in seconds INTERVAL=10 # Time interval between attempts in seconds TARGET_EXIT_CODE=0 start_time=$(date +%s) while true; do current_time=$(date +%s) elapsed_time=$((current_time - start_time)) if [ $elapsed_time -ge $TIMEOUT ]; then echo "Time limit reached: $TIMEOUT seconds" exit 1 fi # Run the command and get the exit code docker exec coturn turnutils_stunclient -p $PORT $SERVER exit_code=$? if [ $exit_code -eq $TARGET_EXIT_CODE ]; then echo "Coturn is ready" exit 0 else echo "Coturn is not ready yet. Waiting $INTERVAL seconds before the next attempt..." sleep $INTERVAL fi done mode: "000755" owner: "root" group: "root" Properties: ImageId: !Ref UbuntuAmi 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 apt-get update && apt-get install -y \ python3-pip \ ec2-instance-connect pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource TurnServerInstance /usr/local/bin/install_docker.sh export HOME="/root" # Check if crontab has already been configured if ! crontab -l | grep -q "coturn.sh"; then # Configure crontab echo "@reboot /usr/local/bin/coturn.sh" | crontab fi # Run coturn /usr/local/bin/coturn.sh # Wait for coturn to be ready /usr/local/bin/wait_for_coturn.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: 25 MyEIP: Type: 'AWS::EC2::EIPAssociation' Properties: InstanceId: !Ref TurnServerInstance EIP: !Ref PublicElasticIP WaitCondition: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT30M Count: '1' WebServerSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: SSH, Proxy and Turn necessaty 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: udp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 443 ToPort: 443 CidrIpv6: ::/0 Outputs: TurnServerURI: Description: Use this URL to connect the TURN Server Value: !Sub 'turns://${MyDomainName}:443?transport=tcp'