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 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: mediasoup MasterNodeInstanceType: Description: "Specifies the EC2 instance type for your OpenVidu Master Node" Type: String Default: c6a.xlarge AllowedValues: - t3.nano - t3.micro - t3.small - t3.medium - t3.large - t3.xlarge - t3.2xlarge - t3a.nano - t3a.micro - t3a.small - t3a.medium - t3a.large - t3a.xlarge - t3a.2xlarge - t4g.nano - t4g.micro - t4g.small - t4g.medium - t4g.large - t4g.xlarge - t4g.2xlarge - c5.large - c5.xlarge - c5.2xlarge - c5.4xlarge - c5.9xlarge - c5.12xlarge - c5.18xlarge - c5.24xlarge - c5.metal - c5a.large - c5a.xlarge - c5a.2xlarge - c5a.4xlarge - c5a.8xlarge - c5a.12xlarge - c5a.16xlarge - c5a.24xlarge - c5ad.large - c5ad.xlarge - c5ad.2xlarge - c5ad.4xlarge - c5ad.8xlarge - c5ad.12xlarge - c5ad.16xlarge - c5ad.24xlarge - c5d.large - c5d.xlarge - c5d.2xlarge - c5d.4xlarge - c5d.9xlarge - c5d.12xlarge - c5d.18xlarge - c5d.24xlarge - c5d.metal - c5n.large - c5n.xlarge - c5n.2xlarge - c5n.4xlarge - c5n.9xlarge - c5n.18xlarge - c5n.metal - c6a.large - c6a.xlarge - c6a.2xlarge - c6a.4xlarge - c6a.8xlarge - c6a.12xlarge - c6a.16xlarge - c6a.24xlarge - c6a.32xlarge - c6a.48xlarge - c6a.metal - c6g.medium - c6g.large - c6g.xlarge - c6g.2xlarge - c6g.4xlarge - c6g.8xlarge - c6g.12xlarge - c6g.16xlarge - c6g.metal - c6gd.medium - c6gd.large - c6gd.xlarge - c6gd.2xlarge - c6gd.4xlarge - c6gd.8xlarge - c6gd.12xlarge - c6gd.16xlarge - c6gd.metal - c6gn.medium - c6gn.large - c6gn.xlarge - c6gn.2xlarge - c6gn.4xlarge - c6gn.8xlarge - c6gn.12xlarge - c6gn.16xlarge - c6i.large - c6i.xlarge - c6i.2xlarge - c6i.4xlarge - c6i.8xlarge - c6i.12xlarge - c6i.16xlarge - c6i.24xlarge - c6i.32xlarge - c6i.metal - c6id.large - c6id.xlarge - c6id.2xlarge - c6id.4xlarge - c6id.8xlarge - c6id.12xlarge - c6id.16xlarge - c6id.24xlarge - c6id.32xlarge - c6id.metal - c6in.large - c6in.xlarge - c6in.2xlarge - c6in.4xlarge - c6in.8xlarge - c6in.12xlarge - c6in.16xlarge - c6in.24xlarge - c6in.32xlarge - c6in.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 - c7g.medium - c7g.large - c7g.xlarge - c7g.2xlarge - c7g.4xlarge - c7g.8xlarge - c7g.12xlarge - c7g.16xlarge - c7g.metal - c7gd.medium - c7gd.large - c7gd.xlarge - c7gd.2xlarge - c7gd.4xlarge - c7gd.8xlarge - c7gd.12xlarge - c7gd.16xlarge - c7gd.metal - c7gn.medium - c7gn.large - c7gn.xlarge - c7gn.2xlarge - c7gn.4xlarge - c7gn.8xlarge - c7gn.12xlarge - c7gn.16xlarge - c7gn.metal - c7i-flex.large - c7i-flex.xlarge - c7i-flex.2xlarge - c7i-flex.4xlarge - c7i-flex.8xlarge - c7i-flex.12xlarge - c7i-flex.16xlarge - c7i.large - c7i.xlarge - c7i.2xlarge - c7i.4xlarge - c7i.8xlarge - c7i.12xlarge - c7i.16xlarge - c7i.24xlarge - c7i.48xlarge - c7i.metal-24xl - c7i.metal-48xl - c8g.medium - c8g.large - c8g.xlarge - c8g.2xlarge - c8g.4xlarge - c8g.8xlarge - c8g.12xlarge - c8g.16xlarge - c8g.24xlarge - c8g.48xlarge - c8g.metal-24xl - c8g.metal-48xl - c8gd.medium - c8gd.large - c8gd.xlarge - c8gd.2xlarge - c8gd.4xlarge - c8gd.8xlarge - c8gd.12xlarge - c8gd.16xlarge - c8gd.24xlarge - c8gd.48xlarge - c8gd.metal-24xl - c8gd.metal-48xl - g4dn.xlarge - g4dn.2xlarge - g4dn.4xlarge - g4dn.8xlarge - g4dn.12xlarge - g4dn.16xlarge - g4dn.metal - g5.xlarge - g5.2xlarge - g5.4xlarge - g5.8xlarge - g5.12xlarge - g5.16xlarge - g5.24xlarge - g5.48xlarge - g5g.xlarge - g5g.2xlarge - g5g.4xlarge - g5g.8xlarge - g5g.16xlarge - g5g.metal - g6.xlarge - g6.2xlarge - g6.4xlarge - g6.8xlarge - g6.12xlarge - g6.16xlarge - g6.24xlarge - g6.48xlarge - g6e.xlarge - g6e.2xlarge - g6e.4xlarge - g6e.8xlarge - g6e.12xlarge - g6e.16xlarge - g6e.24xlarge - g6e.48xlarge - g6f.large - g6f.xlarge - g6f.2xlarge - g6f.4xlarge - g7e.2xlarge - g7e.4xlarge - g7e.8xlarge - g7e.12xlarge - g7e.24xlarge - g7e.48xlarge - gr6.4xlarge - gr6.8xlarge - gr6f.4xlarge - m6a.large - m6a.xlarge - m6a.2xlarge - m6a.4xlarge - m6a.8xlarge - m6a.12xlarge - m6a.16xlarge - m6a.24xlarge - m6a.32xlarge - m6a.48xlarge - m6a.metal - m6g.medium - m6g.large - m6g.xlarge - m6g.2xlarge - m6g.4xlarge - m6g.8xlarge - m6g.12xlarge - m6g.16xlarge - m6g.metal - m6gd.medium - m6gd.large - m6gd.xlarge - m6gd.2xlarge - m6gd.4xlarge - m6gd.8xlarge - m6gd.12xlarge - m6gd.16xlarge - m6gd.metal - m6i.large - m6i.xlarge - m6i.2xlarge - m6i.4xlarge - m6i.8xlarge - m6i.12xlarge - m6i.16xlarge - m6i.24xlarge - m6i.32xlarge - m6i.metal - m6id.large - m6id.xlarge - m6id.2xlarge - m6id.4xlarge - m6id.8xlarge - m6id.12xlarge - m6id.16xlarge - m6id.24xlarge - m6id.32xlarge - m6id.metal - m6idn.large - m6idn.xlarge - m6idn.2xlarge - m6idn.4xlarge - m6idn.8xlarge - m6idn.12xlarge - m6idn.16xlarge - m6idn.24xlarge - m6idn.32xlarge - m6idn.metal - m6in.large - m6in.xlarge - m6in.2xlarge - m6in.4xlarge - m6in.8xlarge - m6in.12xlarge - m6in.16xlarge - m6in.24xlarge - m6in.32xlarge - m6in.metal - m7a.medium - m7a.large - m7a.xlarge - m7a.2xlarge - m7a.4xlarge - m7a.8xlarge - m7a.12xlarge - m7a.16xlarge - m7a.24xlarge - m7a.32xlarge - m7a.48xlarge - m7a.metal-48xl - m7g.medium - m7g.large - m7g.xlarge - m7g.2xlarge - m7g.4xlarge - m7g.8xlarge - m7g.12xlarge - m7g.16xlarge - m7g.metal - m7gd.medium - m7gd.large - m7gd.xlarge - m7gd.2xlarge - m7gd.4xlarge - m7gd.8xlarge - m7gd.12xlarge - m7gd.16xlarge - m7gd.metal - m7i-flex.large - m7i-flex.xlarge - m7i-flex.2xlarge - m7i-flex.4xlarge - m7i-flex.8xlarge - m7i-flex.12xlarge - m7i-flex.16xlarge - m7i.large - m7i.xlarge - m7i.2xlarge - m7i.4xlarge - m7i.8xlarge - m7i.12xlarge - m7i.16xlarge - m7i.24xlarge - m7i.48xlarge - m7i.metal-24xl - m7i.metal-48xl - m8g.medium - m8g.large - m8g.xlarge - m8g.2xlarge - m8g.4xlarge - m8g.8xlarge - m8g.12xlarge - m8g.16xlarge - m8g.24xlarge - m8g.48xlarge - m8g.metal-24xl - m8g.metal-48xl - m8gd.medium - m8gd.large - m8gd.xlarge - m8gd.2xlarge - m8gd.4xlarge - m8gd.8xlarge - m8gd.12xlarge - m8gd.16xlarge - m8gd.24xlarge - m8gd.48xlarge - m8gd.metal-24xl - m8gd.metal-48xl - p4d.24xlarge - p4de.24xlarge - p5.4xlarge - p5.48xlarge - p5e.48xlarge - p5en.48xlarge - p6-b200.48xlarge - p6-b300.48xlarge - p6e-gb200.36xlarge - r6g.medium - r6g.large - r6g.xlarge - r6g.2xlarge - r6g.4xlarge - r6g.8xlarge - r6g.12xlarge - r6g.16xlarge - r6g.metal - r6gd.medium - r6gd.large - r6gd.xlarge - r6gd.2xlarge - r6gd.4xlarge - r6gd.8xlarge - r6gd.12xlarge - r6gd.16xlarge - r6gd.metal - r7g.medium - r7g.large - r7g.xlarge - r7g.2xlarge - r7g.4xlarge - r7g.8xlarge - r7g.12xlarge - r7g.16xlarge - r7g.metal - r7gd.medium - r7gd.large - r7gd.xlarge - r7gd.2xlarge - r7gd.4xlarge - r7gd.8xlarge - r7gd.12xlarge - r7gd.16xlarge - r7gd.metal - r8g.medium - r8g.large - r8g.xlarge - r8g.2xlarge - r8g.4xlarge - r8g.8xlarge - r8g.12xlarge - r8g.16xlarge - r8g.24xlarge - r8g.48xlarge - r8g.metal-24xl - r8g.metal-48xl 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: - t3.nano - t3.micro - t3.small - t3.medium - t3.large - t3.xlarge - t3.2xlarge - t3a.nano - t3a.micro - t3a.small - t3a.medium - t3a.large - t3a.xlarge - t3a.2xlarge - t4g.nano - t4g.micro - t4g.small - t4g.medium - t4g.large - t4g.xlarge - t4g.2xlarge - c5.large - c5.xlarge - c5.2xlarge - c5.4xlarge - c5.9xlarge - c5.12xlarge - c5.18xlarge - c5.24xlarge - c5.metal - c5a.large - c5a.xlarge - c5a.2xlarge - c5a.4xlarge - c5a.8xlarge - c5a.12xlarge - c5a.16xlarge - c5a.24xlarge - c5ad.large - c5ad.xlarge - c5ad.2xlarge - c5ad.4xlarge - c5ad.8xlarge - c5ad.12xlarge - c5ad.16xlarge - c5ad.24xlarge - c5d.large - c5d.xlarge - c5d.2xlarge - c5d.4xlarge - c5d.9xlarge - c5d.12xlarge - c5d.18xlarge - c5d.24xlarge - c5d.metal - c5n.large - c5n.xlarge - c5n.2xlarge - c5n.4xlarge - c5n.9xlarge - c5n.18xlarge - c5n.metal - c6a.large - c6a.xlarge - c6a.2xlarge - c6a.4xlarge - c6a.8xlarge - c6a.12xlarge - c6a.16xlarge - c6a.24xlarge - c6a.32xlarge - c6a.48xlarge - c6a.metal - c6g.medium - c6g.large - c6g.xlarge - c6g.2xlarge - c6g.4xlarge - c6g.8xlarge - c6g.12xlarge - c6g.16xlarge - c6g.metal - c6gd.medium - c6gd.large - c6gd.xlarge - c6gd.2xlarge - c6gd.4xlarge - c6gd.8xlarge - c6gd.12xlarge - c6gd.16xlarge - c6gd.metal - c6gn.medium - c6gn.large - c6gn.xlarge - c6gn.2xlarge - c6gn.4xlarge - c6gn.8xlarge - c6gn.12xlarge - c6gn.16xlarge - c6i.large - c6i.xlarge - c6i.2xlarge - c6i.4xlarge - c6i.8xlarge - c6i.12xlarge - c6i.16xlarge - c6i.24xlarge - c6i.32xlarge - c6i.metal - c6id.large - c6id.xlarge - c6id.2xlarge - c6id.4xlarge - c6id.8xlarge - c6id.12xlarge - c6id.16xlarge - c6id.24xlarge - c6id.32xlarge - c6id.metal - c6in.large - c6in.xlarge - c6in.2xlarge - c6in.4xlarge - c6in.8xlarge - c6in.12xlarge - c6in.16xlarge - c6in.24xlarge - c6in.32xlarge - c6in.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 - c7g.medium - c7g.large - c7g.xlarge - c7g.2xlarge - c7g.4xlarge - c7g.8xlarge - c7g.12xlarge - c7g.16xlarge - c7g.metal - c7gd.medium - c7gd.large - c7gd.xlarge - c7gd.2xlarge - c7gd.4xlarge - c7gd.8xlarge - c7gd.12xlarge - c7gd.16xlarge - c7gd.metal - c7gn.medium - c7gn.large - c7gn.xlarge - c7gn.2xlarge - c7gn.4xlarge - c7gn.8xlarge - c7gn.12xlarge - c7gn.16xlarge - c7gn.metal - c7i-flex.large - c7i-flex.xlarge - c7i-flex.2xlarge - c7i-flex.4xlarge - c7i-flex.8xlarge - c7i-flex.12xlarge - c7i-flex.16xlarge - c7i.large - c7i.xlarge - c7i.2xlarge - c7i.4xlarge - c7i.8xlarge - c7i.12xlarge - c7i.16xlarge - c7i.24xlarge - c7i.48xlarge - c7i.metal-24xl - c7i.metal-48xl - c8g.medium - c8g.large - c8g.xlarge - c8g.2xlarge - c8g.4xlarge - c8g.8xlarge - c8g.12xlarge - c8g.16xlarge - c8g.24xlarge - c8g.48xlarge - c8g.metal-24xl - c8g.metal-48xl - c8gd.medium - c8gd.large - c8gd.xlarge - c8gd.2xlarge - c8gd.4xlarge - c8gd.8xlarge - c8gd.12xlarge - c8gd.16xlarge - c8gd.24xlarge - c8gd.48xlarge - c8gd.metal-24xl - c8gd.metal-48xl - g4dn.xlarge - g4dn.2xlarge - g4dn.4xlarge - g4dn.8xlarge - g4dn.12xlarge - g4dn.16xlarge - g4dn.metal - g5.xlarge - g5.2xlarge - g5.4xlarge - g5.8xlarge - g5.12xlarge - g5.16xlarge - g5.24xlarge - g5.48xlarge - g5g.xlarge - g5g.2xlarge - g5g.4xlarge - g5g.8xlarge - g5g.16xlarge - g5g.metal - g6.xlarge - g6.2xlarge - g6.4xlarge - g6.8xlarge - g6.12xlarge - g6.16xlarge - g6.24xlarge - g6.48xlarge - g6e.xlarge - g6e.2xlarge - g6e.4xlarge - g6e.8xlarge - g6e.12xlarge - g6e.16xlarge - g6e.24xlarge - g6e.48xlarge - g6f.large - g6f.xlarge - g6f.2xlarge - g6f.4xlarge - g7e.2xlarge - g7e.4xlarge - g7e.8xlarge - g7e.12xlarge - g7e.24xlarge - g7e.48xlarge - gr6.4xlarge - gr6.8xlarge - gr6f.4xlarge - m6a.large - m6a.xlarge - m6a.2xlarge - m6a.4xlarge - m6a.8xlarge - m6a.12xlarge - m6a.16xlarge - m6a.24xlarge - m6a.32xlarge - m6a.48xlarge - m6a.metal - m6g.medium - m6g.large - m6g.xlarge - m6g.2xlarge - m6g.4xlarge - m6g.8xlarge - m6g.12xlarge - m6g.16xlarge - m6g.metal - m6gd.medium - m6gd.large - m6gd.xlarge - m6gd.2xlarge - m6gd.4xlarge - m6gd.8xlarge - m6gd.12xlarge - m6gd.16xlarge - m6gd.metal - m6i.large - m6i.xlarge - m6i.2xlarge - m6i.4xlarge - m6i.8xlarge - m6i.12xlarge - m6i.16xlarge - m6i.24xlarge - m6i.32xlarge - m6i.metal - m6id.large - m6id.xlarge - m6id.2xlarge - m6id.4xlarge - m6id.8xlarge - m6id.12xlarge - m6id.16xlarge - m6id.24xlarge - m6id.32xlarge - m6id.metal - m6idn.large - m6idn.xlarge - m6idn.2xlarge - m6idn.4xlarge - m6idn.8xlarge - m6idn.12xlarge - m6idn.16xlarge - m6idn.24xlarge - m6idn.32xlarge - m6idn.metal - m6in.large - m6in.xlarge - m6in.2xlarge - m6in.4xlarge - m6in.8xlarge - m6in.12xlarge - m6in.16xlarge - m6in.24xlarge - m6in.32xlarge - m6in.metal - m7a.medium - m7a.large - m7a.xlarge - m7a.2xlarge - m7a.4xlarge - m7a.8xlarge - m7a.12xlarge - m7a.16xlarge - m7a.24xlarge - m7a.32xlarge - m7a.48xlarge - m7a.metal-48xl - m7g.medium - m7g.large - m7g.xlarge - m7g.2xlarge - m7g.4xlarge - m7g.8xlarge - m7g.12xlarge - m7g.16xlarge - m7g.metal - m7gd.medium - m7gd.large - m7gd.xlarge - m7gd.2xlarge - m7gd.4xlarge - m7gd.8xlarge - m7gd.12xlarge - m7gd.16xlarge - m7gd.metal - m7i-flex.large - m7i-flex.xlarge - m7i-flex.2xlarge - m7i-flex.4xlarge - m7i-flex.8xlarge - m7i-flex.12xlarge - m7i-flex.16xlarge - m7i.large - m7i.xlarge - m7i.2xlarge - m7i.4xlarge - m7i.8xlarge - m7i.12xlarge - m7i.16xlarge - m7i.24xlarge - m7i.48xlarge - m7i.metal-24xl - m7i.metal-48xl - m8g.medium - m8g.large - m8g.xlarge - m8g.2xlarge - m8g.4xlarge - m8g.8xlarge - m8g.12xlarge - m8g.16xlarge - m8g.24xlarge - m8g.48xlarge - m8g.metal-24xl - m8g.metal-48xl - m8gd.medium - m8gd.large - m8gd.xlarge - m8gd.2xlarge - m8gd.4xlarge - m8gd.8xlarge - m8gd.12xlarge - m8gd.16xlarge - m8gd.24xlarge - m8gd.48xlarge - m8gd.metal-24xl - m8gd.metal-48xl - p4d.24xlarge - p4de.24xlarge - p5.4xlarge - p5.48xlarge - p5e.48xlarge - p5en.48xlarge - p6-b200.48xlarge - p6-b300.48xlarge - p6e-gb200.36xlarge - r6g.medium - r6g.large - r6g.xlarge - r6g.2xlarge - r6g.4xlarge - r6g.8xlarge - r6g.12xlarge - r6g.16xlarge - r6g.metal - r6gd.medium - r6gd.large - r6gd.xlarge - r6gd.2xlarge - r6gd.4xlarge - r6gd.8xlarge - r6gd.12xlarge - r6gd.16xlarge - r6gd.metal - r7g.medium - r7g.large - r7g.xlarge - r7g.2xlarge - r7g.4xlarge - r7g.8xlarge - r7g.12xlarge - r7g.16xlarge - r7g.metal - r7gd.medium - r7gd.large - r7gd.xlarge - r7gd.2xlarge - r7gd.4xlarge - r7gd.8xlarge - r7gd.12xlarge - r7gd.16xlarge - r7gd.metal - r8g.medium - r8g.large - r8g.xlarge - r8g.2xlarge - r8g.4xlarge - r8g.8xlarge - r8g.12xlarge - r8g.16xlarge - r8g.24xlarge - r8g.48xlarge - r8g.metal-24xl - r8g.metal-48xl ConstraintDescription: "Must be a valid EC2 instance type" InitialMeetAdminPassword: Description: 'Initial password for the "admin" user in OpenVidu Meet. If not provided, a random password will be generated.' Type: String NoEcho: true Default: '' # Only allow alphanumeric characters AllowedPattern: '^[A-Za-z0-9_-]*$' ConstraintDescription: 'Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to generate a random password.' InitialMeetApiKey: Description: 'Initial API key for OpenVidu Meet. If not provided, no API key will be set and the user can set it later from Meet Console.' Type: String NoEcho: true Default: '' # Only allow alphanumeric characters AllowedPattern: '^[A-Za-z0-9_-]*$' ConstraintDescription: 'Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to not set an initial API key.' 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. OperatingSystem: Description: EC2 Operating System Type: String Default: "Ubuntu-24" AllowedValues: ['Ubuntu-22', 'Ubuntu-24'] 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 LoadBalancerSubnets: Description: "(Optional) Public subnets for the internet-facing Load Balancer. Leave empty to place the Load Balancer in the OpenViduMasterNodeSubnets (default behavior). Set it to dedicated public subnets to keep the Master Nodes in private subnets with outbound internet access (for example, a NAT gateway)." Type: CommaDelimitedList Default: "" TurnDomainName: Description: "(Optional) Domain for TURN over TLS, served by a dedicated TURN Load Balancer that forwards it directly to the Media Nodes instead of through the Master Nodes. Set it together with TurnCertificateARN and point it (A/ALIAS) to the TurnLoadBalancerDNS output. If LoadBalancerSubnets is also set and the Media Nodes are in other (private) subnets, a second Load Balancer (TurnUDPLoadBalancerDNS output) also forwards TURN over UDP 443 to the Media Nodes, so media goes over UDP, with TURN over TLS as the fallback." Type: String Default: "" TurnCertificateARN: Description: "(Optional) ACM certificate ARN for TurnDomainName, used on the TURN Load Balancer 443 TLS listener." Type: String Default: "" 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 - TurnDomainName - TurnCertificateARN - Label: default: OpenVidu High Availability configuration Parameters: - OpenViduLicense - RTCEngine - Label: default: OpenVidu Meet configuration Parameters: - InitialMeetAdminPassword - InitialMeetApiKey - Label: default: EC2 Instance configuration Parameters: - MasterNodeInstanceType - MediaNodeInstanceType - KeyName - OperatingSystem - 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 - LoadBalancerSubnets - Label: default: Volumes configuration Parameters: - MasterNodesDiskSize - Label: default: "(Optional) Additional Installer Flags" Parameters: - AdditionalInstallFlags Conditions: CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""] HasLoadBalancerSubnets: !Not [!Equals [!Join ["", !Ref LoadBalancerSubnets], ""]] TurnTLSIsEnabled: !And [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]] # Media Nodes behind the TURN Load Balancer in other subnets than the Load Balancers (private subnets) TurnUDPIsEnabled: !And - !Condition TurnTLSIsEnabled - !Condition HasLoadBalancerSubnets - !Not [!Equals [!Join [",", !Ref LoadBalancerSubnets], !Join [",", !Ref OpenViduMediaNodeSubnets]]] IsMasterGraviton: !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 't4g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6gn'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7gn'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c8g'] - !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c8gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm6g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm6gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm7g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm7gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm8g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm8gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r6g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r6gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r7g'] - !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r7gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r8g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g5g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6e-gb200'] IsMediaGraviton: !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 't4g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6gn'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7gn'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c8g'] - !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c8gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm6g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm6gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm7g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm7gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm8g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm8gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r6g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r6gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r7g'] - !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r7gd'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r8g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g5g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6e-gb200'] IsMasterNodeNvidia: !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g4dn'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g5'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g5g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g6'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g6e'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g6f'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'gr6'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'gr6f'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g7e'] - !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p4d'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p4de'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p5'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p5e'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p5en'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6-b200'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6-b300'] - !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6e-gb200'] IsMediaNodeNvidia: !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g4dn'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g5'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g5g'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g6'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g6e'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g6f'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'gr6'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'gr6f'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g7e'] - !Or - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p4d'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p4de'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p5'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p5e'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p5en'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6-b200'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6-b300'] - !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6e-gb200'] Mappings: ArmImage: # aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/" --recursive --query "Parameters[*].Name" > canonical-ami.txt Ubuntu-22: ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}' Ubuntu-24: ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/arm64/hvm/ebs-gp3/ami-id}}' AmdImage: Ubuntu-22: ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id}}' Ubuntu-24: ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id}}' 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: | { "OPENVIDU_URL": "none", "MEET_INITIAL_ADMIN_USER": "none", "MEET_INITIAL_ADMIN_PASSWORD": "none", "MEET_INITIAL_API_KEY": "none", "LIVEKIT_URL": "none", "LIVEKIT_API_KEY": "none", "LIVEKIT_API_SECRET": "none", "DASHBOARD_URL": "none", "GRAFANA_URL": "none", "MINIO_URL": "none", "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_ACCESS_KEY": "none", "MINIO_SECRET_KEY": "none", "DASHBOARD_ADMIN_USERNAME": "none", "DASHBOARD_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_USERNAME": "none", "GRAFANA_ADMIN_PASSWORD": "none", "ENABLED_MODULES": "none", "OPENVIDU_VERSION": "none", "ALL_SECRETS_GENERATED": "false" } # One SSM parameter per master: atomic per-key writes MasterNode1PrivateIpParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /openvidu/${AWS::StackName}/master-node-1-private-ip Type: String Value: "none" MasterNode2PrivateIpParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /openvidu/${AWS::StackName}/master-node-2-private-ip Type: String Value: "none" MasterNode3PrivateIpParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /openvidu/${AWS::StackName}/master-node-3-private-ip Type: String Value: "none" MasterNode4PrivateIpParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /openvidu/${AWS::StackName}/master-node-4-private-ip Type: String Value: "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 # ------------------------- # Normalize the master node subnet list to exactly 4 entries, one per OpenVidu Master Node # ------------------------- SubnetProcessorFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub 'SubnetProcessor-${AWS::Region}-${AWS::StackName}' Handler: index.lambda_handler Role: !GetAtt LambdaExecutionRole.Arn Code: ZipFile: | import boto3 import cfnresponse def lambda_handler(event, context): try: # Process event data subnets = event['ResourceProperties']['Subnets'] # 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], } # Media Nodes are private when their subnets do not assign public IPs media_subnets = event['ResourceProperties'].get('MediaSubnets', []) media_private = 'false' if event['RequestType'] != 'Delete' and media_subnets: described = boto3.client('ec2').describe_subnets(SubnetIds=media_subnets)['Subnets'] if any(not s.get('MapPublicIpOnLaunch', False) for s in described): media_private = 'true' responseData['MediaNodesPrivate'] = media_private # TURN over UDP through the TURN UDP Load Balancer, only for private Media Nodes: # public ones keep announcing their own public IP turn_udp = event['ResourceProperties'].get('TurnUDP', 'false') == 'true' responseData['TurnUDPEnabled'] = 'true' if turn_udp and media_private == 'true' else 'false' 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 MediaSubnets: !Ref OpenViduMediaNodeSubnets TurnUDP: !If [TurnUDPIsEnabled, 'true', 'false'] 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: "/" ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore 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 - Effect: Allow Action: - ssm:GetParameter - ssm:PutParameter Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/openvidu/${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} - 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: - ssm:GetParameter Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/openvidu/${AWS::StackName}/* - 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 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.53.6 # 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_$(dpkg --print-architecture).tar.gz -O - |\ tar xz && mv yq_linux_$(dpkg --print-architecture) /usr/bin/yq AWS_CLI_VERSION=2.36.40 # Install aws-cli if not already installed if ! command -v aws &> /dev/null; then curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m)-${!AWS_CLI_VERSION}.zip" -o "awscliv2.zip" unzip -qq awscliv2.zip ./aws/install rm -rf awscliv2.zip aws fi # Signal to notify instance is waiting SIGNAL_NAME="$1" # Exported earlier in this instance's UserData MASTER_NODE_NUM="${!MASTER_NODE_NUM}" # 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)" if [[ "$PRIVATE_IP" == "" ]]; then echo "Error: Private IP not found" exit 1 fi aws ssm put-parameter \ --region ${AWS::Region} \ --name "/openvidu/${AWS::StackName}/master-node-${!MASTER_NODE_NUM}-private-ip" \ --value "${!PRIVATE_IP}" \ --type String \ --overwrite > /dev/null 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}")" # Publish access URLs early so other nodes can read them during phase 2 # (idempotent: after_install rewrites these same byte-identical values later) /usr/local/bin/store_secret.sh save OPENVIDU_URL "https://${!DOMAIN}/" /usr/local/bin/store_secret.sh save LIVEKIT_URL "wss://${!DOMAIN}/" /usr/local/bin/store_secret.sh save DASHBOARD_URL "https://${!DOMAIN}/dashboard/" /usr/local/bin/store_secret.sh save GRAFANA_URL "https://${!DOMAIN}/grafana/" /usr/local/bin/store_secret.sh save MINIO_URL "https://${!DOMAIN}/minio-console/" 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}" # Meet initial admin user and password MEET_INITIAL_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_ADMIN_USER "admin")" if [[ "${InitialMeetAdminPassword}" != '' ]]; then MEET_INITIAL_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_ADMIN_PASSWORD "${InitialMeetAdminPassword}")" else MEET_INITIAL_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MEET_INITIAL_ADMIN_PASSWORD)" fi if [[ "${InitialMeetApiKey}" != '' ]]; then MEET_INITIAL_API_KEY="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_API_KEY "${InitialMeetApiKey}")" else MEET_INITIAL_API_KEY="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_API_KEY "")" fi # 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)" 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")" # Every store_secret.sh call above is a read-modify-write of the whole JSON, and a read # right after a write can still return the previous version (eventual consistency): the # key written a moment earlier is then silently dropped (seen live: LIVEKIT_API_KEY back # to "none", every master waiting 5 minutes on an incomplete secret and failing). So # write the complete set once more from the values above, together with # ALL_SECRETS_GENERATED, and read it back until all of it is really there. SECRETS_WANTED="$(jq -n \ --arg DOMAIN_NAME "${!DOMAIN}" \ --arg OPENVIDU_URL "https://${!DOMAIN}/" \ --arg LIVEKIT_URL "wss://${!DOMAIN}/" \ --arg DASHBOARD_URL "https://${!DOMAIN}/dashboard/" \ --arg GRAFANA_URL "https://${!DOMAIN}/grafana/" \ --arg MINIO_URL "https://${!DOMAIN}/minio-console/" \ --arg OPENVIDU_PRO_LICENSE "${!OPENVIDU_PRO_LICENSE}" \ --arg OPENVIDU_RTC_ENGINE "${!OPENVIDU_RTC_ENGINE}" \ --arg OPENVIDU_VERSION "${!OPENVIDU_VERSION}" \ --arg MEET_INITIAL_ADMIN_USER "${!MEET_INITIAL_ADMIN_USER}" \ --arg MEET_INITIAL_ADMIN_PASSWORD "${!MEET_INITIAL_ADMIN_PASSWORD}" \ --arg MEET_INITIAL_API_KEY "${!MEET_INITIAL_API_KEY}" \ --arg REDIS_PASSWORD "${!REDIS_PASSWORD}" \ --arg MONGO_ADMIN_USERNAME "${!MONGO_ADMIN_USERNAME}" \ --arg MONGO_ADMIN_PASSWORD "${!MONGO_ADMIN_PASSWORD}" \ --arg MONGO_REPLICA_SET_KEY "${!MONGO_REPLICA_SET_KEY}" \ --arg MINIO_ACCESS_KEY "${!MINIO_ACCESS_KEY}" \ --arg MINIO_SECRET_KEY "${!MINIO_SECRET_KEY}" \ --arg DASHBOARD_ADMIN_USERNAME "${!DASHBOARD_ADMIN_USERNAME}" \ --arg DASHBOARD_ADMIN_PASSWORD "${!DASHBOARD_ADMIN_PASSWORD}" \ --arg GRAFANA_ADMIN_USERNAME "${!GRAFANA_ADMIN_USERNAME}" \ --arg GRAFANA_ADMIN_PASSWORD "${!GRAFANA_ADMIN_PASSWORD}" \ --arg LIVEKIT_API_KEY "${!LIVEKIT_API_KEY}" \ --arg LIVEKIT_API_SECRET "${!LIVEKIT_API_SECRET}" \ --arg ENABLED_MODULES "${!ENABLED_MODULES}" \ --arg ALL_SECRETS_GENERATED "true" \ '$ARGS.named')" SECRETS_VERIFIED=false for SECRETS_WRITE_ATTEMPT in $(seq 1 30); do CURRENT_SECRET="$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text)" if echo "$CURRENT_SECRET" | jq -e --argjson want "$SECRETS_WANTED" \ '. as $cur | $want | to_entries | all(.value == $cur[.key])' > /dev/null; then SECRETS_VERIFIED=true break fi aws secretsmanager update-secret \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --secret-string "$(echo "$CURRENT_SECRET" | jq --argjson want "$SECRETS_WANTED" '. + $want')" > /dev/null 2>&1 sleep 2 done if [[ "$SECRETS_VERIFIED" != "true" ]]; then echo "Error: the shared secret could not be verified complete after 30 writes" exit 1 fi ALL_SECRETS_GENERATED="true" fi # Fetch the shared secret again, waiting until master-1 has generated all secrets. # Bounded (up to 900s at 5s): a genuinely stuck master-1 still surfaces after the deadline. SECRETS_WAIT_ATTEMPTS=0 SECRETS_WAIT_MAX=180 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') ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') if [[ "${!ALL_SECRETS_GENERATED}" == "true" ]]; then break fi SECRETS_WAIT_ATTEMPTS=$((SECRETS_WAIT_ATTEMPTS + 1)) if [[ $SECRETS_WAIT_ATTEMPTS -ge $SECRETS_WAIT_MAX ]]; then echo "Error: Secrets not generated" exit 1 fi sleep 5 done # The WaitCondition is signalled by the UserData once OpenVidu is healthy IP_WAIT_ATTEMPTS=0 IP_WAIT_MAX=360 while true; do MASTER_NODE_1_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-1-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_NODE_2_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-2-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_NODE_3_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-3-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_NODE_4_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-4-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') # Check if all master nodes have published their private IPs if [[ "$MASTER_NODE_1_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_1_PRIVATE_IP" ]] && [[ "$MASTER_NODE_2_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_2_PRIVATE_IP" ]] && [[ "$MASTER_NODE_3_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_3_PRIVATE_IP" ]] && [[ "$MASTER_NODE_4_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_4_PRIVATE_IP" ]]; then break fi IP_WAIT_ATTEMPTS=$((IP_WAIT_ATTEMPTS + 1)) if [[ $IP_WAIT_ATTEMPTS -ge $IP_WAIT_MAX ]]; then echo "Error: timed out after 30 minutes waiting for all master nodes to publish their private IPs to SSM" exit 1 fi sleep 5 done # Comma-separated 1-4: must match installer's --master-node-private-ip-list format 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" # Re-read: other (non-IP) values still come from the shared secret. # GetSecretValue is eventually consistent: retry until a read returns the generated values SECRET_READ_ATTEMPTS=0 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) if echo "$SHARED_SECRET" | jq -e '(.ALL_SECRETS_GENERATED == "true") and ([.DOMAIN_NAME, .OPENVIDU_VERSION, .REDIS_PASSWORD, .MONGO_ADMIN_PASSWORD, .MONGO_REPLICA_SET_KEY, .MINIO_SECRET_KEY, .DASHBOARD_ADMIN_PASSWORD, .GRAFANA_ADMIN_PASSWORD, .LIVEKIT_API_KEY, .LIVEKIT_API_SECRET] | all(. != "none"))' > /dev/null; then break fi SECRET_READ_ATTEMPTS=$((SECRET_READ_ATTEMPTS + 1)) if [[ $SECRET_READ_ATTEMPTS -ge 60 ]]; then echo "Error: shared secret still incomplete after 5 minutes of stale reads" exit 1 fi sleep 5 done DOMAIN=$(echo "$SHARED_SECRET" | jq -r '.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_INITIAL_ADMIN_USER=$(echo "$SHARED_SECRET" | jq -r '.MEET_INITIAL_ADMIN_USER') MEET_INITIAL_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.MEET_INITIAL_ADMIN_PASSWORD') MEET_INITIAL_API_KEY="" if [[ "${InitialMeetApiKey}" != '' ]]; then MEET_INITIAL_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.MEET_INITIAL_API_KEY') fi 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') # Download first: sh <(curl ...) would silently run an empty script on a transient curl failure INSTALLER_SCRIPT="/tmp/install_ov_master_node.sh" curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o "$INSTALLER_SCRIPT" "http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_master_node.sh" if [ ! -s "$INSTALLER_SCRIPT" ]; then echo "Downloaded OpenVidu master node installer is empty or missing" >&2 exit 1 fi INSTALL_COMMAND="sh $INSTALLER_SCRIPT" # 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-initial-admin-password=$MEET_INITIAL_ADMIN_PASSWORD" "--meet-initial-api-key=$MEET_INITIAL_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 # Dedicated TURN Load Balancer (only when TurnDomainName and TurnCertificateARN are set, # the same condition as the TURN Load Balancer resources) if [[ "${TurnDomainName}" != "" && "${TurnCertificateARN}" != "" ]]; then COMMON_ARGS+=("--turn-domain-name=${TurnDomainName}") fi # TURN UDP Load Balancer: each Media Node announces the Load Balancer IP of its zone if [[ "${SubnetProcessor.TurnUDPEnabled}" == "true" ]]; then COMMON_ARGS+=("--force-media-node-public-ip") 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/registry_mirror.sh': content: !Sub | #!/bin/bash set -e # Pull-through cache of Docker Hub for the Media Nodes. Only when the Media Nodes are # in private subnets: they all reach Docker Hub from the same NAT IP, and Docker Hub # rate-limits anonymous pulls per IP. Media Nodes with a public IP pull directly. if [[ "${SubnetProcessor.MediaNodesPrivate}" != "true" ]]; then exit 0 fi REGISTRY_VERSION=v3.1.2 ARCH="$(dpkg --print-architecture)" curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o /tmp/registry.tar.gz \ "https://github.com/distribution/distribution/releases/download/${!REGISTRY_VERSION}/registry_${!REGISTRY_VERSION#v}_linux_${!ARCH}.tar.gz" tar xzf /tmp/registry.tar.gz -C /usr/local/bin registry rm -f /tmp/registry.tar.gz mkdir -p /etc/registry-mirror cat > /etc/registry-mirror/config.yml <<'EOF' version: 0.1 log: level: info storage: filesystem: rootdirectory: /var/lib/registry-mirror delete: enabled: true http: addr: :5555 proxy: remoteurl: https://registry-1.docker.io ttl: 168h EOF cat > /etc/systemd/system/registry-mirror.service <<'EOF' [Unit] Description=Docker Hub pull-through cache for the OpenVidu Media Nodes After=network-online.target Wants=network-online.target [Service] # No OpenTelemetry collector: do not export traces Environment=OTEL_TRACES_EXPORTER=none ExecStart=/usr/local/bin/registry serve /etc/registry-mirror/config.yml # Unprivileged: a transient user that can only write its own cache # (/var/lib/registry-mirror); 5555 needs no capabilities DynamicUser=yes StateDirectory=registry-mirror CapabilityBoundingSet= PrivateDevices=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=yes LockPersonality=yes SystemCallArchitectures=native SystemCallFilter=@system-service Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now registry-mirror 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 # Only master 1 writes the shared secret, to avoid a lost-update race if [[ "${!MASTER_NODE_NUM:-}" != "1" ]]; then exit 0 fi # 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') OPENVIDU_URL="https://${!DOMAIN}/" LIVEKIT_URL="wss://${!DOMAIN}/" 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 '. + {"OPENVIDU_URL": "'"$OPENVIDU_URL"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_URL": "'"$LIVEKIT_URL"'" }')" 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 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_INITIAL_ADMIN_USER=.*/MEET_INITIAL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_INITIAL_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env" sed -i "s/MEET_INITIAL_ADMIN_PASSWORD=.*/MEET_INITIAL_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MEET_INITIAL_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env" if [[ "${InitialMeetApiKey}" != '' ]]; then sed -i "s/MEET_INITIAL_API_KEY=.*/MEET_INITIAL_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_INITIAL_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env" fi sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" # Update URLs in secret OPENVIDU_URL="https://${!DOMAIN}/" LIVEKIT_URL="wss://${!DOMAIN}/" DASHBOARD_URL="https://${!DOMAIN}/dashboard/" GRAFANA_URL="https://${!DOMAIN}/grafana/" MINIO_URL="https://${!DOMAIN}/minio-console/" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_URL": "'"$OPENVIDU_URL"'" }')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_URL": "'"$LIVEKIT_URL"'" }')" 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 '. + {"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_INITIAL_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_INITIAL_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')" if [[ "${InitialMeetApiKey}" != '' ]]; then SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_INITIAL_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')" fi 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: !If - IsMasterGraviton - !If - IsMasterNodeNvidia - '{{resolve:ssm:/aws/service/deeplearning/ami/arm64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id}}' - !FindInMap [ArmImage, !Ref OperatingSystem, ImageId] - !If - IsMasterNodeNvidia - '{{resolve:ssm:/aws/service/deeplearning/ami/x86_64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id}}' - !FindInMap [AmdImage, !Ref OperatingSystem, ImageId] 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 # Depends on its own SSM parameter so its put-parameter can't race the parameter's creation DependsOn: MasterNode1PrivateIpParameter 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 echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout apt-get update && apt-get install -y \ python3-pip \ ec2-instance-connect CFN_BOOTSTRAP_VERSION=2.0-39 # Detect Ubuntu version and install cfn-bootstrap accordingly UBUNTU_VERSION=$(lsb_release -rs | cut -d. -f1) if [ "$UBUNTU_VERSION" -ge 24 ]; then python3 -m pip install --break-system-packages https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz else python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz fi cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub) /usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started" export MASTER_NODE_NUM=1 # Report the result to CloudFormation: success only once OpenVidu is healthy on this node signal_cfn() { cfn-signal -e "$1" --reason "$2" --stack ${AWS::StackId} --resource MasterNodesWaitCondition1 --region ${AWS::Region} || true; } fail() { echo "[OpenVidu] $1"; signal_cfn 1 "Master Node 1: $1"; exit 1; } trap 'fail "unexpected error at line $LINENO"' ERR # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition1" || fail "error installing OpenVidu" # Config S3 bucket /usr/local/bin/config_s3.sh || fail "error configuring S3 bucket" # Start OpenVidu systemctl start openvidu || fail "error starting OpenVidu" # Local readiness gate: wait up to 300s for Caddy health, restart once if it does not converge OPENVIDU_READY=false for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done if [ "$OPENVIDU_READY" != "true" ]; then echo "[OpenVidu] not healthy after 300s, restarting once" systemctl restart openvidu || true for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done fi if [ "$OPENVIDU_READY" != "true" ]; then fail "OpenVidu not healthy after installing" fi # Update shared secret /usr/local/bin/after_install.sh || fail "error updating shared secret" signal_cfn 0 "OpenVidu installed and healthy" # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition1: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT20M Count: '1' OpenViduMasterNode2: Type: AWS::EC2::Instance DependsOn: MasterNode2PrivateIpParameter 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 echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout apt-get update && apt-get install -y \ python3-pip \ ec2-instance-connect CFN_BOOTSTRAP_VERSION=2.0-39 # Detect Ubuntu version and install cfn-bootstrap accordingly UBUNTU_VERSION=$(lsb_release -rs | cut -d. -f1) if [ "$UBUNTU_VERSION" -ge 24 ]; then python3 -m pip install --break-system-packages https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz else python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz fi cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub) /usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started" export MASTER_NODE_NUM=2 # Report the result to CloudFormation: success only once OpenVidu is healthy on this node signal_cfn() { cfn-signal -e "$1" --reason "$2" --stack ${AWS::StackId} --resource MasterNodesWaitCondition2 --region ${AWS::Region} || true; } fail() { echo "[OpenVidu] $1"; signal_cfn 1 "Master Node 2: $1"; exit 1; } trap 'fail "unexpected error at line $LINENO"' ERR # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition2" || fail "error installing OpenVidu" # Config S3 bucket /usr/local/bin/config_s3.sh || fail "error configuring S3 bucket" # Start OpenVidu systemctl start openvidu || fail "error starting OpenVidu" # Local readiness gate: wait up to 300s for Caddy health, restart once if it does not converge OPENVIDU_READY=false for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done if [ "$OPENVIDU_READY" != "true" ]; then echo "[OpenVidu] not healthy after 300s, restarting once" systemctl restart openvidu || true for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done fi if [ "$OPENVIDU_READY" != "true" ]; then fail "OpenVidu not healthy after installing" fi # Update shared secret /usr/local/bin/after_install.sh || fail "error updating shared secret" signal_cfn 0 "OpenVidu installed and healthy" # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition2: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT20M Count: '1' OpenViduMasterNode3: Type: AWS::EC2::Instance DependsOn: MasterNode3PrivateIpParameter 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 echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout apt-get update && apt-get install -y \ python3-pip \ ec2-instance-connect CFN_BOOTSTRAP_VERSION=2.0-39 # Detect Ubuntu version and install cfn-bootstrap accordingly UBUNTU_VERSION=$(lsb_release -rs | cut -d. -f1) if [ "$UBUNTU_VERSION" -ge 24 ]; then python3 -m pip install --break-system-packages https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz else python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz fi cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub) /usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started" export MASTER_NODE_NUM=3 # Report the result to CloudFormation: success only once OpenVidu is healthy on this node signal_cfn() { cfn-signal -e "$1" --reason "$2" --stack ${AWS::StackId} --resource MasterNodesWaitCondition3 --region ${AWS::Region} || true; } fail() { echo "[OpenVidu] $1"; signal_cfn 1 "Master Node 3: $1"; exit 1; } trap 'fail "unexpected error at line $LINENO"' ERR # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition3" || fail "error installing OpenVidu" # Config S3 bucket /usr/local/bin/config_s3.sh || fail "error configuring S3 bucket" # Start OpenVidu systemctl start openvidu || fail "error starting OpenVidu" # Local readiness gate: wait up to 300s for Caddy health, restart once if it does not converge OPENVIDU_READY=false for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done if [ "$OPENVIDU_READY" != "true" ]; then echo "[OpenVidu] not healthy after 300s, restarting once" systemctl restart openvidu || true for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done fi if [ "$OPENVIDU_READY" != "true" ]; then fail "OpenVidu not healthy after installing" fi # Update shared secret /usr/local/bin/after_install.sh || fail "error updating shared secret" signal_cfn 0 "OpenVidu installed and healthy" # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition3: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT20M Count: '1' OpenViduMasterNode4: Type: AWS::EC2::Instance DependsOn: MasterNode4PrivateIpParameter 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 echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout apt-get update && apt-get install -y \ python3-pip \ ec2-instance-connect CFN_BOOTSTRAP_VERSION=2.0-39 # Detect Ubuntu version and install cfn-bootstrap accordingly UBUNTU_VERSION=$(lsb_release -rs | cut -d. -f1) if [ "$UBUNTU_VERSION" -ge 24 ]; then python3 -m pip install --break-system-packages https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz else python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz fi cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate # Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub) /usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started" export MASTER_NODE_NUM=4 # Report the result to CloudFormation: success only once OpenVidu is healthy on this node signal_cfn() { cfn-signal -e "$1" --reason "$2" --stack ${AWS::StackId} --resource MasterNodesWaitCondition4 --region ${AWS::Region} || true; } fail() { echo "[OpenVidu] $1"; signal_cfn 1 "Master Node 4: $1"; exit 1; } trap 'fail "unexpected error at line $LINENO"' ERR # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition4" || fail "error installing OpenVidu" # Config S3 bucket /usr/local/bin/config_s3.sh || fail "error configuring S3 bucket" # Start OpenVidu systemctl start openvidu || fail "error starting OpenVidu" # Local readiness gate: wait up to 300s for Caddy health, restart once if it does not converge OPENVIDU_READY=false for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done if [ "$OPENVIDU_READY" != "true" ]; then echo "[OpenVidu] not healthy after 300s, restarting once" systemctl restart openvidu || true for i in $(seq 1 60); do if curl -fsS http://127.0.0.1:7880/health/caddy >/dev/null 2>&1; then OPENVIDU_READY=true break fi sleep 5 done fi if [ "$OPENVIDU_READY" != "true" ]; then fail "OpenVidu not healthy after installing" fi # Update shared secret /usr/local/bin/after_install.sh || fail "error updating shared secret" signal_cfn 0 "OpenVidu installed and healthy" # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition4: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: PT20M Count: '1' OpenViduMediaNodeLaunchTemplate: Type: AWS::EC2::LaunchTemplate Metadata: Comment: Launch template for OpenVidu Media Node AWS::CloudFormation::Init: config: files: '/etc/openvidu/turn-udp-lb-dns': Fn::If: - TurnUDPIsEnabled - content: !GetAtt TurnUDPLoadBalancer.DNSName mode: '000644' owner: root group: root - !Ref AWS::NoValue '/usr/local/bin/install.sh': content: !Sub | #!/bin/bash set -e YQ_VERSION=v4.53.6 # 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_$(dpkg --print-architecture).tar.gz -O - |\ tar xz && mv yq_linux_$(dpkg --print-architecture) /usr/bin/yq AWS_CLI_VERSION=2.36.40 # Install aws-cli if not already installed if ! command -v aws &> /dev/null; then curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m)-${!AWS_CLI_VERSION}.zip" -o "awscliv2.zip" unzip -qq awscliv2.zip ./aws/install rm -rf awscliv2.zip aws fi # 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 SECRETS_WAIT_ATTEMPTS=0 SECRETS_WAIT_MAX=360 while true; do # Validate content, not just the flag: stale eventually-consistent reads can return pre-generation values if echo "$SHARED_SECRET" | jq -e '(.ALL_SECRETS_GENERATED == "true") and (.OPENVIDU_VERSION != "none") and (.REDIS_PASSWORD != "none")' > /dev/null; then break fi SECRETS_WAIT_ATTEMPTS=$((SECRETS_WAIT_ATTEMPTS + 1)) if [[ $SECRETS_WAIT_ATTEMPTS -ge $SECRETS_WAIT_MAX ]]; then echo "Error: timed out after 30 minutes waiting for shared secrets to be generated" exit 1 fi sleep 5 SHARED_SECRET="$(aws secretsmanager get-secret-value \ --region ${AWS::Region} \ --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ --query SecretString --output text || echo 'none')" done # 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 IP_WAIT_ATTEMPTS=0 IP_WAIT_MAX=360 while true; do MASTER_NODE_1_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-1-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_NODE_2_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-2-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_NODE_3_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-3-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_NODE_4_PRIVATE_IP=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-4-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') if [[ "$MASTER_NODE_1_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_1_PRIVATE_IP" ]] && [[ "$MASTER_NODE_2_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_2_PRIVATE_IP" ]] && [[ "$MASTER_NODE_3_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_3_PRIVATE_IP" ]] && [[ "$MASTER_NODE_4_PRIVATE_IP" != "none" ]] && [[ -n "$MASTER_NODE_4_PRIVATE_IP" ]]; then break fi IP_WAIT_ATTEMPTS=$((IP_WAIT_ATTEMPTS + 1)) if [[ $IP_WAIT_ATTEMPTS -ge $IP_WAIT_MAX ]]; then echo "Error: timed out after 30 minutes waiting for all master nodes to publish their private IPs to SSM" exit 1 fi sleep 5 done 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') # Registry mirror of the Master Nodes, before the installer pulls the images /usr/local/bin/config_registry_mirror.sh "$MASTER_NODE_1_PRIVATE_IP" "$MASTER_NODE_2_PRIVATE_IP" "$MASTER_NODE_3_PRIVATE_IP" "$MASTER_NODE_4_PRIVATE_IP" \ || echo "Registry mirror not configured: pulling from Docker Hub" # Download first: sh <(curl ...) would silently run an empty script on a transient curl failure INSTALLER_SCRIPT="/tmp/install_ov_media_node.sh" curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o "$INSTALLER_SCRIPT" "http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh" if [ ! -s "$INSTALLER_SCRIPT" ]; then echo "Downloaded OpenVidu media node installer is empty or missing" >&2 exit 1 fi INSTALL_COMMAND="sh $INSTALLER_SCRIPT" # 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" ) # TURN UDP Load Balancer: announce its public IP in this Availability Zone, so clients get # turn::443 for TURN over UDP. Its zonal DNS name (.) resolves to it. # With cross-zone load balancing any of its IPs reaches every Media Node, so use any of # them if this zone has none. Without it, clients fall back to TURN over TLS. if [[ "${SubnetProcessor.TurnUDPEnabled}" == "true" ]]; then TURN_UDP_LB_DNS="$(cat /etc/openvidu/turn-udp-lb-dns 2>/dev/null || true)" AZ="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/placement/availability-zone)" PUBLIC_IP="" for i in $(seq 1 60); do if [[ "$TURN_UDP_LB_DNS" == "" ]]; then break fi PUBLIC_IP="$(getent ahostsv4 "$AZ.$TURN_UDP_LB_DNS" | awk 'NR == 1 { print $1 }')" if [[ "$PUBLIC_IP" == "" ]]; then PUBLIC_IP="$(getent ahostsv4 "$TURN_UDP_LB_DNS" | awk 'NR == 1 { print $1 }')" fi if [[ "$PUBLIC_IP" != "" ]]; then break fi sleep 5 done if [[ "$PUBLIC_IP" == "" ]]; then echo "TURN UDP Load Balancer IP not found: clients will use TURN over TLS" PUBLIC_IP="$PRIVATE_IP" fi COMMON_ARGS+=("--public-ip=$PUBLIC_IP") fi # 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/config_registry_mirror.sh': content: !Sub | #!/bin/bash set -e # Pull Docker Hub images through the registry mirror of the Master Nodes # (registry_mirror.sh), only when this Media Node has no public IP. Docker falls # back to Docker Hub if no mirror answers. # Usage: config_registry_mirror.sh if [[ "${SubnetProcessor.MediaNodesPrivate}" != "true" ]]; then exit 0 fi TOKEN="$(curl -sf -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" if curl -sf -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4 > /dev/null; then exit 0 fi MASTER_IPS=("$@") # Wait up to 5 minutes for a mirror: on a new stack, the Master Nodes start it at boot MIRROR_READY=false for i in $(seq 1 60); do for MASTER_IP in "${!MASTER_IPS[@]}"; do if curl -sf --max-time 3 "http://$MASTER_IP:5555/v2/" > /dev/null; then MIRROR_READY=true break fi done if [[ "$MIRROR_READY" == "true" ]]; then break fi sleep 5 done if [[ "$MIRROR_READY" != "true" ]]; then echo "No registry mirror answers in the Master Nodes: pulling from Docker Hub" exit 0 fi MIRRORS="$(printf '%s:5555\n' "${!MASTER_IPS[@]}" | jq -R . | jq -s .)" mkdir -p /etc/docker if [[ ! -s /etc/docker/daemon.json ]]; then echo '{}' > /etc/docker/daemon.json fi jq --argjson m "$MIRRORS" \ '."registry-mirrors" = ($m | map("http://" + .)) | ."insecure-registries" = ((."insecure-registries" // []) + $m | unique)' \ /etc/docker/daemon.json > /etc/docker/daemon.json.tmp mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json if systemctl is-active --quiet docker; then systemctl restart docker fi echo "Pulling Docker Hub images through: $(jq -c '."registry-mirrors"' /etc/docker/daemon.json)" mode: "000755" owner: "root" group: "root" '/usr/local/bin/openvidu-media-health.sh': content: | #!/bin/bash # OpenVidu Media Node health watchdog. # # Asks the cloud to replace this Media Node with a new one, keeping the number of # Media Nodes, through /usr/local/bin/openvidu-media-replace.sh, when: # - its bootstrap failed (the user-data wrote the bootstrap-failed marker): after a # cool-down, so a persistent failure does not churn nodes and can be inspected, # - its bootstrap did not finish BOOTSTRAP_DEADLINE_SEC after boot, # - LiveKit fails its health check (HTTP 200 on PROBE_URL): for HARD_UNHEALTHY_AFTER_SEC # while it is down (connection refused or closed), or for SOFT_UNHEALTHY_AFTER_SEC # while it answers an error or times out. Soft failures are not counted while the CPU # is saturated (an overloaded LiveKit reports Not Ready), and no failure is counted in # the WARMUP_SEC after openvidu.service (re)started. # Nothing is replaced while no master answers a Redis PING (MASTER_ADDRS): a master outage # fails the health check of every Media Node at once, and a new node could not bootstrap # either; LiveKit failures are not counted until a master answers again. Once requested, # the replacement is requested again every REPLACE_RETRY_SEC until the node goes away. # # Settings: /etc/openvidu/media-health.env (sourced every cycle; MASTER_ADDRS="ip:port ...") # Pause: touch /etc/openvidu/media-health.disabled (drain scripts use /run/openvidu-media-health.paused) # Logs: journalctl -u openvidu-media-health # # This file is embedded verbatim in CloudFormation !Sub blocks, Bicep strings and # Terraform heredocs, so it must never contain a dollar sign or a percent sign followed # by an opening brace, nor two opening braces in a row. STATE_DIR=/var/lib/openvidu-media-health INSTALLED_MARKER="$STATE_DIR/installed" BOOTSTRAP_FAILED_MARKER="$STATE_DIR/bootstrap-failed" REPLACE_SCRIPT=/usr/local/bin/openvidu-media-replace.sh ENV_FILE=/etc/openvidu/media-health.env PAUSE_FILE=/etc/openvidu/media-health.disabled DRAIN_PAUSE_FILE=/run/openvidu-media-health.paused log() { echo "[media-health] $*"; } # Seconds since boot: monotonic, unaffected by clock steps uptime_sec() { cut -d. -f1 /proc/uptime; } # Busy CPU percentage since the previous call, in BUSY (0 on the first call) cpu_busy() { read -r _ c_user c_nice c_system c_idle c_iowait c_irq c_softirq c_steal _ < /proc/stat total=$((c_user + c_nice + c_system + c_idle + c_iowait + c_irq + c_softirq + c_steal)) idle=$((c_idle + c_iowait)) BUSY=0 if [ -n "$PREV_TOTAL" ] && [ "$total" -gt "$PREV_TOTAL" ]; then BUSY=$((100 * (total - PREV_TOTAL - idle + PREV_IDLE) / (total - PREV_TOTAL))) fi PREV_TOTAL=$total PREV_IDLE=$idle } # openvidu.service is running and entered the active state less than WARMUP_SEC ago # (a stopped or failed openvidu.service is not starting: no warm-up for it) openvidu_starting() { case "$(systemctl show -p ActiveState --value openvidu 2>/dev/null)" in active|activating|reloading) ;; *) return 1 ;; esac since=$(systemctl show -p ActiveEnterTimestampMonotonic --value openvidu 2>/dev/null) case "$since" in ''|0|*[!0-9]*) return 1 ;; esac [ $(($(uptime_sec) - since / 1000000)) -lt "$WARMUP_SEC" ] } # A master answers a Redis PING (+PONG, or -NOAUTH when a password is required). A bare # TCP connect would also succeed against a wedged Redis. master_serving() { [ -n "$MASTER_ADDRS" ] || return 1 for addr in $MASTER_ADDRS; do host=$(echo "$addr" | cut -d: -f1) port=$(echo "$addr" | cut -d: -f2) reply=$(timeout 5 bash -c 'exec 3<>"/dev/tcp/$0/$1" && printf "PING\r\n" >&3 && head -c 7 <&3' "$host" "$port" 2>/dev/null) case "$reply" in +PONG*|-NOAUTH*) return 0 ;; esac done return 1 } # Request the replacement of this node ($1 = reason). Gated on a master answering, except # in the bootstrap phase before the master addresses are known. request_replacement() { now=$(uptime_sec) if [ -n "$REQUESTED_AT" ] && [ $((now - REQUESTED_AT)) -lt "$REPLACE_RETRY_SEC" ]; then return fi if [ -n "$MASTER_ADDRS" ] || [ -f "$INSTALLED_MARKER" ]; then if ! master_serving; then if [ -z "$HOLD_LOGGED_AT" ] || [ $((now - HOLD_LOGGED_AT)) -ge 600 ]; then log "$1, but no master answers a Redis PING (MASTER_ADDRS='$MASTER_ADDRS'): not replacing this node" HOLD_LOGGED_AT=$now fi return fi fi log "$1: requesting the replacement of this node" if "$REPLACE_SCRIPT" "$1"; then REQUESTED_AT=$now log "replacement requested" else log "the replacement request failed, retrying in $INTERVAL_SEC s" fi } mkdir -p "$STATE_DIR" REQUESTED_AT="" HOLD_LOGGED_AT="" FAILED_SEEN_AT="" FAIL_SINCE="" SATURATED="" PREV_TOTAL="" PREV_IDLE="" log "started" while true; do PROBE_URL=http://127.0.0.1:7880/ INTERVAL_SEC=30 HARD_UNHEALTHY_AFTER_SEC=300 SOFT_UNHEALTHY_AFTER_SEC=600 CPU_SATURATED_PCT=90 WARMUP_SEC=900 BOOTSTRAP_DEADLINE_SEC=5400 BOOTSTRAP_FAILURE_COOLDOWN_SEC=600 REPLACE_RETRY_SEC=600 MASTER_ADDRS="" [ -f "$ENV_FILE" ] && . "$ENV_FILE" NOW=$(uptime_sec) cpu_busy if [ -f "$PAUSE_FILE" ] || [ -f "$DRAIN_PAUSE_FILE" ]; then FAIL_SINCE="" elif [ -f "$BOOTSTRAP_FAILED_MARKER" ]; then if [ -z "$FAILED_SEEN_AT" ]; then FAILED_SEEN_AT=$NOW log "bootstrap failed ($(head -c 300 "$BOOTSTRAP_FAILED_MARKER")): replacing this node in $BOOTSTRAP_FAILURE_COOLDOWN_SEC s" fi if [ $((NOW - FAILED_SEEN_AT)) -ge "$BOOTSTRAP_FAILURE_COOLDOWN_SEC" ]; then request_replacement "bootstrap failed: $(head -c 300 "$BOOTSTRAP_FAILED_MARKER")" fi elif [ ! -f "$INSTALLED_MARKER" ]; then if [ "$NOW" -ge "$BOOTSTRAP_DEADLINE_SEC" ]; then request_replacement "bootstrap not finished $BOOTSTRAP_DEADLINE_SEC s after boot" fi elif curl -sf -o /dev/null --connect-timeout 3 --max-time 15 "$PROBE_URL"; then [ -n "$FAIL_SINCE" ] && log "LiveKit healthy again after $((NOW - FAIL_SINCE)) s" FAIL_SINCE="" SATURATED="" else RC=$? case "$RC" in 7|52|56) KIND=hard ;; *) KIND=soft ;; esac if openvidu_starting; then FAIL_SINCE="" elif ! master_serving; then # A master outage fails the health check of every Media Node at once: not counted, # so LiveKit gets the whole window to reconnect once a master answers again FAIL_SINCE="" if [ -z "$HOLD_LOGGED_AT" ] || [ $((NOW - HOLD_LOGGED_AT)) -ge 600 ]; then log "LiveKit health check failed (curl exit $RC), but no master answers a Redis PING (MASTER_ADDRS='$MASTER_ADDRS'): not counted" HOLD_LOGGED_AT=$NOW fi elif [ "$KIND" = soft ] && [ "$BUSY" -ge "$CPU_SATURATED_PCT" ]; then [ -n "$SATURATED" ] || log "LiveKit not ready (curl exit $RC) with the CPU $BUSY% busy: not counted" SATURATED=1 FAIL_SINCE="" else SATURATED="" if [ -z "$FAIL_SINCE" ]; then FAIL_SINCE=$NOW log "LiveKit health check failed (curl exit $RC)" fi LIMIT=$SOFT_UNHEALTHY_AFTER_SEC [ "$KIND" = hard ] && LIMIT=$HARD_UNHEALTHY_AFTER_SEC if [ $((NOW - FAIL_SINCE)) -ge "$LIMIT" ]; then request_replacement "LiveKit unhealthy for $((NOW - FAIL_SINCE)) s (curl exit $RC)" fi fi fi sleep "$INTERVAL_SEC" done mode: "000755" owner: "root" group: "root" '/usr/local/bin/openvidu-media-replace.sh': content: !Sub | #!/bin/bash # Ask the Auto Scaling group to replace this Media Node with a new one, keeping the # count. Called by openvidu-media-health.sh (same cgroup): it never stops or signals # the watchdog, which re-requests until this node goes away. mkdir -p /var/lib/openvidu-media-health echo "$(date -u +%FT%TZ) $1" >> /var/lib/openvidu-media-health/replace-requests.log # Let stop_media_node.sh know this node is unhealthy, so it does not wait for sessions touch /run/openvidu-unhealthy # Withdrawn if the request fails: the node then stays, and if it recovers # a later scale-in must still wait for its sessions trap '[ $? -eq 0 ] || rm -f /run/openvidu-unhealthy' EXIT # 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-health Unhealthy makes the ASG terminate this node through the existing # lifecycle hook (drain) and launch a replacement. Idempotent; retry transient API errors. for attempt in $(seq 1 5); do if aws autoscaling set-instance-health \ --region ${AWS::Region} \ --instance-id "$INSTANCE_ID" \ --health-status Unhealthy; then exit 0 fi echo "[openvidu-media-replace] set-instance-health failed (attempt $attempt/5), retrying in 12s" >&2 sleep 12 done exit 1 mode: "000755" owner: "root" group: "root" '/etc/systemd/system/openvidu-media-health.service': content: | [Unit] Description=OpenVidu Media Node health watchdog After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/openvidu-media-health.sh Restart=always RestartSec=30 [Install] WantedBy=multi-user.target mode: "000644" owner: "root" group: "root" '/usr/local/bin/stop_media_node.sh': content: !Sub | #!/bin/bash set -e # Drain: pause and stop the health watchdog so it does not replace this node touch /run/openvidu-media-health.paused systemctl stop openvidu-media-health 2>/dev/null || true # 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 2>/dev/null || true docker container kill --signal=SIGQUIT ingress 2>/dev/null || true docker container kill --signal=SIGQUIT egress 2>/dev/null || true for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}' 2>/dev/null); do docker container kill --signal=SIGQUIT "$agent_container" 2>/dev/null || true done TIME_PASSED=0 HEARTBEAT_MAX=1800 # An unhealthy node being replaced should not wait for sessions: kill now if [ -f /run/openvidu-unhealthy ]; then docker ps -q | xargs -r docker kill >/dev/null 2>&1 || true fi # Wait for running containers to not be openvidu, ingress, egress or an openvidu agent while [ "$(docker ps --filter 'label=openvidu-agent=true' -q 2>/dev/null | 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: !If - IsMediaGraviton - !If - IsMediaNodeNvidia - '{{resolve:ssm:/aws/service/deeplearning/ami/arm64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id}}' - !FindInMap [ArmImage, !Ref OperatingSystem, ImageId] - !If - IsMediaNodeNvidia - '{{resolve:ssm:/aws/service/deeplearning/ami/x86_64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id}}' - !FindInMap [AmdImage, !Ref OperatingSystem, ImageId] KeyName: !Ref KeyName InstanceType: !Ref MediaNodeInstanceType UserData: Fn::Base64: !Sub | #!/bin/bash set -eu -o pipefail # Record a media-node bootstrap failure so the health watchdog (started below) # replaces this node after a cool-down. Route user-data failure exits through this. ov_media_bootstrap_failed() { echo "[OpenVidu] media node bootstrap failed: $1" mkdir -p /var/lib/openvidu-media-health echo "$1" > /var/lib/openvidu-media-health/bootstrap-failed exit 1 } echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout apt-get update && apt-get install -y \ python3-pip \ ec2-instance-connect \ curl \ unzip CFN_BOOTSTRAP_VERSION=2.0-39 # Detect Ubuntu version and install cfn-bootstrap accordingly UBUNTU_VERSION=$(lsb_release -rs | cut -d. -f1) if [ "$UBUNTU_VERSION" -ge 24 ]; then python3 -m pip install --break-system-packages https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz else python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz fi cfn-init --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMediaNodeLaunchTemplate export HOME="/root" # aws-cli v2, hoisted from install.sh so the watchdog's replace script can call # the ASG API even if bootstrap hangs before install.sh runs AWS_CLI_VERSION=2.36.40 if ! command -v aws &> /dev/null; then curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m)-${!AWS_CLI_VERSION}.zip" -o "awscliv2.zip" unzip -qq awscliv2.zip ./aws/install rm -rf awscliv2.zip aws fi # Start the health watchdog before install.sh so a hung bootstrap is covered too systemctl daemon-reload systemctl enable --now openvidu-media-health # Install OpenVidu /usr/local/bin/install.sh || ov_media_bootstrap_failed "install.sh failed" MASTER_IP_1=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-1-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_IP_2=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-2-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_IP_3=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-3-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') MASTER_IP_4=$(aws ssm get-parameter --region ${AWS::Region} --name "/openvidu/${AWS::StackName}/master-node-4-private-ip" --query Parameter.Value --output text 2>/dev/null || echo 'none') # Health watchdog config: master addresses for the anti-storm Redis PING guard mkdir -p /etc/openvidu MASTER_ADDRS="" for MASTER_IP in "$MASTER_IP_1" "$MASTER_IP_2" "$MASTER_IP_3" "$MASTER_IP_4"; do if [ "$MASTER_IP" != "none" ] && [ -n "$MASTER_IP" ]; then MASTER_ADDRS="$MASTER_ADDRS $MASTER_IP:7001" fi done echo "MASTER_ADDRS=\"$(echo $MASTER_ADDRS | xargs)\"" > /etc/openvidu/media-health.env MASTER_HEALTHY=false for i in $(seq 1 360); do for MASTER_IP in "$MASTER_IP_1" "$MASTER_IP_2" "$MASTER_IP_3" "$MASTER_IP_4"; do if [ "$MASTER_IP" != "none" ] && [ -n "$MASTER_IP" ] && curl -sf "http://$MASTER_IP:7880/health/caddy" >/dev/null 2>&1; then MASTER_HEALTHY=true break fi done if [ "$MASTER_HEALTHY" = "true" ]; then break fi sleep 5 done if [ "$MASTER_HEALTHY" != "true" ]; then ov_media_bootstrap_failed "no master node became healthy after 30 minutes" fi # Start OpenVidu systemctl start openvidu || ov_media_bootstrap_failed "error starting OpenVidu" # Bootstrap finished: switch the watchdog from the bootstrap deadline to runtime checks mkdir -p /var/lib/openvidu-media-health touch /var/lib/openvidu-media-health/installed # Wait for the app # /usr/local/bin/check_app_ready.sh BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeType: gp3 DeleteOnTermination: true VolumeSize: !If [IsMediaNodeNvidia, 100, 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: - !Ref OpenViduMediaNodeRTMPTG - !If [TurnTLSIsEnabled, !Ref OpenViduTurnMediaNodeTG, !Ref AWS::NoValue] - !If [TurnUDPIsEnabled, !Ref OpenViduTurnUDPMediaNodeTG, !Ref AWS::NoValue] 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-execution/* - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${StopMediaNodeAutomationDocument} 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 OpenViduLoadBalancerToMasterHTTPIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG 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 OpenViduMediaNodeToMasterNodeClusterPortIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 7880 ToPort: 7880 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG OpenViduMediaNodeToMasterRegistryMirrorIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 5555 ToPort: 5555 SourceSecurityGroupId: !Ref OpenViduMediaNodeSG 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 OpenViduMasterToMasterMeetIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 9080 ToPort: 9080 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMasterToMasterCustomAppIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMasterNodeSG IpProtocol: tcp FromPort: 6080 ToPort: 6080 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduMediaNodeToMasterMeetWebhookIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !GetAtt OpenViduMasterNodeSG.GroupId IpProtocol: tcp FromPort: 9080 ToPort: 9080 SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId OpenViduMediaNodeToMasterCustomAppIngress: 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: 1935 ToPort: 1935 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG OpenViduLoadBalancerToMediaNodeIngressHealthCheckSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 9092 ToPort: 9092 SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG 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 OpenViduTurnTLSMasterNodeToMediaNodeIngressSG: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 5349 ToPort: 5349 SourceSecurityGroupId: !Ref OpenViduMasterNodeSG OpenViduTurnTLSLoadBalancerToMediaNodeIngressSG: Type: AWS::EC2::SecurityGroupIngress 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: 1945 ToPort: 1945 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 1945 ToPort: 1945 CidrIpv6: ::/0 LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer 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: !If [HasLoadBalancerSubnets, !Ref LoadBalancerSubnets, !Ref OpenViduMasterNodeSubnets] SecurityGroups: - !Ref OpenViduLoadBalancerSG Type: network Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - Load Balancer OpenViduMasterNodeWithTurnTLSHTTPListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduMasterNodeHTTPTG LoadBalancerArn: !Ref LoadBalancer Port: 80 Protocol: TCP OpenViduMasterNodeWithTurnTLSListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - Type: forward TargetGroupArn: !If [TurnTLSIsEnabled, !Ref OpenViduMasterNodeTG, !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 # RTMPS on 1945: the port OpenVidu advertises in the RTMP Ingress URL behind a load balancer Port: 1945 Protocol: TLS Certificates: - CertificateArn: !Ref OpenViduCertificateARN OpenViduMasterNodeHTTPTG: 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 - '' - - OVHTTP- - !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: 80 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 HTTP Target Group OpenViduMasterNodeWithTurnTLSTG: 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 - '' - - 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 - '' - - OVRTMPS- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] VpcId: !Ref OpenViduVPC # RTMP port of the ingress service on the Media Nodes Port: 1935 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 # Master Node HTTP (Caddy 7880) for the main 443 listener when TurnDomainName is set. OpenViduMasterNodeTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: TurnTLSIsEnabled Properties: Name: Fn::Join: - '' - - 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 # Dedicated TURN Load Balancer (only when TurnDomainName + TurnCertificateARN are set). # Terminates TLS on 443 and forwards to the Media Nodes' TURN TLS port 5349, so TURN # over TLS is relayed by the Media Nodes themselves instead of going through the # Master Nodes. TurnLoadBalancerSG: Type: AWS::EC2::SecurityGroup Condition: TurnTLSIsEnabled Properties: GroupDescription: Security group for the dedicated TURN Load Balancer GroupName: !Sub openvidu-ha-turn-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 - !If - TurnUDPIsEnabled - IpProtocol: udp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - !Ref AWS::NoValue TurnLoadBalancerToMediaNode5349Ingress: Type: AWS::EC2::SecurityGroupIngress Condition: TurnTLSIsEnabled Properties: GroupId: !Ref OpenViduMediaNodeSG IpProtocol: tcp FromPort: 5349 ToPort: 5349 SourceSecurityGroupId: !Ref TurnLoadBalancerSG TurnLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Condition: TurnTLSIsEnabled Properties: Name: Fn::Join: - '' - - OpenViduHA-Turn- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] Subnets: !If [HasLoadBalancerSubnets, !Ref LoadBalancerSubnets, !Ref OpenViduMediaNodeSubnets] SecurityGroups: - !Ref TurnLoadBalancerSG Type: network Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN Load Balancer OpenViduTurnMediaNodeTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: TurnTLSIsEnabled Properties: Name: Fn::Join: - '' - - OVTurnMedia- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] VpcId: !Ref OpenViduVPC Port: 5349 Protocol: TCP HealthCheckProtocol: TCP HealthCheckPort: '5349' HealthCheckIntervalSeconds: 10 HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN Media Target Group TurnMediaNodeListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Condition: TurnTLSIsEnabled Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduTurnMediaNodeTG LoadBalancerArn: !Ref TurnLoadBalancer Port: 443 Protocol: TLS Certificates: - CertificateArn: !Ref TurnCertificateARN # TURN over UDP 443 (only with Media Nodes outside the Load Balancer subnets). A second Load Balancer, # because an NLB cannot have a UDP and a TLS listener on the same port. It keeps the static IP that # AWS assigns to it in each zone, and the Media Nodes announce the one of their zone to the clients. TurnUDPLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Condition: TurnUDPIsEnabled Properties: Name: Fn::Join: - '' - - OpenViduHA-TurnUDP- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] Scheme: internet-facing Subnets: !Ref LoadBalancerSubnets SecurityGroups: - !Ref TurnLoadBalancerSG LoadBalancerAttributes: - Key: load_balancing.cross_zone.enabled Value: 'true' Type: network Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN UDP Load Balancer OpenViduTurnUDPMediaNodeTG: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: TurnUDPIsEnabled Properties: Name: Fn::Join: - '' - - OVTurnUDP- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] VpcId: !Ref OpenViduVPC Port: 443 Protocol: UDP HealthCheckProtocol: TCP HealthCheckPort: '5349' HealthCheckIntervalSeconds: 10 HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN UDP Media Target Group TurnUDPMediaNodeListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Condition: TurnUDPIsEnabled Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduTurnUDPMediaNodeTG LoadBalancerArn: !Ref TurnUDPLoadBalancer Port: 443 Protocol: UDP 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} LoadBalancerDNS: Description: DNS of the main Load Balancer - point DomainName (A/ALIAS) here Value: !GetAtt LoadBalancer.DNSName TurnLoadBalancerDNS: Condition: TurnTLSIsEnabled Description: DNS of the dedicated TURN Load Balancer - point TurnDomainName (A/ALIAS) here Value: !GetAtt TurnLoadBalancer.DNSName TurnUDPLoadBalancerDNS: Condition: TurnUDPIsEnabled Description: DNS of the TURN UDP Load Balancer (no record needed; clients send media over UDP 443 to the IPs it resolves to) Value: !GetAtt TurnUDPLoadBalancer.DNSName