openvidu/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.bicep

2278 lines
79 KiB
Bicep

@description('Stack name. Maximum 15 characters: used as the Azure Key Vault name prefix ("<stackName>-keyvault"), which is limited to 24 characters.')
@minLength(3)
@maxLength(15)
param stackName string
@description('''
[selfsigned] Not recommended for production use. Just for testing purposes or development environments. You don't need a FQDN to use this option.
[owncert] Valid for production environments. Use your own certificate. You need a FQDN to use this option.
[letsencrypt] Valid for production environments. Can be used with or without a FQDN (if no FQDN is provided, the public IP will be used).
''')
@allowed([
'selfsigned'
'owncert'
'letsencrypt'
])
param certificateType string = 'letsencrypt'
@description('Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP')
param publicIpAddressObject object
@description('Domain name for the OpenVidu Deployment. Blank will generate default domain')
param domainName string = ''
@description('If certificate type is \'owncert\', this parameter will be used to specify the public certificate in base64 format')
param ownPublicCertificate string = ''
@description('If certificate type is \'owncert\', this parameter will be used to specify the private certificate in base64 format')
param ownPrivateCertificate string = ''
@description('Visit https://openvidu.io/account')
@secure()
param openviduLicense string
@description('RTCEngine media engine to use')
@allowed([
'pion'
'mediasoup'
])
param rtcEngine string = 'mediasoup'
@description('Initial password for the \'admin\' user in OpenVidu Meet. If not provided, a random password will be generated.')
@secure()
param initialMeetAdminPassword string = ''
@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.')
@secure()
param initialMeetApiKey string = ''
@description('Specifies the VM size for your OpenVidu Master Node. You can use any valid Azure VM size (e.g., Standard_B4s, Standard_D4s_v5, Standard_E4ps_v5). See https://learn.microsoft.com/en-us/azure/virtual-machines/sizes for available sizes.')
param masterNodeInstanceType string = 'Standard_B4s'
@description('Specifies the VM size for your OpenVidu Media Nodes. You can use any valid Azure VM size (e.g., Standard_B4s, Standard_D4s_v5, Standard_E4ps_v5). See https://learn.microsoft.com/en-us/azure/virtual-machines/sizes for available sizes.')
param mediaNodeInstanceType string = 'Standard_B4s'
@description('Username for the Virtual Machine.')
param adminUsername string
@description('SSH Key or password for the Virtual Machine')
@secure()
param adminSshKey object
@description('Number of initial media nodes to deploy')
param initialNumberOfMediaNodes int = 1
@description('Minimum number of media nodes to deploy')
param minNumberOfMediaNodes int = 1
@description('Maximum number of media nodes to deploy')
param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
var isEmptyDomain = domainName == ''
// ARM64 instances are detected by checking for 'p' in the instance type name pattern.
// Azure ARM-based VMs use 'p' to indicate ARM processors (Ampere Altra, Microsoft Cobalt, etc.)
// Examples: Standard_D2ps_v5, Standard_E4pds_v5, Standard_B2pls_v2, etc.
// The pattern checks for 'p' followed by optional letters (like 'l', 'd', 's') before '_v' version suffix
var masterNodeInstanceTypeLower = toLower(masterNodeInstanceType)
var mediaNodeInstanceTypeLower = toLower(mediaNodeInstanceType)
var isMasterArm64 = contains(masterNodeInstanceTypeLower, 'ps_v') || contains(masterNodeInstanceTypeLower, 'pls_v') || contains(masterNodeInstanceTypeLower, 'pds_v') || contains(masterNodeInstanceTypeLower, 'plds_v') || contains(masterNodeInstanceTypeLower, 'psv') || contains(masterNodeInstanceTypeLower, 'plsv') || contains(masterNodeInstanceTypeLower, 'pdsv') || contains(masterNodeInstanceTypeLower, 'pldsv')
var isMediaArm64 = contains(mediaNodeInstanceTypeLower, 'ps_v') || contains(mediaNodeInstanceTypeLower, 'pls_v') || contains(mediaNodeInstanceTypeLower, 'pds_v') || contains(mediaNodeInstanceTypeLower, 'plds_v') || contains(mediaNodeInstanceTypeLower, 'psv') || contains(mediaNodeInstanceTypeLower, 'plsv') || contains(mediaNodeInstanceTypeLower, 'pdsv') || contains(mediaNodeInstanceTypeLower, 'pldsv')
var masterUbuntuSku = isMasterArm64 ? 'server-arm64' : 'server'
var mediaUbuntuSku = isMediaArm64 ? 'server-arm64' : 'server'
var masterNodeVMSettings = {
vmName: '${stackName}-VM-MasterNode'
osDiskType: 'StandardSSD_LRS'
ubuntuOSVersion: {
publisher: 'Canonical'
offer: 'ubuntu-24_04-lts'
sku: masterUbuntuSku
version: 'latest'
}
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminSshKey.sshPublicKey
}
]
}
}
}
var mediaNodeVMSettings = {
vmName: '${stackName}-VM-MediaNode'
osDiskType: 'StandardSSD_LRS'
ubuntuOSVersion: {
publisher: 'Canonical'
offer: 'ubuntu-24_04-lts'
sku: mediaUbuntuSku
version: 'latest'
}
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminSshKey.sshPublicKey
}
]
}
}
}
var networkSettings = {
vNetAddressPrefix: '10.0.0.0/16'
subnetAddressPrefixMaster1: '10.0.1.0/24'
subnetAddressPrefixMedia: '10.0.0.0/24'
vNetName: '${stackName}-virtualNetwork'
}
var keyVaultName = '${stackName}-keyvault'
var location = resourceGroup().location
var tenantId = subscription().tenantId
var deploymentUser = az.deployer().objectId
/*------------------------------------------- KEY VAULT -------------------------------------------*/
resource openviduSharedInfo 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
enabledForDeployment: true
enabledForDiskEncryption: false
enabledForTemplateDeployment: true
tenantId: tenantId
enableSoftDelete: false
accessPolicies: [
{
objectId: openviduMasterNode.identity.principalId
tenantId: tenantId
permissions: {
secrets: ['get', 'set', 'list']
}
}
{
objectId: openviduScaleSetMediaNode.identity.principalId
tenantId: tenantId
permissions: {
secrets: ['get']
}
}
{
objectId: deploymentUser
tenantId: tenantId
permissions: {
secrets: ['get', 'list', 'set', 'delete', 'recover', 'backup', 'restore']
}
}
]
sku: {
name: 'standard'
family: 'A'
}
networkAcls: {
defaultAction: 'Allow'
bypass: 'AzureServices'
}
}
}
/*------------------------------------------- MASTER NODE -------------------------------------------*/
var stringInterpolationParamsMaster = {
publicIPId: publicIPId
domainName: domainName
certificateType: certificateType
ownPublicCertificate: ownPublicCertificate
ownPrivateCertificate: ownPrivateCertificate
openviduLicense: openviduLicense
rtcEngine: rtcEngine
initialMeetAdminPassword: initialMeetAdminPassword
initialMeetApiKey: initialMeetApiKey
keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplateMaster = '''
#!/bin/bash
set -e
OPENVIDU_VERSION=main
DOMAIN=
# Assume azure cli is installed
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
curl \
unzip \
jq \
wget
# Configure Domain
if [[ "${domainName}" == '' ]]; then
# Get public IP using the get_public_ip.sh script
PUBLIC_IP=$(/usr/local/bin/get_public_ip.sh 2>/dev/null)
if [[ $? -ne 0 || -z "${PUBLIC_IP}" ]]; then
echo "Could not determine public IP."
exit 1
fi
DOMAIN="$PUBLIC_IP"
else
DOMAIN=${domainName}
fi
# Wait for the keyvault availability
MAX_WAIT=300
WAIT_INTERVAL=1
ELAPSED_TIME=0
while true; do
# Check keyvault availability
set +e
az keyvault secret list --vault-name ${keyVaultName}
# If it is available, exit the loop
if [ $? -eq 0 ]; then
break
fi
# If not, wait and check again incrementing the time
ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))
# If exceeded the maximum time, exit with error
if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then
exit 1
fi
# Wait for the next iteration
sleep $WAIT_INTERVAL
done
set -e
# Get own private IP
PRIVATE_IP=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/privateIpAddress?api-version=2017-08-01&format=text")
# Store usernames and generate random passwords
DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN-NAME "$DOMAIN")"
# 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
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}")"
REDIS_PASSWORD="$(/usr/local/bin/store_secret.sh generate REDIS-PASSWORD)"
MONGO_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save MONGO-ADMIN-USERNAME "mongoadmin")"
MONGO_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MONGO-ADMIN-PASSWORD)"
MONGO_REPLICA_SET_KEY="$(/usr/local/bin/store_secret.sh generate MONGO-REPLICA-SET-KEY)"
MINIO_ACCESS_KEY="$(/usr/local/bin/store_secret.sh save MINIO-ACCESS-KEY "minioadmin")"
MINIO_SECRET_KEY="$(/usr/local/bin/store_secret.sh generate MINIO-SECRET-KEY)"
DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-USERNAME "dashboardadmin")"
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
MEET_INITIAL_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-INITIAL-ADMIN-USER "admin")"
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)"
OPENVIDU_VERSION="$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION "${OPENVIDU_VERSION}")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
# Download to a file first: process substitution 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/elastic/$OPENVIDU_VERSION/install_ov_master_node.sh"
if [ ! -s "$INSTALLER_SCRIPT" ]; then
echo "[OpenVidu] failed to download the master node installer script"
exit 1
fi
# Base command
INSTALL_COMMAND="sh $INSTALLER_SCRIPT"
# Common arguments
COMMON_ARGS=(
"--no-tty"
"--install"
"--environment=azure"
"--deployment-type=elastic"
"--node-role='master-node'"
"--openvidu-pro-license=$OPENVIDU_PRO_LICENSE"
"--private-ip=$PRIVATE_IP"
"--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
# Certificate arguments
if [[ "${certificateType}" == "selfsigned" ]]; then
CERT_ARGS=(
"--certificate-type=selfsigned"
)
elif [[ "${certificateType}" == "letsencrypt" ]]; then
CERT_ARGS=(
"--certificate-type=letsencrypt"
)
else
# Use base64 encoded certificates directly
OWN_CERT_CRT=${ownPublicCertificate}
OWN_CERT_KEY=${ownPrivateCertificate}
CERT_ARGS=(
"--certificate-type=owncert"
"--owncert-public-key=$OWN_CERT_CRT"
"--owncert-private-key=$OWN_CERT_KEY"
)
fi
# Construct the final command with all arguments
FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${COMMON_ARGS[@]}") $(printf "%s " "${CERT_ARGS[@]}")"
# Install OpenVidu
exec bash -c "$FINAL_COMMAND"
'''
var after_installScriptTemplateMaster = '''
#!/bin/bash
set -e
az login --identity --allow-no-subscriptions > /dev/null
# Generate URLs
DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)
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
az keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN
az keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-URL --value $OPENVIDU_URL
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-URL --value $LIVEKIT_URL
az keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL
az keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL
az keyvault secret show --vault-name ${keyVaultName} --name MINIO-URL
if [[ $? -ne 0 ]]; then
echo "Error updating keyvault"
fi
'''
var update_config_from_secretScriptTemplateMaster = '''
#!/bin/bash
set -e
az login --identity --allow-no-subscriptions > /dev/null
# 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=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)
if [[ -n "$DOMAIN" ]]; then
sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
else
exit 1
fi
# Get the rest of the values
export REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)
export OPENVIDU_RTC_ENGINE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --query value -o tsv)
export OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)
export MONGO_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --query value -o tsv)
export MONGO_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --query value -o tsv)
export MONGO_REPLICA_SET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --query value -o tsv)
export DASHBOARD_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --query value -o tsv)
export DASHBOARD_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --query value -o tsv)
export MINIO_ACCESS_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --query value -o tsv)
export MINIO_SECRET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --query value -o tsv)
export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --query value -o tsv)
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export MEET_INITIAL_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-INITIAL-ADMIN-USER --query value -o tsv)
export MEET_INITIAL_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-INITIAL-ADMIN-PASSWORD --query value -o tsv)
if [[ "${initialMeetApiKey}" != '' ]]; then
export MEET_INITIAL_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-INITIAL-API-KEY --query value -o tsv)
fi
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" "${MASTER_NODE_CONFIG_DIR}/master_node.env"
sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$DASHBOARD_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$DASHBOARD_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$MINIO_SECRET_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_INITIAL_ADMIN_USER=.*/MEET_INITIAL_ADMIN_USER=$MEET_INITIAL_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_INITIAL_ADMIN_PASSWORD=.*/MEET_INITIAL_ADMIN_PASSWORD=$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=$MEET_INITIAL_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
fi
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$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/"
# Update shared secret
az keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN
az keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-URL --value $OPENVIDU_URL
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-URL --value $LIVEKIT_URL
az keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL
az keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL
'''
var update_secret_from_configScriptTemplateMaster = '''
#!/bin/bash
set -e
az login --identity --allow-no-subscriptions > /dev/null
# Installation directory
INSTALL_DIR="/opt/openvidu"
CLUSTER_CONFIG_DIR="${INSTALL_DIR}/config/cluster"
MASTER_NODE_CONFIG_DIR="${INSTALL_DIR}/config/node"
# Get current values of the config
REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${MASTER_NODE_CONFIG_DIR}/master_node.env")"
DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CLUSTER_CONFIG_DIR}/openvidu.env")"
OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CLUSTER_CONFIG_DIR}/openvidu.env")"
OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MINIO_ACCESS_KEY="$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MINIO_SECRET_KEY="$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${CLUSTER_CONFIG_DIR}/openvidu.env")"
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${CLUSTER_CONFIG_DIR}/openvidu.env")"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${CLUSTER_CONFIG_DIR}/openvidu.env")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MEET_INITIAL_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
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
MEET_INITIAL_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_INITIAL_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
fi
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret
az keyvault secret set --vault-name ${keyVaultName} --name REDIS-PASSWORD --value $REDIS_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN_NAME
az keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --value $OPENVIDU_RTC_ENGINE
az keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --value $OPENVIDU_PRO_LICENSE
az keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --value $MONGO_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --value $MONGO_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --value $MONGO_REPLICA_SET_KEY
az keyvault secret set --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --value $MINIO_ACCESS_KEY
az keyvault secret set --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --value $MINIO_SECRET_KEY
az keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --value $DASHBOARD_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --value $DASHBOARD_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --value $GRAFANA_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-INITIAL-ADMIN-USER --value $MEET_INITIAL_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-INITIAL-ADMIN-PASSWORD --value $MEET_INITIAL_ADMIN_PASSWORD
if [[ "${initialMeetApiKey}" != '' ]]; then
az keyvault secret set --vault-name ${keyVaultName} --name MEET-INITIAL-API-KEY --value $MEET_INITIAL_API_KEY
fi
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
var get_value_from_configScriptMaster = '''
#!/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"
'''
var store_secretScriptTemplateMaster = '''
#!/bin/bash
set -e
az login --identity --allow-no-subscriptions > /dev/null
# 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"
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}"
az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null
if [[ $? -ne 0 ]]; then
echo "Error generating secret"
fi
echo "$RANDOM_PASSWORD"
elif [[ "$MODE" == "save" ]]; then
SECRET_KEY_NAME="$2"
SECRET_VALUE="$3"
# If empty value, store it empty
if [[ -z "$SECRET_VALUE" ]]; then
az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --file /dev/null > /dev/null
else
az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null
fi
if [[ $? -ne 0 ]]; then
echo "Error generating secret"
fi
echo "$SECRET_VALUE"
else
exit 1
fi
'''
var get_public_ip = '''
#!/bin/bash
az login --identity --allow-no-subscriptions > /dev/null
az network public-ip show \
--id ${publicIPId} \
--query "ipAddress" -o tsv
'''
var check_app_readyScriptMaster = '''
#!/bin/bash
set -e
MAX_WAIT=1200
WAIT_INTERVAL=5
ELAPSED_TIME=0
while true; do
HTTP_STATUS=$(curl -Ik http://localhost:7880/health/caddy | head -n1 | awk '{print $2}')
if [ "$HTTP_STATUS" = "200" ]; then
break
fi
ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))
if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then
exit 1
fi
sleep 5
done
'''
var restartScriptMaster = '''
#!/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
'''
var config_blobStorageTemplate = '''
#!/bin/bash
set -e
# Install dir and config dir
INSTALL_DIR="/opt/openvidu"
CLUSTER_CONFIG_DIR="${INSTALL_DIR}/config/cluster"
# Retry login + storage key fetch to allow the Contributor role assignment to propagate
MAX_WAIT=300
WAIT_INTERVAL=1
ELAPSED_TIME=0
set +e
while true; do
az login --identity
# Config azure blob storage
AZURE_ACCOUNT_NAME="${storageAccountName}"
AZURE_ACCOUNT_KEY=$(az storage account keys list --account-name ${storageAccountName} --query '[0].value' -o tsv)
# If the key was fetched successfully, exit the loop
if [ $? -eq 0 ]; then
break
fi
# If not, wait and check again incrementing the time
ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))
# If exceeded the maximum time, exit with error
if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then
exit 1
fi
# Wait before the next check
sleep $WAIT_INTERVAL
done
set -e
AZURE_CONTAINER_NAME="${storageAccountContainerName}"
sed -i "s|AZURE_ACCOUNT_NAME=.*|AZURE_ACCOUNT_NAME=$AZURE_ACCOUNT_NAME|" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s|AZURE_ACCOUNT_KEY=.*|AZURE_ACCOUNT_KEY=$AZURE_ACCOUNT_KEY|" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s|AZURE_CONTAINER_NAME=.*|AZURE_CONTAINER_NAME=$AZURE_CONTAINER_NAME|" "${CLUSTER_CONFIG_DIR}/openvidu.env"
'''
var installScriptMaster = reduce(
items(stringInterpolationParamsMaster),
{ value: installScriptTemplateMaster },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var after_installScriptMaster = reduce(
items(stringInterpolationParamsMaster),
{ value: after_installScriptTemplateMaster },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var get_public_ip_script = reduce(
items(stringInterpolationParamsMaster),
{ value: get_public_ip },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var update_config_from_secretScriptMaster = reduce(
items(stringInterpolationParamsMaster),
{ value: update_config_from_secretScriptTemplateMaster },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var update_secret_from_configScriptMaster = reduce(
items(stringInterpolationParamsMaster),
{ value: update_secret_from_configScriptTemplateMaster },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var store_secretScriptMaster = reduce(
items(stringInterpolationParamsMaster),
{ value: store_secretScriptTemplateMaster },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var blobStorageParams = {
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : exisitngStorageAccount.name
storageAccountKey: listKeys(storageAccount.id, '2021-04-01').keys[0].value
storageAccountContainerName: isEmptyContainerName ? 'openvidu-appdata' : '${containerName}'
}
var config_blobStorageScript = reduce(
items(blobStorageParams),
{ value: config_blobStorageTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64installMaster = base64(installScriptMaster)
var base64after_installMaster = base64(after_installScriptMaster)
var base64update_config_from_secretMaster = base64(update_config_from_secretScriptMaster)
var base64update_secret_from_configMaster = base64(update_secret_from_configScriptMaster)
var base64get_value_from_configMaster = base64(get_value_from_configScriptMaster)
var base64store_secretMaster = base64(store_secretScriptMaster)
var base64get_public_ipMaster = base64(get_public_ip_script)
var base64check_app_readyMaster = base64(check_app_readyScriptMaster)
var base64restartMaster = base64(restartScriptMaster)
var base64config_blobStorage = base64(config_blobStorageScript)
var userDataParamsMasterNode = {
base64install: base64installMaster
base64after_install: base64after_installMaster
base64update_config_from_secret: base64update_config_from_secretMaster
base64update_secret_from_config: base64update_secret_from_configMaster
base64get_value_from_config: base64get_value_from_configMaster
base64store_secret: base64store_secretMaster
base64get_public_ip: base64get_public_ipMaster
base64check_app_ready: base64check_app_readyMaster
base64restart: base64restartMaster
base64config_blobStorage: base64config_blobStorage
keyVaultName: keyVaultName
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : exisitngStorageAccount.name
}
var userDataTemplateMasterNode = '''
#!/bin/bash
set -eu -o pipefail
# Introduce the scripts in the instance
# install.sh
echo ${base64install} | base64 -d > /usr/local/bin/install.sh
chmod +x /usr/local/bin/install.sh
# after_install.sh
echo ${base64after_install} | base64 -d > /usr/local/bin/after_install.sh
chmod +x /usr/local/bin/after_install.sh
# update_config_from_secret.sh
echo ${base64update_config_from_secret} | base64 -d > /usr/local/bin/update_config_from_secret.sh
chmod +x /usr/local/bin/update_config_from_secret.sh
# update_secret_from_config.sh
echo ${base64update_secret_from_config} | base64 -d > /usr/local/bin/update_secret_from_config.sh
chmod +x /usr/local/bin/update_secret_from_config.sh
# get_value_from_config.sh
echo ${base64get_value_from_config} | base64 -d > /usr/local/bin/get_value_from_config.sh
chmod +x /usr/local/bin/get_value_from_config.sh
# store_secret.sh
echo ${base64store_secret} | base64 -d > /usr/local/bin/store_secret.sh
chmod +x /usr/local/bin/store_secret.sh
# get_public_ip.sh
echo ${base64get_public_ip} | base64 -d > /usr/local/bin/get_public_ip.sh
chmod +x /usr/local/bin/get_public_ip.sh
# check_app_ready.sh
echo ${base64check_app_ready} | base64 -d > /usr/local/bin/check_app_ready.sh
chmod +x /usr/local/bin/check_app_ready.sh
# restart.sh
echo ${base64restart} | base64 -d > /usr/local/bin/restart.sh
chmod +x /usr/local/bin/restart.sh
echo ${base64config_blobStorage} | base64 -d > /usr/local/bin/config_blobStorage.sh
chmod +x /usr/local/bin/config_blobStorage.sh
# Install azure cli
AZURE_CLI_VERSION=2.90.0
apt-get install -y apt-transport-https ca-certificates gnupg lsb-release
curl -sLS https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/azure-cli.list
apt-get update
apt-get install -y azure-cli=${AZURE_CLI_VERSION}-1~$(lsb_release -cs)
az login --identity --allow-no-subscriptions
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
export HOME="/root"
# Install OpenVidu
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; }
#Config blob storage
/usr/local/bin/config_blobStorage.sh || { echo "[OpenVidu] error configuring Blob Storage"; exit 1; }
# Start OpenVidu
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; }
# Update shared secret
/usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; }
# Launch on reboot
echo "@reboot /usr/local/bin/restart.sh >> /var/log/openvidu-restart.log" 2>&1 | crontab
# check_app_ready.sh internally caps its wait at 1200s
/usr/local/bin/check_app_ready.sh || { echo "[OpenVidu] master node did not become healthy"; exit 1; }
az keyvault secret set --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --value "true"
set +e
az storage blob upload --account-name ${storageAccountName} --container-name automation-locks --name lock.txt --file /dev/null --auth-mode key
set -e
'''
var userDataMasterNode = reduce(
items(userDataParamsMasterNode),
{ value: userDataTemplateMasterNode },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
resource openviduMasterNode 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: masterNodeVMSettings.vmName
location: location
identity: { type: 'SystemAssigned' }
properties: {
hardwareProfile: {
vmSize: masterNodeInstanceType
}
storageProfile: {
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: masterNodeVMSettings.osDiskType
}
diskSizeGB: 100
}
imageReference: masterNodeVMSettings.ubuntuOSVersion
}
networkProfile: {
networkInterfaces: [
{
id: netInterfaceMasterNode.id
}
]
}
osProfile: {
computerName: masterNodeVMSettings.vmName
adminUsername: adminUsername
linuxConfiguration: masterNodeVMSettings.linuxConfiguration
}
userData: base64(userDataMasterNode)
}
}
/*------------------------------------------- MEDIA NODES -------------------------------------------*/
var stringInterpolationParamsMedia = {
privateIPMasterNode: netInterfaceMasterNode.properties.ipConfigurations[0].properties.privateIPAddress
keyVaultName: keyVaultName
}
var installScriptTemplateMedia = '''
#!/bin/bash
set -e
DOMAIN=
# Install dependencies
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
curl \
unzip \
jq \
wget
# Get own private IP
PRIVATE_IP=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/privateIpAddress?api-version=2017-08-01&format=text")
# Gate 1: wait for master secrets before installing
WAIT_INTERVAL=5
MAX_RETRIES=360
RETRIES=0
set +e
while true; do
ALL_SECRETS_GENERATED=$(az keyvault secret show --vault-name ${keyVaultName} --name ALL-SECRETS-GENERATED --query value -o tsv 2>/dev/null)
if [ "$ALL_SECRETS_GENERATED" == "true" ]; then
break
fi
RETRIES=$((RETRIES + 1))
if [ $RETRIES -ge $MAX_RETRIES ]; then
echo "[OpenVidu] timed out after 30 min waiting for ALL-SECRETS-GENERATED"
exit 1
fi
sleep $WAIT_INTERVAL
done
set -e
# Get current shared secret
DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)
OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)
REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)
ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
OPENVIDU_VERSION="$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-VERSION --query value -o tsv)"
# Get Master Node private IP
MASTER_NODE_IP=${privateIPMasterNode}
# Download to a file first: process substitution 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/elastic/$OPENVIDU_VERSION/install_ov_media_node.sh"
if [ ! -s "$INSTALLER_SCRIPT" ]; then
echo "[OpenVidu] failed to download the media node installer script"
exit 1
fi
# Base command
INSTALL_COMMAND="sh $INSTALLER_SCRIPT"
# Common arguments
COMMON_ARGS=(
"--no-tty"
"--install"
"--environment=azure"
"--deployment-type=elastic"
"--node-role='media-node'"
"--master-node-private-ip=$MASTER_NODE_IP"
"--private-ip=$PRIVATE_IP"
"--enabled-modules='$ENABLED_MODULES'"
"--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"
'''
var stopMediaNodeParams = {
subscriptionId: subscription().subscriptionId
resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet'
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : exisitngStorageAccount.name
}
var stop_media_nodesScriptMediaTemplate = '''
#!/bin/bash
set -e
# Pause the health watchdog before draining so it never reimages a node that is being
# drained for scale-in (synchronous; the drain runs outside the watchdog's cgroup).
touch /run/openvidu-media-health.paused
systemctl stop openvidu-media-health 2>/dev/null || true
if ! (set -o noclobber ; echo > /tmp/global.lock) ; then
exit 1 # the global.lock already exists
fi
# 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
# If this node is being replaced because it is unhealthy, do not wait for sessions to end.
if [ -f /run/openvidu-unhealthy ]; then
docker ps -q | xargs -r docker kill >/dev/null 2>&1 || true
fi
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent
while [ "$(docker ps --filter 'label=openvidu-agent=true' -q 2>/dev/null | wc -l)" -gt 0 ] || \
[ "$(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null)" = "true" ] || \
[ "$(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null)" = "true" ] || \
[ "$(docker inspect -f '{{.State.Running}}' egress 2>/dev/null)" = "true" ]; do
echo "Waiting for containers to stop..."
sleep 5
done
fi
az login --identity
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
SUBSCRIPTION_ID=${subscriptionId}
BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')
RESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Compute/virtualMachineScaleSets/$VM_SCALE_SET_NAME
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}"
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
var openviduMediaHealthScript = '''
#!/bin/bash
# OpenVidu Media Node health watchdog.
#
# Asks the cloud to replace this Media Node with a new one, keeping the number of
# Media Nodes, through /usr/local/bin/openvidu-media-replace.sh, when:
# - its bootstrap failed (the user-data wrote the bootstrap-failed marker): after a
# cool-down, so a persistent failure does not churn nodes and can be inspected,
# - its bootstrap did not finish BOOTSTRAP_DEADLINE_SEC after boot,
# - LiveKit fails its health check (HTTP 200 on PROBE_URL): for HARD_UNHEALTHY_AFTER_SEC
# while it is down (connection refused or closed), or for SOFT_UNHEALTHY_AFTER_SEC
# while it answers an error or times out. Soft failures are not counted while the CPU
# is saturated (an overloaded LiveKit reports Not Ready), and no failure is counted in
# the WARMUP_SEC after openvidu.service (re)started.
# Nothing is replaced while no master answers a Redis PING (MASTER_ADDRS): a master outage
# fails the health check of every Media Node at once, and a new node could not bootstrap
# either. Once requested, the replacement is requested again every REPLACE_RETRY_SEC until
# the node goes away.
#
# Settings: /etc/openvidu/media-health.env (sourced every cycle; MASTER_ADDRS="ip:port ...")
# Pause: touch /etc/openvidu/media-health.disabled (drain scripts use /run/openvidu-media-health.paused)
# Logs: journalctl -u openvidu-media-health
#
# This file is embedded verbatim in CloudFormation !Sub blocks, Bicep strings and
# Terraform heredocs, so it must never contain a dollar sign or a percent sign followed
# by an opening brace, nor two opening braces in a row.
STATE_DIR=/var/lib/openvidu-media-health
INSTALLED_MARKER="$STATE_DIR/installed"
BOOTSTRAP_FAILED_MARKER="$STATE_DIR/bootstrap-failed"
REPLACE_SCRIPT=/usr/local/bin/openvidu-media-replace.sh
ENV_FILE=/etc/openvidu/media-health.env
PAUSE_FILE=/etc/openvidu/media-health.disabled
DRAIN_PAUSE_FILE=/run/openvidu-media-health.paused
log() { echo "[media-health] $*"; }
# Seconds since boot: monotonic, unaffected by clock steps
uptime_sec() { cut -d. -f1 /proc/uptime; }
# Busy CPU percentage since the previous call, in BUSY (0 on the first call)
cpu_busy() {
read -r _ c_user c_nice c_system c_idle c_iowait c_irq c_softirq c_steal _ < /proc/stat
total=$((c_user + c_nice + c_system + c_idle + c_iowait + c_irq + c_softirq + c_steal))
idle=$((c_idle + c_iowait))
BUSY=0
if [ -n "$PREV_TOTAL" ] && [ "$total" -gt "$PREV_TOTAL" ]; then
BUSY=$((100 * (total - PREV_TOTAL - idle + PREV_IDLE) / (total - PREV_TOTAL)))
fi
PREV_TOTAL=$total
PREV_IDLE=$idle
}
# openvidu.service is running and entered the active state less than WARMUP_SEC ago
# (a stopped or failed openvidu.service is not starting: no warm-up for it)
openvidu_starting() {
case "$(systemctl show -p ActiveState --value openvidu 2>/dev/null)" in
active|activating|reloading) ;;
*) return 1 ;;
esac
since=$(systemctl show -p ActiveEnterTimestampMonotonic --value openvidu 2>/dev/null)
case "$since" in ''|0|*[!0-9]*) return 1 ;; esac
[ $(($(uptime_sec) - since / 1000000)) -lt "$WARMUP_SEC" ]
}
# A master answers a Redis PING (+PONG, or -NOAUTH when a password is required). A bare
# TCP connect would also succeed against a wedged Redis.
master_serving() {
[ -n "$MASTER_ADDRS" ] || return 1
for addr in $MASTER_ADDRS; do
host=$(echo "$addr" | cut -d: -f1)
port=$(echo "$addr" | cut -d: -f2)
reply=$(timeout 5 bash -c 'exec 3<>"/dev/tcp/$0/$1" && printf "PING\r\n" >&3 && head -c 7 <&3' "$host" "$port" 2>/dev/null)
case "$reply" in
+PONG*|-NOAUTH*) return 0 ;;
esac
done
return 1
}
# Request the replacement of this node ($1 = reason). Gated on a master answering, except
# in the bootstrap phase before the master addresses are known.
request_replacement() {
now=$(uptime_sec)
if [ -n "$REQUESTED_AT" ] && [ $((now - REQUESTED_AT)) -lt "$REPLACE_RETRY_SEC" ]; then
return
fi
if [ -n "$MASTER_ADDRS" ] || [ -f "$INSTALLED_MARKER" ]; then
if ! master_serving; then
if [ -z "$HOLD_LOGGED_AT" ] || [ $((now - HOLD_LOGGED_AT)) -ge 600 ]; then
log "$1, but no master answers a Redis PING (MASTER_ADDRS='$MASTER_ADDRS'): not replacing this node"
HOLD_LOGGED_AT=$now
fi
return
fi
fi
log "$1: requesting the replacement of this node"
if "$REPLACE_SCRIPT" "$1"; then
REQUESTED_AT=$now
log "replacement requested"
else
log "the replacement request failed, retrying in $INTERVAL_SEC s"
fi
}
mkdir -p "$STATE_DIR"
REQUESTED_AT=""
HOLD_LOGGED_AT=""
FAILED_SEEN_AT=""
FAIL_SINCE=""
SATURATED=""
PREV_TOTAL=""
PREV_IDLE=""
log "started"
while true; do
PROBE_URL=http://127.0.0.1:7880/
INTERVAL_SEC=30
HARD_UNHEALTHY_AFTER_SEC=300
SOFT_UNHEALTHY_AFTER_SEC=600
CPU_SATURATED_PCT=90
WARMUP_SEC=900
BOOTSTRAP_DEADLINE_SEC=5400
BOOTSTRAP_FAILURE_COOLDOWN_SEC=600
REPLACE_RETRY_SEC=600
MASTER_ADDRS=""
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
NOW=$(uptime_sec)
cpu_busy
if [ -f "$PAUSE_FILE" ] || [ -f "$DRAIN_PAUSE_FILE" ]; then
FAIL_SINCE=""
elif [ -f "$BOOTSTRAP_FAILED_MARKER" ]; then
if [ -z "$FAILED_SEEN_AT" ]; then
FAILED_SEEN_AT=$NOW
log "bootstrap failed ($(head -c 300 "$BOOTSTRAP_FAILED_MARKER")): replacing this node in $BOOTSTRAP_FAILURE_COOLDOWN_SEC s"
fi
if [ $((NOW - FAILED_SEEN_AT)) -ge "$BOOTSTRAP_FAILURE_COOLDOWN_SEC" ]; then
request_replacement "bootstrap failed: $(head -c 300 "$BOOTSTRAP_FAILED_MARKER")"
fi
elif [ ! -f "$INSTALLED_MARKER" ]; then
if [ "$NOW" -ge "$BOOTSTRAP_DEADLINE_SEC" ]; then
request_replacement "bootstrap not finished $BOOTSTRAP_DEADLINE_SEC s after boot"
fi
elif curl -sf -o /dev/null --connect-timeout 3 --max-time 15 "$PROBE_URL"; then
[ -n "$FAIL_SINCE" ] && log "LiveKit healthy again after $((NOW - FAIL_SINCE)) s"
FAIL_SINCE=""
SATURATED=""
else
RC=$?
case "$RC" in
7|52|56) KIND=hard ;;
*) KIND=soft ;;
esac
if openvidu_starting; then
FAIL_SINCE=""
elif [ "$KIND" = soft ] && [ "$BUSY" -ge "$CPU_SATURATED_PCT" ]; then
[ -n "$SATURATED" ] || log "LiveKit not ready (curl exit $RC) with the CPU $BUSY% busy: not counted"
SATURATED=1
FAIL_SINCE=""
else
SATURATED=""
if [ -z "$FAIL_SINCE" ]; then
FAIL_SINCE=$NOW
log "LiveKit health check failed (curl exit $RC)"
fi
LIMIT=$SOFT_UNHEALTHY_AFTER_SEC
[ "$KIND" = hard ] && LIMIT=$HARD_UNHEALTHY_AFTER_SEC
if [ $((NOW - FAIL_SINCE)) -ge "$LIMIT" ]; then
request_replacement "LiveKit unhealthy for $((NOW - FAIL_SINCE)) s (curl exit $RC)"
fi
fi
fi
sleep "$INTERVAL_SEC"
done
'''
var openviduMediaHealthUnit = '''
[Unit]
Description=OpenVidu Media Node health watchdog
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/openvidu-media-health.sh
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
'''
var media_replaceScriptMediaTemplate = '''
#!/bin/bash
# openvidu-media-replace.sh "<reason>"
# Requests the reimage of THIS Media Node, keeping the scale set capacity and the
# scale-in protection: reimage provisions a new OS disk and the userData bootstrap runs
# again on the same instance id. Called by the health watchdog; it never signals it.
mkdir -p /var/lib/openvidu-media-health
echo "$(date -u +%FT%TZ) $1" >> /var/lib/openvidu-media-health/replace-requests.log
touch /run/openvidu-unhealthy
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
INSTANCE_RESOURCE_ID=$(curl -s -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo "$INSTANCE_RESOURCE_ID" | awk -F'/' '{print $NF}')
if [ -z "$INSTANCE_ID" ]; then
echo "[media-replace] could not read this instance id from IMDS"
exit 1
fi
n=0
while true; do
n=$((n + 1))
if az login --identity >/dev/null 2>&1 && \
az vmss reimage --resource-group "$RESOURCE_GROUP_NAME" --name "$VM_SCALE_SET_NAME" --instance-ids "$INSTANCE_ID" --no-wait; then
echo "[media-replace] reimage requested for instance $INSTANCE_ID ($1)"
exit 0
fi
if [ "$n" -ge 5 ]; then
echo "[media-replace] reimage could not be requested after $n attempts"
exit 1
fi
echo "[media-replace] reimage request failed (attempt $n), retrying in 12s"
sleep 12
done
'''
var userDataMediaNodeTemplate = '''
#!/bin/bash
set -eu -o pipefail
# Mark this media node's bootstrap as failed so the health watchdog replaces it
ov_media_bootstrap_failed() {
echo "[OpenVidu] media node bootstrap failed: $1"
mkdir -p /var/lib/openvidu-media-health
echo "$1" > /var/lib/openvidu-media-health/bootstrap-failed
exit 1
}
# Introduce the scripts in the instance
# install.sh
echo ${base64install} | base64 -d > /usr/local/bin/install.sh
chmod +x /usr/local/bin/install.sh
# stop_media_nodes.sh
echo ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh
chmod +x /usr/local/bin/stop_media_node.sh
# openvidu-media-health.sh (health watchdog), openvidu-media-replace.sh (reimage this node) and its unit
echo ${base64mediaHealth} | base64 -d > /usr/local/bin/openvidu-media-health.sh
chmod +x /usr/local/bin/openvidu-media-health.sh
echo ${base64mediaReplace} | base64 -d > /usr/local/bin/openvidu-media-replace.sh
chmod +x /usr/local/bin/openvidu-media-replace.sh
echo ${base64mediaHealthUnit} | base64 -d > /etc/systemd/system/openvidu-media-health.service
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y jq
# Install azure cli
AZURE_CLI_VERSION=2.90.0
apt-get install -y apt-transport-https ca-certificates gnupg lsb-release
curl -sLS https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/azure-cli.list
apt-get update
apt-get install -y azure-cli=${AZURE_CLI_VERSION}-1~$(lsb_release -cs)
az login --identity
# Protect from scale in actions
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')
az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-id $INSTANCE_ID --protect-from-scale-in true
export HOME="/root"
# Start the Media Node health watchdog before install.sh (az CLI is installed and the node
# is scale-in protected): it backstops a hung or failed bootstrap and, once running, reimages
# the node if LiveKit stays unhealthy. Write MASTER_ADDRS first so its master guard is armed.
mkdir -p /etc/openvidu
echo 'MASTER_ADDRS="${privateIPMasterNode}:7000"' > /etc/openvidu/media-health.env
systemctl daemon-reload
systemctl enable --now openvidu-media-health
# Install OpenVidu
/usr/local/bin/install.sh || ov_media_bootstrap_failed "install.sh failed"
# Gate 2: wait for master readiness before starting
WAIT_INTERVAL=5
MAX_RETRIES=360
RETRIES=0
set +e
while true; do
FINISH_MASTER_NODE=$(az keyvault secret show --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --query value -o tsv 2>/dev/null)
if [ "$FINISH_MASTER_NODE" == "true" ]; then
break
fi
RETRIES=$((RETRIES + 1))
if [ $RETRIES -ge $MAX_RETRIES ]; then
ov_media_bootstrap_failed "timed out after 30 min waiting for FINISH-MASTER-NODE"
fi
sleep $WAIT_INTERVAL
done
set -e
# Start OpenVidu
systemctl start openvidu || ov_media_bootstrap_failed "systemctl start openvidu failed"
# Bootstrap finished: the watchdog switches from bootstrap-deadline mode to runtime health checks
mkdir -p /var/lib/openvidu-media-health
touch /var/lib/openvidu-media-health/installed
'''
var installScriptMedia = reduce(
items(stringInterpolationParamsMedia),
{ value: installScriptTemplateMedia },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64installMedia = base64(installScriptMedia)
var stop_media_nodesScriptMedia = reduce(
items(stopMediaNodeParams),
{ value: stop_media_nodesScriptMediaTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64stopMediaNode = base64(stop_media_nodesScriptMedia)
var media_replaceScriptMedia = reduce(
items(stopMediaNodeParams),
{ value: media_replaceScriptMediaTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64media_replaceScriptMedia = base64(media_replaceScriptMedia)
var base64openviduMediaHealth = base64(openviduMediaHealthScript)
var base64openviduMediaHealthUnit = base64(openviduMediaHealthUnit)
var userDataParamsMedia = {
base64install: base64installMedia
base64stop: base64stopMediaNode
base64mediaHealth: base64openviduMediaHealth
base64mediaHealthUnit: base64openviduMediaHealthUnit
base64mediaReplace: base64media_replaceScriptMedia
privateIPMasterNode: netInterfaceMasterNode.properties.ipConfigurations[0].properties.privateIPAddress
resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet'
keyVaultName: keyVaultName
}
var userDataMediaNode = reduce(
items(userDataParamsMedia),
{ value: userDataMediaNodeTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64userDataMediaNode = base64(userDataMediaNode)
param datetime string = utcNow('u')
resource openviduScaleSetMediaNode 'Microsoft.Compute/virtualMachineScaleSets@2024-07-01' = {
name: '${stackName}-mediaNodeScaleSet'
location: location
tags: {
InstanceDeleteTime: datetime
storageAccount: isEmptyStorageAccountName ? storageAccount.name : exisitngStorageAccount.name
}
identity: { type: 'SystemAssigned' }
sku: {
name: mediaNodeInstanceType
tier: 'Standard'
capacity: initialNumberOfMediaNodes
}
properties: {
overprovision: true
upgradePolicy: {
mode: 'Automatic'
}
singlePlacementGroup: true
platformFaultDomainCount: 1
virtualMachineProfile: {
storageProfile: {
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: mediaNodeVMSettings.osDiskType
}
diskSizeGB: 50
}
imageReference: mediaNodeVMSettings.ubuntuOSVersion
}
osProfile: {
computerNamePrefix: mediaNodeVMSettings.vmName
adminUsername: adminUsername
linuxConfiguration: mediaNodeVMSettings.linuxConfiguration
}
networkProfile: {
networkInterfaceConfigurations: [
{
name: '${stackName}-mediaNodeNetInterface'
properties: {
primary: true
ipConfigurations: [
{
name: 'ipconfigMediaNode'
properties: {
subnet: {
id: vnet_OV.properties.subnets[0].id
}
applicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
publicIPAddressConfiguration: {
name: 'publicIPAddressMediaNode'
properties: {
publicIPAddressVersion: 'IPv4'
}
}
}
}
]
networkSecurityGroup: {
id: openviduMediaNodeNSG.id
}
}
}
]
}
userData: base64userDataMediaNode
}
}
}
resource openviduAutoScaleSettings 'Microsoft.Insights/autoscaleSettings@2022-10-01' = {
name: '${stackName}-autoscaleSettings'
location: resourceGroup().location
properties: {
profiles: [
{
name: 'openvidu-medianode-autoscale'
capacity: {
minimum: string(minNumberOfMediaNodes)
maximum: string(maxNumberOfMediaNodes)
default: string(initialNumberOfMediaNodes)
}
rules: [
{
metricTrigger: {
metricName: 'Percentage CPU'
metricNamespace: 'Microsoft.Compute/virtualMachineScaleSets'
metricResourceUri: openviduScaleSetMediaNode.id
statistic: 'Average'
operator: 'GreaterThan'
threshold: scaleTargetCPU
timeAggregation: 'Average'
timeWindow: 'PT5M'
timeGrain: 'PT1M'
}
scaleAction: {
direction: 'Increase'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
{
metricTrigger: {
metricName: 'Percentage CPU'
metricNamespace: 'Microsoft.Compute/virtualMachineScaleSets'
metricResourceUri: openviduScaleSetMediaNode.id
statistic: 'Average'
operator: 'LessThan'
threshold: scaleTargetCPU
timeAggregation: 'Average'
timeWindow: 'PT5M'
timeGrain: 'PT1M'
}
scaleAction: {
direction: 'Decrease'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
]
}
]
enabled: true
targetResourceUri: openviduScaleSetMediaNode.id
}
}
/*------------------------------------------- SCALE IN ------------------------------------------*/
resource roleAssignmentMasterNode 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid('roleAssignmentForMasterNode${openviduMasterNode.name}')
scope: resourceGroup()
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'b24988ac-6180-42a0-ab88-20f7382dd24c'
)
principalId: openviduMasterNode.identity.principalId
principalType: 'ServicePrincipal'
}
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid('roleAssignmentForScaleSet${openviduScaleSetMediaNode.name}')
scope: resourceGroup()
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'b24988ac-6180-42a0-ab88-20f7382dd24c'
)
principalId: openviduScaleSetMediaNode.identity.principalId
principalType: 'ServicePrincipal'
}
}
@description('Automation Account Name to create a runbook inside it for scale in')
param automationAccountName string = ''
var isEmptyAutomationAccountName = automationAccountName == ''
module webhookModule '../../shared/webhookdeployment.json' = {
params: {
automationAccountName: isEmptyAutomationAccountName
? uniqueString(resourceGroup().id, openviduMasterNode.id)
: automationAccountName
runbookName: 'scaleInRunbook'
webhookName: 'webhookForScaleIn'
WebhookExpiryTime: '2035-03-30T00:00:00Z'
_artifactsLocation: 'https://raw.githubusercontent.com/OpenVidu/openvidu/refs/heads/master/openvidu-deployment/pro/shared/scaleInRunbook.ps1'
}
name: 'WebhookDeployment'
}
resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = {
name: 'actiongroupScaleIn'
location: 'global'
properties: {
groupShortName: 'scaleinag'
enabled: true
automationRunbookReceivers: [
{
name: 'scalein'
useCommonAlertSchema: false
automationAccountId: webhookModule.outputs.automationAccountId
runbookName: 'scaleInRunbook'
webhookResourceId: webhookModule.outputs.webhookId
isGlobalRunbook: false
serviceUri: webhookModule.outputs.webhookUri
}
]
}
}
resource scaleInActivityLogRule 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {
name: 'ScaleInAlertRule'
location: 'global'
properties: {
scopes: [
openviduScaleSetMediaNode.id
]
condition: {
allOf: [
{
field: 'category'
equals: 'Administrative'
}
{
field: 'operationName'
equals: 'Microsoft.Compute/virtualMachineScaleSets/write'
}
{
field: 'level'
containsAny: [
'error'
]
}
{
field: 'status'
containsAny: [
'failed'
]
}
{
field: 'caller'
equals: '42628537-ebd8-40bf-941a-dddd338e1fe9'
}
]
}
actions: {
actionGroups: [
{
actionGroupId: actionGroupScaleIn.id
}
]
}
enabled: true
}
}
/*------------------------------------------- NETWORK -------------------------------------------*/
var ipExists = publicIpAddressObject.newOrExistingOrNone == 'existing'
resource publicIP_OV_ifExisting 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (ipExists == true) {
name: publicIpAddressObject.name
}
var ipNew = publicIpAddressObject.newOrExistingOrNone == 'new'
resource publicIP_OV_ifNew 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (ipNew == true) {
name: publicIpAddressObject.name
location: location
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
dnsSettings: {
domainNameLabel: isEmptyDomain ? toLower('${stackName}') : null
}
}
}
var publicIPId = ipNew ? publicIP_OV_ifNew.id : ipExists ? publicIP_OV_ifExisting.id : ''
resource vnet_OV 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: networkSettings.vNetName
location: location
properties: {
addressSpace: {
addressPrefixes: [
networkSettings.vNetAddressPrefix
]
}
subnets: [
{
name: 'subnetForMediaNodes'
properties: {
addressPrefix: networkSettings.subnetAddressPrefixMedia
privateEndpointNetworkPolicies: 'Disabled'
privateLinkServiceNetworkPolicies: 'Enabled'
networkSecurityGroup: {
id: openviduMediaNodeNSG.id
}
}
}
]
}
}
resource subnetMasterNode 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
parent: vnet_OV
name: 'firstSubnetForMasterNodes'
properties: {
addressPrefix: networkSettings.subnetAddressPrefixMaster1
privateEndpointNetworkPolicies: 'Disabled'
privateLinkServiceNetworkPolicies: 'Enabled'
}
}
resource netInterfaceMasterNode 'Microsoft.Network/networkInterfaces@2023-11-01' = {
name: '${stackName}-masterNoderNetInterface'
location: location
properties: {
ipConfigurations: [
{
name: 'primaryIPConfig'
properties: {
privateIPAllocationMethod: 'Dynamic'
subnet: {
id: subnetMasterNode.id
}
applicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
publicIPAddress: isEmptyIp
? null
: {
id: ipNew ? publicIP_OV_ifNew.id : publicIP_OV_ifExisting.id
}
}
}
]
networkSecurityGroup: {
id: openviduMasterNodeNSG.id
}
}
}
resource openviduMasterNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: '${stackName}-masterNoderNSG'
location: location
properties: {
securityRules: [
{
name: 'SSH'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '22'
access: 'Allow'
priority: 100
direction: 'Inbound'
}
}
{
name: 'HTTP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '80'
access: 'Allow'
priority: 110
direction: 'Inbound'
}
}
{
name: 'HTTPS'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '443'
access: 'Allow'
priority: 120
direction: 'Inbound'
}
}
{
name: 'RTMP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '1935'
access: 'Allow'
priority: 130
direction: 'Inbound'
}
}
{
name: 'MinIO'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '9000'
access: 'Allow'
priority: 140
direction: 'Inbound'
}
}
]
}
}
resource openviduMasterNodeASG 'Microsoft.Network/applicationSecurityGroups@2024-03-01' = {
name: '${stackName}-masterNodeASG'
location: location
}
resource mediaToMasterClusterIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_CLUSTER_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '7880'
access: 'Allow'
priority: 145
direction: 'Inbound'
}
}
resource mediaToMasterRedisIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_REDIS_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '7000'
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
resource mediaToMasterMinioIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_MINIO_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '9100'
access: 'Allow'
priority: 160
direction: 'Inbound'
}
}
resource mediaToMasterMongoIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_MONGO_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '20000'
access: 'Allow'
priority: 170
direction: 'Inbound'
}
}
resource mediaToMasterLokiIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_LOKI_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '3100'
access: 'Allow'
priority: 180
direction: 'Inbound'
}
}
resource mediaToMasterMimirIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_MIMIR_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '9009'
access: 'Allow'
priority: 190
direction: 'Inbound'
}
}
resource mediaToMasterV2CompatibilityWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_V2COMPATIBILITY_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '4443'
access: 'Allow'
priority: 200
direction: 'Inbound'
}
}
resource mediaToMasterMeetWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '9080'
access: 'Allow'
priority: 210
direction: 'Inbound'
}
}
resource mediaToMasterCustomAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_CUSTOM_APP_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
destinationPortRange: '6080'
access: 'Allow'
priority: 220
direction: 'Inbound'
}
}
resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: '${stackName}-mediaNoderNSG'
location: location
properties: {
securityRules: [
{
name: 'SSH'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '22'
access: 'Allow'
priority: 100
direction: 'Inbound'
}
}
{
name: 'TURN'
properties: {
protocol: 'Udp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '443'
access: 'Allow'
priority: 110
direction: 'Inbound'
}
}
{
name: 'WebRTC_over_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '7881'
access: 'Allow'
priority: 120
direction: 'Inbound'
}
}
{
name: 'WebRTC_using_WHIP'
properties: {
protocol: 'Udp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '7885'
access: 'Allow'
priority: 130
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_UDP'
properties: {
protocol: 'Udp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '50000-60000'
access: 'Allow'
priority: 140
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
]
}
}
resource openviduMediaNodeASG 'Microsoft.Network/applicationSecurityGroups@2024-03-01' = {
name: '${stackName}-mediaNodeASG'
location: location
}
resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMediaNodeNSG
name: 'masterNode_to_mediaNode_RTMP_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
destinationPortRange: '1935'
access: 'Allow'
priority: 160
direction: 'Inbound'
}
}
resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMediaNodeNSG
name: 'masterNode_to_mediaNode_TURN_TLS_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
destinationPortRange: '5349'
access: 'Allow'
priority: 170
direction: 'Inbound'
}
}
resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMediaNodeNSG
name: 'masterNode_to_mediaNode_SERVER_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
destinationPortRange: '7880'
access: 'Allow'
priority: 180
direction: 'Inbound'
}
}
resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMediaNodeNSG
name: 'masterNode_to_mediaNode_HTTP_WHIP_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
{
id: openviduMasterNodeASG.id
}
]
sourcePortRange: '*'
destinationApplicationSecurityGroups: [
{
id: openviduMediaNodeASG.id
}
]
destinationPortRange: '8080'
access: 'Allow'
priority: 190
direction: 'Inbound'
}
}
/*------------------------------------------- STORAGE ACCOUNT ----------------------------------------*/
@description('Name of the existing storage account. It is essential that this parameter is filled just when you want to save recordings and still using the same container after an update. If not specified, a new storage account will be generated.')
param storageAccountName string = ''
var isEmptyStorageAccountName = storageAccountName == ''
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: uniqueString(resourceGroup().id)
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Cool'
supportsHttpsTrafficOnly: true
}
}
resource exisitngStorageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = if (isEmptyStorageAccountName == false) {
name: storageAccountName
}
resource blobContainerScaleIn 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: '${storageAccount.name}/default/automation-locks'
properties: {
publicAccess: 'None'
}
}
@description('Name of the bucket where OpenVidu will store the recordings if a new Storage account is being creating. If not specified, a default bucket will be created. If you want to use an existing storage account, fill this parameter with the name of the container where the recordings are stored.')
param containerName string = ''
var isEmptyContainerName = containerName == ''
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: isEmptyContainerName
? '${storageAccount.name}/default/openvidu-appdata'
: '${storageAccount.name}/default/${containerName}'
properties: {
publicAccess: 'None'
}
}