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: pion 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 NAT egress." Type: CommaDelimitedList 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 - 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], ""]] 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 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], } cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) except Exception as e: cfnresponse.send(event, context, cfnresponse.FAILED, {'Message': str(e)}) Runtime: python3.12 Timeout: 120 SubnetProcessor: Type: Custom::SubnetProcessor Properties: ServiceToken: !GetAtt SubnetProcessorFunction.Arn Subnets: !Ref OpenViduMasterNodeSubnets LambdaLogGroup: UpdateReplacePolicy: Retain DeletionPolicy: Delete Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}' RetentionInDays: 7 LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: LambdaLogsPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}:*' 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save 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 # sending the signal call cfn-signal -e $? --stack ${AWS::StackId} --resource "$SIGNAL_NAME" --region ${AWS::Region} 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 # Construct the final command FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" # Install OpenVidu exec bash -c "$FINAL_COMMAND" mode: '000755' owner: root group: root '/usr/local/bin/config_s3.sh': content: !Sub - | #!/bin/bash set -e # Install dir and config dir INSTALL_DIR="/opt/openvidu" CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" # Config S3 bucket EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com" EXTERNAL_S3_REGION="${AWS::Region}" EXTERNAL_S3_PATH_STYLE_ACCESS="false" EXTERNAL_S3_BUCKET_APP_DATA=${S3AppDataBucketResourceName} EXTERNAL_S3_BUCKET_CLUSTER_DATA=${S3ClusterDataBucketResourceName} sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s|EXTERNAL_S3_BUCKET_CLUSTER_DATA=.*|EXTERNAL_S3_BUCKET_CLUSTER_DATA=$EXTERNAL_S3_BUCKET_CLUSTER_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" - S3AppDataBucketResourceName: !If - CreateRecordingsBucket - !Ref S3AppDataBucketResource - !Ref S3AppDataBucketName S3ClusterDataBucketResourceName: !If - CreateClusterDataBucket - !Ref S3ClusterDataBucketResource - !Ref S3ClusterDataBucketName mode: "000755" owner: "root" group: "root" '/usr/local/bin/after_install.sh': content: !Sub | #!/bin/bash set -e # 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 export MASTER_NODE_NUM=1 # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition1" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # 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 # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition1: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: 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 export MASTER_NODE_NUM=2 # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition2" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # 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 # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab MasterNodesWaitCondition2: Type: 'AWS::CloudFormation::WaitCondition' CreationPolicy: ResourceSignal: Timeout: 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 export MASTER_NODE_NUM=3 # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition3" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # 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 # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab 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 export MASTER_NODE_NUM=4 # Install OpenVidu /usr/local/bin/install.sh "MasterNodesWaitCondition4" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } # Config S3 bucket /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } # 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 # Update shared secret /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } # Launch on reboot echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab 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: '/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') # 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" ) # Construct the final command with all arguments FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" # Install OpenVidu exec bash -c "$FINAL_COMMAND" mode: '000755' owner: root group: root '/usr/local/bin/set_as_unhealthy.sh': content: !Sub | #!/bin/bash set -e # Token for IMDSv2 TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") # Get own instance ID INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) # Set instance as unhealthy aws autoscaling set-instance-health \ --region ${AWS::Region} \ --instance-id "$INSTANCE_ID" \ --health-status Unhealthy mode: "000755" owner: "root" group: "root" '/usr/local/bin/stop_media_node.sh': content: !Sub | #!/bin/bash set -e # Token for IMDSv2 TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") # Get own instance ID INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) ASG_NAME=openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} # Execute if docker is installed if [ -x "$(command -v docker)" ]; then echo "Stopping media node services and waiting for termination..." docker container kill --signal=SIGQUIT openvidu 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 # 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 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 --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMediaNodeLaunchTemplate export HOME="/root" # Install OpenVidu /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } 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') 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 echo "[OpenVidu] no master node became healthy after 30 minutes" /usr/local/bin/set_as_unhealthy.sh exit 1 fi # Start OpenVidu systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } # Wait for the app # /usr/local/bin/check_app_ready.sh BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeType: gp3 DeleteOnTermination: true VolumeSize: !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 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 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: 1945 ToPort: 1945 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: 1935 ToPort: 1935 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 1935 ToPort: 1935 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: !Ref OpenViduMasterNodeWithTurnTLSTG LoadBalancerArn: !Ref LoadBalancer Port: 443 Protocol: TLS Certificates: - CertificateArn: !Ref OpenViduCertificateARN OpenViduRTMPMediaNodeListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref OpenViduMediaNodeRTMPTG LoadBalancerArn: !Ref LoadBalancer Port: 1935 Protocol: TLS Certificates: - CertificateArn: !Ref OpenViduCertificateARN 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 - '' - - OVRTMP- - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] VpcId: !Ref OpenViduVPC Port: 1945 Protocol: TCP Matcher: HttpCode: '200' HealthCheckIntervalSeconds: 10 HealthCheckPath: / HealthCheckProtocol: HTTP # Ingress health check port HealthCheckPort: '9092' HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 3 UnhealthyThresholdCount: 4 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: !Sub ${AWS::StackName} - OpenVidu HA - RTMP Target Group 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}