mirror of https://github.com/OpenVidu/openvidu.git
3311 lines
114 KiB
Bicep
3311 lines
114 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('Domain name for the OpenVidu Deployment.')
|
|
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('Name of the PublicIPAddress resource in Azure when using certificateType \'owncert\' or \'letsencrypt\'')
|
|
param publicIpAddressObject object
|
|
|
|
@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('Size of the disk in GB')
|
|
param masterNodesDiskSize int = 200
|
|
|
|
@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 for the Virtual Machine.')
|
|
@secure()
|
|
param adminSshKey object
|
|
|
|
@description('Number of initial media nodes to deploy. Ignored when fixedNumberOfMediaNodes > 0')
|
|
param initialNumberOfMediaNodes int = 1
|
|
|
|
@description('Minimum number of media nodes to deploy. Ignored when fixedNumberOfMediaNodes > 0')
|
|
param minNumberOfMediaNodes int = 1
|
|
|
|
@description('Maximum number of media nodes to deploy. Ignored when fixedNumberOfMediaNodes > 0')
|
|
param maxNumberOfMediaNodes int = 5
|
|
|
|
@description('Target CPU percentage to scale up or down. Ignored when fixedNumberOfMediaNodes > 0')
|
|
param scaleTargetCPU int = 50
|
|
|
|
@description('If > 0, keeps this fixed number of media nodes: autoscaling is pinned to it (minimum = maximum = this number) and initialNumberOfMediaNodes, minNumberOfMediaNodes, maxNumberOfMediaNodes and scaleTargetCPU are ignored. When it is lowered, the extra media nodes are drained gracefully, one at a time, by the scale-in runbook. If 0 (default), the deployment is elastic and autoscaling is enabled')
|
|
@minValue(0)
|
|
param fixedNumberOfMediaNodes int = 0
|
|
|
|
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 = {
|
|
osDiskType: 'StandardSSD_LRS'
|
|
osDiskSize: masterNodesDiskSize
|
|
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}-VN-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 keyVaultName = '${stackName}-keyvault'
|
|
|
|
var location = resourceGroup().location
|
|
|
|
var tenantId = subscription().tenantId
|
|
|
|
var deploymentUser = az.deployer().objectId
|
|
|
|
/*------------------------------------------- MANAGED IDENTITIES -------------------------------------------*/
|
|
|
|
// Split in two identities to keep the permission asymmetry: masters write secrets, media nodes only read them
|
|
resource masterIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
|
|
name: '${stackName}-master-identity'
|
|
location: location
|
|
}
|
|
|
|
resource mediaIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
|
|
name: '${stackName}-media-identity'
|
|
location: location
|
|
}
|
|
|
|
/*------------------------------------------- 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: [
|
|
// Pre-created identities: the vault no longer waits for the VMs to exist
|
|
{
|
|
objectId: masterIdentity.properties.principalId
|
|
tenantId: tenantId
|
|
permissions: {
|
|
secrets: ['get', 'set', 'list']
|
|
}
|
|
}
|
|
{
|
|
objectId: mediaIdentity.properties.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 stringInterpolationParamsMaster1 = {
|
|
publicIPId: publicIPId
|
|
domainName: domainName
|
|
certificateType: certificateType
|
|
ownPublicCertificate: ownPublicCertificate
|
|
ownPrivateCertificate: ownPrivateCertificate
|
|
openviduLicense: openviduLicense
|
|
rtcEngine: rtcEngine
|
|
initialMeetAdminPassword: initialMeetAdminPassword
|
|
initialMeetApiKey: initialMeetApiKey
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '1'
|
|
additionalInstallFlags: additionalInstallFlags
|
|
}
|
|
|
|
var stringInterpolationParamsMaster2 = {
|
|
publicIPId: publicIPId
|
|
domainName: domainName
|
|
certificateType: certificateType
|
|
ownPublicCertificate: ownPublicCertificate
|
|
ownPrivateCertificate: ownPrivateCertificate
|
|
openviduLicense: openviduLicense
|
|
rtcEngine: rtcEngine
|
|
initialMeetAdminPassword: initialMeetAdminPassword
|
|
initialMeetApiKey: initialMeetApiKey
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '2'
|
|
additionalInstallFlags: additionalInstallFlags
|
|
}
|
|
|
|
var stringInterpolationParamsMaster3 = {
|
|
publicIPId: publicIPId
|
|
domainName: domainName
|
|
certificateType: certificateType
|
|
ownPublicCertificate: ownPublicCertificate
|
|
ownPrivateCertificate: ownPrivateCertificate
|
|
openviduLicense: openviduLicense
|
|
rtcEngine: rtcEngine
|
|
initialMeetAdminPassword: initialMeetAdminPassword
|
|
initialMeetApiKey: initialMeetApiKey
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '3'
|
|
additionalInstallFlags: additionalInstallFlags
|
|
}
|
|
|
|
var stringInterpolationParamsMaster4 = {
|
|
publicIPId: publicIPId
|
|
domainName: domainName
|
|
certificateType: certificateType
|
|
ownPublicCertificate: ownPublicCertificate
|
|
ownPrivateCertificate: ownPrivateCertificate
|
|
openviduLicense: openviduLicense
|
|
rtcEngine: rtcEngine
|
|
initialMeetAdminPassword: initialMeetAdminPassword
|
|
initialMeetApiKey: initialMeetApiKey
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '4'
|
|
additionalInstallFlags: additionalInstallFlags
|
|
}
|
|
|
|
var installScriptTemplateMaster = '''
|
|
#!/bin/bash -x
|
|
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
|
|
|
|
|
|
# Wait for the keyvault availability
|
|
MAX_WAIT=300
|
|
WAIT_INTERVAL=1
|
|
ELAPSED_TIME=0
|
|
set +e
|
|
while true; do
|
|
# Check keyvault availability
|
|
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
|
|
|
|
# Esperar antes de la próxima comprobación
|
|
sleep $WAIT_INTERVAL
|
|
done
|
|
set -e
|
|
|
|
MASTER_NODE_NUM=${masterNodeNum}
|
|
|
|
# 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 current private IP
|
|
PRIVATE_IP="$(/usr/local/bin/store_secret.sh save MASTER-NODE-${masterNodeNum}-PRIVATE-IP $PRIVATE_IP)"
|
|
|
|
|
|
if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "" || "$ALL_SECRETS_GENERATED" == "false" ]]; then
|
|
|
|
# Configure Domain name
|
|
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
|
|
DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN-NAME "${DOMAIN}")"
|
|
|
|
# Publish service URLs early (idempotent; after_install re-writes byte-identical values)
|
|
OPENVIDU_URL="$(/usr/local/bin/store_secret.sh save OPENVIDU-URL "https://${DOMAIN}/")"
|
|
LIVEKIT_URL="$(/usr/local/bin/store_secret.sh save LIVEKIT-URL "wss://${DOMAIN}/")"
|
|
DASHBOARD_URL="$(/usr/local/bin/store_secret.sh save DASHBOARD-URL "https://${DOMAIN}/dashboard/")"
|
|
GRAFANA_URL="$(/usr/local/bin/store_secret.sh save GRAFANA-URL "https://${DOMAIN}/grafana/")"
|
|
MINIO_URL="$(/usr/local/bin/store_secret.sh save MINIO-URL "https://${DOMAIN}/minio-console/")"
|
|
|
|
# Meet initial admin user and password
|
|
MEET_INITIAL_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-INITIAL-ADMIN-USER "admin")"
|
|
if [[ "${initialMeetAdminPassword}" != '' ]]; then
|
|
MEET_INITIAL_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh save MEET-INITIAL-ADMIN-PASSWORD "${initialMeetAdminPassword}")"
|
|
else
|
|
MEET_INITIAL_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MEET-INITIAL-ADMIN-PASSWORD)"
|
|
fi
|
|
if [[ "${initialMeetApiKey}" != '' ]]; then
|
|
MEET_INITIAL_API_KEY="$(/usr/local/bin/store_secret.sh save MEET-INITIAL-API-KEY "${initialMeetApiKey}")"
|
|
else
|
|
MEET_INITIAL_API_KEY="$(/usr/local/bin/store_secret.sh save MEET-INITIAL-API-KEY "")"
|
|
fi
|
|
|
|
# Store usernames and generate random passwords
|
|
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")"
|
|
fi
|
|
|
|
IP_WAIT_MAX_RETRIES=360
|
|
IP_WAIT_INTERVAL=5
|
|
IP_WAIT_RETRIES=0
|
|
while true; do
|
|
MASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv) || true
|
|
MASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv) || true
|
|
MASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv) || true
|
|
MASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv) || true
|
|
# Check if all master nodes have stored their private IPs
|
|
if [[ "$MASTER_NODE_1_PRIVATE_IP" != "" ]] &&
|
|
[[ "$MASTER_NODE_2_PRIVATE_IP" != "" ]] &&
|
|
[[ "$MASTER_NODE_3_PRIVATE_IP" != "" ]] &&
|
|
[[ "$MASTER_NODE_4_PRIVATE_IP" != "" ]]; then
|
|
break
|
|
fi
|
|
IP_WAIT_RETRIES=$((IP_WAIT_RETRIES + 1))
|
|
if [ $IP_WAIT_RETRIES -ge $IP_WAIT_MAX_RETRIES ]; then
|
|
echo "[OpenVidu] timed out after 30 min waiting for the 4 master nodes to publish their private IPs"
|
|
exit 1
|
|
fi
|
|
sleep $IP_WAIT_INTERVAL
|
|
done
|
|
|
|
# Wait until master-node-1 has generated all shared secrets before fetching them
|
|
SECRETS_WAIT_MAX_RETRIES=360
|
|
SECRETS_WAIT_INTERVAL=5
|
|
SECRETS_WAIT_RETRIES=0
|
|
while [[ "$(az keyvault secret show --vault-name ${keyVaultName} --name ALL-SECRETS-GENERATED --query value -o tsv 2>/dev/null)" != "true" ]]; do
|
|
SECRETS_WAIT_RETRIES=$((SECRETS_WAIT_RETRIES + 1))
|
|
if [ $SECRETS_WAIT_RETRIES -ge $SECRETS_WAIT_MAX_RETRIES ]; then
|
|
echo "[OpenVidu] timed out after 30 min waiting for ALL-SECRETS-GENERATED to become true"
|
|
exit 1
|
|
fi
|
|
sleep $SECRETS_WAIT_INTERVAL
|
|
done
|
|
|
|
|
|
# Fetch the values in the keyvault
|
|
MASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_PRIVATE_IP_LIST="$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP"
|
|
|
|
DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)
|
|
OPENVIDU_RTC_ENGINE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --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)
|
|
MONGO_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --query value -o tsv)
|
|
MONGO_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --query value -o tsv)
|
|
MONGO_REPLICA_SET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --query value -o tsv)
|
|
DASHBOARD_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --query value -o tsv)
|
|
DASHBOARD_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --query value -o tsv)
|
|
MINIO_ACCESS_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --query value -o tsv)
|
|
MINIO_SECRET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --query value -o tsv)
|
|
GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --query value -o tsv)
|
|
GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
|
|
LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
|
|
LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
|
|
MEET_INITIAL_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-INITIAL-ADMIN-USER --query value -o tsv)
|
|
MEET_INITIAL_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-INITIAL-ADMIN-PASSWORD --query value -o tsv)
|
|
if [[ "${initialMeetApiKey}" != '' ]]; then
|
|
MEET_INITIAL_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-INITIAL-API-KEY --query value -o tsv)
|
|
else
|
|
MEET_INITIAL_API_KEY=""
|
|
fi
|
|
ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
|
|
|
|
|
|
# Download first: sh <(curl ...) would silently run an empty script on a transient curl failure
|
|
INSTALLER_SCRIPT="/tmp/install_ov_master_node.sh"
|
|
curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o "$INSTALLER_SCRIPT" "https://get.openvidu.io/pro/ha/$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='ha'"
|
|
"--node-role='master-node'"
|
|
"--external-load-balancer"
|
|
"--internal-tls-termination"
|
|
"--master-node-private-ip-list='$MASTER_NODE_PRIVATE_IP_LIST'"
|
|
"--openvidu-pro-license='$OPENVIDU_PRO_LICENSE'"
|
|
"--domain-name='$DOMAIN'"
|
|
"--enabled-modules='$ENABLED_MODULES'"
|
|
"--rtc-engine=$OPENVIDU_RTC_ENGINE"
|
|
"--redis-password=$REDIS_PASSWORD"
|
|
"--mongo-admin-user=$MONGO_ADMIN_USERNAME"
|
|
"--mongo-admin-password=$MONGO_ADMIN_PASSWORD"
|
|
"--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY"
|
|
"--minio-access-key=$MINIO_ACCESS_KEY"
|
|
"--minio-secret-key=$MINIO_SECRET_KEY"
|
|
"--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME"
|
|
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
|
|
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
|
|
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
|
|
"--meet-initial-admin-password=$MEET_INITIAL_ADMIN_PASSWORD"
|
|
"--meet-initial-api-key=$MEET_INITIAL_API_KEY"
|
|
"--livekit-api-key=$LIVEKIT_API_KEY"
|
|
"--livekit-api-secret=$LIVEKIT_API_SECRET"
|
|
)
|
|
|
|
# Include additional installer flags provided by the user
|
|
if [[ "${additionalInstallFlags}" != "" ]]; then
|
|
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
|
|
for extra_flag in "${EXTRA_FLAGS[@]}"; do
|
|
# Trim whitespace around each flag
|
|
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
|
|
if [[ "$extra_flag" != "" ]]; then
|
|
COMMON_ARGS+=("$extra_flag")
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# 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
|
|
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 --client-id ${masterIdentityClientId} --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 --client-id ${masterIdentityClientId} --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 --client-id ${masterIdentityClientId} --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 --client-id ${masterIdentityClientId} --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 --client-id ${masterIdentityClientId} --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 --client-id ${masterIdentityClientId}
|
|
|
|
# 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
|
|
|
|
# Esperar antes de la próxima comprobación
|
|
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 installScriptMaster1 = reduce(
|
|
items(stringInterpolationParamsMaster1),
|
|
{ value: installScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var installScriptMaster2 = reduce(
|
|
items(stringInterpolationParamsMaster2),
|
|
{ value: installScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var installScriptMaster3 = reduce(
|
|
items(stringInterpolationParamsMaster3),
|
|
{ value: installScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var installScriptMaster4 = reduce(
|
|
items(stringInterpolationParamsMaster4),
|
|
{ value: installScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var after_installScriptMaster = reduce(
|
|
items(stringInterpolationParamsMaster1),
|
|
{ value: after_installScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var update_config_from_secretScriptMaster = reduce(
|
|
items(stringInterpolationParamsMaster1),
|
|
{ value: update_config_from_secretScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var get_public_ip_script = reduce(
|
|
items(stringInterpolationParamsMaster1),
|
|
{ value: get_public_ip },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var update_secret_from_configScriptMaster = reduce(
|
|
items(stringInterpolationParamsMaster1),
|
|
{ value: update_secret_from_configScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var store_secretScriptMaster = reduce(
|
|
items(stringInterpolationParamsMaster1),
|
|
{ value: store_secretScriptTemplateMaster },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var blobStorageParams = {
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.name
|
|
storageAccountKey: listKeys(storageAccount.id, '2021-04-01').keys[0].value
|
|
storageAccountContainerName: isEmptyAppDataContainerName ? 'openvidu-appdata' : '${appDataContainerName}'
|
|
}
|
|
|
|
var config_blobStorageScript = reduce(
|
|
items(blobStorageParams),
|
|
{ value: config_blobStorageTemplate },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var base64installMaster1 = base64(installScriptMaster1)
|
|
var base64installMaster2 = base64(installScriptMaster2)
|
|
var base64installMaster3 = base64(installScriptMaster3)
|
|
var base64installMaster4 = base64(installScriptMaster4)
|
|
var base64after_installMaster = base64(after_installScriptMaster)
|
|
var base64get_public_ip = base64(get_public_ip_script)
|
|
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 base64check_app_readyMaster = base64(check_app_readyScriptMaster)
|
|
var base64restartMaster = base64(restartScriptMaster)
|
|
var base64config_blobStorage = base64(config_blobStorageScript)
|
|
|
|
var userDataParamsMasterNode1 = {
|
|
base64install: base64installMaster1
|
|
base64after_install: base64after_installMaster
|
|
base64get_public_ip: base64get_public_ip
|
|
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
|
|
base64check_app_ready: base64check_app_readyMaster
|
|
base64restart: base64restartMaster
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '1'
|
|
base64config_blobStorage: base64config_blobStorage
|
|
}
|
|
|
|
var userDataParamsMasterNode2 = {
|
|
base64install: base64installMaster2
|
|
base64after_install: base64after_installMaster
|
|
base64get_public_ip: base64get_public_ip
|
|
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
|
|
base64check_app_ready: base64check_app_readyMaster
|
|
base64restart: base64restartMaster
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '2'
|
|
base64config_blobStorage: base64config_blobStorage
|
|
}
|
|
|
|
var userDataParamsMasterNode3 = {
|
|
base64install: base64installMaster3
|
|
base64after_install: base64after_installMaster
|
|
base64get_public_ip: base64get_public_ip
|
|
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
|
|
base64check_app_ready: base64check_app_readyMaster
|
|
base64restart: base64restartMaster
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '3'
|
|
base64config_blobStorage: base64config_blobStorage
|
|
}
|
|
|
|
var userDataParamsMasterNode4 = {
|
|
base64install: base64installMaster4
|
|
base64after_install: base64after_installMaster
|
|
base64get_public_ip: base64get_public_ip
|
|
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
|
|
base64check_app_ready: base64check_app_readyMaster
|
|
base64restart: base64restartMaster
|
|
keyVaultName: keyVaultName
|
|
masterIdentityClientId: masterIdentity.properties.clientId
|
|
masterNodeNum: '4'
|
|
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.name
|
|
base64config_blobStorage: base64config_blobStorage
|
|
}
|
|
|
|
var userDataTemplateMasterNode = '''
|
|
#!/bin/bash -x
|
|
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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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 --client-id ${masterIdentityClientId} --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"
|
|
|
|
MASTER_NODE_NUM=${masterNodeNum}
|
|
if [[ $MASTER_NODE_NUM -eq 4 ]]; then
|
|
# Creating scale in lock
|
|
set +e
|
|
az storage blob upload --account-name ${storageAccountName} --container-name automation-locks --name lock.txt --file /dev/null --auth-mode key
|
|
set -e
|
|
fi
|
|
'''
|
|
|
|
var userDataMasterNode1 = reduce(
|
|
items(userDataParamsMasterNode1),
|
|
{ value: userDataTemplateMasterNode },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var userDataMasterNode2 = reduce(
|
|
items(userDataParamsMasterNode2),
|
|
{ value: userDataTemplateMasterNode },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var userDataMasterNode3 = reduce(
|
|
items(userDataParamsMasterNode3),
|
|
{ value: userDataTemplateMasterNode },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var userDataMasterNode4 = reduce(
|
|
items(userDataParamsMasterNode4),
|
|
{ value: userDataTemplateMasterNode },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
resource openviduMasterNode1 'Microsoft.Compute/virtualMachines@2023-09-01' = {
|
|
name: '${stackName}-VM-MasterNode1'
|
|
location: location
|
|
identity: {
|
|
type: 'UserAssigned'
|
|
userAssignedIdentities: {
|
|
'${masterIdentity.id}': {}
|
|
}
|
|
}
|
|
properties: {
|
|
hardwareProfile: {
|
|
vmSize: masterNodeInstanceType
|
|
}
|
|
storageProfile: {
|
|
osDisk: {
|
|
createOption: 'FromImage'
|
|
managedDisk: {
|
|
storageAccountType: masterNodeVMSettings.osDiskType
|
|
}
|
|
diskSizeGB: masterNodeVMSettings.osDiskSize
|
|
}
|
|
imageReference: masterNodeVMSettings.ubuntuOSVersion
|
|
}
|
|
networkProfile: {
|
|
networkInterfaces: [
|
|
{
|
|
id: netInterfaceMasterNode1.id
|
|
}
|
|
]
|
|
}
|
|
osProfile: {
|
|
computerName: '${stackName}-VM-MasterNode1'
|
|
adminUsername: adminUsername
|
|
linuxConfiguration: masterNodeVMSettings.linuxConfiguration
|
|
}
|
|
userData: base64(userDataMasterNode1)
|
|
}
|
|
}
|
|
|
|
resource openviduMasterNode2 'Microsoft.Compute/virtualMachines@2023-09-01' = {
|
|
name: '${stackName}-VM-MasterNode2'
|
|
location: location
|
|
identity: {
|
|
type: 'UserAssigned'
|
|
userAssignedIdentities: {
|
|
'${masterIdentity.id}': {}
|
|
}
|
|
}
|
|
properties: {
|
|
hardwareProfile: {
|
|
vmSize: masterNodeInstanceType
|
|
}
|
|
storageProfile: {
|
|
osDisk: {
|
|
createOption: 'FromImage'
|
|
managedDisk: {
|
|
storageAccountType: masterNodeVMSettings.osDiskType
|
|
}
|
|
diskSizeGB: masterNodeVMSettings.osDiskSize
|
|
}
|
|
imageReference: masterNodeVMSettings.ubuntuOSVersion
|
|
}
|
|
networkProfile: {
|
|
networkInterfaces: [
|
|
{
|
|
id: netInterfaceMasterNode2.id
|
|
}
|
|
]
|
|
}
|
|
osProfile: {
|
|
computerName: '${stackName}-VM-MasterNode2'
|
|
adminUsername: adminUsername
|
|
linuxConfiguration: masterNodeVMSettings.linuxConfiguration
|
|
}
|
|
userData: base64(userDataMasterNode2)
|
|
}
|
|
}
|
|
|
|
resource openviduMasterNode3 'Microsoft.Compute/virtualMachines@2023-09-01' = {
|
|
name: '${stackName}-VM-MasterNode3'
|
|
location: location
|
|
identity: {
|
|
type: 'UserAssigned'
|
|
userAssignedIdentities: {
|
|
'${masterIdentity.id}': {}
|
|
}
|
|
}
|
|
properties: {
|
|
hardwareProfile: {
|
|
vmSize: masterNodeInstanceType
|
|
}
|
|
storageProfile: {
|
|
osDisk: {
|
|
createOption: 'FromImage'
|
|
managedDisk: {
|
|
storageAccountType: masterNodeVMSettings.osDiskType
|
|
}
|
|
diskSizeGB: masterNodeVMSettings.osDiskSize
|
|
}
|
|
imageReference: masterNodeVMSettings.ubuntuOSVersion
|
|
}
|
|
networkProfile: {
|
|
networkInterfaces: [
|
|
{
|
|
id: netInterfaceMasterNode3.id
|
|
}
|
|
]
|
|
}
|
|
osProfile: {
|
|
computerName: '${stackName}-VM-MasterNode3'
|
|
adminUsername: adminUsername
|
|
linuxConfiguration: masterNodeVMSettings.linuxConfiguration
|
|
}
|
|
userData: base64(userDataMasterNode3)
|
|
}
|
|
}
|
|
|
|
resource openviduMasterNode4 'Microsoft.Compute/virtualMachines@2023-09-01' = {
|
|
name: '${stackName}-VM-MasterNode4'
|
|
location: location
|
|
identity: {
|
|
type: 'UserAssigned'
|
|
userAssignedIdentities: {
|
|
'${masterIdentity.id}': {}
|
|
}
|
|
}
|
|
properties: {
|
|
hardwareProfile: {
|
|
vmSize: masterNodeInstanceType
|
|
}
|
|
storageProfile: {
|
|
osDisk: {
|
|
createOption: 'FromImage'
|
|
managedDisk: {
|
|
storageAccountType: masterNodeVMSettings.osDiskType
|
|
}
|
|
diskSizeGB: masterNodeVMSettings.osDiskSize
|
|
}
|
|
imageReference: masterNodeVMSettings.ubuntuOSVersion
|
|
}
|
|
networkProfile: {
|
|
networkInterfaces: [
|
|
{
|
|
id: netInterfaceMasterNode4.id
|
|
}
|
|
]
|
|
}
|
|
osProfile: {
|
|
computerName: '${stackName}-VM-MasterNode4'
|
|
adminUsername: adminUsername
|
|
linuxConfiguration: masterNodeVMSettings.linuxConfiguration
|
|
}
|
|
userData: base64(userDataMasterNode4)
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------- MEDIA NODES -------------------------------------------*/
|
|
|
|
var privateIPMasterNode1 = netInterfaceMasterNode1.properties.ipConfigurations[0].properties.privateIPAddress
|
|
var privateIPMasterNode2 = netInterfaceMasterNode2.properties.ipConfigurations[0].properties.privateIPAddress
|
|
var privateIPMasterNode3 = netInterfaceMasterNode3.properties.ipConfigurations[0].properties.privateIPAddress
|
|
var privateIPMasterNode4 = netInterfaceMasterNode4.properties.ipConfigurations[0].properties.privateIPAddress
|
|
|
|
var stringInterpolationParamsMedia = {
|
|
privateIPMasterNode1: privateIPMasterNode1
|
|
privateIPMasterNode2: privateIPMasterNode2
|
|
privateIPMasterNode3: privateIPMasterNode3
|
|
privateIPMasterNode4: privateIPMasterNode4
|
|
keyVaultName: keyVaultName
|
|
}
|
|
|
|
var installScriptTemplateMedia = '''
|
|
#!/bin/bash -x
|
|
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 and IPs 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)
|
|
MASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv 2>/dev/null)
|
|
MASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv 2>/dev/null)
|
|
MASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv 2>/dev/null)
|
|
MASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv 2>/dev/null)
|
|
|
|
if [ "$ALL_SECRETS_GENERATED" == "true" ] &&
|
|
[ "$MASTER_NODE_1_PRIVATE_IP" != "" ] &&
|
|
[ "$MASTER_NODE_2_PRIVATE_IP" != "" ] &&
|
|
[ "$MASTER_NODE_3_PRIVATE_IP" != "" ] &&
|
|
[ "$MASTER_NODE_4_PRIVATE_IP" != "" ]; then
|
|
break
|
|
fi
|
|
|
|
RETRIES=$((RETRIES + 1))
|
|
if [ $RETRIES -ge $MAX_RETRIES ]; then
|
|
echo "[OpenVidu] timed out after 30 min waiting for master nodes to generate secrets and publish their IPs"
|
|
exit 1
|
|
fi
|
|
|
|
sleep $WAIT_INTERVAL
|
|
done
|
|
set -e
|
|
|
|
MASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv)
|
|
MASTER_NODE_PRIVATE_IP_LIST="$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP"
|
|
REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)
|
|
OPENVIDU_VERSION=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-VERSION --query value -o tsv)
|
|
|
|
# Download first: sh <(curl ...) would silently run an empty script on a transient curl failure
|
|
INSTALLER_SCRIPT="/tmp/install_ov_media_node.sh"
|
|
curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 -o "$INSTALLER_SCRIPT" "https://get.openvidu.io/pro/ha/$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='ha'"
|
|
"--node-role='media-node'"
|
|
"--master-node-private-ip-list=$MASTER_NODE_PRIVATE_IP_LIST"
|
|
"--private-ip=$PRIVATE_IP"
|
|
"--redis-password=$REDIS_PASSWORD"
|
|
)
|
|
|
|
# Construct the final command with all arguments
|
|
FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${COMMON_ARGS[@]}")"
|
|
|
|
# Install OpenVidu
|
|
exec bash -c "$FINAL_COMMAND"
|
|
'''
|
|
|
|
var stopMediaNodeParams = {
|
|
mediaIdentityClientId: mediaIdentity.properties.clientId
|
|
subscriptionId: subscription().subscriptionId
|
|
resourceGroupName: resourceGroup().name
|
|
vmScaleSetName: '${stackName}-mediaNodeScaleSet'
|
|
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.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 or egress
|
|
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 --client-id ${mediaIdentityClientId}
|
|
|
|
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 "<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
|
|
# 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 --client-id ${mediaIdentityClientId} >/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 -x
|
|
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 --client-id ${mediaIdentityClientId}
|
|
|
|
# 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="${privateIPMasterNode1}:7001 ${privateIPMasterNode2}:7001 ${privateIPMasterNode3}:7001 ${privateIPMasterNode4}:7001"' > /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 stop_media_nodesScriptMedia = reduce(
|
|
items(stopMediaNodeParams),
|
|
{ value: stop_media_nodesScriptMediaTemplate },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var media_replaceScriptMedia = reduce(
|
|
items(stopMediaNodeParams),
|
|
{ value: media_replaceScriptMediaTemplate },
|
|
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
|
).value
|
|
|
|
var base64installMedia = base64(installScriptMedia)
|
|
var base64stopMediaNode = base64(stop_media_nodesScriptMedia)
|
|
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
|
|
privateIPMasterNode1: privateIPMasterNode1
|
|
privateIPMasterNode2: privateIPMasterNode2
|
|
privateIPMasterNode3: privateIPMasterNode3
|
|
privateIPMasterNode4: privateIPMasterNode4
|
|
mediaIdentityClientId: mediaIdentity.properties.clientId
|
|
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 : existingStorageAccount.name
|
|
}
|
|
identity: {
|
|
type: 'UserAssigned'
|
|
userAssignedIdentities: {
|
|
'${mediaIdentity.id}': {}
|
|
}
|
|
}
|
|
sku: {
|
|
name: mediaNodeInstanceType
|
|
tier: 'Standard'
|
|
// No capacity: the template never writes the number of Media Nodes, so a redeploy can
|
|
// never fail on the scale-in-protected Media Nodes. The autoscale profile's default
|
|
// seeds the size at creation and its minimum/maximum keep it.
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always deployed and always enabled, so a redeploy switches modes both ways (an incremental
|
|
// ARM deployment never deletes a resource the template stops declaring). With a fixed number
|
|
// of Media Nodes autoscaling is pinned to it, not disabled: minimum = maximum = default, so
|
|
// the CPU rules are inert. Azure refuses a minimum or a maximum below the Media Nodes that are
|
|
// running, because they protect themselves from scale-in: a redeploy can raise the fixed
|
|
// number (or minNumberOfMediaNodes) but not lower it below the Media Nodes running. To lower
|
|
// it, switch the scale set to manual scale, drain the extra Media Nodes with
|
|
// /usr/local/bin/stop_media_node.sh (each one removes itself once its Rooms end), and then
|
|
// redeploy.
|
|
resource openviduAutoScaleSettingsMediaNode 'Microsoft.Insights/autoscaleSettings@2022-10-01' = {
|
|
name: '${stackName}-autoscaleSettings'
|
|
location: resourceGroup().location
|
|
properties: {
|
|
profiles: [
|
|
{
|
|
name: 'openvidu-medianode-autoscale'
|
|
capacity: {
|
|
minimum: string(fixedNumberOfMediaNodes > 0 ? fixedNumberOfMediaNodes : minNumberOfMediaNodes)
|
|
maximum: string(fixedNumberOfMediaNodes > 0 ? fixedNumberOfMediaNodes : maxNumberOfMediaNodes)
|
|
default: string(fixedNumberOfMediaNodes > 0 ? fixedNumberOfMediaNodes : 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 ------------------------------------------*/
|
|
|
|
// One assignment for the shared master identity replaces the four per-VM ones
|
|
resource roleAssignmentMasterNodes 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
name: guid('roleAssignmentForMasterNodes', masterIdentity.id)
|
|
scope: resourceGroup()
|
|
properties: {
|
|
roleDefinitionId: subscriptionResourceId(
|
|
'Microsoft.Authorization/roleDefinitions',
|
|
'b24988ac-6180-42a0-ab88-20f7382dd24c'
|
|
)
|
|
principalId: masterIdentity.properties.principalId
|
|
principalType: 'ServicePrincipal'
|
|
}
|
|
}
|
|
|
|
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
name: guid('roleAssignmentForScaleSet', mediaIdentity.id)
|
|
scope: resourceGroup()
|
|
properties: {
|
|
roleDefinitionId: subscriptionResourceId(
|
|
'Microsoft.Authorization/roleDefinitions',
|
|
'b24988ac-6180-42a0-ab88-20f7382dd24c'
|
|
)
|
|
principalId: mediaIdentity.properties.principalId
|
|
principalType: 'ServicePrincipal'
|
|
}
|
|
}
|
|
|
|
@description('Automation Account Name to create a runbook inside it for scale in')
|
|
param automationAccountName string = ''
|
|
|
|
var isEmptyAutomationAccountName = automationAccountName == ''
|
|
|
|
@description('Do not set it. Azure Automation returns the URI of a webhook only when it creates it, and the scale-in action group needs that URI on every deployment, so every deployment creates the scale-in webhook under a new name: from this value (a new one each time it is not given) and the name of the deployment (a new one on every redeploy from the portal).')
|
|
param scaleInWebhookNameSeed string = utcNow()
|
|
|
|
module webhookModule '../../shared/webhookdeployment.json' = {
|
|
params: {
|
|
automationAccountName: isEmptyAutomationAccountName
|
|
? uniqueString(resourceGroup().id, openviduMasterNode1.id)
|
|
: automationAccountName
|
|
runbookName: 'scaleInRunbook'
|
|
webhookName: 'webhookForScaleIn-${uniqueString(deployment().name, scaleInWebhookNameSeed)}'
|
|
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 lbName = '${stackName}-loadBalancer'
|
|
var lbFrontEndName = 'LoadBalancerFrontEnd'
|
|
var lbBackendPoolNameMasterNode = 'LoadBalancerBackEndMasterNode'
|
|
|
|
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 LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = {
|
|
name: lbName
|
|
location: location
|
|
sku: {
|
|
name: 'Standard'
|
|
}
|
|
properties: {
|
|
frontendIPConfigurations: [
|
|
{
|
|
name: lbFrontEndName
|
|
properties: {
|
|
publicIPAddress: isEmptyIp
|
|
? null
|
|
: {
|
|
id: ipNew ? publicIP_OV_ifNew.id : publicIP_OV_ifExisting.id
|
|
}
|
|
}
|
|
}
|
|
]
|
|
backendAddressPools: [
|
|
{
|
|
name: lbBackendPoolNameMasterNode
|
|
}
|
|
]
|
|
loadBalancingRules: [
|
|
{
|
|
name: 'HTTPSRuleforMasterNode'
|
|
properties: {
|
|
frontendIPConfiguration: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', lbName, lbFrontEndName)
|
|
}
|
|
backendAddressPool: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', lbName, lbBackendPoolNameMasterNode)
|
|
}
|
|
frontendPort: 443
|
|
backendPort: 443
|
|
enableFloatingIP: false
|
|
protocol: 'Tcp'
|
|
enableTcpReset: true
|
|
loadDistribution: 'Default'
|
|
disableOutboundSnat: true
|
|
probe: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/probes', lbName, 'probeForHTTPSRuleMasterNode')
|
|
}
|
|
}
|
|
}
|
|
{
|
|
name: 'RTMPRuleforMasterNode'
|
|
properties: {
|
|
frontendIPConfiguration: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', lbName, lbFrontEndName)
|
|
}
|
|
backendAddressPool: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', lbName, lbBackendPoolNameMasterNode)
|
|
}
|
|
frontendPort: 1945
|
|
backendPort: 1945
|
|
enableFloatingIP: false
|
|
protocol: 'Tcp'
|
|
enableTcpReset: true
|
|
loadDistribution: 'Default'
|
|
disableOutboundSnat: true
|
|
probe: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/probes', lbName, 'probeForRTMPRuleMasterNode')
|
|
}
|
|
}
|
|
}
|
|
{
|
|
name: 'HTTPRuleforMasterNode'
|
|
properties: {
|
|
frontendIPConfiguration: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', lbName, lbFrontEndName)
|
|
}
|
|
backendAddressPool: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', lbName, lbBackendPoolNameMasterNode)
|
|
}
|
|
frontendPort: 80
|
|
backendPort: 80
|
|
enableFloatingIP: false
|
|
protocol: 'Tcp'
|
|
enableTcpReset: true
|
|
loadDistribution: 'Default'
|
|
disableOutboundSnat: true
|
|
probe: {
|
|
id: resourceId('Microsoft.Network/loadBalancers/probes', lbName, 'probeForHTTPSRuleMasterNode')
|
|
}
|
|
}
|
|
}
|
|
]
|
|
probes: [
|
|
{
|
|
name: 'probeForHTTPSRuleMasterNode'
|
|
properties: {
|
|
protocol: 'Http'
|
|
requestPath: '/health/caddy'
|
|
port: 7880
|
|
probeThreshold: 3
|
|
intervalInSeconds: 10
|
|
numberOfProbes: 5
|
|
}
|
|
}
|
|
{
|
|
name: 'probeForRTMPRuleMasterNode'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
port: 1945
|
|
intervalInSeconds: 5
|
|
numberOfProbes: 2
|
|
}
|
|
}
|
|
]
|
|
outboundRules: []
|
|
}
|
|
}
|
|
|
|
resource natGateway 'Microsoft.Network/natGateways@2021-05-01' = {
|
|
name: '${stackName}-natGateway'
|
|
location: location
|
|
sku: {
|
|
name: 'Standard'
|
|
}
|
|
properties: {
|
|
idleTimeoutInMinutes: 4
|
|
publicIpAddresses: [
|
|
{
|
|
id: natGatewayPublicIPAddress.id
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
resource natGatewayPublicIPAddress 'Microsoft.Network/publicIPAddresses@2021-05-01' = {
|
|
name: '${stackName}-publicIPnatGateway'
|
|
location: location
|
|
sku: {
|
|
name: 'Standard'
|
|
}
|
|
properties: {
|
|
publicIPAddressVersion: 'IPv4'
|
|
publicIPAllocationMethod: 'Static'
|
|
idleTimeoutInMinutes: 4
|
|
}
|
|
}
|
|
|
|
var networkSettings = {
|
|
vNetAddressPrefix: '10.0.0.0/16'
|
|
subnetAddressPrefixMaster1: '10.0.1.0/24'
|
|
subnetAddressPrefixMaster2: '10.0.2.0/24'
|
|
subnetAddressPrefixMedia: '10.0.0.0/24'
|
|
vNetName: '${stackName}-virtualNetwork'
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
{
|
|
name: 'firstSubnetForMasterNodes'
|
|
properties: {
|
|
addressPrefix: networkSettings.subnetAddressPrefixMaster1
|
|
privateEndpointNetworkPolicies: 'Disabled'
|
|
privateLinkServiceNetworkPolicies: 'Enabled'
|
|
natGateway: {
|
|
id: natGateway.id
|
|
}
|
|
}
|
|
}
|
|
{
|
|
name: 'secondSubnetForMasterNodes'
|
|
properties: {
|
|
addressPrefix: networkSettings.subnetAddressPrefixMaster2
|
|
privateEndpointNetworkPolicies: 'Disabled'
|
|
privateLinkServiceNetworkPolicies: 'Enabled'
|
|
natGateway: {
|
|
id: natGateway.id
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
// Every subnet is declared INSIDE the virtual network. A redeploy PUTs the virtual network with
|
|
// the subnets the template lists, so a subnet declared as a separate child resource was missing
|
|
// from that list and ARM tried to delete it while the Master Node used it
|
|
// (InUseSubnetCannotBeDeleted): no Elastic/HA deployment could be redeployed. The symbolic names
|
|
// below are `existing` references, so the rest of the template uses them as before.
|
|
resource subnetMasterNode1 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' existing = {
|
|
parent: vnet_OV
|
|
name: 'firstSubnetForMasterNodes'
|
|
}
|
|
|
|
resource subnetMasterNode2 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' existing = {
|
|
parent: vnet_OV
|
|
name: 'secondSubnetForMasterNodes'
|
|
}
|
|
|
|
resource netInterfaceMasterNode1 'Microsoft.Network/networkInterfaces@2023-11-01' = {
|
|
name: '${stackName}-masterNodeNetInterface1'
|
|
location: location
|
|
properties: {
|
|
ipConfigurations: [
|
|
{
|
|
name: 'primaryIPConfig'
|
|
properties: {
|
|
privateIPAllocationMethod: 'Dynamic'
|
|
subnet: {
|
|
id: subnetMasterNode1.id
|
|
}
|
|
applicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
loadBalancerBackendAddressPools: [
|
|
{
|
|
id: LoadBalancer.properties.backendAddressPools[0].id
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
networkSecurityGroup: {
|
|
id: openviduMasterNodeNSG.id
|
|
}
|
|
}
|
|
}
|
|
|
|
resource netInterfaceMasterNode2 'Microsoft.Network/networkInterfaces@2023-11-01' = {
|
|
name: '${stackName}-masterNodeNetInterface2'
|
|
location: location
|
|
properties: {
|
|
ipConfigurations: [
|
|
{
|
|
name: 'primaryIPConfig'
|
|
properties: {
|
|
privateIPAllocationMethod: 'Dynamic'
|
|
subnet: {
|
|
id: subnetMasterNode2.id
|
|
}
|
|
applicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
loadBalancerBackendAddressPools: [
|
|
{
|
|
id: LoadBalancer.properties.backendAddressPools[0].id
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
networkSecurityGroup: {
|
|
id: openviduMasterNodeNSG.id
|
|
}
|
|
}
|
|
}
|
|
|
|
resource netInterfaceMasterNode3 'Microsoft.Network/networkInterfaces@2023-11-01' = {
|
|
name: '${stackName}-masterNodeNetInterface3'
|
|
location: location
|
|
properties: {
|
|
ipConfigurations: [
|
|
{
|
|
name: 'primaryIPConfig'
|
|
properties: {
|
|
privateIPAllocationMethod: 'Dynamic'
|
|
subnet: {
|
|
id: subnetMasterNode1.id
|
|
}
|
|
applicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
loadBalancerBackendAddressPools: [
|
|
{
|
|
id: LoadBalancer.properties.backendAddressPools[0].id
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
networkSecurityGroup: {
|
|
id: openviduMasterNodeNSG.id
|
|
}
|
|
}
|
|
}
|
|
|
|
resource netInterfaceMasterNode4 'Microsoft.Network/networkInterfaces@2023-11-01' = {
|
|
name: '${stackName}-masterNodeNetInterface4'
|
|
location: location
|
|
properties: {
|
|
ipConfigurations: [
|
|
{
|
|
name: 'primaryIPConfig'
|
|
properties: {
|
|
privateIPAllocationMethod: 'Dynamic'
|
|
subnet: {
|
|
id: subnetMasterNode2.id
|
|
}
|
|
applicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
loadBalancerBackendAddressPools: [
|
|
{
|
|
id: LoadBalancer.properties.backendAddressPools[0].id
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
networkSecurityGroup: {
|
|
id: openviduMasterNodeNSG.id
|
|
}
|
|
}
|
|
}
|
|
|
|
resource openviduMasterNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
|
|
name: '${stackName}-masterNodeNSG'
|
|
location: location
|
|
properties: {
|
|
securityRules: [
|
|
{
|
|
name: 'SSH'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: '*'
|
|
sourcePortRange: '*'
|
|
destinationAddressPrefix: '*'
|
|
destinationPortRange: '22'
|
|
access: 'Allow'
|
|
priority: 100
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
{
|
|
name: 'ProbeAPI'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: 'AzureLoadBalancer'
|
|
sourcePortRange: '*'
|
|
destinationAddressPrefix: '*'
|
|
destinationPortRange: '7880'
|
|
access: 'Allow'
|
|
priority: 500
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
{
|
|
name: 'HTTP'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: '*'
|
|
sourcePortRange: '*'
|
|
destinationAddressPrefix: '*'
|
|
destinationPortRange: '80'
|
|
access: 'Allow'
|
|
priority: 510
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
resource openviduMasterNodeASG 'Microsoft.Network/applicationSecurityGroups@2024-03-01' = {
|
|
name: '${stackName}-masterNodeASG'
|
|
location: location
|
|
}
|
|
|
|
resource loadBalancerToMasterIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'loadBalancer_to_masterNode_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: '*'
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '443'
|
|
access: 'Allow'
|
|
priority: 110
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
// RTMPS Ingress: the Load Balancer keeps the client IP, so the masters must accept 1945 from anywhere
|
|
resource loadBalancerToMasterRtmpIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'loadBalancer_to_masterNode_RTMP_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: '*'
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '1945'
|
|
access: 'Allow'
|
|
priority: 111
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterClusterIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_CLUSTER_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '7880'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '7880'
|
|
access: 'Allow'
|
|
priority: 115
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterRedisIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_REDIS_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '7000'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '7001'
|
|
access: 'Allow'
|
|
priority: 120
|
|
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: '7000'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '7001'
|
|
access: 'Allow'
|
|
priority: 130
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterMinioIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_MINIO_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '9100'
|
|
access: 'Allow'
|
|
priority: 140
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterMinioConsoleIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_MINIO_CONSOLE_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '9101'
|
|
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 masterToMasterMongoIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_MONGO_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '20000'
|
|
access: 'Allow'
|
|
priority: 170
|
|
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: 180
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterMimirGrpcIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_MIMIRGRPC_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '9095'
|
|
access: 'Allow'
|
|
priority: 190
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterMimirGossipIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_MIMIRGOSSIP_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '7946'
|
|
access: 'Allow'
|
|
priority: 200
|
|
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: 210
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterLokiGrpcIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_LOKIGRPC_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '9096'
|
|
access: 'Allow'
|
|
priority: 220
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterLokiGossipIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_LOKIGOSSIP_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '7947'
|
|
access: 'Allow'
|
|
priority: 230
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterDashboardIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_DASHBOARD_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '5000'
|
|
access: 'Allow'
|
|
priority: 240
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterGrafanaIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_GRAFANA_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '3000'
|
|
access: 'Allow'
|
|
priority: 250
|
|
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: 260
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterV2CompatibilityIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_V2COMPATIBILITY_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '4443'
|
|
access: 'Allow'
|
|
priority: 270
|
|
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: 280
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterMeet 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_MEET_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '9080'
|
|
access: 'Allow'
|
|
priority: 290
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMasterCustomApp 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMasterNodeNSG
|
|
name: 'masterNode_to_masterNode_CUSTOM_APP_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '6080'
|
|
access: 'Allow'
|
|
priority: 310
|
|
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: 300
|
|
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: 320
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
|
|
name: '${stackName}-mediaNodeNSG'
|
|
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 loadBalancerToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMediaNodeNSG
|
|
name: 'loadBalancer_to_mediaNode_RTMP_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: 'AzureLoadBalancer'
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMediaNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '1945'
|
|
access: 'Allow'
|
|
priority: 160
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource loadBalancerToMediaHealthcheckIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMediaNodeNSG
|
|
name: 'loadBalancer_to_mediaNode_HEALTHCHECK_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceAddressPrefix: 'AzureLoadBalancer'
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMediaNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '9092'
|
|
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: 200
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
resource masterToMediaClientIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
|
parent: openviduMediaNodeNSG
|
|
name: 'masterNode_to_mediaNode_CLIENT_INGRESS'
|
|
properties: {
|
|
protocol: 'Tcp'
|
|
sourceApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMasterNodeASG.id
|
|
}
|
|
]
|
|
sourcePortRange: '*'
|
|
destinationApplicationSecurityGroups: [
|
|
{
|
|
id: openviduMediaNodeASG.id
|
|
}
|
|
]
|
|
destinationPortRange: '8080'
|
|
access: 'Allow'
|
|
priority: 210
|
|
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: 220
|
|
direction: 'Inbound'
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------- STORAGE ACCOUNT ----------------------------------------*/
|
|
|
|
@description('Name of an 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 existingStorageAccount '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 appDataContainerName string = ''
|
|
|
|
var isEmptyAppDataContainerName = appDataContainerName == ''
|
|
|
|
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
|
|
name: isEmptyAppDataContainerName
|
|
? '${storageAccount.name}/default/openvidu-appdata'
|
|
: '${storageAccount.name}/default/${appDataContainerName}'
|
|
properties: {
|
|
publicAccess: 'None'
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------- OUTPUTS -------------------------------------------*/
|