AWSTemplateFormatVersion: 2010-09-09 Description: OpenVidu Pro - High Availability Parameters: DomainName: Type: String Description: Domain name for the OpenVidu High Availability cluster 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 OpenViduCertificateARN: Description: 'Amazon certificate arn resource to load into the LoadBalancer' Type: String AllowedPattern: '.+' ConstraintDescription: The Load Balancer domain name must be defined TurnDomainName: Description: '(Optional) Domain name for the TURN server with TLS.' Type: String Default: '' TurnCertificateARN: Description: '(Optional) Amazon certificate arn resource to load into the TURN LoadBalancer' Type: String Default: '' OpenViduLicense: Description: "Visit https://openvidu.io/account" Type: String AllowedPattern: ^(?!\s*$).+$ NoEcho: true ConstraintDescription: OpenVidu Pro License is mandatory RTCEngine: Description: "RTCEngine media engine to use" Type: String AllowedValues: - pion - mediasoup Default: pion MasterNodeInstanceType: Description: "Specifies the EC2 instance type for your OpenVidu Master Node" Type: String Default: c6a.xlarge AllowedValues: - t2.large - t2.xlarge - t2.2xlarge - t3.medium - 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 - m6i.large - m6i.xlarge - m6i.2xlarge - m6i.4xlarge - m6i.8xlarge - m6i.12xlarge - m6i.16xlarge - m6i.24xlarge - m6i.32xlarge - m6i.metal - 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 - c6i.large - c6i.xlarge - c6i.2xlarge - c6i.4xlarge - c6i.8xlarge - c6i.12xlarge - c6i.16xlarge - c6i.24xlarge - c6i.32xlarge - c6i.metal - c7a.medium - c7a.large - c7a.xlarge - c7a.2xlarge - c7a.4xlarge - c7a.8xlarge - c7a.12xlarge - c7a.16xlarge - c7a.24xlarge - c7a.32xlarge - c7a.48xlarge - c7a.metal-48xl - c7i.large - c7i.xlarge - c7i.2xlarge - c7i.4xlarge - c7i.8xlarge - c7i.12xlarge - c7i.16xlarge - c7i.24xlarge - c7i.48xlarge - c7i.metal-24xl - c7i.metal-48xl - c5n.large - c5n.xlarge - c5n.2xlarge - c5n.4xlarge - c5n.9xlarge - c5n.18xlarge - m5n.large - m5n.xlarge - m5n.2xlarge - m5n.4xlarge - m5n.8xlarge - m5n.12xlarge - m5n.16xlarge - m5n.24xlarge - m6in.large - m6in.xlarge - m6in.2xlarge - m6in.4xlarge - m6in.8xlarge - m6in.12xlarge - m6in.16xlarge - m6in.24xlarge - m6in.32xlarge - r5n.large - r5n.xlarge - r5n.2xlarge - r5n.4xlarge - r5n.8xlarge - r5n.12xlarge - r5n.16xlarge - r5n.24xlarge ConstraintDescription: "Must be a valid EC2 instance type" MediaNodeInstanceType: Description: "Specifies the EC2 instance type for your OpenVidu Media Nodes" Type: String Default: c6a.xlarge AllowedValues: - t2.large - t2.xlarge - t2.2xlarge - t3.medium - 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 - m6i.large - m6i.xlarge - m6i.2xlarge - m6i.4xlarge - m6i.8xlarge - m6i.12xlarge - m6i.16xlarge - m6i.24xlarge - m6i.32xlarge - m6i.metal - 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 - c6i.large - c6i.xlarge - c6i.2xlarge - c6i.4xlarge - c6i.8xlarge - c6i.12xlarge - c6i.16xlarge - c6i.24xlarge - c6i.32xlarge - c6i.metal - c7a.medium - c7a.large - c7a.xlarge - c7a.2xlarge - c7a.4xlarge - c7a.8xlarge - c7a.12xlarge - c7a.16xlarge - c7a.24xlarge - c7a.32xlarge - c7a.48xlarge - c7a.metal-48xl - c7i.large - c7i.xlarge - c7i.2xlarge - c7i.4xlarge - c7i.8xlarge - c7i.12xlarge - c7i.16xlarge - c7i.24xlarge - c7i.48xlarge - c7i.metal-24xl - c7i.metal-48xl - c5n.large - c5n.xlarge - c5n.2xlarge - c5n.4xlarge - c5n.9xlarge - c5n.18xlarge - m5n.large - m5n.xlarge - m5n.2xlarge - m5n.4xlarge - m5n.8xlarge - m5n.12xlarge - m5n.16xlarge - m5n.24xlarge - m6in.large - m6in.xlarge - m6in.2xlarge - m6in.4xlarge - m6in.8xlarge - m6in.12xlarge - m6in.16xlarge - m6in.24xlarge - m6in.32xlarge - r5n.large - r5n.xlarge - r5n.2xlarge - r5n.4xlarge - r5n.8xlarge - r5n.12xlarge - r5n.16xlarge - r5n.24xlarge ConstraintDescription: "Must be a valid EC2 instance type" KeyName: Type: AWS::EC2::KeyPair::KeyName Description: Name of an existing EC2 KeyPair to enable SSH access to the instances AllowedPattern: ^.+$ ConstraintDescription: must be the name of an existing EC2 KeyPair. AmiId: Type: AWS::SSM::Parameter::Value Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id Description: AMI ID for the EC2 instances InitialNumberOfMediaNodes: Type: Number Default: 1 Description: Number of initial media nodes to deploy MinNumberOfMediaNodes: Type: Number Default: 1 Description: Minimum number of media nodes to deploy MaxNumberOfMediaNodes: Type: Number Default: 5 Description: Maximum number of media nodes to deploy ScaleTargetCPU: Type: Number Default: 50 Description: Target CPU percentage to scale up or down S3AppDataBucketName: Type: String Description: Name of the S3 bucket to store data and recordings. If empty, a bucket will be created S3ClusterDataBucketName: Type: String Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created AdditionalInstallFlags: Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2"). Type: String Default: "" AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag). OpenViduVPC: Description: "Dedicated VPC for OpenVidu cluster" Type: AWS::EC2::VPC::Id AllowedPattern: ^.+$ ConstraintDescription: You must specify a VPC ID OpenViduMasterNodeSubnets: Description: "Subnets for OpenVidu Master Node" Type: List AllowedPattern: ^.+$ ConstraintDescription: You must specify a list of subnet IDs OpenViduMediaNodeSubnets: Description: "Subnets for OpenVidu Media Nodes" Type: List AllowedPattern: ^.+$ ConstraintDescription: You must specify a list of subnet IDs MasterNodesDiskSize: Description: Size of the disk in GB Type: Number Default: 100 MinValue: 50 ConstraintDescription: The disk size must be at least 50 GB Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: Domain and Load Blancer configuration Parameters: - DomainName - OpenViduCertificateARN - Label: default: OpenVidu High Availability configuration Parameters: - OpenViduLicense - RTCEngine - Label: default: EC2 Instance configuration Parameters: - MasterNodeInstanceType - MediaNodeInstanceType - KeyName - AmiId - Label: default: Media Nodes Autoscaling Group configuration Parameters: - InitialNumberOfMediaNodes - MinNumberOfMediaNodes - MaxNumberOfMediaNodes - ScaleTargetCPU - Label: default: S3 bucket for application data, cluster data and recordings Parameters: - S3AppDataBucketName - S3ClusterDataBucketName - Label: default: VPC configuration Parameters: - OpenViduVPC - OpenViduMasterNodeSubnets - OpenViduMediaNodeSubnets - Label: default: Volumes configuration Parameters: - MasterNodesDiskSize - Label: default: "(Optional) Additional Installer Flags" Parameters: - AdditionalInstallFlags - Label: default: (Optional) TURN server configuration with TLS Parameters: - TurnDomainName - TurnCertificateARN Conditions: TurnTLSIsEnabled: !Or [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]] CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""] # --- # Experimental TURN TLS with main domain ExperimentalTurnTLSWithMainDomain: Fn::Not: - Fn::Equals: - !Ref AdditionalInstallFlags - !Select [0, !Split ["--experimental-turn-tls-with-main-domain", !Ref AdditionalInstallFlags]] NotExperimentalTurnTLSWithMainDomain: Fn::Or: - Fn::Equals: - !Ref AdditionalInstallFlags - !Select [0, !Split ["--experimental-turn-tls-with-main-domain", !Ref AdditionalInstallFlags]] - Fn::Equals: - !Ref AdditionalInstallFlags - "" # --- Resources: OpenViduSharedInfo: Type: AWS::SecretsManager::Secret UpdateReplacePolicy: Retain DeletionPolicy: Delete Properties: Name: !Sub openvidu-ha-${AWS::Region}-${AWS::StackName} Description: Secret for OpenVidu High Availability to store deployment info and seed secret # All the values are initialized by one master node and shared with the rest of the nodes SecretString: | { "ALL_SECRETS_GENERATED": "false", "DOMAIN_NAME": "none", "LIVEKIT_TURN_DOMAIN_NAME": "none", "OPENVIDU_PRO_LICENSE": "none", "OPENVIDU_RTC_ENGINE": "none", "REDIS_PASSWORD": "none", "MONGO_ADMIN_USERNAME": "none", "MONGO_ADMIN_PASSWORD": "none", "MONGO_REPLICA_SET_KEY": "none", "MINIO_URL": "none", "MINIO_ACCESS_KEY": "none", "MINIO_SECRET_KEY": "none", "DASHBOARD_URL": "none", "DASHBOARD_ADMIN_USERNAME": "none", "DASHBOARD_ADMIN_PASSWORD": "none", "GRAFANA_URL": "none", "GRAFANA_ADMIN_USERNAME": "none", "GRAFANA_ADMIN_PASSWORD": "none", "MEET_ADMIN_USER": "none", "MEET_ADMIN_SECRET": "none", "MEET_API_KEY": "none", "LIVEKIT_API_KEY": "none", "LIVEKIT_API_SECRET": "none", "ENABLED_MODULES": "none", "MASTER_NODE_1_PRIVATE_IP": "none", "MASTER_NODE_2_PRIVATE_IP": "none", "MASTER_NODE_3_PRIVATE_IP": "none", "MASTER_NODE_4_PRIVATE_IP": "none", "OPENVIDU_VERSION": "none" } S3AppDataBucketResource: Type: 'AWS::S3::Bucket' Properties: ### Unique bucket name using Stack ID BucketName: !Join ["-" , [ 'openvidu-appdata', !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: CreateRecordingsBucket S3ClusterDataBucketResource: Type: 'AWS::S3::Bucket' Properties: ### Unique bucket name using Stack ID BucketName: !Join ["-" , [ 'openvidu-clusterdata', !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: CreateClusterDataBucket # ------------------------- # Preprocess subnets to allocate Volumes and ENIs across Availability Zones # For OpenVidu Master Nodes # ------------------------- SubnetProcessorFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub 'SubnetProcessor-${AWS::Region}-${AWS::StackName}' Handler: index.lambda_handler Role: !GetAtt LambdaExecutionRole.Arn Code: ZipFile: | import cfnresponse import boto3 def lambda_handler(event, context): try: # Process event data subnets = event['ResourceProperties']['Subnets'] ec2 = boto3.client('ec2') # Ensure we have at least four subnets by cycling through the available subnets subnets = (subnets * 4)[:4] # Repeat the list to have at least 4 elements and then take the first 4 # Prepare the response responseData = { 'Subnet1': subnets[0], 'Subnet2': subnets[1], 'Subnet3': subnets[2], 'Subnet4': subnets[3], } cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) except Exception as e: cfnresponse.send(event, context, cfnresponse.FAILED, {'Message': str(e)}) Runtime: python3.12 Timeout: 120 SubnetProcessor: Type: Custom::SubnetProcessor Properties: ServiceToken: !GetAtt SubnetProcessorFunction.Arn Subnets: !Ref OpenViduMasterNodeSubnets LambdaLogGroup: UpdateReplacePolicy: Retain DeletionPolicy: Delete Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}' RetentionInDays: 7 LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: LambdaLogsPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}:*' - Effect: Allow Action: - ec2:DescribeSubnets Resource: '*' OpenViduMasterNodeRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: "/" Policies: - PolicyName: !Sub openvidu-master-policy-${AWS::Region}-${AWS::StackName} PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - autoscaling:SetInstanceHealth Resource: '*' Condition: StringEquals: 'aws:ResourceTag/aws:cloudformation:stack-id': !Ref 'AWS::StackId' - Effect: Allow Action: - secretsmanager:GetSecretValue - secretsmanager:UpdateSecret Resource: !Ref OpenViduSharedInfo - Fn::If: - CreateRecordingsBucket - Effect: Allow Action: - s3:DeleteObject - s3:GetObject - s3:PutObject Resource: !Sub ${S3AppDataBucketResource.Arn}/* - Effect: Allow Action: - s3:DeleteObject - s3:GetObject - s3:PutObject Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* - Fn::If: - CreateRecordingsBucket - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !GetAtt S3AppDataBucketResource.Arn - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} - Fn::If: - CreateClusterDataBucket - Effect: Allow Action: - s3:DeleteObject - s3:GetObject - s3:PutObject Resource: !Sub ${S3ClusterDataBucketResource.Arn}/* - Effect: Allow Action: - s3:DeleteObject - s3:GetObject - s3:PutObject Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName}/* - Fn::If: - CreateClusterDataBucket - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !GetAtt S3ClusterDataBucketResource.Arn - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName} RoleName: Fn::Join: # Generate a not too long and unique role name # Getting a unique identifier from the stack id - '' - - openvidu-master-role- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] OpenViduMediaNodeRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore Policies: - PolicyName: !Sub openvidu-media-policy-${AWS::Region}-${AWS::StackName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: !Ref OpenViduSharedInfo - Effect: Allow Action: - autoscaling:SetInstanceHealth - autoscaling:CompleteLifecycleAction - autoscaling:RecordLifecycleActionHeartbeat Resource: '*' Condition: StringEquals: 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' - Fn::If: - CreateRecordingsBucket - Effect: Allow Action: - s3:DeleteObject - s3:GetObject - s3:PutObject Resource: !Sub ${S3AppDataBucketResource.Arn}/* - Effect: Allow Action: - s3:DeleteObject - s3:GetObject - s3:PutObject Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* - Fn::If: - CreateRecordingsBucket - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !GetAtt S3AppDataBucketResource.Arn - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} RoleName: Fn::Join: # Generate a not too long and unique role name # Getting a unique identifier from the stack id - '' - - openvidu-media-role- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] OpenViduMasterInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Sub OpenViduMasterInstanceProfile-${AWS::Region}-${AWS::StackName} Roles: - !Ref OpenViduMasterNodeRole OpenViduMediaInstanceProfile: Type: AWS::IAM::InstanceProfile DependsOn: - MasterNodesWaitCondition4 Properties: InstanceProfileName: !Sub OpenViduMediaInstanceProfile-${AWS::Region}-${AWS::StackName} Roles: - !Ref OpenViduMediaNodeRole OpenViduMasterLaunchTemplate: Type: AWS::EC2::LaunchTemplate Metadata: Comment: Launch template for OpenVidu Master Node AWS::CloudFormation::Init: config: files: '/usr/local/bin/install.sh': content: !Sub | #!/bin/bash -x set -e OPENVIDU_VERSION=main DOMAIN= YQ_VERSION=v4.44.5 # Install dependencies apt-get update && apt-get install -y \ curl \ unzip \ jq \ wget wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ tar xz && mv yq_linux_amd64 /usr/bin/yq # Install aws-cli curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip -qq awscliv2.zip ./aws/install rm -rf awscliv2.zip aws # Singal to notify instance is waiting SIGNAL_NAME="$1" # Token for IMDSv2 TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" # Subnets IDs SUBNETS=( "${SubnetProcessor.Subnet1}" "${SubnetProcessor.Subnet2}" "${SubnetProcessor.Subnet3}" "${SubnetProcessor.Subnet4}" ) MAC_ADDRESS="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254//latest/meta-data/mac)" SUBNET_ID="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s "http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS/subnet-id")" # Check master node number MASTER_NODE_NUM=1 for subnet in "${!SUBNETS[@]}"; do if [[ "$subnet" == "$SUBNET_ID" ]]; then SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text || echo 'none') # Check if current master node is reacheable with ping ACUTAL_MASTER_NODE_IP=$(echo "$SHARED_SECRET" | jq -r ".MASTER_NODE_${!MASTER_NODE_NUM}_PRIVATE_IP") if [[ "$ACUTAL_MASTER_NODE_IP" == "none" ]]; then break fi fi MASTER_NODE_NUM=$((MASTER_NODE_NUM + 1)) done # Get own private IP PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)" if [[ "$PRIVATE_IP" == "" ]]; then echo "Error: Private IP not found" exit 1 fi # Store current private IP /usr/local/bin/store_secret.sh save MASTER_NODE_${!MASTER_NODE_NUM}_PRIVATE_IP "${!PRIVATE_IP}" SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text) ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') # If the private IP is the same as the first master node, generate the secrets if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "false" ]]; then DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN_NAME "${DomainName}")" if [[ -n "${TurnDomainName}" ]]; then LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")" fi OPENVIDU_PRO_LICENSE="$(/usr/local/bin/store_secret.sh save OPENVIDU_PRO_LICENSE "${OpenViduLicense}")" OPENVIDU_RTC_ENGINE="$(/usr/local/bin/store_secret.sh save OPENVIDU_RTC_ENGINE "${RTCEngine}")" # Store version so media nodes can use it to install the same version /usr/local/bin/store_secret.sh save OPENVIDU_VERSION "${!OPENVIDU_VERSION}" # Store usernames and generate random passwords REDIS_PASSWORD="$(/usr/local/bin/store_secret.sh generate REDIS_PASSWORD)" MONGO_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save MONGO_ADMIN_USERNAME "mongoadmin")" MONGO_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MONGO_ADMIN_PASSWORD)" MONGO_REPLICA_SET_KEY="$(/usr/local/bin/store_secret.sh generate MONGO_REPLICA_SET_KEY)" MINIO_ACCESS_KEY="$(/usr/local/bin/store_secret.sh save MINIO_ACCESS_KEY "minioadmin")" MINIO_SECRET_KEY="$(/usr/local/bin/store_secret.sh generate MINIO_SECRET_KEY)" DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD_ADMIN_USERNAME "dashboardadmin")" DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")" GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)" LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)" ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")" fi # Fetch the shared secret again SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text) ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') if [[ "${!ALL_SECRETS_GENERATED}" == "false" ]]; then echo "Error: Secrets not generated" exit 1 fi # sending the signal call cfn-signal -e $? --stack ${AWS::StackId} --resource "$SIGNAL_NAME" --region ${AWS::Region} # Wait for all master nodes to store their private IPs while true; do SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text || echo 'none') MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') # Check if all master nodes have stored their private IPs if [[ "$MASTER_NODE_1_PRIVATE_IP" != "none" ]] && [[ "$MASTER_NODE_2_PRIVATE_IP" != "none" ]] && [[ "$MASTER_NODE_3_PRIVATE_IP" != "none" ]] && [[ "$MASTER_NODE_4_PRIVATE_IP" != "none" ]]; then break fi sleep 5 done SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text) MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') MASTER_NODE_PRIVATE_IP_LIST="$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP" DOMAIN=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME') LIVEKIT_TURN_DOMAIN_NAME=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_TURN_DOMAIN_NAME') OPENVIDU_PRO_LICENSE=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_PRO_LICENSE') OPENVIDU_RTC_ENGINE=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_RTC_ENGINE') REDIS_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.REDIS_PASSWORD') MONGO_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.MONGO_ADMIN_USERNAME') MONGO_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.MONGO_ADMIN_PASSWORD') MONGO_REPLICA_SET_KEY=$(echo "$SHARED_SECRET" | jq -r '.MONGO_REPLICA_SET_KEY') MINIO_ACCESS_KEY=$(echo "$SHARED_SECRET" | jq -r '.MINIO_ACCESS_KEY') MINIO_SECRET_KEY=$(echo "$SHARED_SECRET" | jq -r '.MINIO_SECRET_KEY') DASHBOARD_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_USERNAME') DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD') GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME') GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD') MEET_ADMIN_USER=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_USER') MEET_ADMIN_SECRET=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_SECRET') MEET_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.MEET_API_KEY') LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY') LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET') ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES') # Base command INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_master_node.sh)" # Common arguments COMMON_ARGS=( "--no-tty" "--install" "--environment=aws" "--deployment-type='ha'" "--node-role='master-node'" "--external-load-balancer" "--master-node-private-ip-list='$MASTER_NODE_PRIVATE_IP_LIST'" "--openvidu-pro-license='$OPENVIDU_PRO_LICENSE'" "--domain-name='$DOMAIN'" "--enabled-modules='$ENABLED_MODULES'" "--rtc-engine=$OPENVIDU_RTC_ENGINE" "--redis-password=$REDIS_PASSWORD" "--mongo-admin-user=$MONGO_ADMIN_USERNAME" "--mongo-admin-password=$MONGO_ADMIN_PASSWORD" "--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY" "--minio-access-key=$MINIO_ACCESS_KEY" "--minio-secret-key=$MINIO_SECRET_KEY" "--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--meet-admin-user=$MEET_ADMIN_USER" "--meet-admin-password=$MEET_ADMIN_SECRET" "--meet-api-key=$MEET_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-secret=$LIVEKIT_API_SECRET" ) # Include additional installer flags provided by the user if [[ "${AdditionalInstallFlags}" != "" ]]; then IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}" for extra_flag in "${!EXTRA_FLAGS[@]}"; do # Trim whitespace around each flag extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')" if [[ "$extra_flag" != "" ]]; then COMMON_ARGS+=("$extra_flag") fi done fi if [[ "${!LIVEKIT_TURN_DOMAIN_NAME}" != "none" ]]; then COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'") fi # Construct the final command FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" # Install OpenVidu exec bash -c "$FINAL_COMMAND" mode: '000755' owner: root group: root '/usr/local/bin/config_s3.sh': content: !Sub - | #!/bin/bash set -e # Install dir and config dir INSTALL_DIR="/opt/openvidu" CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" # Config S3 bucket EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com" EXTERNAL_S3_REGION="${AWS::Region}" EXTERNAL_S3_PATH_STYLE_ACCESS="false" EXTERNAL_S3_BUCKET_APP_DATA=${S3AppDataBucketResourceName} EXTERNAL_S3_BUCKET_CLUSTER_DATA=${S3ClusterDataBucketResourceName} sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_BUCKET_CLUSTER_DATA=.*|EXTERNAL_S3_BUCKET_CLUSTER_DATA=$EXTERNAL_S3_BUCKET_CLUSTER_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" - S3AppDataBucketResourceName: !If - CreateRecordingsBucket - !Ref S3AppDataBucketResource - !Ref S3AppDataBucketName S3ClusterDataBucketResourceName: !If - CreateClusterDataBucket - !Ref S3ClusterDataBucketResource - !Ref S3ClusterDataBucketName mode: "000755" owner: "root" group: "root" '/usr/local/bin/after_install.sh': content: !Sub | #!/bin/bash set -e # Get current shared secret SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text) # Save access URLs DOMAIN=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME') DASHBOARD_URL="https://${!DOMAIN}/dashboard/" GRAFANA_URL="https://${!DOMAIN}/grafana/" MINIO_URL="https://${!DOMAIN}/minio-console/" # Update shared secret SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" # Update shared secret aws secretsmanager update-secret \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --secret-string "$SHARED_SECRET" mode: "000755" owner: "root" group: "root" '/usr/local/bin/update_config_from_secret.sh': content: !Sub | #!/bin/bash set -e # Token for IMDSv2 TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") # Get current shared secret SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text) # Installation directory INSTALL_DIR="/opt/openvidu" CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" # Replace DOMAIN_NAME export DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME) if [[ -n "$DOMAIN" ]]; then sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" else exit 1 fi # Replace LIVEKIT_TURN_DOMAIN_NAME export LIVEKIT_TURN_DOMAIN_NAME=$(echo $SHARED_SECRET | jq -r .LIVEKIT_TURN_DOMAIN_NAME) if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" fi # Replace rest of the values sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD)/" "${!MASTER_NODE_CONFIG_DIR}/master_node.env" sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_RTC_ENGINE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_PRO_LICENSE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_ACCESS_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_SECRET_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env" sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env" sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" # Update URLs in secret DASHBOARD_URL="https://${!DOMAIN}/dashboard/" GRAFANA_URL="https://${!DOMAIN}/grafana/" MINIO_URL="https://${!DOMAIN}/minio-console/" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" aws secretsmanager update-secret \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --secret-string "$SHARED_SECRET" mode: "000755" owner: "root" group: "root" '/usr/local/bin/update_secret_from_config.sh': content: !Sub | #!/bin/bash set -e # Get current shared secret SHARED_SECRET=$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text) # Installation directory INSTALL_DIR="/opt/openvidu" CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" # Update shared secret SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"REDIS_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${!MASTER_NODE_CONFIG_DIR}/master_node.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_TURN_DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_RTC_ENGINE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_PRO_LICENSE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_ACCESS_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_SECRET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" # Update shared secret aws secretsmanager update-secret \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --secret-string "$SHARED_SECRET" mode: "000755" owner: "root" group: "root" '/usr/local/bin/get_value_from_config.sh': content: | #!/bin/bash set -e # Function to get the value of a given key from the environment file get_value() { local key="$1" local file_path="$2" # Use grep to find the line with the key, ignoring lines starting with # # Use awk to split on '=' and print the second field, which is the value local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') # If the value is empty, return "none" if [ -z "$value" ]; then echo "none" else echo "$value" fi } # Check if the correct number of arguments are supplied if [ "$#" -ne 2 ]; then echo "Usage: $0 " exit 1 fi # Get the key and file path from the arguments key="$1" file_path="$2" # Get and print the value get_value "$key" "$file_path" mode: "000755" owner: "root" group: "root" '/usr/local/bin/store_secret.sh': content: !Sub | #!/bin/bash set -e # Modes: save, generate # save mode: save the secret in the secret manager # generate mode: generate a random password and save it in the secret manager MODE="$1" SHARED_SECRET="$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id ${OpenViduSharedInfo} \ --query SecretString --output text)" if [[ "$MODE" == "generate" ]]; then SECRET_KEY_NAME="$2" PREFIX="${!3:-}" LENGTH="${!4:-44}" RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${!LENGTH})" RANDOM_PASSWORD="${!PREFIX}${!RANDOM_PASSWORD}" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$RANDOM_PASSWORD"'"}')" aws secretsmanager update-secret \ --region ${AWS::Region} \ --secret-id ${OpenViduSharedInfo} \ --secret-string "$SHARED_SECRET" > /dev/null 2>&1 echo "$RANDOM_PASSWORD" elif [[ "$MODE" == "save" ]]; then SECRET_KEY_NAME="$2" SECRET_VALUE="$3" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$SECRET_VALUE"'"}')" aws secretsmanager update-secret \ --region ${AWS::Region} \ --secret-id ${OpenViduSharedInfo} \ --secret-string "$SHARED_SECRET" > /dev/null 2>&1 echo "$SECRET_VALUE" else exit 1 fi mode: "000755" owner: "root" group: "root" '/usr/local/bin/restart.sh': content: | #!/bin/bash set -e # Stop all services systemctl stop openvidu # Update config from secret /usr/local/bin/update_config_from_secret.sh # Start all services systemctl start openvidu mode: "000755" owner: "root" group: "root" Properties: LaunchTemplateName: !Sub 'openvidu-ha-master-${AWS::Region}-${AWS::StackName}' LaunchTemplateData: # Enable IMDSv2 MetadataOptions: HttpEndpoint: enabled HttpPutResponseHopLimit: 1 HttpTokens: required IamInstanceProfile: Name: !Ref OpenViduMasterInstanceProfile ImageId: !Ref AmiId InstanceType: !Ref MasterNodeInstanceType KeyName: !Ref KeyName SecurityGroupIds: - !Ref OpenViduMasterNodeSG BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeSize: !Ref MasterNodesDiskSize VolumeType: gp3 DeleteOnTermination: true OpenViduMasterNode1: Type: AWS::EC2::Instance Properties: LaunchTemplate: LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 1 SubnetId: !GetAtt SubnetProcessor.Subnet1 UserData: Fn::Base64: !Sub | #!/bin/bash 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 -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition1" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition1: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT10M Count: '1' OpenViduMasterNode2: Type: AWS::EC2::Instance DependsOn: MasterNodesWaitCondition1 Properties: LaunchTemplate: LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 2 SubnetId: !GetAtt SubnetProcessor.Subnet2 UserData: Fn::Base64: !Sub | #!/bin/bash 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 -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition2" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition2: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT10M Count: '1' OpenViduMasterNode3: Type: AWS::EC2::Instance DependsOn: MasterNodesWaitCondition2 Properties: LaunchTemplate: LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 3 SubnetId: !GetAtt SubnetProcessor.Subnet3 UserData: Fn::Base64: !Sub | #!/bin/bash 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 -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition3" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot systemctl enable crond.service systemctl start crond.service echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition3: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT10M Count: '1' OpenViduMasterNode4: Type: AWS::EC2::Instance DependsOn: MasterNodesWaitCondition3 Properties: LaunchTemplate: LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 4 SubnetId: !GetAtt SubnetProcessor.Subnet4 UserData: Fn::Base64: !Sub | #!/bin/bash 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 -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition4" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot systemctl enable crond.service systemctl start crond.service echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition4: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT10M Count: '1' OpenViduMediaNodeLaunchTemplate: Type: AWS::EC2::LaunchTemplate Metadata: Comment: Launch template for OpenVidu Media Node AWS::CloudFormation::Init: config: files: '/usr/local/bin/install.sh': content: !Sub | #!/bin/bash set -e YQ_VERSION=v4.44.5 # Install dependencies apt-get update && apt-get install -y \ curl \ unzip \ jq \ wget wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ tar xz && mv yq_linux_amd64 /usr/bin/yq # Install aws-cli curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip -qq awscliv2.zip ./aws/install rm -rf awscliv2.zip aws # Token for IMDSv2 TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" # Get own private IP PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)" SHARED_SECRET="$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text || echo 'none')" if [[ "$SHARED_SECRET" == "none" ]]; then echo "Error: Shared secret not found" exit 1 fi # Get OpenVidu Media Nodes version to deploy OPENVIDU_VERSION=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_VERSION') if [[ "$OPENVIDU_VERSION" == "none" ]]; then echo "OpenVidu version not found" exit 1 fi ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') if [[ "$ALL_SECRETS_GENERATED" == "none" ]]; then echo "Error: Secrets not generated" exit 1 fi MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') MASTER_NODE_PRIVATE_IP_LIST="$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP" REDIS_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.REDIS_PASSWORD') # Base command INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh)" # Common arguments COMMON_ARGS=( "--no-tty" "--install" "--environment=aws" "--deployment-type='ha'" "--node-role='media-node'" "--master-node-private-ip-list=$MASTER_NODE_PRIVATE_IP_LIST" "--private-ip=$PRIVATE_IP" "--redis-password=$REDIS_PASSWORD" ) # Construct the final command with all arguments FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" # Install OpenVidu exec bash -c "$FINAL_COMMAND" mode: '000755' owner: root group: root '/usr/local/bin/set_as_unhealthy.sh': content: !Sub | #!/bin/bash set -e # Token for IMDSv2 TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") # Get own instance ID INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) # Set instance as unhealthy aws autoscaling set-instance-health \ --region ${AWS::Region} \ --instance-id "$INSTANCE_ID" \ --health-status Unhealthy mode: "000755" owner: "root" group: "root" '/usr/local/bin/stop_media_node.sh': content: !Sub | #!/bin/bash set -e # Token for IMDSv2 TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") # Get own instance ID INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) ASG_NAME=openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} # Execute if docker is installed if [ -x "$(command -v docker)" ]; then echo "Stopping media node services and waiting for termination..." docker container kill --signal=SIGQUIT openvidu || true docker container kill --signal=SIGQUIT ingress || true docker container kill --signal=SIGQUIT egress || true for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do docker container kill --signal=SIGQUIT "$agent_container" done TIME_PASSED=0 HEARTBEAT_MAX=1800 # Wait for running containers to not be openvidu, ingress, egress or an openvidu agent while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \ [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \ [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do echo "Waiting for containers to stop..." sleep 5 TIME_PASSED=$((TIME_PASSED+5)) if [ $TIME_PASSED -ge $HEARTBEAT_MAX ]; then echo "Increase lifecycle hook timeout to continue waiting for termination" # Increase lifecycle hook timeout aws autoscaling record-lifecycle-action-heartbeat \ --region ${AWS::Region} \ --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ --auto-scaling-group-name "$ASG_NAME" \ --instance-id "$INSTANCE_ID" TIME_PASSED=0 fi done fi aws autoscaling complete-lifecycle-action \ --region ${AWS::Region} \ --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ --auto-scaling-group-name "$ASG_NAME" \ --lifecycle-action-result CONTINUE \ --instance-id "$INSTANCE_ID" mode: "000755" owner: "root" group: "root" Properties: LaunchTemplateName: !Sub 'openvidu-ha-media-${AWS::Region}-${AWS::StackName}' LaunchTemplateData: # Enable IMDSv2 by default MetadataOptions: HttpEndpoint: enabled HttpPutResponseHopLimit: 1 HttpTokens: required IamInstanceProfile: Arn: !GetAtt OpenViduMediaInstanceProfile.Arn SecurityGroupIds: - !GetAtt OpenViduMediaNodeSG.GroupId ImageId: !Ref AmiId KeyName: !Ref KeyName InstanceType: !Ref MediaNodeInstanceType UserData: Fn::Base64: !Sub | #!/bin/bash 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 OpenViduMediaNodeLaunchTemplate export HOME="/root" # Install OpenVidu /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } # Wait for the app # /usr/local/bin/check_app_ready.sh BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeType: gp3 DeleteOnTermination: true VolumeSize: 50 OpenViduMediaNodeASG: DependsOn: - OpenViduMediaInstanceProfile - OpenViduMasterInstanceProfile - StopMediaNodeCloudWatchEventRule Type: AWS::AutoScaling::AutoScalingGroup Properties: AutoScalingGroupName: !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} LaunchTemplate: LaunchTemplateId: !Ref OpenViduMediaNodeLaunchTemplate Version: !GetAtt OpenViduMediaNodeLaunchTemplate.DefaultVersionNumber TargetGroupARNs: Fn::If: - TurnTLSIsEnabled - - !Ref OpenViduMediaNodeRTMPTG - !Ref OpenViduMediaNodeTurnTLSTG - - !Ref OpenViduMediaNodeRTMPTG MinSize: !Ref MinNumberOfMediaNodes MaxSize: !Ref MaxNumberOfMediaNodes DesiredCapacity: !Ref InitialNumberOfMediaNodes VPCZoneIdentifier: !Ref OpenViduMediaNodeSubnets Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Media Node PropagateAtLaunch: true StopMediaNodeLifecycleHook: Type: 'AWS::AutoScaling::LifecycleHook' Properties: LifecycleHookName: !Sub StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} AutoScalingGroupName: !Ref OpenViduMediaNodeASG LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' DefaultResult: 'CONTINUE' HeartbeatTimeout: 3600 StopMediaNodeDocumentRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ssm.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ssm:DescribeInstanceInformation - ssm:ListCommands - ssm:ListCommandInvocations Resource: "*" - Effect: Allow Action: - ssm:SendCommand Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunShellScript - Action: - ssm:SendCommand Resource: !Sub arn:${AWS::Partition}:ec2:*:*:instance/* Condition: StringEquals: 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' Effect: Allow PolicyName: SSM-Automation-Policy StopMediaNodeAutomationDocument: Type: AWS::SSM::Document Properties: Name: Fn::Join: # Generate a not too long and unique document name # Getting a unique identifier from the stack id - '' - - 'StopMediaNodeAutomationDocument-' - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] DocumentType: Automation Content: schemaVersion: '0.3' assumeRole: "{{automationAssumeRole}}" description: This document stops the OpenVidu services in a Media Node and terminates the instance. It stop the OpenVidu services without interrupting the running sessions, ingress and egress running in the Media Node. Also it sends a CONTINUE signal to the Auto Scaling Group to continue the instance termination when the services are stopped. parameters: InstanceId: type: String automationAssumeRole: type: String default: !GetAtt StopMediaNodeDocumentRole.Arn description: "(Required) The ARN of the role that allows Automation to perform the actions." mainSteps: - name: RunCommand action: aws:runCommand inputs: DocumentName: AWS-RunShellScript InstanceIds: - "{{ InstanceId }}" Parameters: # 24 hours as a timeout to wait for the instance to stop all services executionTimeout: "60" commands: - nohup /usr/local/bin/stop_media_node.sh > /var/log/stop_media_node.log 2>&1 & StopMediaNodeCloudWatchEventRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ssm:StartAutomationExecution Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT PolicyName: !Sub StopMediaNodeCloudWatchEventPolicy-${AWS::Region}-${AWS::StackName} - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - iam:PassRole Resource: !GetAtt StopMediaNodeDocumentRole.Arn PolicyName: Pass-Role-SSM-Automation-Policy StopMediaNodeCloudWatchEventRule: Type: AWS::Events::Rule Properties: Description: Rule to trigger the StopMediaNodeAutomationDocument when an instance is terminated EventPattern: source: - aws.autoscaling detail-type: - EC2 Instance-terminate Lifecycle Action detail: AutoScalingGroupName: - !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} Name: Fn::Join: # Generate a not too long and unique rule name # Getting a unique identifier from the stack id - '' - - StopMediaNodeCloudWatchEventRule- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] Targets: - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT RoleArn: !GetAtt StopMediaNodeCloudWatchEventRole.Arn Id: Fn::Join: # Generate a not too long and unique target id # Getting a unique identifier from the stack id - '' - - StopMediaNodeCloudWatchEventRule- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] InputTransformer: InputPathsMap: instanceid: "$.detail.EC2InstanceId" InputTemplate: | { "InstanceId": [ ] } OpenViduMediaNodeASGScalingPolicy: Type: AWS::AutoScaling::ScalingPolicy Properties: AutoScalingGroupName: !Ref OpenViduMediaNodeASG PolicyType: TargetTrackingScaling EstimatedInstanceWarmup: 120 TargetTrackingConfiguration: PredefinedMetricSpecification: PredefinedMetricType: ASGAverageCPUUtilization TargetValue: !Ref ScaleTargetCPU OpenViduMasterNodeSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Security group for OpenVidu Master Node GroupName: !Sub openvidu-ha-master-sg-${AWS::Region}-${AWS::StackName} VpcId: !Ref OpenViduVPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIpv6: ::/0 OpenViduLoadBalancerToMasterIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 7880 ToPort: 7880 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG OpenViduMasterToMasterRedisIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 7000 ToPort: 7001 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterRedisIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 7000 ToPort: 7001 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG OpenViduMasterToMasterMinioIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9100 ToPort: 9100 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMasterMinioConsoleSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9101 ToPort: 9101 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterMinioIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9100 ToPort: 9100 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG OpenViduMasterToMasterMongoIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 20000 ToPort: 20000 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterMongoIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 20000 ToPort: 20000 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG OpenViduMasterToMasterMimirGrpcIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9095 ToPort: 9095 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMasterMimirGossipIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 7946 ToPort: 7946 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterHTTPMimirSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9009 ToPort: 9009 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG OpenViduMasterToMasterLokiGrpcIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9096 ToPort: 9096 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMasterLokiGossipIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 7947 ToPort: 7947 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMasterDashboardsIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 5000 ToPort: 5000 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMasterGrafanaIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 3000 ToPort: 3000 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterHTTPLokiSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 3100 ToPort: 3100 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG OpenViduMasterToMasterV2CompatibilityIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 4443 ToPort: 4443 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterV2CompatibilityWebhookIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !GetAtt OpenViduMasterNodeSG.GroupId IpProtocol: tcp FromPort: 4443 ToPort: 4443 SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId OpenViduMasterToMasterDefaultAppIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 6080 ToPort: 6080 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterDefaultAppWebhookIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !GetAtt OpenViduMasterNodeSG.GroupId IpProtocol: tcp FromPort: 6080 ToPort: 6080 SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId OpenViduMediaNodeSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Security group for OpenVidu Media Node GroupName: !Sub openvidu-ha-media-sg-${AWS::Region}-${AWS::StackName} 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: udp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 443 ToPort: 443 CidrIpv6: ::/0 - IpProtocol: tcp FromPort: 7881 ToPort: 7881 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 7881 ToPort: 7881 CidrIpv6: ::/0 - IpProtocol: udp FromPort: 7885 ToPort: 7885 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 7885 ToPort: 7885 CidrIpv6: ::/0 - IpProtocol: udp FromPort: 50000 ToPort: 60000 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 50000 ToPort: 60000 CidrIpv6: ::/0 - IpProtocol: tcp FromPort: 50000 ToPort: 60000 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 50000 ToPort: 60000 CidrIpv6: ::/0 OpenViduLoadBalancerToMediaNodeRTMPIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 1945 ToPort: 1945 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG OpenViduLoadBalancerToMediaNodeIngressHealthCheckSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 9092 ToPort: 9092 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG OpenViduLoadBalancerTurnTLSToMediaNodeIngressSG: Type: AWS::EC2::SecurityGroupIngress Condition: TurnTLSIsEnabled Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 5349 ToPort: 5349 SourceSecurityGroupId: !Ref OpenViduTurnTLSLoadBalancerSG OpenViduLoadBalancerTurnTLSToMediaNodeHealthCheckSG: Type: AWS::EC2::SecurityGroupIngress Condition: TurnTLSIsEnabled Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 7880 ToPort: 7880 SourceSecurityGroupId: !Ref OpenViduTurnTLSLoadBalancerSG OpenViduMasterToMediaNodeServerIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 7880 ToPort: 7880 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMediaNodeClientIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 8080 ToPort: 8080 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG # --- # Experimental TURN TLS with main domain OpenViduTurnTLSMasterNodeToMediaNodeIngressSG: Type: AWS::EC2::SecurityGroupIngress Condition: ExperimentalTurnTLSWithMainDomain Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 5349 ToPort: 5349 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduTurnTLSLoadBalancerToMediaNodeIngressSG: Type: AWS::EC2::SecurityGroupIngress Condition: ExperimentalTurnTLSWithMainDomain Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 443 ToPort: 443 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG # --- OpenViduLoadBalancerSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Security group for the Load Balancer GroupName: !Sub openvidu-ha-lb-sg-${AWS::Region}-${AWS::StackName} VpcId: !Ref OpenViduVPC SecurityGroupIngress: - 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: 1935 ToPort: 1935 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 1935 ToPort: 1935 CidrIpv6: ::/0 OpenViduTurnTLSLoadBalancerSG: Type: AWS::EC2::SecurityGroup Condition: TurnTLSIsEnabled Properties: GroupDescription: Security group for the Load Balancer for TURN with TLS GroupName: !Sub openvidu-ha-turn-tls-lb-sg-${AWS::Region}-${AWS::StackName} VpcId: !Ref OpenViduVPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIpv6: ::/0 LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer DependsOn: - MasterNodesWaitCondition4 Properties: Name: Fn::Join: # Generate a not too long and unique load balancer name # Getting a unique identifier from the stack id - '' - - OpenViduHA- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] Subnets: !Ref OpenViduMasterNodeSubnets SecurityGroups: - !Ref OpenViduLoadBalancerSG Type: network Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Load Balancer TurnTLSLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Condition: TurnTLSIsEnabled Properties: Name: Fn::Join: # Generate a not too long and unique load balancer name # Getting a unique identifier from the stack id - '' - - OpenViduHA-TurnTLS- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] Subnets: !Ref OpenViduMediaNodeSubnets SecurityGroups: - !Ref OpenViduTurnTLSLoadBalancerSG Type: network Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN with TLS Load Balancer OpenViduMasterNodeListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Condition: NotExperimentalTurnTLSWithMainDomain Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduMasterNodeTG LoadBalancerArn: !Ref LoadBalancer Port: 443 Protocol: TLS Certificates: - CertificateArn: !Ref OpenViduCertificateARN # --- # Experimental TURN TLS with main domain OpenViduMasterNodeWithTurnTLSListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Condition: ExperimentalTurnTLSWithMainDomain Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduMasterNodeWithTurnTLSTG LoadBalancerArn: !Ref LoadBalancer Port: 443 Protocol: TLS Certificates: - CertificateArn: !Ref OpenViduCertificateARN # --- OpenViduRTMPMediaNodeListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduMediaNodeRTMPTG LoadBalancerArn: !Ref LoadBalancer Port: 1935 Protocol: TLS Certificates: - CertificateArn: !Ref OpenViduCertificateARN OpenViduTurnTLSMediaNodeListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Condition: TurnTLSIsEnabled Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduMediaNodeTurnTLSTG LoadBalancerArn: !Ref TurnTLSLoadBalancer Port: 443 Protocol: TLS Certificates: - CertificateArn: !Ref TurnCertificateARN OpenViduMasterNodeTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: NotExperimentalTurnTLSWithMainDomain Properties: Name: Fn::Join: # Generate a not too long and unique target id # Getting a unique identifier from the stack id - '' - - OpenVidu- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] TargetType: instance Targets: - Id: !Ref OpenViduMasterNode1 - Id: !Ref OpenViduMasterNode2 - Id: !Ref OpenViduMasterNode3 - Id: !Ref OpenViduMasterNode4 VpcId: !Ref OpenViduVPC Port: 7880 Protocol: TCP Matcher: HttpCode: '200' HealthCheckIntervalSeconds: 10 HealthCheckPath: /health/caddy HealthCheckProtocol: HTTP HealthCheckPort: '7880' HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group # --- # Experimental TURN TLS with main domain OpenViduMasterNodeWithTurnTLSTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: ExperimentalTurnTLSWithMainDomain Properties: Name: Fn::Join: # Generate a not too long and unique target id # Getting a unique identifier from the stack id - '' - - OVTurnTLSMaster- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] TargetType: instance Targets: - Id: !Ref OpenViduMasterNode1 - Id: !Ref OpenViduMasterNode2 - Id: !Ref OpenViduMasterNode3 - Id: !Ref OpenViduMasterNode4 VpcId: !Ref OpenViduVPC Port: 443 Protocol: TCP Matcher: HttpCode: '200' HealthCheckIntervalSeconds: 10 HealthCheckPath: /health/caddy HealthCheckProtocol: HTTP HealthCheckPort: '7880' HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN TLS Master Target Group # --- OpenViduMediaNodeRTMPTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: Fn::Join: # Generate a not too long and unique target id # Getting a unique identifier from the stack id - '' - - OVRTMP- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] VpcId: !Ref OpenViduVPC Port: 1945 Protocol: TCP Matcher: HttpCode: '200' HealthCheckIntervalSeconds: 10 HealthCheckPath: / HealthCheckProtocol: HTTP # Ingress health check port HealthCheckPort: '9092' HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - RTMP Target Group OpenViduMediaNodeTurnTLSTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: TurnTLSIsEnabled Properties: Name: Fn::Join: # Generate a not too long and unique target id # Getting a unique identifier from the stack id - '' - - OVTurnTLS- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] VpcId: !Ref OpenViduVPC Port: 5349 Protocol: TCP Matcher: HttpCode: '200' HealthCheckIntervalSeconds: 10 HealthCheckPath: / HealthCheckProtocol: HTTP HealthCheckPort: '7880' HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN TLS Target Group Outputs: ServicesAndCredentials: Description: Services and credentials Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/home?region=${AWS::Region}#!/secret?name=openvidu-ha-${AWS::Region}-${AWS::StackName}