openvidu/openvidu-deployment/pro/ha/aws/cf-openvidu-ha.yaml

3814 lines
144 KiB
YAML

AWSTemplateFormatVersion: 2010-09-09
Description: OpenVidu Pro - High Availability
Parameters:
DomainName:
Type: String
Description: Domain name for the OpenVidu High Availability cluster
AllowedPattern: ^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
ConstraintDescription: The domain name does not have a valid domain name format
OpenViduCertificateARN:
Description: 'Amazon certificate arn resource to load into the LoadBalancer'
Type: String
AllowedPattern: '.+'
ConstraintDescription: The Load Balancer domain name must be defined
OpenViduLicense:
Description: "Visit https://openvidu.io/account"
Type: String
AllowedPattern: ^(?!\s*$).+$
NoEcho: true
ConstraintDescription: OpenVidu Pro License is mandatory
RTCEngine:
Description: "RTCEngine media engine to use"
Type: String
AllowedValues:
- pion
- mediasoup
Default: mediasoup
MasterNodeInstanceType:
Description: "Specifies the EC2 instance type for your OpenVidu Master Node"
Type: String
Default: c6a.xlarge
AllowedValues:
- t3.nano
- t3.micro
- t3.small
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- t3a.nano
- t3a.micro
- t3a.small
- t3a.medium
- t3a.large
- t3a.xlarge
- t3a.2xlarge
- t4g.nano
- t4g.micro
- t4g.small
- t4g.medium
- t4g.large
- t4g.xlarge
- t4g.2xlarge
- c5.large
- c5.xlarge
- c5.2xlarge
- c5.4xlarge
- c5.9xlarge
- c5.12xlarge
- c5.18xlarge
- c5.24xlarge
- c5.metal
- c5a.large
- c5a.xlarge
- c5a.2xlarge
- c5a.4xlarge
- c5a.8xlarge
- c5a.12xlarge
- c5a.16xlarge
- c5a.24xlarge
- c5ad.large
- c5ad.xlarge
- c5ad.2xlarge
- c5ad.4xlarge
- c5ad.8xlarge
- c5ad.12xlarge
- c5ad.16xlarge
- c5ad.24xlarge
- c5d.large
- c5d.xlarge
- c5d.2xlarge
- c5d.4xlarge
- c5d.9xlarge
- c5d.12xlarge
- c5d.18xlarge
- c5d.24xlarge
- c5d.metal
- c5n.large
- c5n.xlarge
- c5n.2xlarge
- c5n.4xlarge
- c5n.9xlarge
- c5n.18xlarge
- c5n.metal
- c6a.large
- c6a.xlarge
- c6a.2xlarge
- c6a.4xlarge
- c6a.8xlarge
- c6a.12xlarge
- c6a.16xlarge
- c6a.24xlarge
- c6a.32xlarge
- c6a.48xlarge
- c6a.metal
- c6g.medium
- c6g.large
- c6g.xlarge
- c6g.2xlarge
- c6g.4xlarge
- c6g.8xlarge
- c6g.12xlarge
- c6g.16xlarge
- c6g.metal
- c6gd.medium
- c6gd.large
- c6gd.xlarge
- c6gd.2xlarge
- c6gd.4xlarge
- c6gd.8xlarge
- c6gd.12xlarge
- c6gd.16xlarge
- c6gd.metal
- c6gn.medium
- c6gn.large
- c6gn.xlarge
- c6gn.2xlarge
- c6gn.4xlarge
- c6gn.8xlarge
- c6gn.12xlarge
- c6gn.16xlarge
- c6i.large
- c6i.xlarge
- c6i.2xlarge
- c6i.4xlarge
- c6i.8xlarge
- c6i.12xlarge
- c6i.16xlarge
- c6i.24xlarge
- c6i.32xlarge
- c6i.metal
- c6id.large
- c6id.xlarge
- c6id.2xlarge
- c6id.4xlarge
- c6id.8xlarge
- c6id.12xlarge
- c6id.16xlarge
- c6id.24xlarge
- c6id.32xlarge
- c6id.metal
- c6in.large
- c6in.xlarge
- c6in.2xlarge
- c6in.4xlarge
- c6in.8xlarge
- c6in.12xlarge
- c6in.16xlarge
- c6in.24xlarge
- c6in.32xlarge
- c6in.metal
- c7a.medium
- c7a.large
- c7a.xlarge
- c7a.2xlarge
- c7a.4xlarge
- c7a.8xlarge
- c7a.12xlarge
- c7a.16xlarge
- c7a.24xlarge
- c7a.32xlarge
- c7a.48xlarge
- c7a.metal-48xl
- c7g.medium
- c7g.large
- c7g.xlarge
- c7g.2xlarge
- c7g.4xlarge
- c7g.8xlarge
- c7g.12xlarge
- c7g.16xlarge
- c7g.metal
- c7gd.medium
- c7gd.large
- c7gd.xlarge
- c7gd.2xlarge
- c7gd.4xlarge
- c7gd.8xlarge
- c7gd.12xlarge
- c7gd.16xlarge
- c7gd.metal
- c7gn.medium
- c7gn.large
- c7gn.xlarge
- c7gn.2xlarge
- c7gn.4xlarge
- c7gn.8xlarge
- c7gn.12xlarge
- c7gn.16xlarge
- c7gn.metal
- c7i-flex.large
- c7i-flex.xlarge
- c7i-flex.2xlarge
- c7i-flex.4xlarge
- c7i-flex.8xlarge
- c7i-flex.12xlarge
- c7i-flex.16xlarge
- c7i.large
- c7i.xlarge
- c7i.2xlarge
- c7i.4xlarge
- c7i.8xlarge
- c7i.12xlarge
- c7i.16xlarge
- c7i.24xlarge
- c7i.48xlarge
- c7i.metal-24xl
- c7i.metal-48xl
- c8g.medium
- c8g.large
- c8g.xlarge
- c8g.2xlarge
- c8g.4xlarge
- c8g.8xlarge
- c8g.12xlarge
- c8g.16xlarge
- c8g.24xlarge
- c8g.48xlarge
- c8g.metal-24xl
- c8g.metal-48xl
- c8gd.medium
- c8gd.large
- c8gd.xlarge
- c8gd.2xlarge
- c8gd.4xlarge
- c8gd.8xlarge
- c8gd.12xlarge
- c8gd.16xlarge
- c8gd.24xlarge
- c8gd.48xlarge
- c8gd.metal-24xl
- c8gd.metal-48xl
- g4dn.xlarge
- g4dn.2xlarge
- g4dn.4xlarge
- g4dn.8xlarge
- g4dn.12xlarge
- g4dn.16xlarge
- g4dn.metal
- g5.xlarge
- g5.2xlarge
- g5.4xlarge
- g5.8xlarge
- g5.12xlarge
- g5.16xlarge
- g5.24xlarge
- g5.48xlarge
- g5g.xlarge
- g5g.2xlarge
- g5g.4xlarge
- g5g.8xlarge
- g5g.16xlarge
- g5g.metal
- g6.xlarge
- g6.2xlarge
- g6.4xlarge
- g6.8xlarge
- g6.12xlarge
- g6.16xlarge
- g6.24xlarge
- g6.48xlarge
- g6e.xlarge
- g6e.2xlarge
- g6e.4xlarge
- g6e.8xlarge
- g6e.12xlarge
- g6e.16xlarge
- g6e.24xlarge
- g6e.48xlarge
- g6f.large
- g6f.xlarge
- g6f.2xlarge
- g6f.4xlarge
- g7e.2xlarge
- g7e.4xlarge
- g7e.8xlarge
- g7e.12xlarge
- g7e.24xlarge
- g7e.48xlarge
- gr6.4xlarge
- gr6.8xlarge
- gr6f.4xlarge
- m6a.large
- m6a.xlarge
- m6a.2xlarge
- m6a.4xlarge
- m6a.8xlarge
- m6a.12xlarge
- m6a.16xlarge
- m6a.24xlarge
- m6a.32xlarge
- m6a.48xlarge
- m6a.metal
- m6g.medium
- m6g.large
- m6g.xlarge
- m6g.2xlarge
- m6g.4xlarge
- m6g.8xlarge
- m6g.12xlarge
- m6g.16xlarge
- m6g.metal
- m6gd.medium
- m6gd.large
- m6gd.xlarge
- m6gd.2xlarge
- m6gd.4xlarge
- m6gd.8xlarge
- m6gd.12xlarge
- m6gd.16xlarge
- m6gd.metal
- m6i.large
- m6i.xlarge
- m6i.2xlarge
- m6i.4xlarge
- m6i.8xlarge
- m6i.12xlarge
- m6i.16xlarge
- m6i.24xlarge
- m6i.32xlarge
- m6i.metal
- m6id.large
- m6id.xlarge
- m6id.2xlarge
- m6id.4xlarge
- m6id.8xlarge
- m6id.12xlarge
- m6id.16xlarge
- m6id.24xlarge
- m6id.32xlarge
- m6id.metal
- m6idn.large
- m6idn.xlarge
- m6idn.2xlarge
- m6idn.4xlarge
- m6idn.8xlarge
- m6idn.12xlarge
- m6idn.16xlarge
- m6idn.24xlarge
- m6idn.32xlarge
- m6idn.metal
- m6in.large
- m6in.xlarge
- m6in.2xlarge
- m6in.4xlarge
- m6in.8xlarge
- m6in.12xlarge
- m6in.16xlarge
- m6in.24xlarge
- m6in.32xlarge
- m6in.metal
- m7a.medium
- m7a.large
- m7a.xlarge
- m7a.2xlarge
- m7a.4xlarge
- m7a.8xlarge
- m7a.12xlarge
- m7a.16xlarge
- m7a.24xlarge
- m7a.32xlarge
- m7a.48xlarge
- m7a.metal-48xl
- m7g.medium
- m7g.large
- m7g.xlarge
- m7g.2xlarge
- m7g.4xlarge
- m7g.8xlarge
- m7g.12xlarge
- m7g.16xlarge
- m7g.metal
- m7gd.medium
- m7gd.large
- m7gd.xlarge
- m7gd.2xlarge
- m7gd.4xlarge
- m7gd.8xlarge
- m7gd.12xlarge
- m7gd.16xlarge
- m7gd.metal
- m7i-flex.large
- m7i-flex.xlarge
- m7i-flex.2xlarge
- m7i-flex.4xlarge
- m7i-flex.8xlarge
- m7i-flex.12xlarge
- m7i-flex.16xlarge
- m7i.large
- m7i.xlarge
- m7i.2xlarge
- m7i.4xlarge
- m7i.8xlarge
- m7i.12xlarge
- m7i.16xlarge
- m7i.24xlarge
- m7i.48xlarge
- m7i.metal-24xl
- m7i.metal-48xl
- m8g.medium
- m8g.large
- m8g.xlarge
- m8g.2xlarge
- m8g.4xlarge
- m8g.8xlarge
- m8g.12xlarge
- m8g.16xlarge
- m8g.24xlarge
- m8g.48xlarge
- m8g.metal-24xl
- m8g.metal-48xl
- m8gd.medium
- m8gd.large
- m8gd.xlarge
- m8gd.2xlarge
- m8gd.4xlarge
- m8gd.8xlarge
- m8gd.12xlarge
- m8gd.16xlarge
- m8gd.24xlarge
- m8gd.48xlarge
- m8gd.metal-24xl
- m8gd.metal-48xl
- p4d.24xlarge
- p4de.24xlarge
- p5.4xlarge
- p5.48xlarge
- p5e.48xlarge
- p5en.48xlarge
- p6-b200.48xlarge
- p6-b300.48xlarge
- p6e-gb200.36xlarge
- r6g.medium
- r6g.large
- r6g.xlarge
- r6g.2xlarge
- r6g.4xlarge
- r6g.8xlarge
- r6g.12xlarge
- r6g.16xlarge
- r6g.metal
- r6gd.medium
- r6gd.large
- r6gd.xlarge
- r6gd.2xlarge
- r6gd.4xlarge
- r6gd.8xlarge
- r6gd.12xlarge
- r6gd.16xlarge
- r6gd.metal
- r7g.medium
- r7g.large
- r7g.xlarge
- r7g.2xlarge
- r7g.4xlarge
- r7g.8xlarge
- r7g.12xlarge
- r7g.16xlarge
- r7g.metal
- r7gd.medium
- r7gd.large
- r7gd.xlarge
- r7gd.2xlarge
- r7gd.4xlarge
- r7gd.8xlarge
- r7gd.12xlarge
- r7gd.16xlarge
- r7gd.metal
- r8g.medium
- r8g.large
- r8g.xlarge
- r8g.2xlarge
- r8g.4xlarge
- r8g.8xlarge
- r8g.12xlarge
- r8g.16xlarge
- r8g.24xlarge
- r8g.48xlarge
- r8g.metal-24xl
- r8g.metal-48xl
ConstraintDescription: "Must be a valid EC2 instance type"
MediaNodeInstanceType:
Description: "Specifies the EC2 instance type for your OpenVidu Media Nodes"
Type: String
Default: c6a.xlarge
AllowedValues:
- t3.nano
- t3.micro
- t3.small
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- t3a.nano
- t3a.micro
- t3a.small
- t3a.medium
- t3a.large
- t3a.xlarge
- t3a.2xlarge
- t4g.nano
- t4g.micro
- t4g.small
- t4g.medium
- t4g.large
- t4g.xlarge
- t4g.2xlarge
- c5.large
- c5.xlarge
- c5.2xlarge
- c5.4xlarge
- c5.9xlarge
- c5.12xlarge
- c5.18xlarge
- c5.24xlarge
- c5.metal
- c5a.large
- c5a.xlarge
- c5a.2xlarge
- c5a.4xlarge
- c5a.8xlarge
- c5a.12xlarge
- c5a.16xlarge
- c5a.24xlarge
- c5ad.large
- c5ad.xlarge
- c5ad.2xlarge
- c5ad.4xlarge
- c5ad.8xlarge
- c5ad.12xlarge
- c5ad.16xlarge
- c5ad.24xlarge
- c5d.large
- c5d.xlarge
- c5d.2xlarge
- c5d.4xlarge
- c5d.9xlarge
- c5d.12xlarge
- c5d.18xlarge
- c5d.24xlarge
- c5d.metal
- c5n.large
- c5n.xlarge
- c5n.2xlarge
- c5n.4xlarge
- c5n.9xlarge
- c5n.18xlarge
- c5n.metal
- c6a.large
- c6a.xlarge
- c6a.2xlarge
- c6a.4xlarge
- c6a.8xlarge
- c6a.12xlarge
- c6a.16xlarge
- c6a.24xlarge
- c6a.32xlarge
- c6a.48xlarge
- c6a.metal
- c6g.medium
- c6g.large
- c6g.xlarge
- c6g.2xlarge
- c6g.4xlarge
- c6g.8xlarge
- c6g.12xlarge
- c6g.16xlarge
- c6g.metal
- c6gd.medium
- c6gd.large
- c6gd.xlarge
- c6gd.2xlarge
- c6gd.4xlarge
- c6gd.8xlarge
- c6gd.12xlarge
- c6gd.16xlarge
- c6gd.metal
- c6gn.medium
- c6gn.large
- c6gn.xlarge
- c6gn.2xlarge
- c6gn.4xlarge
- c6gn.8xlarge
- c6gn.12xlarge
- c6gn.16xlarge
- c6i.large
- c6i.xlarge
- c6i.2xlarge
- c6i.4xlarge
- c6i.8xlarge
- c6i.12xlarge
- c6i.16xlarge
- c6i.24xlarge
- c6i.32xlarge
- c6i.metal
- c6id.large
- c6id.xlarge
- c6id.2xlarge
- c6id.4xlarge
- c6id.8xlarge
- c6id.12xlarge
- c6id.16xlarge
- c6id.24xlarge
- c6id.32xlarge
- c6id.metal
- c6in.large
- c6in.xlarge
- c6in.2xlarge
- c6in.4xlarge
- c6in.8xlarge
- c6in.12xlarge
- c6in.16xlarge
- c6in.24xlarge
- c6in.32xlarge
- c6in.metal
- c7a.medium
- c7a.large
- c7a.xlarge
- c7a.2xlarge
- c7a.4xlarge
- c7a.8xlarge
- c7a.12xlarge
- c7a.16xlarge
- c7a.24xlarge
- c7a.32xlarge
- c7a.48xlarge
- c7a.metal-48xl
- c7g.medium
- c7g.large
- c7g.xlarge
- c7g.2xlarge
- c7g.4xlarge
- c7g.8xlarge
- c7g.12xlarge
- c7g.16xlarge
- c7g.metal
- c7gd.medium
- c7gd.large
- c7gd.xlarge
- c7gd.2xlarge
- c7gd.4xlarge
- c7gd.8xlarge
- c7gd.12xlarge
- c7gd.16xlarge
- c7gd.metal
- c7gn.medium
- c7gn.large
- c7gn.xlarge
- c7gn.2xlarge
- c7gn.4xlarge
- c7gn.8xlarge
- c7gn.12xlarge
- c7gn.16xlarge
- c7gn.metal
- c7i-flex.large
- c7i-flex.xlarge
- c7i-flex.2xlarge
- c7i-flex.4xlarge
- c7i-flex.8xlarge
- c7i-flex.12xlarge
- c7i-flex.16xlarge
- c7i.large
- c7i.xlarge
- c7i.2xlarge
- c7i.4xlarge
- c7i.8xlarge
- c7i.12xlarge
- c7i.16xlarge
- c7i.24xlarge
- c7i.48xlarge
- c7i.metal-24xl
- c7i.metal-48xl
- c8g.medium
- c8g.large
- c8g.xlarge
- c8g.2xlarge
- c8g.4xlarge
- c8g.8xlarge
- c8g.12xlarge
- c8g.16xlarge
- c8g.24xlarge
- c8g.48xlarge
- c8g.metal-24xl
- c8g.metal-48xl
- c8gd.medium
- c8gd.large
- c8gd.xlarge
- c8gd.2xlarge
- c8gd.4xlarge
- c8gd.8xlarge
- c8gd.12xlarge
- c8gd.16xlarge
- c8gd.24xlarge
- c8gd.48xlarge
- c8gd.metal-24xl
- c8gd.metal-48xl
- g4dn.xlarge
- g4dn.2xlarge
- g4dn.4xlarge
- g4dn.8xlarge
- g4dn.12xlarge
- g4dn.16xlarge
- g4dn.metal
- g5.xlarge
- g5.2xlarge
- g5.4xlarge
- g5.8xlarge
- g5.12xlarge
- g5.16xlarge
- g5.24xlarge
- g5.48xlarge
- g5g.xlarge
- g5g.2xlarge
- g5g.4xlarge
- g5g.8xlarge
- g5g.16xlarge
- g5g.metal
- g6.xlarge
- g6.2xlarge
- g6.4xlarge
- g6.8xlarge
- g6.12xlarge
- g6.16xlarge
- g6.24xlarge
- g6.48xlarge
- g6e.xlarge
- g6e.2xlarge
- g6e.4xlarge
- g6e.8xlarge
- g6e.12xlarge
- g6e.16xlarge
- g6e.24xlarge
- g6e.48xlarge
- g6f.large
- g6f.xlarge
- g6f.2xlarge
- g6f.4xlarge
- g7e.2xlarge
- g7e.4xlarge
- g7e.8xlarge
- g7e.12xlarge
- g7e.24xlarge
- g7e.48xlarge
- gr6.4xlarge
- gr6.8xlarge
- gr6f.4xlarge
- m6a.large
- m6a.xlarge
- m6a.2xlarge
- m6a.4xlarge
- m6a.8xlarge
- m6a.12xlarge
- m6a.16xlarge
- m6a.24xlarge
- m6a.32xlarge
- m6a.48xlarge
- m6a.metal
- m6g.medium
- m6g.large
- m6g.xlarge
- m6g.2xlarge
- m6g.4xlarge
- m6g.8xlarge
- m6g.12xlarge
- m6g.16xlarge
- m6g.metal
- m6gd.medium
- m6gd.large
- m6gd.xlarge
- m6gd.2xlarge
- m6gd.4xlarge
- m6gd.8xlarge
- m6gd.12xlarge
- m6gd.16xlarge
- m6gd.metal
- m6i.large
- m6i.xlarge
- m6i.2xlarge
- m6i.4xlarge
- m6i.8xlarge
- m6i.12xlarge
- m6i.16xlarge
- m6i.24xlarge
- m6i.32xlarge
- m6i.metal
- m6id.large
- m6id.xlarge
- m6id.2xlarge
- m6id.4xlarge
- m6id.8xlarge
- m6id.12xlarge
- m6id.16xlarge
- m6id.24xlarge
- m6id.32xlarge
- m6id.metal
- m6idn.large
- m6idn.xlarge
- m6idn.2xlarge
- m6idn.4xlarge
- m6idn.8xlarge
- m6idn.12xlarge
- m6idn.16xlarge
- m6idn.24xlarge
- m6idn.32xlarge
- m6idn.metal
- m6in.large
- m6in.xlarge
- m6in.2xlarge
- m6in.4xlarge
- m6in.8xlarge
- m6in.12xlarge
- m6in.16xlarge
- m6in.24xlarge
- m6in.32xlarge
- m6in.metal
- m7a.medium
- m7a.large
- m7a.xlarge
- m7a.2xlarge
- m7a.4xlarge
- m7a.8xlarge
- m7a.12xlarge
- m7a.16xlarge
- m7a.24xlarge
- m7a.32xlarge
- m7a.48xlarge
- m7a.metal-48xl
- m7g.medium
- m7g.large
- m7g.xlarge
- m7g.2xlarge
- m7g.4xlarge
- m7g.8xlarge
- m7g.12xlarge
- m7g.16xlarge
- m7g.metal
- m7gd.medium
- m7gd.large
- m7gd.xlarge
- m7gd.2xlarge
- m7gd.4xlarge
- m7gd.8xlarge
- m7gd.12xlarge
- m7gd.16xlarge
- m7gd.metal
- m7i-flex.large
- m7i-flex.xlarge
- m7i-flex.2xlarge
- m7i-flex.4xlarge
- m7i-flex.8xlarge
- m7i-flex.12xlarge
- m7i-flex.16xlarge
- m7i.large
- m7i.xlarge
- m7i.2xlarge
- m7i.4xlarge
- m7i.8xlarge
- m7i.12xlarge
- m7i.16xlarge
- m7i.24xlarge
- m7i.48xlarge
- m7i.metal-24xl
- m7i.metal-48xl
- m8g.medium
- m8g.large
- m8g.xlarge
- m8g.2xlarge
- m8g.4xlarge
- m8g.8xlarge
- m8g.12xlarge
- m8g.16xlarge
- m8g.24xlarge
- m8g.48xlarge
- m8g.metal-24xl
- m8g.metal-48xl
- m8gd.medium
- m8gd.large
- m8gd.xlarge
- m8gd.2xlarge
- m8gd.4xlarge
- m8gd.8xlarge
- m8gd.12xlarge
- m8gd.16xlarge
- m8gd.24xlarge
- m8gd.48xlarge
- m8gd.metal-24xl
- m8gd.metal-48xl
- p4d.24xlarge
- p4de.24xlarge
- p5.4xlarge
- p5.48xlarge
- p5e.48xlarge
- p5en.48xlarge
- p6-b200.48xlarge
- p6-b300.48xlarge
- p6e-gb200.36xlarge
- r6g.medium
- r6g.large
- r6g.xlarge
- r6g.2xlarge
- r6g.4xlarge
- r6g.8xlarge
- r6g.12xlarge
- r6g.16xlarge
- r6g.metal
- r6gd.medium
- r6gd.large
- r6gd.xlarge
- r6gd.2xlarge
- r6gd.4xlarge
- r6gd.8xlarge
- r6gd.12xlarge
- r6gd.16xlarge
- r6gd.metal
- r7g.medium
- r7g.large
- r7g.xlarge
- r7g.2xlarge
- r7g.4xlarge
- r7g.8xlarge
- r7g.12xlarge
- r7g.16xlarge
- r7g.metal
- r7gd.medium
- r7gd.large
- r7gd.xlarge
- r7gd.2xlarge
- r7gd.4xlarge
- r7gd.8xlarge
- r7gd.12xlarge
- r7gd.16xlarge
- r7gd.metal
- r8g.medium
- r8g.large
- r8g.xlarge
- r8g.2xlarge
- r8g.4xlarge
- r8g.8xlarge
- r8g.12xlarge
- r8g.16xlarge
- r8g.24xlarge
- r8g.48xlarge
- r8g.metal-24xl
- r8g.metal-48xl
ConstraintDescription: "Must be a valid EC2 instance type"
InitialMeetAdminPassword:
Description: 'Initial password for the "admin" user in OpenVidu Meet. If not provided, a random password will be generated.'
Type: String
NoEcho: true
Default: ''
# Only allow alphanumeric characters
AllowedPattern: '^[A-Za-z0-9_-]*$'
ConstraintDescription: 'Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to generate a random password.'
InitialMeetApiKey:
Description: 'Initial API key for OpenVidu Meet. If not provided, no API key will be set and the user can set it later from Meet Console.'
Type: String
NoEcho: true
Default: ''
# Only allow alphanumeric characters
AllowedPattern: '^[A-Za-z0-9_-]*$'
ConstraintDescription: 'Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to not set an initial API key.'
KeyName:
Type: AWS::EC2::KeyPair::KeyName
Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
AllowedPattern: ^.+$
ConstraintDescription: must be the name of an existing EC2 KeyPair.
OperatingSystem:
Description: EC2 Operating System
Type: String
Default: "Ubuntu-24"
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
InitialNumberOfMediaNodes:
Type: Number
Default: 1
Description: Number of initial media nodes to deploy
MinNumberOfMediaNodes:
Type: Number
Default: 1
Description: Minimum number of media nodes to deploy
MaxNumberOfMediaNodes:
Type: Number
Default: 5
Description: Maximum number of media nodes to deploy
ScaleTargetCPU:
Type: Number
Default: 50
Description: Target CPU percentage to scale up or down
S3AppDataBucketName:
Type: String
Description: Name of the S3 bucket to store data and recordings. If empty, a bucket will be created
S3ClusterDataBucketName:
Type: String
Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
OpenViduVPC:
Description: "Dedicated VPC for OpenVidu cluster"
Type: AWS::EC2::VPC::Id
AllowedPattern: ^.+$
ConstraintDescription: You must specify a VPC ID
OpenViduMasterNodeSubnets:
Description: "Subnets for OpenVidu Master Node"
Type: List<AWS::EC2::Subnet::Id>
AllowedPattern: ^.+$
ConstraintDescription: You must specify a list of subnet IDs
OpenViduMediaNodeSubnets:
Description: "Subnets for OpenVidu Media Nodes"
Type: List<AWS::EC2::Subnet::Id>
AllowedPattern: ^.+$
ConstraintDescription: You must specify a list of subnet IDs
LoadBalancerSubnets:
Description: "(Optional) Public subnets for the internet-facing Load Balancer. Leave empty to place the Load Balancer in the OpenViduMasterNodeSubnets (default behavior). Set it to dedicated public subnets to keep the Master Nodes in private subnets with outbound internet access (for example, a NAT gateway)."
Type: CommaDelimitedList
Default: ""
TurnDomainName:
Description: "(Optional) Domain for TURN over TLS, served by a dedicated TURN Load Balancer that forwards it directly to the Media Nodes instead of through the Master Nodes. Set it together with TurnCertificateARN and point it (A/ALIAS) to the TurnLoadBalancerDNS output."
Type: String
Default: ""
TurnCertificateARN:
Description: "(Optional) ACM certificate ARN for TurnDomainName, used on the TURN Load Balancer 443 TLS listener."
Type: String
Default: ""
MasterNodesDiskSize:
Description: Size of the disk in GB
Type: Number
Default: 100
MinValue: 50
ConstraintDescription: The disk size must be at least 50 GB
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: Domain and Load Blancer configuration
Parameters:
- DomainName
- OpenViduCertificateARN
- TurnDomainName
- TurnCertificateARN
- Label:
default: OpenVidu High Availability configuration
Parameters:
- OpenViduLicense
- RTCEngine
- Label:
default: OpenVidu Meet configuration
Parameters:
- InitialMeetAdminPassword
- InitialMeetApiKey
- Label:
default: EC2 Instance configuration
Parameters:
- MasterNodeInstanceType
- MediaNodeInstanceType
- KeyName
- OperatingSystem
- Label:
default: Media Nodes Autoscaling Group configuration
Parameters:
- InitialNumberOfMediaNodes
- MinNumberOfMediaNodes
- MaxNumberOfMediaNodes
- ScaleTargetCPU
- Label:
default: S3 bucket for application data, cluster data and recordings
Parameters:
- S3AppDataBucketName
- S3ClusterDataBucketName
- Label:
default: VPC configuration
Parameters:
- OpenViduVPC
- OpenViduMasterNodeSubnets
- OpenViduMediaNodeSubnets
- LoadBalancerSubnets
- Label:
default: Volumes configuration
Parameters:
- MasterNodesDiskSize
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
Conditions:
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""]
HasLoadBalancerSubnets: !Not [!Equals [!Join ["", !Ref LoadBalancerSubnets], ""]]
TurnTLSIsEnabled: !And [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]]
IsMasterGraviton: !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 't4g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6gn']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7gn']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c8g']
- !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c8gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm6g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm6gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm7g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm7gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm8g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm8gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r6g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r6gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r7g']
- !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r7gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'r8g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g5g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6e-gb200']
IsMediaGraviton: !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 't4g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6gn']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7gn']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c8g']
- !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c8gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm6g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm6gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm7g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm7gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm8g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm8gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r6g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r6gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r7g']
- !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r7gd']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'r8g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g5g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6e-gb200']
IsMasterNodeNvidia: !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g4dn']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g5']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g5g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g6']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g6e']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g6f']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'gr6']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'gr6f']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'g7e']
- !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p4d']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p4de']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p5']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p5e']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p5en']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6-b200']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6-b300']
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'p6e-gb200']
IsMediaNodeNvidia: !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g4dn']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g5']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g5g']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g6']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g6e']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g6f']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'gr6']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'gr6f']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'g7e']
- !Or
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p4d']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p4de']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p5']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p5e']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p5en']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6-b200']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6-b300']
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'p6e-gb200']
Mappings:
ArmImage:
# aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/" --recursive --query "Parameters[*].Name" > canonical-ami.txt
Ubuntu-22:
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}'
Ubuntu-24:
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/arm64/hvm/ebs-gp3/ami-id}}'
AmdImage:
Ubuntu-22:
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id}}'
Ubuntu-24:
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id}}'
Resources:
OpenViduSharedInfo:
Type: AWS::SecretsManager::Secret
UpdateReplacePolicy: Retain
DeletionPolicy: Delete
Properties:
Name: !Sub openvidu-ha-${AWS::Region}-${AWS::StackName}
Description: Secret for OpenVidu High Availability to store deployment info and seed secret
# All the values are initialized by one master node and shared with the rest of the nodes
SecretString: |
{
"OPENVIDU_URL": "none",
"MEET_INITIAL_ADMIN_USER": "none",
"MEET_INITIAL_ADMIN_PASSWORD": "none",
"MEET_INITIAL_API_KEY": "none",
"LIVEKIT_URL": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"DASHBOARD_URL": "none",
"GRAFANA_URL": "none",
"MINIO_URL": "none",
"DOMAIN_NAME": "none",
"OPENVIDU_PRO_LICENSE": "none",
"OPENVIDU_RTC_ENGINE": "none",
"REDIS_PASSWORD": "none",
"MONGO_ADMIN_USERNAME": "none",
"MONGO_ADMIN_PASSWORD": "none",
"MONGO_REPLICA_SET_KEY": "none",
"MINIO_ACCESS_KEY": "none",
"MINIO_SECRET_KEY": "none",
"DASHBOARD_ADMIN_USERNAME": "none",
"DASHBOARD_ADMIN_PASSWORD": "none",
"GRAFANA_ADMIN_USERNAME": "none",
"GRAFANA_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none",
"OPENVIDU_VERSION": "none",
"ALL_SECRETS_GENERATED": "false"
}
# One SSM parameter per master: atomic per-key writes
MasterNode1PrivateIpParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub /openvidu/${AWS::StackName}/master-node-1-private-ip
Type: String
Value: "none"
MasterNode2PrivateIpParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub /openvidu/${AWS::StackName}/master-node-2-private-ip
Type: String
Value: "none"
MasterNode3PrivateIpParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub /openvidu/${AWS::StackName}/master-node-3-private-ip
Type: String
Value: "none"
MasterNode4PrivateIpParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub /openvidu/${AWS::StackName}/master-node-4-private-ip
Type: String
Value: "none"
S3AppDataBucketResource:
Type: 'AWS::S3::Bucket'
Properties:
### Unique bucket name using Stack ID
BucketName: !Join ["-" , [ 'openvidu-appdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls : true
RestrictPublicBuckets: true
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Condition: CreateRecordingsBucket
S3ClusterDataBucketResource:
Type: 'AWS::S3::Bucket'
Properties:
### Unique bucket name using Stack ID
BucketName: !Join ["-" , [ 'openvidu-clusterdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls : true
RestrictPublicBuckets: true
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Condition: CreateClusterDataBucket
# -------------------------
# Normalize the master node subnet list to exactly 4 entries, one per OpenVidu Master Node
# -------------------------
SubnetProcessorFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub 'SubnetProcessor-${AWS::Region}-${AWS::StackName}'
Handler: index.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
import boto3
import cfnresponse
def lambda_handler(event, context):
try:
# Process event data
subnets = event['ResourceProperties']['Subnets']
# Ensure we have at least four subnets by cycling through the available subnets
subnets = (subnets * 4)[:4] # Repeat the list to have at least 4 elements and then take the first 4
# Prepare the response
responseData = {
'Subnet1': subnets[0],
'Subnet2': subnets[1],
'Subnet3': subnets[2],
'Subnet4': subnets[3],
}
# Media Nodes are private when their subnets do not assign public IPs
media_subnets = event['ResourceProperties'].get('MediaSubnets', [])
media_private = 'false'
if event['RequestType'] != 'Delete' and media_subnets:
described = boto3.client('ec2').describe_subnets(SubnetIds=media_subnets)['Subnets']
if any(not s.get('MapPublicIpOnLaunch', False) for s in described):
media_private = 'true'
responseData['MediaNodesPrivate'] = media_private
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
except Exception as e:
cfnresponse.send(event, context, cfnresponse.FAILED, {'Message': str(e)})
Runtime: python3.12
Timeout: 120
SubnetProcessor:
Type: Custom::SubnetProcessor
Properties:
ServiceToken: !GetAtt SubnetProcessorFunction.Arn
Subnets: !Ref OpenViduMasterNodeSubnets
MediaSubnets: !Ref OpenViduMediaNodeSubnets
LambdaLogGroup:
UpdateReplacePolicy: Retain
DeletionPolicy: Delete
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}'
RetentionInDays: 7
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: LambdaLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}:*'
- Effect: Allow
Action:
- ec2:DescribeSubnets
Resource: '*'
OpenViduMasterNodeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- 'sts:AssumeRole'
Path: "/"
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: !Sub openvidu-master-policy-${AWS::Region}-${AWS::StackName}
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- autoscaling:SetInstanceHealth
Resource: '*'
Condition:
StringEquals:
'aws:ResourceTag/aws:cloudformation:stack-id': !Ref 'AWS::StackId'
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- secretsmanager:UpdateSecret
Resource: !Ref OpenViduSharedInfo
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:PutParameter
Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/openvidu/${AWS::StackName}/*
- Fn::If:
- CreateRecordingsBucket
- Effect: Allow
Action:
- s3:DeleteObject
- s3:GetObject
- s3:PutObject
Resource: !Sub ${S3AppDataBucketResource.Arn}/*
- Effect: Allow
Action:
- s3:DeleteObject
- s3:GetObject
- s3:PutObject
Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/*
- Fn::If:
- CreateRecordingsBucket
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !GetAtt S3AppDataBucketResource.Arn
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}
- Fn::If:
- CreateClusterDataBucket
- Effect: Allow
Action:
- s3:DeleteObject
- s3:GetObject
- s3:PutObject
Resource: !Sub ${S3ClusterDataBucketResource.Arn}/*
- Effect: Allow
Action:
- s3:DeleteObject
- s3:GetObject
- s3:PutObject
Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName}/*
- Fn::If:
- CreateClusterDataBucket
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !GetAtt S3ClusterDataBucketResource.Arn
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName}
RoleName:
Fn::Join:
# Generate a not too long and unique role name
# Getting a unique identifier from the stack id
- ''
- - openvidu-master-role-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
OpenViduMediaNodeRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- 'sts:AssumeRole'
Path: /
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: !Sub openvidu-media-policy-${AWS::Region}-${AWS::StackName}
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref OpenViduSharedInfo
- Effect: Allow
Action:
- ssm:GetParameter
Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/openvidu/${AWS::StackName}/*
- Effect: Allow
Action:
- autoscaling:SetInstanceHealth
- autoscaling:CompleteLifecycleAction
- autoscaling:RecordLifecycleActionHeartbeat
Resource: '*'
Condition:
StringEquals:
'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName'
- Fn::If:
- CreateRecordingsBucket
- Effect: Allow
Action:
- s3:DeleteObject
- s3:GetObject
- s3:PutObject
Resource: !Sub ${S3AppDataBucketResource.Arn}/*
- Effect: Allow
Action:
- s3:DeleteObject
- s3:GetObject
- s3:PutObject
Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/*
- Fn::If:
- CreateRecordingsBucket
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !GetAtt S3AppDataBucketResource.Arn
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}
RoleName:
Fn::Join:
# Generate a not too long and unique role name
# Getting a unique identifier from the stack id
- ''
- - openvidu-media-role-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
OpenViduMasterInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub OpenViduMasterInstanceProfile-${AWS::Region}-${AWS::StackName}
Roles:
- !Ref OpenViduMasterNodeRole
OpenViduMediaInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub OpenViduMediaInstanceProfile-${AWS::Region}-${AWS::StackName}
Roles:
- !Ref OpenViduMediaNodeRole
OpenViduMasterLaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Metadata:
Comment: Launch template for OpenVidu Master Node
AWS::CloudFormation::Init:
config:
files:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
set -e
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.53.6
# Install dependencies
apt-get update && apt-get install -y \
curl \
unzip \
jq \
wget
wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_$(dpkg --print-architecture).tar.gz -O - |\
tar xz && mv yq_linux_$(dpkg --print-architecture) /usr/bin/yq
AWS_CLI_VERSION=2.36.40
# Install aws-cli if not already installed
if ! command -v aws &> /dev/null; then
curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m)-${!AWS_CLI_VERSION}.zip" -o "awscliv2.zip"
unzip -qq awscliv2.zip
./aws/install
rm -rf awscliv2.zip aws
fi
# Signal to notify instance is waiting
SIGNAL_NAME="$1"
# Exported earlier in this instance's UserData
MASTER_NODE_NUM="${!MASTER_NODE_NUM}"
# Token for IMDSv2
TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")"
# Get own private IP
PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)"
if [[ "$PRIVATE_IP" == "" ]]; then
echo "Error: Private IP not found"
exit 1
fi
aws ssm put-parameter \
--region ${AWS::Region} \
--name "/openvidu/${AWS::StackName}/master-node-${!MASTER_NODE_NUM}-private-ip" \
--value "${!PRIVATE_IP}" \
--type String \
--overwrite > /dev/null
SHARED_SECRET=$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--query SecretString --output text)
ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED')
# If the private IP is the same as the first master node, generate the secrets
if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "false" ]]; then
DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN_NAME "${DomainName}")"
# Publish access URLs early so other nodes can read them during phase 2
# (idempotent: after_install rewrites these same byte-identical values later)
/usr/local/bin/store_secret.sh save OPENVIDU_URL "https://${!DOMAIN}/"
/usr/local/bin/store_secret.sh save LIVEKIT_URL "wss://${!DOMAIN}/"
/usr/local/bin/store_secret.sh save DASHBOARD_URL "https://${!DOMAIN}/dashboard/"
/usr/local/bin/store_secret.sh save GRAFANA_URL "https://${!DOMAIN}/grafana/"
/usr/local/bin/store_secret.sh save MINIO_URL "https://${!DOMAIN}/minio-console/"
OPENVIDU_PRO_LICENSE="$(/usr/local/bin/store_secret.sh save OPENVIDU_PRO_LICENSE "${OpenViduLicense}")"
OPENVIDU_RTC_ENGINE="$(/usr/local/bin/store_secret.sh save OPENVIDU_RTC_ENGINE "${RTCEngine}")"
# Store version so media nodes can use it to install the same version
/usr/local/bin/store_secret.sh save OPENVIDU_VERSION "${!OPENVIDU_VERSION}"
# Meet initial admin user and password
MEET_INITIAL_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_ADMIN_USER "admin")"
if [[ "${InitialMeetAdminPassword}" != '' ]]; then
MEET_INITIAL_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_ADMIN_PASSWORD "${InitialMeetAdminPassword}")"
else
MEET_INITIAL_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MEET_INITIAL_ADMIN_PASSWORD)"
fi
if [[ "${InitialMeetApiKey}" != '' ]]; then
MEET_INITIAL_API_KEY="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_API_KEY "${InitialMeetApiKey}")"
else
MEET_INITIAL_API_KEY="$(/usr/local/bin/store_secret.sh save MEET_INITIAL_API_KEY "")"
fi
# Store usernames and generate random passwords
REDIS_PASSWORD="$(/usr/local/bin/store_secret.sh generate REDIS_PASSWORD)"
MONGO_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save MONGO_ADMIN_USERNAME "mongoadmin")"
MONGO_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MONGO_ADMIN_PASSWORD)"
MONGO_REPLICA_SET_KEY="$(/usr/local/bin/store_secret.sh generate MONGO_REPLICA_SET_KEY)"
MINIO_ACCESS_KEY="$(/usr/local/bin/store_secret.sh save MINIO_ACCESS_KEY "minioadmin")"
MINIO_SECRET_KEY="$(/usr/local/bin/store_secret.sh generate MINIO_SECRET_KEY)"
DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD_ADMIN_USERNAME "dashboardadmin")"
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")"
# Every store_secret.sh call above is a read-modify-write of the whole JSON, and a read
# right after a write can still return the previous version (eventual consistency): the
# key written a moment earlier is then silently dropped (seen live: LIVEKIT_API_KEY back
# to "none", every master waiting 5 minutes on an incomplete secret and failing). So
# write the complete set once more from the values above, together with
# ALL_SECRETS_GENERATED, and read it back until all of it is really there.
SECRETS_WANTED="$(jq -n \
--arg DOMAIN_NAME "${!DOMAIN}" \
--arg OPENVIDU_URL "https://${!DOMAIN}/" \
--arg LIVEKIT_URL "wss://${!DOMAIN}/" \
--arg DASHBOARD_URL "https://${!DOMAIN}/dashboard/" \
--arg GRAFANA_URL "https://${!DOMAIN}/grafana/" \
--arg MINIO_URL "https://${!DOMAIN}/minio-console/" \
--arg OPENVIDU_PRO_LICENSE "${!OPENVIDU_PRO_LICENSE}" \
--arg OPENVIDU_RTC_ENGINE "${!OPENVIDU_RTC_ENGINE}" \
--arg OPENVIDU_VERSION "${!OPENVIDU_VERSION}" \
--arg MEET_INITIAL_ADMIN_USER "${!MEET_INITIAL_ADMIN_USER}" \
--arg MEET_INITIAL_ADMIN_PASSWORD "${!MEET_INITIAL_ADMIN_PASSWORD}" \
--arg MEET_INITIAL_API_KEY "${!MEET_INITIAL_API_KEY}" \
--arg REDIS_PASSWORD "${!REDIS_PASSWORD}" \
--arg MONGO_ADMIN_USERNAME "${!MONGO_ADMIN_USERNAME}" \
--arg MONGO_ADMIN_PASSWORD "${!MONGO_ADMIN_PASSWORD}" \
--arg MONGO_REPLICA_SET_KEY "${!MONGO_REPLICA_SET_KEY}" \
--arg MINIO_ACCESS_KEY "${!MINIO_ACCESS_KEY}" \
--arg MINIO_SECRET_KEY "${!MINIO_SECRET_KEY}" \
--arg DASHBOARD_ADMIN_USERNAME "${!DASHBOARD_ADMIN_USERNAME}" \
--arg DASHBOARD_ADMIN_PASSWORD "${!DASHBOARD_ADMIN_PASSWORD}" \
--arg GRAFANA_ADMIN_USERNAME "${!GRAFANA_ADMIN_USERNAME}" \
--arg GRAFANA_ADMIN_PASSWORD "${!GRAFANA_ADMIN_PASSWORD}" \
--arg LIVEKIT_API_KEY "${!LIVEKIT_API_KEY}" \
--arg LIVEKIT_API_SECRET "${!LIVEKIT_API_SECRET}" \
--arg ENABLED_MODULES "${!ENABLED_MODULES}" \
--arg ALL_SECRETS_GENERATED "true" \
'$ARGS.named')"
SECRETS_VERIFIED=false
for SECRETS_WRITE_ATTEMPT in $(seq 1 30); do
CURRENT_SECRET="$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--query SecretString --output text)"
if echo "$CURRENT_SECRET" | jq -e --argjson want "$SECRETS_WANTED" \
'. as $cur | $want | to_entries | all(.value == $cur[.key])' > /dev/null; then
SECRETS_VERIFIED=true
break
fi
aws secretsmanager update-secret \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--secret-string "$(echo "$CURRENT_SECRET" | jq --argjson want "$SECRETS_WANTED" '. + $want')" > /dev/null 2>&1
sleep 2
done
if [[ "$SECRETS_VERIFIED" != "true" ]]; then
echo "Error: the shared secret could not be verified complete after 30 writes"
exit 1
fi
ALL_SECRETS_GENERATED="true"
fi
# Fetch the shared secret again, waiting until master-1 has generated all secrets.
# Bounded (up to 900s at 5s): a genuinely stuck master-1 still surfaces after the deadline.
SECRETS_WAIT_ATTEMPTS=0
SECRETS_WAIT_MAX=180
while true; do
SHARED_SECRET=$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--query SecretString --output text || echo 'none')
ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED')
if [[ "${!ALL_SECRETS_GENERATED}" == "true" ]]; then
break
fi
SECRETS_WAIT_ATTEMPTS=$((SECRETS_WAIT_ATTEMPTS + 1))
if [[ $SECRETS_WAIT_ATTEMPTS -ge $SECRETS_WAIT_MAX ]]; then
echo "Error: Secrets not generated"
exit 1
fi
sleep 5
done
# 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
# Dedicated TURN Load Balancer (only when TurnDomainName and TurnCertificateARN are set,
# the same condition as the TURN Load Balancer resources)
if [[ "${TurnDomainName}" != "" && "${TurnCertificateARN}" != "" ]]; then
COMMON_ARGS+=("--turn-domain-name=${TurnDomainName}")
fi
# Construct the final command
FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")"
# Install OpenVidu
exec bash -c "$FINAL_COMMAND"
mode: '000755'
owner: root
group: root
'/usr/local/bin/registry_mirror.sh':
content: !Sub |
#!/bin/bash
set -e
# Pull-through cache of Docker Hub for the Media Nodes. Only when the Media Nodes are
# in private subnets: they all reach Docker Hub from the same NAT IP, and Docker Hub
# rate-limits anonymous pulls per IP. Media Nodes with a public IP pull directly.
if [[ "${SubnetProcessor.MediaNodesPrivate}" != "true" ]]; then
exit 0
fi
REGISTRY_VERSION=v3.1.2
ARCH="$(dpkg --print-architecture)"
curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o /tmp/registry.tar.gz \
"https://github.com/distribution/distribution/releases/download/${!REGISTRY_VERSION}/registry_${!REGISTRY_VERSION#v}_linux_${!ARCH}.tar.gz"
tar xzf /tmp/registry.tar.gz -C /usr/local/bin registry
rm -f /tmp/registry.tar.gz
mkdir -p /etc/registry-mirror
cat > /etc/registry-mirror/config.yml <<'EOF'
version: 0.1
log:
level: info
storage:
filesystem:
rootdirectory: /var/lib/registry-mirror
delete:
enabled: true
http:
addr: :5555
proxy:
remoteurl: https://registry-1.docker.io
ttl: 168h
EOF
cat > /etc/systemd/system/registry-mirror.service <<'EOF'
[Unit]
Description=Docker Hub pull-through cache for the OpenVidu Media Nodes
After=network-online.target
Wants=network-online.target
[Service]
# No OpenTelemetry collector: do not export traces
Environment=OTEL_TRACES_EXPORTER=none
ExecStart=/usr/local/bin/registry serve /etc/registry-mirror/config.yml
# Unprivileged: a transient user that can only write its own cache
# (/var/lib/registry-mirror); 5555 needs no capabilities
DynamicUser=yes
StateDirectory=registry-mirror
CapabilityBoundingSet=
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
LockPersonality=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now registry-mirror
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/config_s3.sh':
content: !Sub
- |
#!/bin/bash
set -e
# Install dir and config dir
INSTALL_DIR="/opt/openvidu"
CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster"
# Config S3 bucket
EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com"
EXTERNAL_S3_REGION="${AWS::Region}"
EXTERNAL_S3_PATH_STYLE_ACCESS="false"
EXTERNAL_S3_BUCKET_APP_DATA=${S3AppDataBucketResourceName}
EXTERNAL_S3_BUCKET_CLUSTER_DATA=${S3ClusterDataBucketResourceName}
sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s|EXTERNAL_S3_BUCKET_CLUSTER_DATA=.*|EXTERNAL_S3_BUCKET_CLUSTER_DATA=$EXTERNAL_S3_BUCKET_CLUSTER_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
- S3AppDataBucketResourceName: !If
- CreateRecordingsBucket
- !Ref S3AppDataBucketResource
- !Ref S3AppDataBucketName
S3ClusterDataBucketResourceName: !If
- CreateClusterDataBucket
- !Ref S3ClusterDataBucketResource
- !Ref S3ClusterDataBucketName
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/after_install.sh':
content: !Sub |
#!/bin/bash
set -e
# Only master 1 writes the shared secret, to avoid a lost-update race
if [[ "${!MASTER_NODE_NUM:-}" != "1" ]]; then
exit 0
fi
# Get current shared secret
SHARED_SECRET=$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--query SecretString --output text)
# Save access URLs
DOMAIN=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME')
OPENVIDU_URL="https://${!DOMAIN}/"
LIVEKIT_URL="wss://${!DOMAIN}/"
DASHBOARD_URL="https://${!DOMAIN}/dashboard/"
GRAFANA_URL="https://${!DOMAIN}/grafana/"
MINIO_URL="https://${!DOMAIN}/minio-console/"
# Update shared secret
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_URL": "'"$OPENVIDU_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_URL": "'"$LIVEKIT_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')"
# Update shared secret
aws secretsmanager update-secret \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--secret-string "$SHARED_SECRET"
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/update_config_from_secret.sh':
content: !Sub |
#!/bin/bash
set -e
# Token for IMDSv2
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Get current shared secret
SHARED_SECRET=$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--query SecretString --output text)
# Installation directory
INSTALL_DIR="/opt/openvidu"
CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster"
MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node"
# Replace DOMAIN_NAME
export DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME)
if [[ -n "$DOMAIN" ]]; then
sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
else
exit 1
fi
# Replace rest of the values
sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD)/" "${!MASTER_NODE_CONFIG_DIR}/master_node.env"
sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_RTC_ENGINE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_PRO_LICENSE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_ACCESS_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_SECRET_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_INITIAL_ADMIN_USER=.*/MEET_INITIAL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_INITIAL_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_INITIAL_ADMIN_PASSWORD=.*/MEET_INITIAL_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MEET_INITIAL_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
if [[ "${InitialMeetApiKey}" != '' ]]; then
sed -i "s/MEET_INITIAL_API_KEY=.*/MEET_INITIAL_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_INITIAL_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
fi
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
OPENVIDU_URL="https://${!DOMAIN}/"
LIVEKIT_URL="wss://${!DOMAIN}/"
DASHBOARD_URL="https://${!DOMAIN}/dashboard/"
GRAFANA_URL="https://${!DOMAIN}/grafana/"
MINIO_URL="https://${!DOMAIN}/minio-console/"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_URL": "'"$OPENVIDU_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_URL": "'"$LIVEKIT_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')"
aws secretsmanager update-secret \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--secret-string "$SHARED_SECRET"
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/update_secret_from_config.sh':
content: !Sub |
#!/bin/bash
set -e
# Get current shared secret
SHARED_SECRET=$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--query SecretString --output text)
# Installation directory
INSTALL_DIR="/opt/openvidu"
CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster"
MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node"
# Update shared secret
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"REDIS_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${!MASTER_NODE_CONFIG_DIR}/master_node.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_RTC_ENGINE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_PRO_LICENSE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_ACCESS_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_SECRET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_INITIAL_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_INITIAL_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
if [[ "${InitialMeetApiKey}" != '' ]]; then
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_INITIAL_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
fi
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
aws secretsmanager update-secret \
--region ${AWS::Region} \
--secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \
--secret-string "$SHARED_SECRET"
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/get_value_from_config.sh':
content: |
#!/bin/bash
set -e
# Function to get the value of a given key from the environment file
get_value() {
local key="$1"
local file_path="$2"
# Use grep to find the line with the key, ignoring lines starting with #
# Use awk to split on '=' and print the second field, which is the value
local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//')
# If the value is empty, return "none"
if [ -z "$value" ]; then
echo "none"
else
echo "$value"
fi
}
# Check if the correct number of arguments are supplied
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <key> <file_path>"
exit 1
fi
# Get the key and file path from the arguments
key="$1"
file_path="$2"
# Get and print the value
get_value "$key" "$file_path"
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/store_secret.sh':
content: !Sub |
#!/bin/bash
set -e
# Modes: save, generate
# save mode: save the secret in the secret manager
# generate mode: generate a random password and save it in the secret manager
MODE="$1"
SHARED_SECRET="$(aws secretsmanager get-secret-value \
--region ${AWS::Region} \
--secret-id ${OpenViduSharedInfo} \
--query SecretString --output text)"
if [[ "$MODE" == "generate" ]]; then
SECRET_KEY_NAME="$2"
PREFIX="${!3:-}"
LENGTH="${!4:-44}"
RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${!LENGTH})"
RANDOM_PASSWORD="${!PREFIX}${!RANDOM_PASSWORD}"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$RANDOM_PASSWORD"'"}')"
aws secretsmanager update-secret \
--region ${AWS::Region} \
--secret-id ${OpenViduSharedInfo} \
--secret-string "$SHARED_SECRET" > /dev/null 2>&1
echo "$RANDOM_PASSWORD"
elif [[ "$MODE" == "save" ]]; then
SECRET_KEY_NAME="$2"
SECRET_VALUE="$3"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$SECRET_VALUE"'"}')"
aws secretsmanager update-secret \
--region ${AWS::Region} \
--secret-id ${OpenViduSharedInfo} \
--secret-string "$SHARED_SECRET" > /dev/null 2>&1
echo "$SECRET_VALUE"
else
exit 1
fi
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/restart.sh':
content: |
#!/bin/bash
set -e
# Stop all services
systemctl stop openvidu
# Update config from secret
/usr/local/bin/update_config_from_secret.sh
# Start all services
systemctl start openvidu
mode: "000755"
owner: "root"
group: "root"
Properties:
LaunchTemplateName: !Sub 'openvidu-ha-master-${AWS::Region}-${AWS::StackName}'
LaunchTemplateData:
# Enable IMDSv2
MetadataOptions:
HttpEndpoint: enabled
HttpPutResponseHopLimit: 1
HttpTokens: required
IamInstanceProfile:
Name: !Ref OpenViduMasterInstanceProfile
ImageId: !If
- IsMasterGraviton
- !If
- IsMasterNodeNvidia
- '{{resolve:ssm:/aws/service/deeplearning/ami/arm64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id}}'
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
- !If
- IsMasterNodeNvidia
- '{{resolve:ssm:/aws/service/deeplearning/ami/x86_64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id}}'
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
InstanceType: !Ref MasterNodeInstanceType
KeyName: !Ref KeyName
SecurityGroupIds:
- !Ref OpenViduMasterNodeSG
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeSize: !Ref MasterNodesDiskSize
VolumeType: gp3
DeleteOnTermination: true
OpenViduMasterNode1:
Type: AWS::EC2::Instance
# Depends on its own SSM parameter so its put-parameter can't race the parameter's creation
DependsOn: MasterNode1PrivateIpParameter
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate
Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 1
SubnetId: !GetAtt SubnetProcessor.Subnet1
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -eu -o pipefail
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
python3-pip \
ec2-instance-connect
CFN_BOOTSTRAP_VERSION=2.0-39
# Detect Ubuntu version and install cfn-bootstrap accordingly
UBUNTU_VERSION=$(lsb_release -rs | cut -d. -f1)
if [ "$UBUNTU_VERSION" -ge 24 ]; then
python3 -m pip install --break-system-packages https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz
else
python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-${!CFN_BOOTSTRAP_VERSION}.tar.gz
fi
cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate
# Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub)
/usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started"
export MASTER_NODE_NUM=1
# 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
# Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub)
/usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started"
export MASTER_NODE_NUM=2
# 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
# Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub)
/usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started"
export MASTER_NODE_NUM=3
# 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
# Registry mirror for Media Nodes in private subnets (not required: they fall back to Docker Hub)
/usr/local/bin/registry_mirror.sh || echo "[OpenVidu] registry mirror not started"
export MASTER_NODE_NUM=4
# 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')
# Registry mirror of the Master Nodes, before the installer pulls the images
/usr/local/bin/config_registry_mirror.sh "$MASTER_NODE_1_PRIVATE_IP" "$MASTER_NODE_2_PRIVATE_IP" "$MASTER_NODE_3_PRIVATE_IP" "$MASTER_NODE_4_PRIVATE_IP" \
|| echo "Registry mirror not configured: pulling from Docker Hub"
# Download first: sh <(curl ...) would silently run an empty script on a transient curl failure
INSTALLER_SCRIPT="/tmp/install_ov_media_node.sh"
curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o "$INSTALLER_SCRIPT" "http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh"
if [ ! -s "$INSTALLER_SCRIPT" ]; then
echo "Downloaded OpenVidu media node installer is empty or missing" >&2
exit 1
fi
INSTALL_COMMAND="sh $INSTALLER_SCRIPT"
# Common arguments
COMMON_ARGS=(
"--no-tty"
"--install"
"--environment=aws"
"--deployment-type='ha'"
"--node-role='media-node'"
"--master-node-private-ip-list=$MASTER_NODE_PRIVATE_IP_LIST"
"--private-ip=$PRIVATE_IP"
"--redis-password=$REDIS_PASSWORD"
)
# Construct the final command with all arguments
FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")"
# Install OpenVidu
exec bash -c "$FINAL_COMMAND"
mode: '000755'
owner: root
group: root
'/usr/local/bin/config_registry_mirror.sh':
content: !Sub |
#!/bin/bash
set -e
# Pull Docker Hub images through the registry mirror of the Master Nodes
# (registry_mirror.sh), only when this Media Node has no public IP. Docker falls
# back to Docker Hub if no mirror answers.
# Usage: config_registry_mirror.sh <master-1-ip> <master-2-ip> <master-3-ip> <master-4-ip>
if [[ "${SubnetProcessor.MediaNodesPrivate}" != "true" ]]; then
exit 0
fi
TOKEN="$(curl -sf -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")"
if curl -sf -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4 > /dev/null; then
exit 0
fi
MASTER_IPS=("$@")
# Wait up to 5 minutes for a mirror: on a new stack, the Master Nodes start it at boot
MIRROR_READY=false
for i in $(seq 1 60); do
for MASTER_IP in "${!MASTER_IPS[@]}"; do
if curl -sf --max-time 3 "http://$MASTER_IP:5555/v2/" > /dev/null; then
MIRROR_READY=true
break
fi
done
if [[ "$MIRROR_READY" == "true" ]]; then
break
fi
sleep 5
done
if [[ "$MIRROR_READY" != "true" ]]; then
echo "No registry mirror answers in the Master Nodes: pulling from Docker Hub"
exit 0
fi
MIRRORS="$(printf '%s:5555\n' "${!MASTER_IPS[@]}" | jq -R . | jq -s .)"
mkdir -p /etc/docker
if [[ ! -s /etc/docker/daemon.json ]]; then
echo '{}' > /etc/docker/daemon.json
fi
jq --argjson m "$MIRRORS" \
'."registry-mirrors" = ($m | map("http://" + .)) | ."insecure-registries" = ((."insecure-registries" // []) + $m | unique)' \
/etc/docker/daemon.json > /etc/docker/daemon.json.tmp
mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json
if systemctl is-active --quiet docker; then
systemctl restart docker
fi
echo "Pulling Docker Hub images through: $(jq -c '."registry-mirrors"' /etc/docker/daemon.json)"
mode: "000755"
owner: "root"
group: "root"
'/usr/local/bin/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
- !If [TurnTLSIsEnabled, !Ref OpenViduTurnMediaNodeTG, !Ref AWS::NoValue]
MinSize: !Ref MinNumberOfMediaNodes
MaxSize: !Ref MaxNumberOfMediaNodes
DesiredCapacity: !Ref InitialNumberOfMediaNodes
VPCZoneIdentifier: !Ref OpenViduMediaNodeSubnets
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - Media Node
PropagateAtLaunch: true
StopMediaNodeLifecycleHook:
Type: 'AWS::AutoScaling::LifecycleHook'
Properties:
LifecycleHookName: !Sub StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName}
AutoScalingGroupName: !Ref OpenViduMediaNodeASG
LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING'
DefaultResult: 'CONTINUE'
HeartbeatTimeout: 3600
StopMediaNodeDocumentRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- ssm.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ssm:DescribeInstanceInformation
- ssm:ListCommands
- ssm:ListCommandInvocations
Resource: "*"
- Effect: Allow
Action:
- ssm:SendCommand
Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunShellScript
- Action:
- ssm:SendCommand
Resource: !Sub arn:${AWS::Partition}:ec2:*:*:instance/*
Condition:
StringEquals:
'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName'
Effect: Allow
PolicyName: SSM-Automation-Policy
StopMediaNodeAutomationDocument:
Type: AWS::SSM::Document
Properties:
Name:
Fn::Join:
# Generate a not too long and unique document name
# Getting a unique identifier from the stack id
- ''
- - 'StopMediaNodeAutomationDocument-'
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
DocumentType: Automation
Content:
schemaVersion: '0.3'
assumeRole: "{{automationAssumeRole}}"
description: This document stops the OpenVidu services in a Media Node and
terminates the instance. It stop the OpenVidu services without interrupting
the running sessions, ingress and egress running in the Media Node.
Also it sends a CONTINUE signal to the Auto Scaling Group to continue the
instance termination when the services are stopped.
parameters:
InstanceId:
type: String
automationAssumeRole:
type: String
default: !GetAtt StopMediaNodeDocumentRole.Arn
description: "(Required) The ARN of the role that allows Automation to
perform the actions."
mainSteps:
- name: RunCommand
action: aws:runCommand
inputs:
DocumentName: AWS-RunShellScript
InstanceIds:
- "{{ InstanceId }}"
Parameters:
# 24 hours as a timeout to wait for the instance to stop all services
executionTimeout: "60"
commands:
- nohup /usr/local/bin/stop_media_node.sh > /var/log/stop_media_node.log 2>&1 &
StopMediaNodeCloudWatchEventRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ssm:StartAutomationExecution
Resource:
- !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-execution/*
- !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${StopMediaNodeAutomationDocument}
PolicyName: !Sub StopMediaNodeCloudWatchEventPolicy-${AWS::Region}-${AWS::StackName}
- PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:PassRole
Resource: !GetAtt StopMediaNodeDocumentRole.Arn
PolicyName: Pass-Role-SSM-Automation-Policy
StopMediaNodeCloudWatchEventRule:
Type: AWS::Events::Rule
Properties:
Description: Rule to trigger the StopMediaNodeAutomationDocument when an instance is terminated
EventPattern:
source:
- aws.autoscaling
detail-type:
- EC2 Instance-terminate Lifecycle Action
detail:
AutoScalingGroupName:
- !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName}
Name:
Fn::Join:
# Generate a not too long and unique rule name
# Getting a unique identifier from the stack id
- ''
- - StopMediaNodeCloudWatchEventRule-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
Targets:
- Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT
RoleArn: !GetAtt StopMediaNodeCloudWatchEventRole.Arn
Id:
Fn::Join:
# Generate a not too long and unique target id
# Getting a unique identifier from the stack id
- ''
- - StopMediaNodeCloudWatchEventRule-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
InputTransformer:
InputPathsMap:
instanceid: "$.detail.EC2InstanceId"
InputTemplate: |
{
"InstanceId": [ <instanceid> ]
}
OpenViduMediaNodeASGScalingPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref OpenViduMediaNodeASG
PolicyType: TargetTrackingScaling
EstimatedInstanceWarmup: 120
TargetTrackingConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ASGAverageCPUUtilization
TargetValue: !Ref ScaleTargetCPU
OpenViduMasterNodeSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for OpenVidu Master Node
GroupName: !Sub openvidu-ha-master-sg-${AWS::Region}-${AWS::StackName}
VpcId: !Ref OpenViduVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIpv6: ::/0
OpenViduLoadBalancerToMasterHTTPIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG
OpenViduLoadBalancerToMasterIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 7880
ToPort: 7880
SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG
OpenViduMasterToMasterRedisIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 7000
ToPort: 7001
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterNodeClusterPortIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 7880
ToPort: 7880
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMediaNodeToMasterRegistryMirrorIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 5555
ToPort: 5555
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMediaNodeToMasterRedisIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 7000
ToPort: 7001
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMasterToMasterMinioIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9100
ToPort: 9100
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMasterToMasterMinioConsoleSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9101
ToPort: 9101
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterMinioIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9100
ToPort: 9100
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMasterToMasterMongoIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 20000
ToPort: 20000
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterMongoIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 20000
ToPort: 20000
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMasterToMasterMimirGrpcIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9095
ToPort: 9095
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMasterToMasterMimirGossipIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 7946
ToPort: 7946
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterHTTPMimirSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9009
ToPort: 9009
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMasterToMasterLokiGrpcIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9096
ToPort: 9096
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMasterToMasterLokiGossipIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 7947
ToPort: 7947
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMasterToMasterDashboardsIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 5000
ToPort: 5000
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMasterToMasterGrafanaIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 3000
ToPort: 3000
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterHTTPLokiSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 3100
ToPort: 3100
SourceSecurityGroupId: !Ref OpenViduMediaNodeSG
OpenViduMasterToMasterV2CompatibilityIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 4443
ToPort: 4443
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterV2CompatibilityWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
IpProtocol: tcp
FromPort: 4443
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMasterToMasterMeetIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 9080
ToPort: 9080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMasterToMasterCustomAppIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 6080
ToPort: 6080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
IpProtocol: tcp
FromPort: 9080
ToPort: 9080
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeToMasterCustomAppIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
IpProtocol: tcp
FromPort: 6080
ToPort: 6080
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for OpenVidu Media Node
GroupName: !Sub openvidu-ha-media-sg-${AWS::Region}-${AWS::StackName}
VpcId: !Ref OpenViduVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduLoadBalancerToMediaNodeRTMPIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp
FromPort: 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: !If [TurnTLSIsEnabled, !Ref OpenViduMasterNodeTG, !Ref OpenViduMasterNodeWithTurnTLSTG]
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: TLS
Certificates:
- CertificateArn: !Ref OpenViduCertificateARN
OpenViduRTMPMediaNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref OpenViduMediaNodeRTMPTG
LoadBalancerArn: !Ref LoadBalancer
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
# Master Node HTTP (Caddy 7880) for the main 443 listener when TurnDomainName is set.
OpenViduMasterNodeTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: TurnTLSIsEnabled
Properties:
Name:
Fn::Join:
- ''
- - OpenVidu-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
TargetType: instance
Targets:
- Id: !Ref OpenViduMasterNode1
- Id: !Ref OpenViduMasterNode2
- Id: !Ref OpenViduMasterNode3
- Id: !Ref OpenViduMasterNode4
VpcId: !Ref OpenViduVPC
Port: 7880
Protocol: TCP
Matcher:
HttpCode: '200'
HealthCheckIntervalSeconds: 10
HealthCheckPath: /health/caddy
HealthCheckProtocol: HTTP
HealthCheckPort: '7880'
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 3
UnhealthyThresholdCount: 4
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group
# Dedicated TURN Load Balancer (only when TurnDomainName + TurnCertificateARN are set).
# Terminates TLS on 443 and forwards to the Media Nodes' TURN TLS port 5349, so TURN
# over TLS is relayed by the Media Nodes themselves instead of going through the
# Master Nodes.
TurnLoadBalancerSG:
Type: AWS::EC2::SecurityGroup
Condition: TurnTLSIsEnabled
Properties:
GroupDescription: Security group for the dedicated TURN Load Balancer
GroupName: !Sub openvidu-ha-turn-lb-sg-${AWS::Region}-${AWS::StackName}
VpcId: !Ref OpenViduVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
TurnLoadBalancerToMediaNode5349Ingress:
Type: AWS::EC2::SecurityGroupIngress
Condition: TurnTLSIsEnabled
Properties:
GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp
FromPort: 5349
ToPort: 5349
SourceSecurityGroupId: !Ref TurnLoadBalancerSG
TurnLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Condition: TurnTLSIsEnabled
Properties:
Name:
Fn::Join:
- ''
- - OpenViduHA-Turn-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
Subnets: !If [HasLoadBalancerSubnets, !Ref LoadBalancerSubnets, !Ref OpenViduMediaNodeSubnets]
SecurityGroups:
- !Ref TurnLoadBalancerSG
Type: network
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN Load Balancer
OpenViduTurnMediaNodeTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: TurnTLSIsEnabled
Properties:
Name:
Fn::Join:
- ''
- - OVTurnMedia-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
VpcId: !Ref OpenViduVPC
Port: 5349
Protocol: TCP
HealthCheckProtocol: TCP
HealthCheckPort: '5349'
HealthCheckIntervalSeconds: 10
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 3
UnhealthyThresholdCount: 4
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN Media Target Group
TurnMediaNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: TurnTLSIsEnabled
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref OpenViduTurnMediaNodeTG
LoadBalancerArn: !Ref TurnLoadBalancer
Port: 443
Protocol: TLS
Certificates:
- CertificateArn: !Ref TurnCertificateARN
Outputs:
ServicesAndCredentials:
Description: Services and credentials
Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/home?region=${AWS::Region}#!/secret?name=openvidu-ha-${AWS::Region}-${AWS::StackName}
LoadBalancerDNS:
Description: DNS of the main Load Balancer - point DomainName (A/ALIAS) here
Value: !GetAtt LoadBalancer.DNSName
TurnLoadBalancerDNS:
Condition: TurnTLSIsEnabled
Description: DNS of the dedicated TURN Load Balancer - point TurnDomainName (A/ALIAS) here
Value: !GetAtt TurnLoadBalancer.DNSName