@description('Stack name. Maximum 15 characters: used as the Azure Key Vault name prefix ("-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 " 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; LiveKit failures are not counted until a master answers again. Once requested, # the replacement is requested again every REPLACE_RETRY_SEC until the node goes away. # # Settings: /etc/openvidu/media-health.env (sourced every cycle; MASTER_ADDRS="ip:port ...") # Pause: touch /etc/openvidu/media-health.disabled (drain scripts use /run/openvidu-media-health.paused) # Logs: journalctl -u openvidu-media-health # # This file is embedded verbatim in CloudFormation !Sub blocks, Bicep strings and # Terraform heredocs, so it must never contain a dollar sign or a percent sign followed # by an opening brace, nor two opening braces in a row. STATE_DIR=/var/lib/openvidu-media-health INSTALLED_MARKER="$STATE_DIR/installed" BOOTSTRAP_FAILED_MARKER="$STATE_DIR/bootstrap-failed" REPLACE_SCRIPT=/usr/local/bin/openvidu-media-replace.sh ENV_FILE=/etc/openvidu/media-health.env PAUSE_FILE=/etc/openvidu/media-health.disabled DRAIN_PAUSE_FILE=/run/openvidu-media-health.paused log() { echo "[media-health] $*"; } # Seconds since boot: monotonic, unaffected by clock steps uptime_sec() { cut -d. -f1 /proc/uptime; } # Busy CPU percentage since the previous call, in BUSY (0 on the first call) cpu_busy() { read -r _ c_user c_nice c_system c_idle c_iowait c_irq c_softirq c_steal _ < /proc/stat total=$((c_user + c_nice + c_system + c_idle + c_iowait + c_irq + c_softirq + c_steal)) idle=$((c_idle + c_iowait)) BUSY=0 if [ -n "$PREV_TOTAL" ] && [ "$total" -gt "$PREV_TOTAL" ]; then BUSY=$((100 * (total - PREV_TOTAL - idle + PREV_IDLE) / (total - PREV_TOTAL))) fi PREV_TOTAL=$total PREV_IDLE=$idle } # openvidu.service is running and entered the active state less than WARMUP_SEC ago # (a stopped or failed openvidu.service is not starting: no warm-up for it) openvidu_starting() { case "$(systemctl show -p ActiveState --value openvidu 2>/dev/null)" in active|activating|reloading) ;; *) return 1 ;; esac since=$(systemctl show -p ActiveEnterTimestampMonotonic --value openvidu 2>/dev/null) case "$since" in ''|0|*[!0-9]*) return 1 ;; esac [ $(($(uptime_sec) - since / 1000000)) -lt "$WARMUP_SEC" ] } # A master answers a Redis PING (+PONG, or -NOAUTH when a password is required). A bare # TCP connect would also succeed against a wedged Redis. master_serving() { [ -n "$MASTER_ADDRS" ] || return 1 for addr in $MASTER_ADDRS; do host=$(echo "$addr" | cut -d: -f1) port=$(echo "$addr" | cut -d: -f2) reply=$(timeout 5 bash -c 'exec 3<>"/dev/tcp/$0/$1" && printf "PING\r\n" >&3 && head -c 7 <&3' "$host" "$port" 2>/dev/null) case "$reply" in +PONG*|-NOAUTH*) return 0 ;; esac done return 1 } # Request the replacement of this node ($1 = reason). Gated on a master answering, except # in the bootstrap phase before the master addresses are known. request_replacement() { now=$(uptime_sec) if [ -n "$REQUESTED_AT" ] && [ $((now - REQUESTED_AT)) -lt "$REPLACE_RETRY_SEC" ]; then return fi if [ -n "$MASTER_ADDRS" ] || [ -f "$INSTALLED_MARKER" ]; then if ! master_serving; then if [ -z "$HOLD_LOGGED_AT" ] || [ $((now - HOLD_LOGGED_AT)) -ge 600 ]; then log "$1, but no master answers a Redis PING (MASTER_ADDRS='$MASTER_ADDRS'): not replacing this node" HOLD_LOGGED_AT=$now fi return fi fi log "$1: requesting the replacement of this node" if "$REPLACE_SCRIPT" "$1"; then REQUESTED_AT=$now log "replacement requested" else log "the replacement request failed, retrying in $INTERVAL_SEC s" fi } mkdir -p "$STATE_DIR" REQUESTED_AT="" HOLD_LOGGED_AT="" FAILED_SEEN_AT="" FAIL_SINCE="" SATURATED="" PREV_TOTAL="" PREV_IDLE="" log "started" while true; do PROBE_URL=http://127.0.0.1:7880/ INTERVAL_SEC=30 HARD_UNHEALTHY_AFTER_SEC=300 SOFT_UNHEALTHY_AFTER_SEC=600 CPU_SATURATED_PCT=90 WARMUP_SEC=900 BOOTSTRAP_DEADLINE_SEC=5400 BOOTSTRAP_FAILURE_COOLDOWN_SEC=600 REPLACE_RETRY_SEC=600 MASTER_ADDRS="" [ -f "$ENV_FILE" ] && . "$ENV_FILE" NOW=$(uptime_sec) cpu_busy if [ -f "$PAUSE_FILE" ] || [ -f "$DRAIN_PAUSE_FILE" ]; then FAIL_SINCE="" elif [ -f "$BOOTSTRAP_FAILED_MARKER" ]; then if [ -z "$FAILED_SEEN_AT" ]; then FAILED_SEEN_AT=$NOW log "bootstrap failed ($(head -c 300 "$BOOTSTRAP_FAILED_MARKER")): replacing this node in $BOOTSTRAP_FAILURE_COOLDOWN_SEC s" fi if [ $((NOW - FAILED_SEEN_AT)) -ge "$BOOTSTRAP_FAILURE_COOLDOWN_SEC" ]; then request_replacement "bootstrap failed: $(head -c 300 "$BOOTSTRAP_FAILED_MARKER")" fi elif [ ! -f "$INSTALLED_MARKER" ]; then if [ "$NOW" -ge "$BOOTSTRAP_DEADLINE_SEC" ]; then request_replacement "bootstrap not finished $BOOTSTRAP_DEADLINE_SEC s after boot" fi elif curl -sf -o /dev/null --connect-timeout 3 --max-time 15 "$PROBE_URL"; then [ -n "$FAIL_SINCE" ] && log "LiveKit healthy again after $((NOW - FAIL_SINCE)) s" FAIL_SINCE="" SATURATED="" else RC=$? case "$RC" in 7|52|56) KIND=hard ;; *) KIND=soft ;; esac if openvidu_starting; then FAIL_SINCE="" elif ! master_serving; then # A master outage fails the health check of every Media Node at once: not counted, # so LiveKit gets the whole window to reconnect once a master answers again FAIL_SINCE="" if [ -z "$HOLD_LOGGED_AT" ] || [ $((NOW - HOLD_LOGGED_AT)) -ge 600 ]; then log "LiveKit health check failed (curl exit $RC), but no master answers a Redis PING (MASTER_ADDRS='$MASTER_ADDRS'): not counted" HOLD_LOGGED_AT=$NOW fi elif [ "$KIND" = soft ] && [ "$BUSY" -ge "$CPU_SATURATED_PCT" ]; then [ -n "$SATURATED" ] || log "LiveKit not ready (curl exit $RC) with the CPU $BUSY% busy: not counted" SATURATED=1 FAIL_SINCE="" else SATURATED="" if [ -z "$FAIL_SINCE" ]; then FAIL_SINCE=$NOW log "LiveKit health check failed (curl exit $RC)" fi LIMIT=$SOFT_UNHEALTHY_AFTER_SEC [ "$KIND" = hard ] && LIMIT=$HARD_UNHEALTHY_AFTER_SEC if [ $((NOW - FAIL_SINCE)) -ge "$LIMIT" ]; then request_replacement "LiveKit unhealthy for $((NOW - FAIL_SINCE)) s (curl exit $RC)" fi fi fi sleep "$INTERVAL_SEC" done ''' 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 "" # 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 # Withdrawn if the request fails: the node then stays, and if it recovers # a later scale-in must still wait for its sessions trap '[ $? -eq 0 ] || rm -f /run/openvidu-unhealthy' EXIT 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' } }