From 9e04d59b61c15e4f22e66d71b8ce12aae4332167 Mon Sep 17 00:00:00 2001 From: cruizba Date: Tue, 22 Apr 2025 11:46:24 +0200 Subject: [PATCH] Add openvidu-deployment scripts --- .../aws/cf-openvidu-singlenode.yaml | 944 ++++++ .../azure/cf-openvidu-singlenode.bicep | 1063 ++++++ .../azure/cf-openvidu-singlenode.json | 626 ++++ .../singlenode/azure/createUiDefinition.json | 0 .../community/singlenode/install.sh | 165 + .../pro/elastic/aws/cf-openvidu-elastic.yaml | 1768 ++++++++++ .../elastic/azure/cf-openvidu-elastic.bicep | 2022 +++++++++++ .../elastic/azure/cf-openvidu-elastic.json | 1604 +++++++++ .../pro/elastic/azure/createUiDefinition.json | 0 .../pro/elastic/install_ov_master_node.sh | 173 + .../pro/elastic/install_ov_media_node.sh | 172 + .../ha/aws/cf-openvidu-ha-internal-tls.yaml | 2394 ++++++++++++++ .../pro/ha/aws/cf-openvidu-ha.yaml | 2341 +++++++++++++ .../pro/ha/azure/cf-openvidu-ha.bicep | 2945 +++++++++++++++++ .../pro/ha/azure/cf-openvidu-ha.json | 2597 +++++++++++++++ .../pro/ha/azure/createUiDefinition.json | 0 .../pro/ha/install_ov_master_node.sh | 165 + .../pro/ha/install_ov_media_node.sh | 173 + .../pro/shared/scaleInRunbook.ps1 | 166 + .../pro/shared/webhookdeployment.json | 116 + openvidu-deployment/update.sh | 63 + 21 files changed, 19497 insertions(+) create mode 100644 openvidu-deployment/community/singlenode/aws/cf-openvidu-singlenode.yaml create mode 100644 openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.bicep create mode 100644 openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.json create mode 100644 openvidu-deployment/community/singlenode/azure/createUiDefinition.json create mode 100644 openvidu-deployment/community/singlenode/install.sh create mode 100644 openvidu-deployment/pro/elastic/aws/cf-openvidu-elastic.yaml create mode 100644 openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.bicep create mode 100644 openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.json create mode 100644 openvidu-deployment/pro/elastic/azure/createUiDefinition.json create mode 100644 openvidu-deployment/pro/elastic/install_ov_master_node.sh create mode 100644 openvidu-deployment/pro/elastic/install_ov_media_node.sh create mode 100644 openvidu-deployment/pro/ha/aws/cf-openvidu-ha-internal-tls.yaml create mode 100644 openvidu-deployment/pro/ha/aws/cf-openvidu-ha.yaml create mode 100644 openvidu-deployment/pro/ha/azure/cf-openvidu-ha.bicep create mode 100644 openvidu-deployment/pro/ha/azure/cf-openvidu-ha.json create mode 100644 openvidu-deployment/pro/ha/azure/createUiDefinition.json create mode 100644 openvidu-deployment/pro/ha/install_ov_master_node.sh create mode 100644 openvidu-deployment/pro/ha/install_ov_media_node.sh create mode 100644 openvidu-deployment/pro/shared/scaleInRunbook.ps1 create mode 100644 openvidu-deployment/pro/shared/webhookdeployment.json create mode 100644 openvidu-deployment/update.sh diff --git a/openvidu-deployment/community/singlenode/aws/cf-openvidu-singlenode.yaml b/openvidu-deployment/community/singlenode/aws/cf-openvidu-singlenode.yaml new file mode 100644 index 00000000..012e7099 --- /dev/null +++ b/openvidu-deployment/community/singlenode/aws/cf-openvidu-singlenode.yaml @@ -0,0 +1,944 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: OpenVidu Community - Single Node + +Parameters: + + CertificateType: + Description: | + [selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate. + [owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter) + and an Elastic IP, you can use this option to use your own certificate. + [letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter) + and an Elastic IP, you can use this option to generate a Let's Encrypt certificate. + Type: String + AllowedValues: + - selfsigned + - owncert + - letsencrypt + Default: selfsigned + + PublicElasticIP: + Type: String + Description: Previously created Elastic IP for the OpenVidu Deployment. + AllowedPattern: ^$|^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$ + ConstraintDescription: The Public Elastic IP does not have a valid IPv4 format + + DomainName: + Type: String + Description: Domain name for the OpenVidu Deployment. + AllowedPattern: ^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + ConstraintDescription: The domain name does not have a valid domain name format + + OwnPublicCertificate: + Description: "If certificate type is 'owncert', this parameter will be used to specify the public certificate" + Type: String + + OwnPrivateCertificate: + Description: "If certificate type is 'owncert', this parameter will be used to specify the private certificate" + Type: String + + LetsEncryptEmail: + Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" + Type: String + + TurnDomainName: + Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls' + Type: String + Default: '' + + TurnOwnPublicCertificate: + Description: "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + Type: String + Default: '' + + TurnOwnPrivateCertificate: + Description: "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + Type: String + Default: '' + + # EC2 Instance configuration + InstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu instance" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: Name of an existing EC2 KeyPair to enable SSH access to the Deployment. + AllowedPattern: ^.+$ + ConstraintDescription: must be the name of an existing EC2 KeyPair. + + AmiId: + Type: AWS::SSM::Parameter::Value + Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id + Description: AMI ID for the EC2 instances + + S3AppDataBucketName: + Type: String + Description: Name of the S3 bucket to store data and recordings. If empty, a bucket will be created + +Metadata: + 'AWS::CloudFormation::Interface': + ParameterGroups: + - Label: + default: Domain and SSL certificate configuration + Parameters: + - CertificateType + - PublicElasticIP + - DomainName + - OwnPublicCertificate + - OwnPrivateCertificate + - LetsEncryptEmail + - Label: + default: EC2 Instance configuration + Parameters: + - InstanceType + - KeyName + - AmiId + - Label: + default: S3 bucket for application data and recordings + Parameters: + - S3AppDataBucketName + - Label: + default: (Optional) TURN server configuration with TLS + Parameters: + - TurnDomainName + - TurnOwnPublicCertificate + - TurnOwnPrivateCertificate + +Conditions: + PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ] + CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] + +Resources: + + OpenViduSharedInfo: + Type: AWS::SecretsManager::Secret + UpdateReplacePolicy: Retain + DeletionPolicy: Delete + Properties: + Name: !Sub openvidu-${AWS::Region}-${AWS::StackName} + Description: Secret for OpenVidu to store deployment info and seed secrets + SecretString: | + { + "DOMAIN_NAME": "none", + "LIVEKIT_TURN_DOMAIN_NAME": "none", + "LETSENCRYPT_EMAIL": "none", + "REDIS_PASSWORD": "none", + "MONGO_ADMIN_USERNAME": "none", + "MONGO_ADMIN_PASSWORD": "none", + "MONGO_REPLICA_SET_KEY": "none", + "MINIO_URL": "none", + "MINIO_ACCESS_KEY": "none", + "MINIO_SECRET_KEY": "none", + "DASHBOARD_URL": "none", + "DASHBOARD_ADMIN_USERNAME": "none", + "DASHBOARD_ADMIN_PASSWORD": "none", + "GRAFANA_URL": "none", + "GRAFANA_ADMIN_USERNAME": "none", + "GRAFANA_ADMIN_PASSWORD": "none", + "LIVEKIT_API_KEY": "none", + "LIVEKIT_API_SECRET": "none", + "DEFAULT_APP_USERNAME": "none", + "DEFAULT_APP_PASSWORD": "none", + "DEFAULT_APP_ADMIN_USERNAME": "none", + "DEFAULT_APP_ADMIN_PASSWORD": "none", + "ENABLED_MODULES": "none" + } + + S3AppDataBucketResource: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-appdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateRecordingsBucket + + OpenViduServerRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: / + Policies: + - PolicyName: !Sub openvidu-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:UpdateSecret + Resource: !Ref OpenViduSharedInfo + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduServerInstanceProfile: + Type: 'AWS::IAM::InstanceProfile' + Properties: + Roles: + - !Ref OpenViduServerRole + InstanceProfileName: !Sub openvidu-instance-profile-${AWS::Region}-${AWS::StackName} + + OpenviduServer: + Type: 'AWS::EC2::Instance' + Metadata: + Comment: 'Install and configure OpenVidu Community - Single Node' + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash -x + OPENVIDU_VERSION=main + DOMAIN= + YQ_VERSION=v4.44.5 + + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Configure Domain + if [[ "${DomainName}" == '' ]]; then + [ ! -d "/usr/share/openvidu" ] && mkdir -p /usr/share/openvidu + PublicHostname=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-hostname) + DOMAIN=$PublicHostname + echo $PublicHostname > /usr/share/openvidu/old-host-name + else + DOMAIN=${DomainName} + fi + DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN_NAME "$DOMAIN")" + + # Store usernames and generate random passwords + REDIS_PASSWORD="$(/usr/local/bin/store_secret.sh generate REDIS_PASSWORD)" + MONGO_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save MONGO_ADMIN_USERNAME "mongoadmin")" + MONGO_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MONGO_ADMIN_PASSWORD)" + MONGO_REPLICA_SET_KEY="$(/usr/local/bin/store_secret.sh generate MONGO_REPLICA_SET_KEY)" + MINIO_ACCESS_KEY="$(/usr/local/bin/store_secret.sh save MINIO_ACCESS_KEY "minioadmin")" + MINIO_SECRET_KEY="$(/usr/local/bin/store_secret.sh generate MINIO_SECRET_KEY)" + DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD_ADMIN_USERNAME "dashboardadmin")" + DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" + GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")" + GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)" + DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")" + DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" + DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" + DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)" + ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app")" + 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)" + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--deployment-type=single_node" + "--domain-name=$DOMAIN" + "--enabled-modules='$ENABLED_MODULES'" + "--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" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" + ) + + # Turn with TLS + if [[ "${TurnDomainName}" != '' ]]; then + LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}") + COMMON_ARGS+=( + "--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME" + ) + fi + + # Certificate arguments + if [[ "${CertificateType}" == "selfsigned" ]]; then + CERT_ARGS=( + "--certificate-type=selfsigned" + ) + elif [[ "${CertificateType}" == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT_EMAIL "${LetsEncryptEmail}") + CERT_ARGS=( + "--certificate-type=letsencrypt" + "--letsencrypt-email=$LETSENCRYPT_EMAIL" + ) + else + # Download owncert files + mkdir -p /tmp/owncert + wget -O /tmp/owncert/fullchain.pem ${OwnPublicCertificate} + wget -O /tmp/owncert/privkey.pem ${OwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem) + OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem) + + CERT_ARGS=( + "--certificate-type=owncert" + "--owncert-public-key=$OWN_CERT_CRT" + "--owncert-private-key=$OWN_CERT_KEY" + ) + + # Turn with TLS and own certificate + if [[ "${TurnDomainName}" != '' ]]; then + # Download owncert files + mkdir -p /tmp/owncert-turn + wget -O /tmp/owncert-turn/fullchain.pem ${TurnOwnPublicCertificate} + wget -O /tmp/owncert-turn/privkey.pem ${TurnOwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem) + OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem) + + CERT_ARGS+=( + "--turn-owncert-private-key=$OWN_CERT_KEY_TURN" + "--turn-owncert-public-key=$OWN_CERT_CRT_TURN" + ) + fi + fi + + # Construct the final command with all arguments + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}") $(printf "%s " "${!CERT_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/config_s3.sh': + content: !Sub + - | + #!/bin/bash + set -e + + # Install dir and config dir + INSTALL_DIR="/opt/openvidu" + CONFIG_DIR="${!INSTALL_DIR}/config" + + # Config S3 bucket + EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com" + EXTERNAL_S3_REGION="${AWS::Region}" + EXTERNAL_S3_PATH_STYLE_ACCESS="false" + EXTERNAL_S3_BUCKET_APP_DATA=${S3RecordingsBucketResourceName} + sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CONFIG_DIR}/openvidu.env" + - S3RecordingsBucketResourceName: !If + - CreateRecordingsBucket + - !Ref S3AppDataBucketResource + - !Ref S3AppDataBucketName + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/after_install.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + if [[ "${DomainName}" == '' ]]; then + PublicHostname=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-hostname) + DOMAIN=$PublicHostname + else + DOMAIN=${DomainName} + fi + + # Generate URLs + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_config_from_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CONFIG_DIR="${!INSTALL_DIR}/config" + + # Replace DOMAIN_NAME + export DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME) + if [[ $DOMAIN == *"compute.amazonaws.com"* ]] || [[ -z $DOMAIN ]]; then + PublicHostname=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-hostname) + DOMAIN=$PublicHostname + fi + if [[ -n "$DOMAIN" ]]; then + sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${!CONFIG_DIR}/openvidu.env" + else + exit 1 + fi + + # Replace LIVEKIT_TURN_DOMAIN_NAME + export LIVEKIT_TURN_DOMAIN_NAME=$(echo $SHARED_SECRET | jq -r .LIVEKIT_TURN_DOMAIN_NAME) + if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${!CONFIG_DIR}/openvidu.env" + fi + + if [[ ${CertificateType} == "letsencrypt" ]]; then + export LETSENCRYPT_EMAIL=$(echo $SHARED_SECRET | jq -r .LETSENCRYPT_EMAIL) + sed -i "s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/" "${!CONFIG_DIR}/openvidu.env" + fi + + # Replace rest of the values + sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_USERNAME)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$(echo $SHARED_SECRET | jq -r .MONGO_REPLICA_SET_KEY)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_USERNAME)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_ACCESS_KEY)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_SECRET_KEY)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_USERNAME)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CONFIG_DIR}/openvidu.env" + sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.env" + sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.env" + sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.env" + sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env" + sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env" + + # Update URLs in secret + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_secret_from_config.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CONFIG_DIR="${!INSTALL_DIR}/config" + + if [[ ${CertificateType} == "letsencrypt" ]]; then + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LETSENCRYPT_EMAIL": "'"$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL "${!CONFIG_DIR}/openvidu.env")"'"}')" + fi + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"REDIS_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_TURN_DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_REPLICA_SET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_ACCESS_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_SECRET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CONFIG_DIR}/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CONFIG_DIR}/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.env")"'"}')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/get_value_from_config.sh': + content: | + #!/bin/bash + set -e + + # Function to get the value of a given key from the environment file + get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi + } + + # Check if the correct number of arguments are supplied + if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Get the key and file path from the arguments + key="$1" + file_path="$2" + + # Get and print the value + get_value "$key" "$file_path" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/store_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Modes: save, generate + # save mode: save the secret in the secret manager + # generate mode: generate a random password and save it in the secret manager + MODE="$1" + SHARED_SECRET="$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --query SecretString --output text)" + if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${!3:-}" + LENGTH="${!4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${!LENGTH})" + RANDOM_PASSWORD="${!PREFIX}${!RANDOM_PASSWORD}" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$RANDOM_PASSWORD"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$RANDOM_PASSWORD" + elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$SECRET_VALUE"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$SECRET_VALUE" + else + exit 1 + fi + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/check_app_ready.sh': + content: | + #!/bin/bash + while true; do + HTTP_STATUS=$(curl -Ik http://localhost:7880 | head -n1 | awk '{print $2}') + if [ $HTTP_STATUS == 200 ]; then + break + fi + sleep 5 + done + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/restart.sh': + content: | + #!/bin/bash + set -e + # Stop all services + systemctl stop openvidu + + # Update config from secret + /usr/local/bin/update_config_from_secret.sh + + # Start all services + systemctl start openvidu + mode: "000755" + owner: "root" + group: "root" + Properties: + ImageId: !Ref AmiId + LaunchTemplate: + # Enable IMDSv2 by default + LaunchTemplateName: IMDSV2 + Version: !GetAtt IMDSv2LaunchTemplate.DefaultVersionNumber + InstanceType: !Ref InstanceType + IamInstanceProfile: !Ref OpenViduServerInstanceProfile + SecurityGroups: + - !Ref WebServerSecurityGroup + KeyName: !Ref KeyName + Tags: + - Key: Name + Value: !Ref 'AWS::StackName' + UserData: + Fn::Base64: !Sub | + #!/bin/bash -x + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenviduServer + + export HOME="/root" + + # Install OpenVidu + /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # 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" | crontab + + # Wait for the app + /usr/local/bin/check_app_ready.sh + + # sending the finish call + /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region} + + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeType: gp2 + DeleteOnTermination: true + VolumeSize: 200 + + MyEIP: + Type: 'AWS::EC2::EIPAssociation' + Condition: PublicElasticIPPresent + Properties: + InstanceId: !Ref OpenviduServer + EIP: !Ref PublicElasticIP + + IMDSv2LaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Properties: + LaunchTemplateName: IMDSV2 + LaunchTemplateData: + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + + WaitCondition: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + WebServerSecurityGroup: + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: SSH, Proxy and OpenVidu WebRTC Ports + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 50000 + ToPort: 60000 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 50000 + ToPort: 60000 + CidrIpv6: ::/0 + +Outputs: + ServicesAndCredentials: + Description: Services and credentials + Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/home?region=${AWS::Region}#!/secret?name=openvidu-${AWS::Region}-${AWS::StackName} diff --git a/openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.bicep b/openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.bicep new file mode 100644 index 00000000..a8fba187 --- /dev/null +++ b/openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.bicep @@ -0,0 +1,1063 @@ +@description('Stack name') +param stackName string + +@description(''' +[selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate. +[owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter) +and an Elastic IP, you can use this option to use your own certificate. +[letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter) +and an Elastic IP, you can use this option to generate a Let's Encrypt certificate. +''') +@allowed([ + 'selfsigned' + 'owncert' + 'letsencrypt' +]) +param certificateType string = 'selfsigned' + +@description('Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP') +param publicIpAddress string = '' + +@description('Name of the PublicIPAddress resource in your azure if you have a resource of publicIPAddress') +param publicIpAddressResourceName string = '' + +@description('Domain name for the OpenVidu Deployment. Blank will generate default domain') +param domainName string = '' + +@description('If certificate type is \'owncert\', this parameter will be used to specify the public certificate') +param ownPublicCertificate string = '' + +@description('If certificate type is \'owncert\', this parameter will be used to specify the private certificate') +param ownPrivateCertificate string = '' + +@description('If certificate type is \'letsencrypt\', this email will be used for Let\'s Encrypt notifications') +param letsEncryptEmail string = '' + +@description('(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls') +param turnDomainName string = '' + +@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') +param turnOwnPublicCertificate string = '' + +@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') +param turnOwnPrivateCertificate string = '' + +// Azure instance config +@description('Specifies the azure vm size for your OpenVidu instance') +@allowed([ + 'Standard_B1s' + 'Standard_B1ms' + 'Standard_B2s' + 'Standard_B2ms' + 'Standard_B4ms' + 'Standard_B8ms' + 'Standard_D2_v3' + 'Standard_D4_v3' + 'Standard_D8_v3' + 'Standard_D16_v3' + 'Standard_D32_v3' + 'Standard_D48_v3' + 'Standard_D64_v3' + 'Standard_D2_v4' + 'Standard_D4_v4' + 'Standard_D8_v4' + 'Standard_D16_v4' + 'Standard_D32_v4' + 'Standard_D48_v4' + 'Standard_D64_v4' + 'Standard_D96_v4' + 'Standard_D2_v5' + 'Standard_D4_v5' + 'Standard_D8_v5' + 'Standard_D16_v5' + 'Standard_D32_v5' + 'Standard_D48_v5' + 'Standard_D64_v5' + 'Standard_D96_v5' + 'Standard_F2' + 'Standard_F4' + 'Standard_F8' + 'Standard_F16' + 'Standard_F32' + 'Standard_F64' + 'Standard_F72' + 'Standard_F2s_v2' + 'Standard_F4s_v2' + 'Standard_F8s_v2' + 'Standard_F16s_v2' + 'Standard_F32s_v2' + 'Standard_F64s_v2' + 'Standard_F72s_v2' + 'Standard_E2_v3' + 'Standard_E4_v3' + 'Standard_E8_v3' + 'Standard_E16_v3' + 'Standard_E32_v3' + 'Standard_E48_v3' + 'Standard_E64_v3' + 'Standard_E96_v3' + 'Standard_E2_v4' + 'Standard_E4_v4' + 'Standard_E8_v4' + 'Standard_E16_v4' + 'Standard_E32_v4' + 'Standard_E48_v4' + 'Standard_E64_v4' + 'Standard_E2_v5' + 'Standard_E4_v5' + 'Standard_E8_v5' + 'Standard_E16_v5' + 'Standard_E32_v5' + 'Standard_E48_v5' + 'Standard_E64_v5' + 'Standard_E96_v5' + 'Standard_M64' + 'Standard_M128' + 'Standard_M208ms_v2' + 'Standard_M416ms_v2' + 'Standard_L4s_v2' + 'Standard_L8s_v2' + 'Standard_L16s_v2' + 'Standard_L32s_v2' + 'Standard_L64s_v2' + 'Standard_L80s_v2' + 'Standard_NC6' + 'Standard_NC12' + 'Standard_NC24' + 'Standard_NC24r' + 'Standard_ND6s' + 'Standard_ND12s' + 'Standard_ND24s' + 'Standard_ND24rs' + 'Standard_NV6' + 'Standard_NV12' + 'Standard_NV24' + 'Standard_H8' + 'Standard_H16' + 'Standard_H16r' + 'Standard_H16mr' + 'Standard_HB120rs_v2' + 'Standard_HC44rs' + 'Standard_DC2s' + 'Standard_DC4s' + 'Standard_DC2s_v2' + 'Standard_DC4s_v2' + 'Standard_DC8s_v2' + 'Standard_DC16s_v2' + 'Standard_DC32s_v2' + 'Standard_A1_v2' + 'Standard_A2_v2' + 'Standard_A4_v2' + 'Standard_A8_v2' + 'Standard_A2m_v2' + 'Standard_A4m_v2' + 'Standard_A8m_v2' +]) +param instanceType string = 'Standard_B2s' // Azure instance types. + +@description('Username for the Virtual Machine.') +param adminUsername string + +@description('SSH Key or password for the Virtual Machine.') +@secure() +param adminSshKey string + +/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ + +//Condition for ipValid if is filled +var isEmptyIp = publicIpAddress == '' +var ipSegments = split(publicIpAddress, '.') +var isFourSegments = length(ipSegments) == 4 +var seg1valid = isEmptyIp ? true : int(ipSegments[0]) >= 0 && int(ipSegments[0]) <= 255 +var seg2valid = isEmptyIp ? true : int(ipSegments[1]) >= 0 && int(ipSegments[1]) <= 255 +var seg3valid = isEmptyIp ? true : int(ipSegments[2]) >= 0 && int(ipSegments[2]) <= 255 +var seg4valid = isEmptyIp ? true : int(ipSegments[3]) >= 0 && int(ipSegments[3]) <= 255 +var isValidIP = !isEmptyIp && isFourSegments && seg1valid && seg2valid && seg3valid && seg4valid + +//Condition for the domain name +var isEmptyDomain = domainName == '' +var domainParts = split(domainName, '.') +var validNumberParts = length(domainParts) >= 2 +var allPartsValid = [ + for part in domainParts: length(part) >= 1 && length(part) <= 63 && !empty(part) && part == toLower(part) && !contains( + part, + '--' + ) && empty(replace(part, '[a-z0-9-]', '')) +] + +var isDomainValid = !isEmptyDomain && validNumberParts && !contains(allPartsValid, false) + +//Variables for deployment +var networkSettings = { + privateIPaddressNetInterface: '10.0.0.5' + vNetAddressPrefix: '10.0.0.0/16' + subnetAddressPrefix: '10.0.0.0/24' + netInterfaceName: '${stackName}-netInteface' + vNetName: '${stackName}-vnet' + subnetName: 'default' +} + +var openviduVMSettings = { + vmName: '${stackName}-VM-CE' + osDiskType: 'StandardSSD_LRS' + ubuntuOSVersion: { + publisher: 'Canonical' + offer: '0001-com-ubuntu-server-jammy' + sku: '22_04-lts-gen2' + version: 'latest' + } + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${adminUsername}/.ssh/authorized_keys' + keyData: adminSshKey + } + ] + } + } +} + +var fqdn = isEmptyIp ? publicIP_OV.properties.dnsSettings.fqdn : domainName +//KeyVault for secrets +var keyVaultName = '${stackName}-keyvault' + +var location = resourceGroup().location + +var tenantId = subscription().tenantId + +var deploymentUser = az.deployer().objectId + +/*------------------------------------------- KEY VAULT -------------------------------------------*/ + +resource openviduSharedInfo 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: keyVaultName + location: location + properties: { + enabledForDeployment: true + enabledForDiskEncryption: false + enabledForTemplateDeployment: true + tenantId: tenantId + enableSoftDelete: false + accessPolicies: [ + { + //Rules for the master node when using key vault for secrets + objectId: openviduServer.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get', 'set', 'list'] + } + } + { + //Rules for the user to check key vault for secrets + 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 -------------------------------------------*/ + +//Parms for not string interpolation support for multiline +var stringInterpolationParams = { + domainName: domainName + fqdn: fqdn + turnDomainName: turnDomainName + certificateType: certificateType + letsEncryptEmail: letsEncryptEmail + ownPublicCertificate: ownPublicCertificate + ownPrivateCertificate: ownPrivateCertificate + turnOwnPublicCertificate: turnOwnPublicCertificate + turnOwnPrivateCertificate: turnOwnPrivateCertificate + keyVaultName: keyVaultName +} + +var installScriptTemplate = ''' +#!/bin/bash -x +OPENVIDU_VERSION=main +DOMAIN= + +apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + +# Configure Domain +if [[ "${domainName}" == '' ]]; then + [ ! -d "/usr/share/openvidu" ] && mkdir -p /usr/share/openvidu + DOMAIN=${fqdn} + echo ${fqdn} > /usr/share/openvidu/old-host-name +else + DOMAIN=${domainName} +fi + +DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN-NAME "$DOMAIN")" + +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)" +DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")" +DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" +DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" +DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)" +LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" +LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)" +ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app")" + +# Base command +INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)" + +# Common arguments +COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=azure" + "--deployment-type=single_node" + "--domain-name=$DOMAIN" + "--enabled-modules='$ENABLED_MODULES'" + "--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" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" +) + +# Turn with TLS +if [[ "${turnDomainName}" != '' ]]; then + LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}") + COMMON_ARGS+=( + "--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME" + ) +fi + +# Certificate arguments +if [[ "${certificateType}" == "selfsigned" ]]; then + CERT_ARGS=( + "--certificate-type=selfsigned" + ) +elif [[ "${certificateType}" == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT-EMAIL "${letsEncryptEmail}") + CERT_ARGS=( + "--certificate-type=letsencrypt" + "--letsencrypt-email=${letsEncryptEmail}" + ) +else + # Download owncert files + mkdir -p /tmp/owncert + wget -O /tmp/owncert/fullchain.pem ${ownPublicCertificate} + wget -O /tmp/owncert/privkey.pem ${ownPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem) + OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem) + + CERT_ARGS=( + "--certificate-type=owncert" + "--owncert-public-key=$OWN_CERT_CRT" + "--owncert-private-key=$OWN_CERT_KEY" + ) + + # Turn with TLS and own certificate + if [[ "${turnDomainName}" != '' ]]; then + # Download owncert files + mkdir -p /tmp/owncert-turn + wget -O /tmp/owncert-turn/fullchain.pem ${turnOwnPublicCertificate} + wget -O /tmp/owncert-turn/privkey.pem ${turnOwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem) + OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem) + + CERT_ARGS+=( + "--turn-owncert-private-key=$OWN_CERT_KEY_TURN" + "--turn-owncert-public-key=$OWN_CERT_CRT_TURN" + ) + fi +fi + +# Construct the final command with all arguments +FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${COMMON_ARGS[@]}") $(printf "%s " "${CERT_ARGS[@]}")" + +# Install OpenVidu +exec bash -c "$FINAL_COMMAND" +''' + +//DONE +var after_installScriptTemplate = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Generate URLs +DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv) +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 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 +''' +//DONE +var update_config_from_secretScriptTemplate = ''' +#!/bin/bash -x +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Installation directory +INSTALL_DIR="/opt/openvidu" +CONFIG_DIR="${INSTALL_DIR}/config" + +# 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/" "${CONFIG_DIR}/openvidu.env" +else + exit 1 +fi + +# Replace LIVEKIT_TURN_DOMAIN_NAME +export LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv) +if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${CONFIG_DIR}/openvidu.env" +fi + +if [[ ${certificateType} == "letsencrypt" ]]; then + export LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv) + sed -i "s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/" "${CONFIG_DIR}/openvidu.env" +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 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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv) +export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) +export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv) +export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv) +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/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$DASHBOARD_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$DASHBOARD_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$MINIO_SECRET_KEY/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CONFIG_DIR}/openvidu.env" +sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env" +sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env" +sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env" +sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env" +sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env" + + +# Update URLs in secret +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 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 +''' + +//DONE +var update_secret_from_configScriptTemplate = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Installation directory +INSTALL_DIR="/opt/openvidu" +CONFIG_DIR="${INSTALL_DIR}/config" + +if [[ ${certificateType} == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL="$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL "${CONFIG_DIR}/openvidu.env")" + az keyvault secret set --vault-name ${keyVaultName} --name "LETSENCRYPT-EMAIL" --value $LETSENCRYPT_EMAIL +fi + +# Get current values of the config +REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${CONFIG_DIR}/openvidu.env")" +DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")" +LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")" +MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")" +MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")" +MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CONFIG_DIR}/openvidu.env")" +MINIO_ACCESS_KEY="$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${CONFIG_DIR}/openvidu.env")" +MINIO_SECRET_KEY="$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${CONFIG_DIR}/openvidu.env")" +DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")" +DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")" +GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")" +GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")" +LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CONFIG_DIR}/openvidu.env")" +LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CONFIG_DIR}/openvidu.env")" +DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")" +DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")" +DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")" +DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")" +ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${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 LIVEKIT-TURN-DOMAIN-NAME --value $LIVEKIT_TURN_DOMAIN_NAME +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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD +az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES +''' + +//DONE +var get_value_from_configScript = ''' +#!/bin/bash -x +set -e + +# Function to get the value of a given key from the environment file +get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi +} + +# Check if the correct number of arguments are supplied +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Get the key and file path from the arguments +key="$1" +file_path="$2" + +# Get and print the value +get_value "$key" "$file_path" +''' + +//DONE +var store_secretScriptTemplate = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Modes: save, generate +# save mode: save the secret in the secret manager +# generate mode: generate a random password and save it in the secret manager +MODE="$1" + +if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${3:-}" + LENGTH="${4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${LENGTH})" + RANDOM_PASSWORD="${PREFIX}${RANDOM_PASSWORD}" + az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null + if [[ $? -ne 0 ]]; then + echo "Error generating secret" + fi + echo "$RANDOM_PASSWORD" +elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null + if [[ $? -ne 0 ]]; then + echo "Error generating secret" + fi + echo "$SECRET_VALUE" +else + exit 1 +fi +''' + +var check_app_ready = ''' +#!/bin/bash +while true; do + HTTP_STATUS=$(curl -Ik http://localhost:7880 | head -n1 | awk '{print $2}') + if [ $HTTP_STATUS == 200 ]; then + break + fi + sleep 5 +done +''' + +var restart = ''' +#!/bin/bash -x +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 formattedTemplateInstallScript = reduce( + items(stringInterpolationParams), + { value: installScriptTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var after_installScriptMaster = reduce( + items(stringInterpolationParams), + { value: after_installScriptTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var update_config_from_secretScript = reduce( + items(stringInterpolationParams), + { value: update_config_from_secretScriptTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var update_secret_from_configScript = reduce( + items(stringInterpolationParams), + { value: update_secret_from_configScriptTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var store_secretScript = reduce( + items(stringInterpolationParams), + { value: store_secretScriptTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var base64install = base64(formattedTemplateInstallScript) +var base64after_install = base64(after_installScriptMaster) +var base64update_config_from_secret = base64(update_config_from_secretScript) +var base64update_secret_from_config = base64(update_secret_from_configScript) +var base64get_value_from_config = base64(get_value_from_configScript) +var base64store_secret = base64(store_secretScript) +var base64check_app_ready = base64(check_app_ready) +var base64restart = base64(restart) + +var userDataParams = { + base64install: base64install + base64after_install: base64after_install + base64update_config_from_secret: base64update_config_from_secret + base64update_secret_from_config: base64update_secret_from_config + base64get_value_from_config: base64get_value_from_config + base64store_secret: base64store_secret + base64check_app_ready: base64check_app_ready + base64restart: base64restart +} + +var userDataTemplate = ''' +#!/bin/bash -x +set -eu -o pipefail + +echo ${base64install} | base64 -d > /usr/local/bin/install.sh +chmod +x /usr/local/bin/install.sh + +# after_install.sh +echo ${base64after_install} | base64 -d > /usr/local/bin/after_install.sh +chmod +x /usr/local/bin/after_install.sh + +# update_config_from_secret.sh +echo ${base64update_config_from_secret} | base64 -d > /usr/local/bin/update_config_from_secret.sh +chmod +x /usr/local/bin/update_config_from_secret.sh + +# update_secret_from_config.sh +echo ${base64update_secret_from_config} | base64 -d > /usr/local/bin/update_secret_from_config.sh +chmod +x /usr/local/bin/update_secret_from_config.sh + +# get_value_from_config.sh +echo ${base64get_value_from_config} | base64 -d > /usr/local/bin/get_value_from_config.sh +chmod +x /usr/local/bin/get_value_from_config.sh + +# store_secret.sh +echo ${base64store_secret} | base64 -d > /usr/local/bin/store_secret.sh +chmod +x /usr/local/bin/store_secret.sh + +echo ${base64check_app_ready} | base64 -d > /usr/local/bin/check_app_ready.sh +chmod +x /usr/local/bin/check_app_ready.sh + +echo ${base64restart} | base64 -d > /usr/local/bin/restart.sh +chmod +x /usr/local/bin/restart.sh + +# Install azure cli +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + +az login --identity --allow-no-subscriptions + +apt-get update && apt-get install -y + +export HOME="/root" + +# Install OpenVidu +/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; 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 + +# Wait for the app +/usr/local/bin/check_app_ready.sh +''' + +var userData = reduce( + items(userDataParams), + { value: userDataTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +resource openviduServer 'Microsoft.Compute/virtualMachines@2023-09-01' = { + name: openviduVMSettings.vmName + location: location + identity: { type: 'SystemAssigned' } + properties: { + hardwareProfile: { + vmSize: instanceType + } + storageProfile: { + osDisk: { + createOption: 'FromImage' + managedDisk: { + storageAccountType: openviduVMSettings.osDiskType + } + diskSizeGB: 100 + } + imageReference: openviduVMSettings.ubuntuOSVersion + } + networkProfile: { + networkInterfaces: [ + { + id: netInterface_OV.id + } + ] + } + osProfile: { + computerName: openviduVMSettings.vmName + adminUsername: adminUsername + adminPassword: adminSshKey + linuxConfiguration: openviduVMSettings.linuxConfiguration + } + userData: base64(userData) + } +} + +/*------------------------------------------- NETWORK -------------------------------------------*/ + +//Create publicIPAddress if convinient +resource publicIP_OV 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (isEmptyIp == true) { + name: '${stackName}-publicIP' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAddressVersion: 'IPv4' + publicIPAllocationMethod: 'Static' + dnsSettings: { + domainNameLabel: isEmptyDomain ? toLower('${stackName}') : null + fqdn: isEmptyDomain ? null : domainName + } + } +} + +resource publicIP_OV_ifNotEmpty 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (!isEmptyIp == true) { + name: publicIpAddressResourceName +} + +// Create the virtual network +resource vnet_OV 'Microsoft.Network/virtualNetworks@2023-11-01' = { + name: networkSettings.vNetName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + networkSettings.vNetAddressPrefix + ] + } + subnets: [ + { + name: networkSettings.subnetName + properties: { + addressPrefix: networkSettings.subnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroup: { + id: webServerSecurityGroup.id + } + } + } + ] + } +} + +resource netInterface_OV 'Microsoft.Network/networkInterfaces@2023-11-01' = { + name: networkSettings.netInterfaceName + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnet_OV.name, networkSettings.subnetName) + } + publicIPAddress: { + id: isEmptyIp ? publicIP_OV.id : publicIP_OV_ifNotEmpty.id + } + } + } + ] + networkSecurityGroup: { + id: webServerSecurityGroup.id + } + } +} + +// SecurityGroup for OpenviduSN +resource webServerSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11-01' = { + name: '${stackName}-nsg' + location: location + properties: { + securityRules: [ + { + name: 'SSH' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + access: 'Allow' + priority: 100 + direction: 'Inbound' + } + } + { + name: 'HTTP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '80' + access: 'Allow' + priority: 110 + direction: 'Inbound' + } + } + { + name: 'HTTPS' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '443' + access: 'Allow' + priority: 120 + direction: 'Inbound' + } + } + { + name: 'TURN' + properties: { + protocol: 'Udp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '443' + access: 'Allow' + priority: 130 + direction: 'Inbound' + } + } + { + name: 'RTMP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '1935' + access: 'Allow' + priority: 140 + direction: 'Inbound' + } + } + { + name: 'WebRTC_over_TCP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '7881' + access: 'Allow' + priority: 150 + direction: 'Inbound' + } + } + { + name: 'WebRTC_using_WHIP' + properties: { + protocol: 'Udp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '7885' + access: 'Allow' + priority: 160 + direction: 'Inbound' + } + } + { + name: 'MinIO' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '9000' + access: 'Allow' + priority: 170 + direction: 'Inbound' + } + } + { + name: 'WebRTC_traffic_UDP' + properties: { + protocol: 'Udp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '50000' + '60000' + ] + access: 'Allow' + priority: 180 + direction: 'Inbound' + } + } + { + name: 'WebRTC_traffic_TCP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '50000' + '60000' + ] + access: 'Allow' + priority: 190 + direction: 'Inbound' + } + } + ] + } +} + +/*------------------------------------------- OUTPUTS -------------------------------------------*/ + +output ipValidationStatus string = isValidIP ? 'IP address is valid' : 'IP address not valid' + +output domainValidationStatus string = isDomainValid ? 'Domain is valid' : 'Domain is not valid' + +//Condition if owncert is selected +output ownCertValidationStatus string = (certificateType == 'owncert' && ownPrivateCertificate != '' && ownPublicCertificate != '') + ? 'owncert selected and valid' + : 'You need to fill \'Own Public Certificate\' and \'Own Private Certificate\'' + +//Condition if letsEncrypt is selected +output letsEncryptValidationStatus string = (certificateType == 'letsencrypt' && letsEncryptEmail != '') + ? 'letsEncrypt selected and valid' + : 'You need to fill \'Lets Encrypt Email\'' diff --git a/openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.json b/openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.json new file mode 100644 index 00000000..afc873bb --- /dev/null +++ b/openvidu-deployment/community/singlenode/azure/cf-openvidu-singlenode.json @@ -0,0 +1,626 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "11233116334022044173" + } + }, + "parameters": { + "stackName": { + "type": "string", + "metadata": { + "description": "Stack name" + } + }, + "certificateType": { + "type": "string", + "defaultValue": "selfsigned", + "allowedValues": [ + "selfsigned", + "owncert", + "letsencrypt" + ], + "metadata": { + "description": "[selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate.\n[owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter)\nand an Elastic IP, you can use this option to use your own certificate.\n[letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter)\nand an Elastic IP, you can use this option to generate a Let's Encrypt certificate.\n" + } + }, + "publicIpAddress": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP" + } + }, + "publicIpAddressResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the PublicIPAddress resource in your azure if you have a resource of publicIPAddress" + } + }, + "domainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Domain name for the OpenVidu Deployment. Blank will generate default domain" + } + }, + "ownPublicCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'owncert', this parameter will be used to specify the public certificate" + } + }, + "ownPrivateCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'owncert', this parameter will be used to specify the private certificate" + } + }, + "letsEncryptEmail": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" + } + }, + "turnDomainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls" + } + }, + "turnOwnPublicCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + } + }, + "turnOwnPrivateCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + } + }, + "instanceType": { + "type": "string", + "defaultValue": "Standard_B2s", + "allowedValues": [ + "Standard_B1s", + "Standard_B1ms", + "Standard_B2s", + "Standard_B2ms", + "Standard_B4ms", + "Standard_B8ms", + "Standard_D2_v3", + "Standard_D4_v3", + "Standard_D8_v3", + "Standard_D16_v3", + "Standard_D32_v3", + "Standard_D48_v3", + "Standard_D64_v3", + "Standard_D2_v4", + "Standard_D4_v4", + "Standard_D8_v4", + "Standard_D16_v4", + "Standard_D32_v4", + "Standard_D48_v4", + "Standard_D64_v4", + "Standard_D96_v4", + "Standard_D2_v5", + "Standard_D4_v5", + "Standard_D8_v5", + "Standard_D16_v5", + "Standard_D32_v5", + "Standard_D48_v5", + "Standard_D64_v5", + "Standard_D96_v5", + "Standard_F2", + "Standard_F4", + "Standard_F8", + "Standard_F16", + "Standard_F32", + "Standard_F64", + "Standard_F72", + "Standard_F2s_v2", + "Standard_F4s_v2", + "Standard_F8s_v2", + "Standard_F16s_v2", + "Standard_F32s_v2", + "Standard_F64s_v2", + "Standard_F72s_v2", + "Standard_E2_v3", + "Standard_E4_v3", + "Standard_E8_v3", + "Standard_E16_v3", + "Standard_E32_v3", + "Standard_E48_v3", + "Standard_E64_v3", + "Standard_E96_v3", + "Standard_E2_v4", + "Standard_E4_v4", + "Standard_E8_v4", + "Standard_E16_v4", + "Standard_E32_v4", + "Standard_E48_v4", + "Standard_E64_v4", + "Standard_E2_v5", + "Standard_E4_v5", + "Standard_E8_v5", + "Standard_E16_v5", + "Standard_E32_v5", + "Standard_E48_v5", + "Standard_E64_v5", + "Standard_E96_v5", + "Standard_M64", + "Standard_M128", + "Standard_M208ms_v2", + "Standard_M416ms_v2", + "Standard_L4s_v2", + "Standard_L8s_v2", + "Standard_L16s_v2", + "Standard_L32s_v2", + "Standard_L64s_v2", + "Standard_L80s_v2", + "Standard_NC6", + "Standard_NC12", + "Standard_NC24", + "Standard_NC24r", + "Standard_ND6s", + "Standard_ND12s", + "Standard_ND24s", + "Standard_ND24rs", + "Standard_NV6", + "Standard_NV12", + "Standard_NV24", + "Standard_H8", + "Standard_H16", + "Standard_H16r", + "Standard_H16mr", + "Standard_HB120rs_v2", + "Standard_HC44rs", + "Standard_DC2s", + "Standard_DC4s", + "Standard_DC2s_v2", + "Standard_DC4s_v2", + "Standard_DC8s_v2", + "Standard_DC16s_v2", + "Standard_DC32s_v2", + "Standard_A1_v2", + "Standard_A2_v2", + "Standard_A4_v2", + "Standard_A8_v2", + "Standard_A2m_v2", + "Standard_A4m_v2", + "Standard_A8m_v2" + ], + "metadata": { + "description": "Specifies the azure vm size for your OpenVidu instance" + } + }, + "adminUsername": { + "type": "string", + "metadata": { + "description": "Username for the Virtual Machine." + } + }, + "adminSshKey": { + "type": "securestring", + "metadata": { + "description": "SSH Key or password for the Virtual Machine." + } + } + }, + "variables": { + "copy": [ + { + "name": "allPartsValid", + "count": "[length(variables('domainParts'))]", + "input": "[and(and(and(and(and(greaterOrEquals(length(variables('domainParts')[copyIndex('allPartsValid')]), 1), lessOrEquals(length(variables('domainParts')[copyIndex('allPartsValid')]), 63)), not(empty(variables('domainParts')[copyIndex('allPartsValid')]))), equals(variables('domainParts')[copyIndex('allPartsValid')], toLower(variables('domainParts')[copyIndex('allPartsValid')]))), not(contains(variables('domainParts')[copyIndex('allPartsValid')], '--'))), empty(replace(variables('domainParts')[copyIndex('allPartsValid')], '[a-z0-9-]', '')))]" + } + ], + "isEmptyIp": "[equals(parameters('publicIpAddress'), '')]", + "ipSegments": "[split(parameters('publicIpAddress'), '.')]", + "isFourSegments": "[equals(length(variables('ipSegments')), 4)]", + "seg1valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[0]), 0), lessOrEquals(int(variables('ipSegments')[0]), 255)))]", + "seg2valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[1]), 0), lessOrEquals(int(variables('ipSegments')[1]), 255)))]", + "seg3valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[2]), 0), lessOrEquals(int(variables('ipSegments')[2]), 255)))]", + "seg4valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[3]), 0), lessOrEquals(int(variables('ipSegments')[3]), 255)))]", + "isValidIP": "[and(and(and(and(and(not(variables('isEmptyIp')), variables('isFourSegments')), variables('seg1valid')), variables('seg2valid')), variables('seg3valid')), variables('seg4valid'))]", + "isEmptyDomain": "[equals(parameters('domainName'), '')]", + "domainParts": "[split(parameters('domainName'), '.')]", + "validNumberParts": "[greaterOrEquals(length(variables('domainParts')), 2)]", + "isDomainValid": "[and(and(not(variables('isEmptyDomain')), variables('validNumberParts')), not(contains(variables('allPartsValid'), false())))]", + "networkSettings": { + "privateIPaddressNetInterface": "10.0.0.5", + "vNetAddressPrefix": "10.0.0.0/16", + "subnetAddressPrefix": "10.0.0.0/24", + "netInterfaceName": "[format('{0}-netInteface', parameters('stackName'))]", + "vNetName": "[format('{0}-vnet', parameters('stackName'))]", + "subnetName": "default" + }, + "openviduVMSettings": { + "vmName": "[format('{0}-VM-CE', parameters('stackName'))]", + "osDiskType": "StandardSSD_LRS", + "ubuntuOSVersion": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest" + }, + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", + "keyData": "[parameters('adminSshKey')]" + } + ] + } + } + }, + "keyVaultName": "[format('{0}-keyvault', parameters('stackName'))]", + "location": "[resourceGroup().location]", + "tenantId": "[subscription().tenantId]", + "deploymentUser": "[deployer().objectId]", + "installScriptTemplate": "#!/bin/bash -x\nOPENVIDU_VERSION=main\nDOMAIN=\n\napt-get update && apt-get install -y \\\n curl \\\n unzip \\\n jq \\\n wget\n\n# Configure Domain\nif [[ \"${domainName}\" == '' ]]; then\n [ ! -d \"/usr/share/openvidu\" ] && mkdir -p /usr/share/openvidu\n DOMAIN=${fqdn}\n echo ${fqdn} > /usr/share/openvidu/old-host-name\nelse\n DOMAIN=${domainName}\nfi\n\nDOMAIN=\"$(/usr/local/bin/store_secret.sh save DOMAIN-NAME \"$DOMAIN\")\"\n\nREDIS_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate REDIS-PASSWORD)\"\nMONGO_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save MONGO-ADMIN-USERNAME \"mongoadmin\")\"\nMONGO_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate MONGO-ADMIN-PASSWORD)\"\nMONGO_REPLICA_SET_KEY=\"$(/usr/local/bin/store_secret.sh generate MONGO-REPLICA-SET-KEY)\"\nMINIO_ACCESS_KEY=\"$(/usr/local/bin/store_secret.sh save MINIO-ACCESS-KEY \"minioadmin\")\"\nMINIO_SECRET_KEY=\"$(/usr/local/bin/store_secret.sh generate MINIO-SECRET-KEY)\"\nDASHBOARD_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-USERNAME \"dashboardadmin\")\"\nDASHBOARD_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)\"\nGRAFANA_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME \"grafanaadmin\")\"\nGRAFANA_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)\"\nDEFAULT_APP_USERNAME=\"$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME \"calluser\")\"\nDEFAULT_APP_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)\"\nDEFAULT_APP_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME \"calladmin\")\"\nDEFAULT_APP_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)\"\nLIVEKIT_API_KEY=\"$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY \"API\" 12)\"\nLIVEKIT_API_SECRET=\"$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)\"\nENABLED_MODULES=\"$(/usr/local/bin/store_secret.sh save ENABLED-MODULES \"observability,app\")\"\n\n# Base command\nINSTALL_COMMAND=\"sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)\"\n\n# Common arguments\nCOMMON_ARGS=(\n \"--no-tty\"\n \"--install\"\n \"--environment=azure\"\n \"--deployment-type=single_node\"\n \"--domain-name=$DOMAIN\"\n \"--enabled-modules='$ENABLED_MODULES'\"\n \"--redis-password=$REDIS_PASSWORD\"\n \"--mongo-admin-user=$MONGO_ADMIN_USERNAME\"\n \"--mongo-admin-password=$MONGO_ADMIN_PASSWORD\"\n \"--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY\"\n \"--minio-access-key=$MINIO_ACCESS_KEY\"\n \"--minio-secret-key=$MINIO_SECRET_KEY\"\n \"--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME\"\n \"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD\"\n \"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME\"\n \"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD\"\n \"--default-app-user=$DEFAULT_APP_USERNAME\"\n \"--default-app-password=$DEFAULT_APP_PASSWORD\"\n \"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME\"\n \"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD\"\n \"--livekit-api-key=$LIVEKIT_API_KEY\"\n \"--livekit-api-secret=$LIVEKIT_API_SECRET\"\n)\n\n# Turn with TLS\nif [[ \"${turnDomainName}\" != '' ]]; then\n LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME \"${turnDomainName}\")\n COMMON_ARGS+=(\n \"--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME\"\n )\nfi\n\n# Certificate arguments\nif [[ \"${certificateType}\" == \"selfsigned\" ]]; then\n CERT_ARGS=(\n \"--certificate-type=selfsigned\"\n )\nelif [[ \"${certificateType}\" == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT-EMAIL \"${letsEncryptEmail}\")\n CERT_ARGS=(\n \"--certificate-type=letsencrypt\"\n \"--letsencrypt-email=${letsEncryptEmail}\"\n )\nelse\n # Download owncert files\n mkdir -p /tmp/owncert\n wget -O /tmp/owncert/fullchain.pem ${ownPublicCertificate}\n wget -O /tmp/owncert/privkey.pem ${ownPrivateCertificate}\n\n # Convert to base64\n OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem)\n OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem)\n\n CERT_ARGS=(\n \"--certificate-type=owncert\"\n \"--owncert-public-key=$OWN_CERT_CRT\"\n \"--owncert-private-key=$OWN_CERT_KEY\"\n )\n\n # Turn with TLS and own certificate\n if [[ \"${turnDomainName}\" != '' ]]; then\n # Download owncert files\n mkdir -p /tmp/owncert-turn\n wget -O /tmp/owncert-turn/fullchain.pem ${turnOwnPublicCertificate}\n wget -O /tmp/owncert-turn/privkey.pem ${turnOwnPrivateCertificate}\n\n # Convert to base64\n OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem)\n OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem)\n\n CERT_ARGS+=(\n \"--turn-owncert-private-key=$OWN_CERT_KEY_TURN\"\n \"--turn-owncert-public-key=$OWN_CERT_CRT_TURN\"\n )\n fi\nfi\n\n# Construct the final command with all arguments\nFINAL_COMMAND=\"$INSTALL_COMMAND $(printf \"%s \" \"${COMMON_ARGS[@]}\") $(printf \"%s \" \"${CERT_ARGS[@]}\")\"\n\n# Install OpenVidu\nexec bash -c \"$FINAL_COMMAND\"\n", + "after_installScriptTemplate": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Generate URLs\nDOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nDASHBOARD_URL=\"https://${DOMAIN}/dashboard/\"\nGRAFANA_URL=\"https://${DOMAIN}/grafana/\"\nMINIO_URL=\"https://${DOMAIN}/minio-console/\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL\n\naz keyvault secret show --vault-name ${keyVaultName} --name MINIO-URL\n\nif [[ $? -ne 0 ]]; then\n echo \"Error updating keyvault\"\nfi\n", + "update_config_from_secretScriptTemplate": "#!/bin/bash -x\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Installation directory\nINSTALL_DIR=\"/opt/openvidu\"\nCONFIG_DIR=\"${INSTALL_DIR}/config\"\n\n# Replace DOMAIN_NAME\nexport DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nif [[ -n \"$DOMAIN\" ]]; then\n sed -i \"s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/\" \"${CONFIG_DIR}/openvidu.env\"\nelse\n exit 1\nfi\n\n# Replace LIVEKIT_TURN_DOMAIN_NAME\nexport LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv)\nif [[ -n \"$LIVEKIT_TURN_DOMAIN_NAME\" ]]; then\n sed -i \"s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/\" \"${CONFIG_DIR}/openvidu.env\"\nfi\n\nif [[ ${certificateType} == \"letsencrypt\" ]]; then\n export LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv)\n sed -i \"s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/\" \"${CONFIG_DIR}/openvidu.env\"\nfi\n\n# Get the rest of the values\nexport REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)\nexport MONGO_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --query value -o tsv)\nexport MONGO_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --query value -o tsv)\nexport MONGO_REPLICA_SET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --query value -o tsv)\nexport DASHBOARD_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --query value -o tsv)\nexport DASHBOARD_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --query value -o tsv)\nexport MINIO_ACCESS_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --query value -o tsv)\nexport MINIO_SECRET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --query value -o tsv)\nexport GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --query value -o tsv)\nexport GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)\nexport LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)\nexport LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)\nexport DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)\nexport DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)\nexport DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)\nexport DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)\nexport ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)\n\n\n# Replace rest of the values\nsed -i \"s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$DASHBOARD_ADMIN_USERNAME/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$DASHBOARD_ADMIN_PASSWORD/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$MINIO_SECRET_KEY/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNAME/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/\" \"${CONFIG_DIR}/openvidu.env\"\nsed -i \"s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/\" \"${CONFIG_DIR}/app.env\"\nsed -i \"s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/\" \"${CONFIG_DIR}/app.env\"\nsed -i \"s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/\" \"${CONFIG_DIR}/app.env\"\nsed -i \"s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/\" \"${CONFIG_DIR}/app.env\"\nsed -i \"s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/\" \"${CONFIG_DIR}/openvidu.env\"\n\n\n# Update URLs in secret\nDASHBOARD_URL=\"https://${DOMAIN}/dashboard/\"\nGRAFANA_URL=\"https://${DOMAIN}/grafana/\"\nMINIO_URL=\"https://${DOMAIN}/minio-console/\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL\n", + "update_secret_from_configScriptTemplate": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Installation directory\nINSTALL_DIR=\"/opt/openvidu\"\nCONFIG_DIR=\"${INSTALL_DIR}/config\"\n\nif [[ ${certificateType} == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=\"$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL \"${CONFIG_DIR}/openvidu.env\")\"\n az keyvault secret set --vault-name ${keyVaultName} --name \"LETSENCRYPT-EMAIL\" --value $LETSENCRYPT_EMAIL\nfi\n\n# Get current values of the config\nREDIS_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD \"${CONFIG_DIR}/openvidu.env\")\"\nDOMAIN_NAME=\"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME \"${CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_TURN_DOMAIN_NAME=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME \"${CONFIG_DIR}/openvidu.env\")\"\nMONGO_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME \"${CONFIG_DIR}/openvidu.env\")\"\nMONGO_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD \"${CONFIG_DIR}/openvidu.env\")\"\nMONGO_REPLICA_SET_KEY=\"$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY \"${CONFIG_DIR}/openvidu.env\")\"\nMINIO_ACCESS_KEY=\"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY \"${CONFIG_DIR}/openvidu.env\")\"\nMINIO_SECRET_KEY=\"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY \"${CONFIG_DIR}/openvidu.env\")\"\nDASHBOARD_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME \"${CONFIG_DIR}/openvidu.env\")\"\nDASHBOARD_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD \"${CONFIG_DIR}/openvidu.env\")\"\nGRAFANA_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME \"${CONFIG_DIR}/openvidu.env\")\"\nGRAFANA_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD \"${CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_API_KEY=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY \"${CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_API_SECRET=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET \"${CONFIG_DIR}/openvidu.env\")\"\nDEFAULT_APP_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh CALL_USER \"${CONFIG_DIR}/app.env\")\"\nDEFAULT_APP_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET \"${CONFIG_DIR}/app.env\")\"\nDEFAULT_APP_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER \"${CONFIG_DIR}/app.env\")\"\nDEFAULT_APP_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET \"${CONFIG_DIR}/app.env\")\"\nENABLED_MODULES=\"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES \"${CONFIG_DIR}/openvidu.env\")\"\n\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name REDIS-PASSWORD --value $REDIS_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN_NAME\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --value $LIVEKIT_TURN_DOMAIN_NAME\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --value $MONGO_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --value $MONGO_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --value $MONGO_REPLICA_SET_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --value $MINIO_ACCESS_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --value $MINIO_SECRET_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --value $DASHBOARD_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --value $DASHBOARD_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --value $GRAFANA_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES\n", + "get_value_from_configScript": "#!/bin/bash -x\nset -e\n\n# Function to get the value of a given key from the environment file\nget_value() {\n local key=\"$1\"\n local file_path=\"$2\"\n\n # Use grep to find the line with the key, ignoring lines starting with #\n # Use awk to split on '=' and print the second field, which is the value\n local value=$(grep -E \"^\\s*$key\\s*=\" \"$file_path\" | awk -F= '{print $2}' | sed 's/#.*//; s/^\\s*//; s/\\s*$//')\n\n # If the value is empty, return \"none\"\n if [ -z \"$value\" ]; then\n echo \"none\"\n else\n echo \"$value\"\n fi\n}\n\n# Check if the correct number of arguments are supplied\nif [ \"$#\" -ne 2 ]; then\n echo \"Usage: $0 \"\n exit 1\nfi\n\n# Get the key and file path from the arguments\nkey=\"$1\"\nfile_path=\"$2\"\n\n# Get and print the value\nget_value \"$key\" \"$file_path\"\n", + "store_secretScriptTemplate": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Modes: save, generate\n# save mode: save the secret in the secret manager\n# generate mode: generate a random password and save it in the secret manager\nMODE=\"$1\"\n\nif [[ \"$MODE\" == \"generate\" ]]; then\n SECRET_KEY_NAME=\"$2\"\n PREFIX=\"${3:-}\"\n LENGTH=\"${4:-44}\"\n RANDOM_PASSWORD=\"$(openssl rand -base64 64 | tr -d '+/=\\n' | cut -c -${LENGTH})\"\n RANDOM_PASSWORD=\"${PREFIX}${RANDOM_PASSWORD}\"\n az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null\n if [[ $? -ne 0 ]]; then\n echo \"Error generating secret\"\n fi\n echo \"$RANDOM_PASSWORD\"\nelif [[ \"$MODE\" == \"save\" ]]; then\n SECRET_KEY_NAME=\"$2\"\n SECRET_VALUE=\"$3\"\n az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null\n if [[ $? -ne 0 ]]; then\n echo \"Error generating secret\"\n fi\n echo \"$SECRET_VALUE\"\nelse\n exit 1\nfi\n", + "check_app_ready": "#!/bin/bash\nwhile true; do\n HTTP_STATUS=$(curl -Ik http://localhost:7880 | head -n1 | awk '{print $2}')\n if [ $HTTP_STATUS == 200 ]; then\n break\n fi\n sleep 5\ndone\n", + "restart": "#!/bin/bash -x\nset -e\n# Stop all services\nsystemctl stop openvidu\n\n# Update config from secret\n/usr/local/bin/update_config_from_secret.sh\n\n# Start all services\nsystemctl start openvidu\n", + "base64get_value_from_config": "[base64(variables('get_value_from_configScript'))]", + "base64check_app_ready": "[base64(variables('check_app_ready'))]", + "base64restart": "[base64(variables('restart'))]", + "userDataTemplate": "#!/bin/bash -x\nset -eu -o pipefail\n\necho ${base64install} | base64 -d > /usr/local/bin/install.sh\nchmod +x /usr/local/bin/install.sh\n\n# after_install.sh\necho ${base64after_install} | base64 -d > /usr/local/bin/after_install.sh\nchmod +x /usr/local/bin/after_install.sh\n\n# update_config_from_secret.sh\necho ${base64update_config_from_secret} | base64 -d > /usr/local/bin/update_config_from_secret.sh\nchmod +x /usr/local/bin/update_config_from_secret.sh\n\n# update_secret_from_config.sh\necho ${base64update_secret_from_config} | base64 -d > /usr/local/bin/update_secret_from_config.sh\nchmod +x /usr/local/bin/update_secret_from_config.sh\n\n# get_value_from_config.sh\necho ${base64get_value_from_config} | base64 -d > /usr/local/bin/get_value_from_config.sh\nchmod +x /usr/local/bin/get_value_from_config.sh\n\n# store_secret.sh\necho ${base64store_secret} | base64 -d > /usr/local/bin/store_secret.sh\nchmod +x /usr/local/bin/store_secret.sh\n\necho ${base64check_app_ready} | base64 -d > /usr/local/bin/check_app_ready.sh\nchmod +x /usr/local/bin/check_app_ready.sh\n\necho ${base64restart} | base64 -d > /usr/local/bin/restart.sh\nchmod +x /usr/local/bin/restart.sh\n\n# Install azure cli\ncurl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash\n\naz login --identity --allow-no-subscriptions\n\napt-get update && apt-get install -y\n\nexport HOME=\"/root\"\n\n# Install OpenVidu\n/usr/local/bin/install.sh || { echo \"[OpenVidu] error installing OpenVidu\"; exit 1; }\n\n# Start OpenVidu\nsystemctl start openvidu || { echo \"[OpenVidu] error starting OpenVidu\"; exit 1; }\n\n# Update shared secret\n/usr/local/bin/after_install.sh || { echo \"[OpenVidu] error updating shared secret\"; exit 1; }\n\n# Launch on reboot\necho \"@reboot /usr/local/bin/restart.sh >> /var/log/openvidu-restart.log\" 2>&1 | crontab\n\n# Wait for the app\n/usr/local/bin/check_app_ready.sh\n" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-07-01", + "name": "[variables('keyVaultName')]", + "location": "[variables('location')]", + "properties": { + "enabledForDeployment": true, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true, + "tenantId": "[variables('tenantId')]", + "enableSoftDelete": false, + "accessPolicies": [ + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachines', variables('openviduVMSettings').vmName), '2023-09-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "set", + "list" + ] + } + }, + { + "objectId": "[variables('deploymentUser')]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "list", + "set", + "delete", + "recover", + "backup", + "restore" + ] + } + } + ], + "sku": { + "name": "standard", + "family": "A" + }, + "networkAcls": { + "defaultAction": "Allow", + "bypass": "AzureServices" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', variables('openviduVMSettings').vmName)]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[variables('openviduVMSettings').vmName]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('instanceType')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('openviduVMSettings').osDiskType]" + }, + "diskSizeGB": 100 + }, + "imageReference": "[variables('openviduVMSettings').ubuntuOSVersion]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkSettings').netInterfaceName)]" + } + ] + }, + "osProfile": { + "computerName": "[variables('openviduVMSettings').vmName]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('openviduVMSettings').linuxConfiguration]" + }, + "userData": "[base64(reduce(items(createObject('base64install', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('installScriptTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64after_install', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('after_installScriptTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64update_config_from_secret', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('update_config_from_secretScriptTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64update_secret_from_config', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('update_secret_from_configScriptTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64get_value_from_config', variables('base64get_value_from_config'), 'base64store_secret', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('store_secretScriptTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64check_app_ready', variables('base64check_app_ready'), 'base64restart', variables('base64restart'))), createObject('value', variables('userDataTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value)]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', variables('networkSettings').netInterfaceName)]", + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName')))]" + ] + }, + { + "condition": "[equals(variables('isEmptyIp'), true())]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2023-11-01", + "name": "[format('{0}-publicIP', parameters('stackName'))]", + "location": "[variables('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static", + "dnsSettings": { + "domainNameLabel": "[if(variables('isEmptyDomain'), toLower(format('{0}', parameters('stackName'))), null())]", + "fqdn": "[if(variables('isEmptyDomain'), null(), parameters('domainName'))]" + } + } + }, + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2023-11-01", + "name": "[variables('networkSettings').vNetName]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('networkSettings').vNetAddressPrefix]" + ] + }, + "subnets": [ + { + "name": "[variables('networkSettings').subnetName]", + "properties": { + "addressPrefix": "[variables('networkSettings').subnetAddressPrefix]", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg', parameters('stackName')))]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-11-01", + "name": "[variables('networkSettings').netInterfaceName]", + "location": "[variables('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, variables('networkSettings').subnetName)]" + }, + "publicIPAddress": { + "id": "[if(variables('isEmptyIp'), resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIpAddressResourceName')))]" + } + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName)]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}-nsg', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "securityRules": [ + { + "name": "SSH", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "22", + "access": "Allow", + "priority": 100, + "direction": "Inbound" + } + }, + { + "name": "HTTP", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "80", + "access": "Allow", + "priority": 110, + "direction": "Inbound" + } + }, + { + "name": "HTTPS", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "443", + "access": "Allow", + "priority": 120, + "direction": "Inbound" + } + }, + { + "name": "TURN", + "properties": { + "protocol": "Udp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "443", + "access": "Allow", + "priority": 130, + "direction": "Inbound" + } + }, + { + "name": "RTMP", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "1935", + "access": "Allow", + "priority": 140, + "direction": "Inbound" + } + }, + { + "name": "WebRTC_over_TCP", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "7881", + "access": "Allow", + "priority": 150, + "direction": "Inbound" + } + }, + { + "name": "WebRTC_using_WHIP", + "properties": { + "protocol": "Udp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "7885", + "access": "Allow", + "priority": 160, + "direction": "Inbound" + } + }, + { + "name": "MinIO", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "9000", + "access": "Allow", + "priority": 170, + "direction": "Inbound" + } + }, + { + "name": "WebRTC_traffic_UDP", + "properties": { + "protocol": "Udp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRanges": [ + "50000", + "60000" + ], + "access": "Allow", + "priority": 180, + "direction": "Inbound" + } + }, + { + "name": "WebRTC_traffic_TCP", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRanges": [ + "50000", + "60000" + ], + "access": "Allow", + "priority": 190, + "direction": "Inbound" + } + } + ] + } + } + ], + "outputs": { + "ipValidationStatus": { + "type": "string", + "value": "[if(variables('isValidIP'), 'IP address is valid', 'IP address not valid')]" + }, + "domainValidationStatus": { + "type": "string", + "value": "[if(variables('isDomainValid'), 'Domain is valid', 'Domain is not valid')]" + }, + "ownCertValidationStatus": { + "type": "string", + "value": "[if(and(and(equals(parameters('certificateType'), 'owncert'), not(equals(parameters('ownPrivateCertificate'), ''))), not(equals(parameters('ownPublicCertificate'), ''))), 'owncert selected and valid', 'You need to fill ''Own Public Certificate'' and ''Own Private Certificate''')]" + }, + "letsEncryptValidationStatus": { + "type": "string", + "value": "[if(and(equals(parameters('certificateType'), 'letsencrypt'), not(equals(parameters('letsEncryptEmail'), ''))), 'letsEncrypt selected and valid', 'You need to fill ''Lets Encrypt Email''')]" + } + } +} \ No newline at end of file diff --git a/openvidu-deployment/community/singlenode/azure/createUiDefinition.json b/openvidu-deployment/community/singlenode/azure/createUiDefinition.json new file mode 100644 index 00000000..e69de29b diff --git a/openvidu-deployment/community/singlenode/install.sh b/openvidu-deployment/community/singlenode/install.sh new file mode 100644 index 00000000..1fb6cd37 --- /dev/null +++ b/openvidu-deployment/community/singlenode/install.sh @@ -0,0 +1,165 @@ +#!/bin/sh +# Docker & Docker Compose will need to be installed on the machine +set -eu +export DOCKER_VERSION="${DOCKER_VERSION:-27.5.1}" +export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.32.4}" +export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" +export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" +export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.2.7-debian-12-r0}" +export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-02-08T19-14-21Z}" +export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.4}" +export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.2-alpine}" +export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}" +export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}" +export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}" +export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}" +export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}" +export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}" +export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.0}" +export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.1.0}" +export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.3.2}" +export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.3.2}" +export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.15.0}" +export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.5.1}" + +wait_for_docker() { + echo "Waiting for Docker to start..." + + # Set a countdown (in seconds) + COUNTDOWN=60 + + while [ "$COUNTDOWN" -gt 0 ]; do + if docker info >/dev/null 2>&1; then + echo "Docker started successfully." + break + else + # Reduce the countdown by 1 each iteration. + COUNTDOWN=$(( COUNTDOWN - 1 )) + + if [ "$COUNTDOWN" -eq 0 ]; then + echo "ERROR: Docker did not start within the allocated time." + break + fi + + sleep 1 + fi + done +} + +# Check if executing as root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if ! command -v docker > /dev/null 2>&1 +then + curl -fsSL https://get.docker.com -o /tmp/get-docker.sh + sh /tmp/get-docker.sh --version "${DOCKER_VERSION}" || { echo "Can't install Docker automatically. Install it manually and run this script again"; exit 1; } +else + echo "Docker already installed. Check you have the latest version for best compatibility" +fi + +if ! command -v docker-compose > /dev/null 2>&1 +then + TIME_LIMIT_SECONDS=20 + START_TIME=$(awk 'BEGIN{srand(); print srand()}') + while true + do + CURRENT_TIME=$(awk 'BEGIN{srand(); print srand()}') + if [ $((CURRENT_TIME-START_TIME)) -gt $TIME_LIMIT_SECONDS ]; then + echo "Error downloading docker-compose. Could not download it in $TIME_LIMIT_SECONDS seconds" + rm -rf /usr/local/bin/docker-compose + exit 1 + fi + STATUS_RECEIVED=$(curl --retry 5 --retry-max-time 40 --write-out "%{http_code}\n" -L "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose) + CURL_EXIT_CODE=$? + if [ $CURL_EXIT_CODE -ne 0 ]; then + echo "Error downloading docker-compose. curl failed with exit code $CURL_EXIT_CODE. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + if [ "${STATUS_RECEIVED}" -ne "200" ]; then + echo "Error downloading docker-compose. Received HTTP status code $STATUS_RECEIVED. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + echo "Success downloading docker-compose" + chmod 755 /usr/local/bin/docker-compose + break + done + + # Create a symbolic link to docker-compose in the Docker CLI plugins directory + # so docker compose can be used also + mkdir -p /usr/local/lib/docker/cli-plugins + ln -s /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose +else + echo "Docker Compose already installed. Check you have the latest version for best compatibility" +fi + +# Restart Docker and wait for it to start +systemctl enable docker +systemctl stop docker +systemctl start docker +wait_for_docker + +# Create random temp directory +TMP_DIR=$(mktemp -d) +docker pull "${INSTALLER_IMAGE}" + +# Generate installation scripts +COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \ + -e OPENVIDU_VERSION=$OPENVIDU_VERSION \ + -e CADDY_SERVER_IMAGE=$CADDY_SERVER_IMAGE \ + -e CADDY_SERVER_PRO_IMAGE=$CADDY_SERVER_PRO_IMAGE \ + -e MINIO_SERVER_IMAGE=$MINIO_SERVER_IMAGE \ + -e MINIO_CLIENT_IMAGE=$MINIO_CLIENT_IMAGE \ + -e MONGO_SERVER_IMAGE=$MONGO_SERVER_IMAGE \ + -e REDIS_SERVER_IMAGE=$REDIS_SERVER_IMAGE \ + -e BUSYBOX_IMAGE=$BUSYBOX_IMAGE \ + -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ + -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ + -e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \ + -e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \ + -e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \ + -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ + -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ + -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ + -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ + -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ + -e PROMTAIL_IMAGE=$PROMTAIL_IMAGE \ + -e LOKI_IMAGE=$LOKI_IMAGE \ + -e MIMIR_IMAGE=$MIMIR_IMAGE \ + -e GRAFANA_IMAGE=$GRAFANA_IMAGE \ + ${INSTALLER_IMAGE} \ + --deployment-type=single_node \ + --install \ + $*" + +INTERACTIVE_MODE=true +for arg in "$@"; do + if [ "$arg" = "--no-tty" ]; then + INTERACTIVE_MODE=false; + break + fi +done + +if [ "$INTERACTIVE_MODE" = true ]; then + docker run -it ${COMMON_DOCKER_OPTIONS} > /dev/tty +else + docker run -i ${COMMON_DOCKER_OPTIONS} +fi + +cd "$TMP_DIR/installation-scripts/openvidu/" +chmod +x install.sh +./install.sh + +cat finish-message.txt +echo diff --git a/openvidu-deployment/pro/elastic/aws/cf-openvidu-elastic.yaml b/openvidu-deployment/pro/elastic/aws/cf-openvidu-elastic.yaml new file mode 100644 index 00000000..596ce54d --- /dev/null +++ b/openvidu-deployment/pro/elastic/aws/cf-openvidu-elastic.yaml @@ -0,0 +1,1768 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: OpenVidu Pro - Elastic + +Parameters: + + CertificateType: + Description: | + [selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate. + [owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter) + and an Elastic IP, you can use this option to use your own certificate. + [letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter) + and an Elastic IP, you can use this option to generate a Let's Encrypt certificate. + Type: String + AllowedValues: + - selfsigned + - owncert + - letsencrypt + Default: selfsigned + + PublicElasticIP: + Type: String + Description: Previously created Elastic IP for the OpenVidu Deployment. + AllowedPattern: ^$|^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$ + ConstraintDescription: The Public Elastic IP does not have a valid IPv4 format + + DomainName: + Type: String + Description: Domain name for the OpenVidu Deployment. + AllowedPattern: ^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + ConstraintDescription: The domain name does not have a valid domain name format + + OwnPublicCertificate: + Description: "If certificate type is 'owncert', this parameter will be used to specify the public certificate" + Type: String + + OwnPrivateCertificate: + Description: "If certificate type is 'owncert', this parameter will be used to specify the private certificate" + Type: String + + LetsEncryptEmail: + Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" + Type: String + + TurnDomainName: + Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls' + Type: String + Default: '' + + TurnOwnPublicCertificate: + Description: "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + Type: String + Default: '' + + TurnOwnPrivateCertificate: + Description: "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + Type: String + Default: '' + + OpenViduLicense: + Description: "Visit https://openvidu.io/account" + Type: String + AllowedPattern: ^(?!\s*$).+$ + NoEcho: true + ConstraintDescription: OpenVidu Pro License is mandatory + + RTCEngine: + Description: "RTCEngine media engine to use" + Type: String + AllowedValues: + - pion + - mediasoup + Default: pion + + MasterNodeInstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu Master Node" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + MediaNodeInstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu Media Nodes" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: Name of an existing EC2 KeyPair to enable SSH access to the Deployment. + AllowedPattern: ^.+$ + ConstraintDescription: must be the name of an existing EC2 KeyPair. + + AmiId: + Type: AWS::SSM::Parameter::Value + Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id + Description: AMI ID for the EC2 instances + + InitialNumberOfMediaNodes: + Type: Number + Default: 1 + Description: Number of initial media nodes to deploy + + MinNumberOfMediaNodes: + Type: Number + Default: 1 + Description: Minimum number of media nodes to deploy + + MaxNumberOfMediaNodes: + Type: Number + Default: 5 + Description: Maximum number of media nodes to deploy + + ScaleTargetCPU: + Type: Number + Default: 50 + Description: Target CPU percentage to scale up or down + + S3AppDataBucketName: + Type: String + Description: Name of the S3 bucket to store data and recordings. If empty, a bucket will be created + + OpenViduVPC: + Description: "Dedicated VPC for OpenVidu cluster" + Type: AWS::EC2::VPC::Id + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a VPC ID + + OpenViduMasterNodeSubnet: + Description: "Subnet for OpenVidu cluster" + Type: AWS::EC2::Subnet::Id + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a subnet ID + + OpenViduMediaNodeSubnets: + Description: "Subnets for OpenVidu Media Nodes" + Type: List + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a list of subnet IDs + +Metadata: + 'AWS::CloudFormation::Interface': + ParameterGroups: + - Label: + default: Domain and SSL certificate configuration + Parameters: + - CertificateType + - PublicElasticIP + - DomainName + - OwnPublicCertificate + - OwnPrivateCertificate + - LetsEncryptEmail + - Label: + default: OpenVidu Elastic configuration + Parameters: + - OpenViduLicense + - RTCEngine + - Label: + default: EC2 Instance configuration + Parameters: + - MasterNodeInstanceType + - MediaNodeInstanceType + - KeyName + - AmiId + - Label: + default: Media Nodes Autoscaling Group configuration + Parameters: + - InitialNumberOfMediaNodes + - MinNumberOfMediaNodes + - MaxNumberOfMediaNodes + - ScaleTargetCPU + - Label: + default: S3 bucket for application data and recordings + Parameters: + - S3AppDataBucketName + - Label: + default: VPC configuration + Parameters: + - OpenViduVPC + - OpenViduMasterNodeSubnet + - OpenViduMediaNodeSubnets + - Label: + default: (Optional) TURN server configuration with TLS + Parameters: + - TurnDomainName + - TurnOwnPublicCertificate + - TurnOwnPrivateCertificate + +Conditions: + PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ] + TurnTLSIsEnabled: !Not [!Equals [!Ref TurnDomainName, ""] ] + CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] + +Resources: + + OpenViduSharedInfo: + Type: AWS::SecretsManager::Secret + UpdateReplacePolicy: Retain + DeletionPolicy: Delete + Properties: + Name: !Sub openvidu-elastic-${AWS::Region}-${AWS::StackName} + Description: Secret for OpenVidu Elastic to store deployment info and seed secret + # All the values are initialized by the master node + SecretString: | + { + "ALL_SECRETS_GENERATED": "false", + "DOMAIN_NAME": "none", + "LIVEKIT_TURN_DOMAIN_NAME": "none", + "LETSENCRYPT_EMAIL": "none", + "OPENVIDU_PRO_LICENSE": "none", + "OPENVIDU_RTC_ENGINE": "none", + "REDIS_PASSWORD": "none", + "MONGO_ADMIN_USERNAME": "none", + "MONGO_ADMIN_PASSWORD": "none", + "MONGO_REPLICA_SET_KEY": "none", + "MINIO_URL": "none", + "MINIO_ACCESS_KEY": "none", + "MINIO_SECRET_KEY": "none", + "DASHBOARD_URL": "none", + "DASHBOARD_ADMIN_USERNAME": "none", + "DASHBOARD_ADMIN_PASSWORD": "none", + "GRAFANA_URL": "none", + "GRAFANA_ADMIN_USERNAME": "none", + "GRAFANA_ADMIN_PASSWORD": "none", + "LIVEKIT_API_KEY": "none", + "LIVEKIT_API_SECRET": "none", + "DEFAULT_APP_USERNAME": "none", + "DEFAULT_APP_PASSWORD": "none", + "DEFAULT_APP_ADMIN_USERNAME": "none", + "DEFAULT_APP_ADMIN_PASSWORD": "none", + "OPENVIDU_VERSION": "none", + "ENABLED_MODULES": "none" + } + + S3AppDataBucketResource: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-appdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateRecordingsBucket + + OpenViduMasterNodeRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: / + Policies: + - PolicyName: !Sub openvidu-master-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:UpdateSecret + Resource: !Ref OpenViduSharedInfo + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-master-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduMediaNodeRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: / + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore + Policies: + - PolicyName: !Sub openvidu-media-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref OpenViduSharedInfo + - Effect: Allow + Action: + - autoscaling:SetInstanceHealth + - autoscaling:CompleteLifecycleAction + - autoscaling:RecordLifecycleActionHeartbeat + Resource: '*' + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-media-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduMasterNodeInstanceProfile: + Type: 'AWS::IAM::InstanceProfile' + Properties: + Roles: + - !Ref OpenViduMasterNodeRole + InstanceProfileName: !Sub openvidu-elastic-instance-profile-${AWS::Region}-${AWS::StackName} + + OpenViduMediaNodeInstanceProfile: + Type: 'AWS::IAM::InstanceProfile' + Properties: + Roles: + - !Ref OpenViduMediaNodeRole + InstanceProfileName: !Sub openvidu-elastic-media-instance-profile-${AWS::Region}-${AWS::StackName} + + IMDSv2LaunchTemplateMasterNode: + Type: AWS::EC2::LaunchTemplate + Properties: + LaunchTemplateName: !Sub openvidu-elastic-master-node-${AWS::Region}-${AWS::StackName} + LaunchTemplateData: + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + + WaitCondition: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode: + Type: AWS::EC2::Instance + Metadata: + Comment: 'Install and configure OpenVidu Elastic Master Node' + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash + set -e + OPENVIDU_VERSION=main + DOMAIN= + YQ_VERSION=v4.44.5 + + # Install dependencies + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Configure Domain and other parameters + if [[ "${DomainName}" == '' ]]; then + PublicHostname=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-hostname) + DOMAIN=$PublicHostname + else + DOMAIN=${DomainName} + fi + OPENVIDU_PRO_LICENSE="$(/usr/local/bin/store_secret.sh save OPENVIDU_PRO_LICENSE "${OpenViduLicense}")" + OPENVIDU_RTC_ENGINE="$(/usr/local/bin/store_secret.sh save OPENVIDU_RTC_ENGINE "${RTCEngine}")" + # Store version so media nodes can use it to install the same version + /usr/local/bin/store_secret.sh save OPENVIDU_VERSION "${!OPENVIDU_VERSION}" + + # Get own private IP + PRIVATE_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-ipv4) + + # Unfortunately, EC2 instance assigned role is not immediately available after the instance is launched. + # Therefore, we need to retry this operation until the aws-cli command is successful. + MAX_RETRIES=10 + RETRY_COUNT=0 + while : ; do + # Get current shared secret and random seed + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none') + + if [[ "$SHARED_SECRET" != "none" ]]; then + break + fi + + RETRY_COUNT=RETRY_COUNT+1 + if [[ $RETRY_COUNT -ge $MAX_RETRIES ]]; then + exit 1 + fi + sleep 6 + done + + # 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)" + DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")" + DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" + DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" + DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)" + LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" + LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)" + ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")" + ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")" + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/elastic/$OPENVIDU_VERSION/install_ov_master_node.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--deployment-type=elastic" + "--node-role='master-node'" + "--openvidu-pro-license=$OPENVIDU_PRO_LICENSE" + "--private-ip=$PRIVATE_IP" + "--domain-name=$DOMAIN" + "--enabled-modules='$ENABLED_MODULES'" + "--rtc-engine=$OPENVIDU_RTC_ENGINE" + "--redis-password=$REDIS_PASSWORD" + "--mongo-admin-user=$MONGO_ADMIN_USERNAME" + "--mongo-admin-password=$MONGO_ADMIN_PASSWORD" + "--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY" + "--minio-access-key=$MINIO_ACCESS_KEY" + "--minio-secret-key=$MINIO_SECRET_KEY" + "--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME" + "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" + "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" + "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" + ) + + # Turn with TLS + if [[ "${TurnDomainName}" != '' ]]; then + LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}") + COMMON_ARGS+=( + "--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME" + ) + fi + + # Certificate arguments + if [[ "${CertificateType}" == "selfsigned" ]]; then + CERT_ARGS=( + "--certificate-type=selfsigned" + ) + elif [[ "${CertificateType}" == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT_EMAIL "${LetsEncryptEmail}") + CERT_ARGS=( + "--certificate-type=letsencrypt" + "--letsencrypt-email=$LETSENCRYPT_EMAIL" + ) + else + # Download owncert files + mkdir -p /tmp/owncert + wget -O /tmp/owncert/fullchain.pem ${OwnPublicCertificate} + wget -O /tmp/owncert/privkey.pem ${OwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem) + OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem) + + CERT_ARGS=( + "--certificate-type=owncert" + "--owncert-public-key=$OWN_CERT_CRT" + "--owncert-private-key=$OWN_CERT_KEY" + ) + + # Turn with TLS and own certificate + if [[ "${TurnDomainName}" != '' ]]; then + # Download owncert files + mkdir -p /tmp/owncert-turn + wget -O /tmp/owncert-turn/fullchain.pem ${TurnOwnPublicCertificate} + wget -O /tmp/owncert-turn/privkey.pem ${TurnOwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem) + OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem) + + CERT_ARGS+=( + "--turn-owncert-private-key=$OWN_CERT_KEY_TURN" + "--turn-owncert-public-key=$OWN_CERT_CRT_TURN" + ) + fi + fi + + # Construct the final command with all arguments + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}") $(printf "%s " "${!CERT_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/config_s3.sh': + content: !Sub + - | + #!/bin/bash + set -e + + # Install dir and config dir + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + + # Config S3 bucket + EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com" + EXTERNAL_S3_REGION="${AWS::Region}" + EXTERNAL_S3_PATH_STYLE_ACCESS="false" + EXTERNAL_S3_BUCKET_APP_DATA=${S3RecordingsBucketResourceName} + sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + - S3RecordingsBucketResourceName: !If + - CreateRecordingsBucket + - !Ref S3AppDataBucketResource + - !Ref S3AppDataBucketName + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/after_install.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + if [[ "${DomainName}" == '' ]]; then + PublicHostname=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-hostname) + DOMAIN=$PublicHostname + else + DOMAIN=${DomainName} + fi + + # Generate URLs + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_config_from_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" + + # Replace DOMAIN_NAME + export DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME) + if [[ $DOMAIN == *"compute.amazonaws.com"* ]] || [[ -z $DOMAIN ]]; then + PublicHostname=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-hostname) + DOMAIN=$PublicHostname + fi + if [[ -n "$DOMAIN" ]]; then + sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + else + exit 1 + fi + + # Replace LIVEKIT_TURN_DOMAIN_NAME + export LIVEKIT_TURN_DOMAIN_NAME=$(echo $SHARED_SECRET | jq -r .LIVEKIT_TURN_DOMAIN_NAME) + if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + fi + + if [[ ${CertificateType} == "letsencrypt" ]]; then + export LETSENCRYPT_EMAIL=$(echo $SHARED_SECRET | jq -r .LETSENCRYPT_EMAIL) + sed -i "s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + fi + + # Replace rest of the values + sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD)/" "${!MASTER_NODE_CONFIG_DIR}/master_node.env" + sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_RTC_ENGINE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_PRO_LICENSE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$(echo $SHARED_SECRET | jq -r .MONGO_REPLICA_SET_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_ACCESS_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_SECRET_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + + # Update URLs in secret + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_secret_from_config.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" + + + if [[ ${CertificateType} == "letsencrypt" ]]; then + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LETSENCRYPT_EMAIL": "'"$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + fi + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"REDIS_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${!MASTER_NODE_CONFIG_DIR}/master_node.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_TURN_DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_RTC_ENGINE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_PRO_LICENSE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_REPLICA_SET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_ACCESS_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_SECRET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/get_value_from_config.sh': + content: | + #!/bin/bash + set -e + + # Function to get the value of a given key from the environment file + get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi + } + + # Check if the correct number of arguments are supplied + if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Get the key and file path from the arguments + key="$1" + file_path="$2" + + # Get and print the value + get_value "$key" "$file_path" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/store_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Modes: save, generate + # save mode: save the secret in the secret manager + # generate mode: generate a random password and save it in the secret manager + MODE="$1" + SHARED_SECRET="$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --query SecretString --output text)" + if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${!3:-}" + LENGTH="${!4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${!LENGTH})" + RANDOM_PASSWORD="${!PREFIX}${!RANDOM_PASSWORD}" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$RANDOM_PASSWORD"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$RANDOM_PASSWORD" + elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$SECRET_VALUE"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$SECRET_VALUE" + else + exit 1 + fi + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/check_app_ready.sh': + content: | + #!/bin/bash + set -e + while true; do + HTTP_STATUS=$(curl -Ik http://localhost:7880/twirp/health | head -n1 | awk '{print $2}') + if [ $HTTP_STATUS == 200 ]; then + break + fi + sleep 5 + done + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/restart.sh': + content: | + #!/bin/bash + set -e + # Stop all services + systemctl stop openvidu + + # Update config from secret + /usr/local/bin/update_config_from_secret.sh + + # Start all services + systemctl start openvidu + mode: "000755" + owner: "root" + group: "root" + Properties: + ImageId: !Ref AmiId + LaunchTemplate: + # Enable IMDSv2 by default + LaunchTemplateId: !Ref IMDSv2LaunchTemplateMasterNode + Version: !GetAtt IMDSv2LaunchTemplateMasterNode.DefaultVersionNumber + InstanceType: !Ref MasterNodeInstanceType + IamInstanceProfile: !Ref OpenViduMasterNodeInstanceProfile + SubnetId: !Ref OpenViduMasterNodeSubnet + SecurityGroupIds: + - !GetAtt OpenViduMasterNodeSG.GroupId + KeyName: !Ref KeyName + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu Elastic - Master Node + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenViduMasterNode + + export HOME="/root" + + # Install OpenVidu + /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + # Wait for the app + sleep 20 + /usr/local/bin/check_app_ready.sh + + # sending the finish call + /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region} + + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeType: gp2 + DeleteOnTermination: true + VolumeSize: 200 + + OpenViduMediaNodeLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Metadata: + Comment: 'Launch template for OpenVidu Elastic Media Node' + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash + set -e + DOMAIN= + YQ_VERSION=v4.44.5 + + # Install dependencies + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own private IP + PRIVATE_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-ipv4) + + # Unfortunately, EC2 instance role is not immediately available after the instance is launched. + # Therefore, we need to retry this operation until the aws-cli command is successful. + MAX_RETRIES=10 + RETRY_COUNT=0 + while : ; do + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-elastic-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none') + + if [[ "$SHARED_SECRET" != "none" ]]; then + break + fi + RETRY_COUNT=RETRY_COUNT+1 + if [[ $RETRY_COUNT -ge $MAX_RETRIES ]]; then + exit 1 + fi + sleep 6 + done + + # Get current shared secret + DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME) + OPENVIDU_PRO_LICENSE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_PRO_LICENSE) + REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD) + + # Get OpenVidu Media Nodes version to deploy + OPENVIDU_VERSION=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_VERSION') + + if [[ "$OPENVIDU_VERSION" == "none" ]]; then + echo "OpenVidu version not found" + exit 1 + fi + + ALL_SECRETS_GENERATED=$(echo $SHARED_SECRET | jq -r .ALL_SECRETS_GENERATED) + if [[ "$ALL_SECRETS_GENERATED" == "false" ]]; then + echo "Master node not ready" + /usr/local/bin/set_as_unhealthy.sh + exit 1 + fi + + # Get Master Node private IP + MASTER_NODE_IP=${OpenViduMasterNode.PrivateIp} + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/elastic/$OPENVIDU_VERSION/install_ov_media_node.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--deployment-type=elastic" + "--node-role='media-node'" + "--master-node-private-ip=$MASTER_NODE_IP" + "--private-ip=$PRIVATE_IP" + "--redis-password=$REDIS_PASSWORD" + ) + + # Construct the final command with all arguments + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/set_as_unhealthy.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own instance ID + INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + + # Set instance as unhealthy + aws autoscaling set-instance-health \ + --region ${AWS::Region} \ + --instance-id "$INSTANCE_ID" \ + --health-status Unhealthy + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/stop_media_node.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own instance ID + INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + ASG_NAME=openvidu-elastic-media-asg-${AWS::Region}-${AWS::StackName} + + # Execute if docker is installed + if [ -x "$(command -v docker)" ]; then + echo "Stopping media node services and waiting for termination..." + docker container kill --signal=SIGQUIT openvidu || true + docker container kill --signal=SIGQUIT ingress || true + docker container kill --signal=SIGQUIT egress || true + TIME_PASSED=0 + HEARTBEAT_MAX=1800 + # Wait for running containers to not be openvidu, ingress or egress + while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do + echo "Waiting for containers to stop..." + sleep 5 + TIME_PASSED=$((TIME_PASSED+5)) + if [ $TIME_PASSED -ge $HEARTBEAT_MAX ]; then + echo "Increase lifecycle hook timeout to continue waiting for termination" + # Increase lifecycle hook timeout + aws autoscaling record-lifecycle-action-heartbeat \ + --region ${AWS::Region} \ + --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ + --auto-scaling-group-name "$ASG_NAME" \ + --instance-id "$INSTANCE_ID" + TIME_PASSED=0 + fi + done + fi + + aws autoscaling complete-lifecycle-action \ + --region ${AWS::Region} \ + --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ + --auto-scaling-group-name "$ASG_NAME" \ + --lifecycle-action-result CONTINUE \ + --instance-id "$INSTANCE_ID" + mode: "000755" + owner: "root" + group: "root" + + Properties: + LaunchTemplateName: !Sub openvidu-elastic-media-${AWS::Region}-${AWS::StackName} + LaunchTemplateData: + # Enable IMDSv2 by default + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + IamInstanceProfile: + Arn: !GetAtt OpenViduMediaNodeInstanceProfile.Arn + SecurityGroupIds: + - !GetAtt OpenViduMediaNodeSG.GroupId + ImageId: !Ref AmiId + KeyName: !Ref KeyName + InstanceType: !Ref MediaNodeInstanceType + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenViduMediaNodeLaunchTemplate + + export HOME="/root" + + # Install OpenVidu + /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } + + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeType: gp2 + DeleteOnTermination: true + VolumeSize: 50 + + OpenViduMediaNodeASG: + DependsOn: + - StopMediaNodeCloudWatchEventRule + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: !Sub openvidu-elastic-media-asg-${AWS::Region}-${AWS::StackName} + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMediaNodeLaunchTemplate + Version: !GetAtt OpenViduMediaNodeLaunchTemplate.DefaultVersionNumber + MinSize: !Ref MinNumberOfMediaNodes + MaxSize: !Ref MaxNumberOfMediaNodes + DesiredCapacity: !Ref InitialNumberOfMediaNodes + VPCZoneIdentifier: !Ref OpenViduMediaNodeSubnets + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu Elastic - Media Node + PropagateAtLaunch: true + + StopMediaNodeLifecycleHook: + Type: 'AWS::AutoScaling::LifecycleHook' + Properties: + LifecycleHookName: !Sub StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} + AutoScalingGroupName: !Ref OpenViduMediaNodeASG + LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' + DefaultResult: 'CONTINUE' + HeartbeatTimeout: 3600 + + StopMediaNodeDocumentRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - ssm.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ssm:DescribeInstanceInformation + - ssm:ListCommands + - ssm:ListCommandInvocations + Resource: "*" + - Effect: Allow + Action: + - ssm:SendCommand + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunShellScript + - Action: + - ssm:SendCommand + Resource: !Sub arn:${AWS::Partition}:ec2:*:*:instance/* + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' + Effect: Allow + PolicyName: SSM-Automation-Policy + + StopMediaNodeAutomationDocument: + Type: AWS::SSM::Document + Properties: + Name: + Fn::Join: + # Generate a not too long and unique document name + # Getting a unique identifier from the stack id + - '' + - - 'StopMediaNodeAutomationDocument-' + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + DocumentType: Automation + Content: + schemaVersion: '0.3' + assumeRole: "{{automationAssumeRole}}" + description: This document stops the OpenVidu services in a Media Node and + terminates the instance. It stop the OpenVidu services without interrupting + the running sessions, ingress and egress running in the Media Node. + Also it sends a CONTINUE signal to the Auto Scaling Group to continue the + instance termination when the services are stopped. + parameters: + InstanceId: + type: String + automationAssumeRole: + type: String + default: !GetAtt StopMediaNodeDocumentRole.Arn + description: "(Required) The ARN of the role that allows Automation to + perform the actions." + mainSteps: + - name: RunCommand + action: aws:runCommand + inputs: + DocumentName: AWS-RunShellScript + InstanceIds: + - "{{ InstanceId }}" + Parameters: + # 24 hours as a timeout to wait for the instance to stop all services + executionTimeout: "60" + commands: + - nohup /usr/local/bin/stop_media_node.sh > /var/log/stop_media_node.log 2>&1 & + + + StopMediaNodeCloudWatchEventRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - events.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ssm:StartAutomationExecution + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT + PolicyName: !Sub StopMediaNodeCloudWatchEventPolicy-${AWS::Region}-${AWS::StackName} + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - iam:PassRole + Resource: !GetAtt StopMediaNodeDocumentRole.Arn + PolicyName: Pass-Role-SSM-Automation-Policy + + StopMediaNodeCloudWatchEventRule: + Type: AWS::Events::Rule + Properties: + Description: Rule to trigger the StopMediaNodeAutomationDocument when an instance is terminated + EventPattern: + source: + - aws.autoscaling + detail-type: + - EC2 Instance-terminate Lifecycle Action + detail: + AutoScalingGroupName: + - !Sub openvidu-elastic-media-asg-${AWS::Region}-${AWS::StackName} + Name: + Fn::Join: + # Generate a not too long and unique rule name + # Getting a unique identifier from the stack id + - '' + - - StopMediaNodeCloudWatchEventRule- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Targets: + - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT + RoleArn: !GetAtt StopMediaNodeCloudWatchEventRole.Arn + Id: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - StopMediaNodeCloudWatchEventRule- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + InputTransformer: + InputPathsMap: + instanceid: "$.detail.EC2InstanceId" + InputTemplate: | + { + "InstanceId": [ ] + } + + OpenViduMediaNodeASGScalingPolicy: + Type: AWS::AutoScaling::ScalingPolicy + Properties: + AutoScalingGroupName: !Ref OpenViduMediaNodeASG + PolicyType: TargetTrackingScaling + EstimatedInstanceWarmup: 120 + TargetTrackingConfiguration: + PredefinedMetricSpecification: + PredefinedMetricType: ASGAverageCPUUtilization + TargetValue: !Ref ScaleTargetCPU + + OpenViduMasterNodeSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OpenVidu Master Node + GroupName: !Sub openvidu-elastic-master-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIpv6: ::/0 + + OpenViduMediaNodeToMasterNodeRedisIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 7000 + ToPort: 7000 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeToMasterNodeMinioIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 9100 + ToPort: 9100 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeToMasterNodeMongoIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 20000 + ToPort: 20000 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeToMasterNodeLokiIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 3100 + ToPort: 3100 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeToMasterNodeMimirIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 9009 + ToPort: 9009 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeToMasterV2CompatibilityWebhookIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 4443 + ToPort: 4443 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeToMasterDefaultAppWebhookIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 6080 + ToPort: 6080 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OpenVidu Media Node + GroupName: !Sub openvidu-elastic-media-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIpv6: ::/0 + + OpenViduMasterNodeToMediaNodeRTMPIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMediaNodeSG.GroupId + IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + SourceSecurityGroupId: !GetAtt OpenViduMasterNodeSG.GroupId + + OpenViduMasterNodeTurnTLSToMediaNodeIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Condition: TurnTLSIsEnabled + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 5349 + ToPort: 5349 + SourceSecurityGroupId: !GetAtt OpenViduMasterNodeSG.GroupId + + OpenViduMasterNodeToMediaNodeServerIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMediaNodeSG.GroupId + IpProtocol: tcp + FromPort: 7880 + ToPort: 7880 + SourceSecurityGroupId: !GetAtt OpenViduMasterNodeSG.GroupId + + OpenViduMasterToMediaNodeHTTPWHIPIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMediaNodeSG.GroupId + IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !GetAtt OpenViduMasterNodeSG.GroupId + + ElasticIPAssociation: + Type: AWS::EC2::EIPAssociation + Condition: PublicElasticIPPresent + Properties: + InstanceId: !Ref OpenViduMasterNode + EIP: !Ref PublicElasticIP + +Outputs: + ServicesAndCredentials: + Description: Services and credentials + Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/home?region=${AWS::Region}#!/secret?name=openvidu-elastic-${AWS::Region}-${AWS::StackName} diff --git a/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.bicep b/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.bicep new file mode 100644 index 00000000..e821191c --- /dev/null +++ b/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.bicep @@ -0,0 +1,2022 @@ +@description('Stack name') +param stackName string + +@description(''' +[selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate. +[owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter) +and an Elastic IP, you can use this option to use your own certificate. +[letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter) +and an Elastic IP, you can use this option to generate a Let's Encrypt certificate. +''') +@allowed([ + 'selfsigned' + 'owncert' + 'letsencrypt' +]) +param certificateType string = 'selfsigned' + +@description('Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP') +param publicIpAddress string = '' + +@description('Name of the PublicIPAddress resource in your azure if you have a resource of publicIPAddress') +param publicIpAddressResourceName string = '' + +@description('Domain name for the OpenVidu Deployment. Blank will generate default domain') +param domainName string = '' + +@description('If certificate type is \'owncert\', this parameter will be used to specify the public certificate') +param ownPublicCertificate string = '' + +@description('If certificate type is \'owncert\', this parameter will be used to specify the private certificate') +param ownPrivateCertificate string = '' + +@description('If certificate type is \'letsencrypt\', this email will be used for Let\'s Encrypt notifications') +param letsEncryptEmail string = '' + +@description('(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls') +param turnDomainName string = '' + +@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') +param turnOwnPublicCertificate string = '' + +@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') +param turnOwnPrivateCertificate string = '' + +@description('Visit https://openvidu.io/account') +@secure() +param openviduLicense string + +@description('RTCEngine media engine to use') +@allowed([ + 'pion' + 'mediasoup' +]) +param rtcEngine string = 'pion' + +@description('Specifies the EC2 instance type for your OpenVidu Master Node') +@allowed([ + 'Standard_B1s' + 'Standard_B1ms' + 'Standard_B2s' + 'Standard_B2ms' + 'Standard_B4ms' + 'Standard_B8ms' + 'Standard_D2_v3' + 'Standard_D4_v3' + 'Standard_D8_v3' + 'Standard_D16_v3' + 'Standard_D32_v3' + 'Standard_D48_v3' + 'Standard_D64_v3' + 'Standard_D2_v4' + 'Standard_D4_v4' + 'Standard_D8_v4' + 'Standard_D16_v4' + 'Standard_D32_v4' + 'Standard_D48_v4' + 'Standard_D64_v4' + 'Standard_D96_v4' + 'Standard_D2_v5' + 'Standard_D4_v5' + 'Standard_D8_v5' + 'Standard_D16_v5' + 'Standard_D32_v5' + 'Standard_D48_v5' + 'Standard_D64_v5' + 'Standard_D96_v5' + 'Standard_F2' + 'Standard_F4' + 'Standard_F8' + 'Standard_F16' + 'Standard_F32' + 'Standard_F64' + 'Standard_F72' + 'Standard_F2s_v2' + 'Standard_F4s_v2' + 'Standard_F8s_v2' + 'Standard_F16s_v2' + 'Standard_F32s_v2' + 'Standard_F64s_v2' + 'Standard_F72s_v2' + 'Standard_E2_v3' + 'Standard_E4_v3' + 'Standard_E8_v3' + 'Standard_E16_v3' + 'Standard_E32_v3' + 'Standard_E48_v3' + 'Standard_E64_v3' + 'Standard_E96_v3' + 'Standard_E2_v4' + 'Standard_E4_v4' + 'Standard_E8_v4' + 'Standard_E16_v4' + 'Standard_E32_v4' + 'Standard_E48_v4' + 'Standard_E64_v4' + 'Standard_E2_v5' + 'Standard_E4_v5' + 'Standard_E8_v5' + 'Standard_E16_v5' + 'Standard_E32_v5' + 'Standard_E48_v5' + 'Standard_E64_v5' + 'Standard_E96_v5' + 'Standard_M64' + 'Standard_M128' + 'Standard_M208ms_v2' + 'Standard_M416ms_v2' + 'Standard_L4s_v2' + 'Standard_L8s_v2' + 'Standard_L16s_v2' + 'Standard_L32s_v2' + 'Standard_L64s_v2' + 'Standard_L80s_v2' + 'Standard_NC6' + 'Standard_NC12' + 'Standard_NC24' + 'Standard_NC24r' + 'Standard_ND6s' + 'Standard_ND12s' + 'Standard_ND24s' + 'Standard_ND24rs' + 'Standard_NV6' + 'Standard_NV12' + 'Standard_NV24' + 'Standard_H8' + 'Standard_H16' + 'Standard_H16r' + 'Standard_H16mr' + 'Standard_HB120rs_v2' + 'Standard_HC44rs' + 'Standard_DC2s' + 'Standard_DC4s' + 'Standard_DC2s_v2' + 'Standard_DC4s_v2' + 'Standard_DC8s_v2' + 'Standard_DC16s_v2' + 'Standard_DC32s_v2' + 'Standard_A1_v2' + 'Standard_A2_v2' + 'Standard_A4_v2' + 'Standard_A8_v2' + 'Standard_A2m_v2' + 'Standard_A4m_v2' + 'Standard_A8m_v2' +]) +param masterNodeInstanceType string = 'Standard_B2s' + +@description('Specifies the EC2 instance type for your OpenVidu Media Nodes') +@allowed([ + 'Standard_B1s' + 'Standard_B1ms' + 'Standard_B2s' + 'Standard_B2ms' + 'Standard_B4ms' + 'Standard_B8ms' + 'Standard_D2_v3' + 'Standard_D4_v3' + 'Standard_D8_v3' + 'Standard_D16_v3' + 'Standard_D32_v3' + 'Standard_D48_v3' + 'Standard_D64_v3' + 'Standard_D2_v4' + 'Standard_D4_v4' + 'Standard_D8_v4' + 'Standard_D16_v4' + 'Standard_D32_v4' + 'Standard_D48_v4' + 'Standard_D64_v4' + 'Standard_D96_v4' + 'Standard_D2_v5' + 'Standard_D4_v5' + 'Standard_D8_v5' + 'Standard_D16_v5' + 'Standard_D32_v5' + 'Standard_D48_v5' + 'Standard_D64_v5' + 'Standard_D96_v5' + 'Standard_F2' + 'Standard_F4' + 'Standard_F8' + 'Standard_F16' + 'Standard_F32' + 'Standard_F64' + 'Standard_F72' + 'Standard_F2s_v2' + 'Standard_F4s_v2' + 'Standard_F8s_v2' + 'Standard_F16s_v2' + 'Standard_F32s_v2' + 'Standard_F64s_v2' + 'Standard_F72s_v2' + 'Standard_E2_v3' + 'Standard_E4_v3' + 'Standard_E8_v3' + 'Standard_E16_v3' + 'Standard_E32_v3' + 'Standard_E48_v3' + 'Standard_E64_v3' + 'Standard_E96_v3' + 'Standard_E2_v4' + 'Standard_E4_v4' + 'Standard_E8_v4' + 'Standard_E16_v4' + 'Standard_E32_v4' + 'Standard_E48_v4' + 'Standard_E64_v4' + 'Standard_E2_v5' + 'Standard_E4_v5' + 'Standard_E8_v5' + 'Standard_E16_v5' + 'Standard_E32_v5' + 'Standard_E48_v5' + 'Standard_E64_v5' + 'Standard_E96_v5' + 'Standard_M64' + 'Standard_M128' + 'Standard_M208ms_v2' + 'Standard_M416ms_v2' + 'Standard_L4s_v2' + 'Standard_L8s_v2' + 'Standard_L16s_v2' + 'Standard_L32s_v2' + 'Standard_L64s_v2' + 'Standard_L80s_v2' + 'Standard_NC6' + 'Standard_NC12' + 'Standard_NC24' + 'Standard_NC24r' + 'Standard_ND6s' + 'Standard_ND12s' + 'Standard_ND24s' + 'Standard_ND24rs' + 'Standard_NV6' + 'Standard_NV12' + 'Standard_NV24' + 'Standard_H8' + 'Standard_H16' + 'Standard_H16r' + 'Standard_H16mr' + 'Standard_HB120rs_v2' + 'Standard_HC44rs' + 'Standard_DC2s' + 'Standard_DC4s' + 'Standard_DC2s_v2' + 'Standard_DC4s_v2' + 'Standard_DC8s_v2' + 'Standard_DC16s_v2' + 'Standard_DC32s_v2' + 'Standard_A1_v2' + 'Standard_A2_v2' + 'Standard_A4_v2' + 'Standard_A8_v2' + 'Standard_A2m_v2' + 'Standard_A4m_v2' + 'Standard_A8m_v2' +]) +param mediaNodeInstanceType string = 'Standard_B2s' + +@description('Username for the Virtual Machine.') +param adminUsername string + +@description('SSH Key or password for the Virtual Machine') +@secure() +param adminSshKey string + +@description('Number of initial media nodes to deploy') +param initialNumberOfMediaNodes int = 1 + +@description('Minimum number of media nodes to deploy') +param minNumberOfMediaNodes int = 1 + +@description('Maximum number of media nodes to deploy') +param maxNumberOfMediaNodes int = 5 + +@description('Target CPU percentage to scale up or down') +param scaleTargetCPU int = 50 + +/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ + +var isEmptyIp = publicIpAddress == '' +var ipSegments = split(publicIpAddress, '.') +var isFourSegments = length(ipSegments) == 4 +var seg1valid = isEmptyIp ? true : int(ipSegments[0]) >= 0 && int(ipSegments[0]) <= 255 +var seg2valid = isEmptyIp ? true : int(ipSegments[1]) >= 0 && int(ipSegments[1]) <= 255 +var seg3valid = isEmptyIp ? true : int(ipSegments[2]) >= 0 && int(ipSegments[2]) <= 255 +var seg4valid = isEmptyIp ? true : int(ipSegments[3]) >= 0 && int(ipSegments[3]) <= 255 +var isValidIP = !isEmptyIp && isFourSegments && seg1valid && seg2valid && seg3valid && seg4valid + +var isEmptyDomain = domainName == '' +var domainParts = split(domainName, '.') +var validNumberParts = length(domainParts) >= 2 +var allPartsValid = [ + for part in domainParts: length(part) >= 1 && length(part) <= 63 && !empty(part) && part == toLower(part) && !contains( + part, + '--' + ) && empty(replace(part, '[a-z0-9-]', '')) +] + +var isDomainValid = !isEmptyDomain && validNumberParts && !contains(allPartsValid, false) + +var masterNodeVMSettings = { + vmName: '${stackName}-VN-MasterNode' + osDiskType: 'StandardSSD_LRS' + ubuntuOSVersion: { + publisher: 'Canonical' + offer: '0001-com-ubuntu-server-jammy' + sku: '22_04-lts-gen2' + version: 'latest' + } + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${adminUsername}/.ssh/authorized_keys' + keyData: adminSshKey + } + ] + } + } +} + +var mediaNodeVMSettings = { + vmName: '${stackName}-VM-MediaNode' + osDiskType: 'StandardSSD_LRS' + ubuntuOSVersion: { + publisher: 'Canonical' + offer: '0001-com-ubuntu-server-jammy' + sku: '22_04-lts-gen2' + version: 'latest' + } + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${adminUsername}/.ssh/authorized_keys' + keyData: adminSshKey + } + ] + } + } +} + +var networkSettings = { + vNetAddressPrefix: '10.0.0.0/16' + subnetAddressPrefixMaster1: '10.0.1.0/24' + subnetAddressPrefixMedia: '10.0.0.0/24' + vNetName: '${stackName}-virtualNetwork' +} + +var fqdn = isEmptyIp ? publicIP_OV.properties.dnsSettings.fqdn : domainName + +var keyVaultName = '${stackName}-keyvault' + +var location = resourceGroup().location + +var tenantId = subscription().tenantId + +var deploymentUser = az.deployer().objectId + +/*------------------------------------------- KEY VAULT -------------------------------------------*/ + +resource openviduSharedInfo 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: keyVaultName + location: location + properties: { + enabledForDeployment: true + enabledForDiskEncryption: false + enabledForTemplateDeployment: true + tenantId: tenantId + enableSoftDelete: false + accessPolicies: [ + { + objectId: openviduMasterNode.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get', 'set', 'list'] + } + } + { + objectId: openviduScaleSetMediaNode.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get'] + } + } + { + objectId: deploymentUser + tenantId: tenantId + permissions: { + secrets: ['get', 'list', 'set', 'delete', 'recover', 'backup', 'restore'] + } + } + ] + sku: { + name: 'standard' + family: 'A' + } + networkAcls: { + defaultAction: 'Allow' + bypass: 'AzureServices' + } + } +} + +/*------------------------------------------- MASTER NODE -------------------------------------------*/ + +var stringInterpolationParamsMaster = { + domainName: domainName + fqdn: fqdn + turnDomainName: turnDomainName + certificateType: certificateType + letsEncryptEmail: letsEncryptEmail + ownPublicCertificate: ownPublicCertificate + ownPrivateCertificate: ownPrivateCertificate + turnOwnPublicCertificate: turnOwnPublicCertificate + turnOwnPrivateCertificate: turnOwnPrivateCertificate + openviduLicense: openviduLicense + rtcEngine: rtcEngine + keyVaultName: keyVaultName +} + +var installScriptTemplateMaster = ''' +#!/bin/bash -x +OPENVIDU_VERSION=main +DOMAIN= + +# Assume azure cli is installed + +apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + +# Configure Domain +if [[ "${domainName}" == '' ]]; then + DOMAIN=${fqdn} +else + DOMAIN=${domainName} +fi + +# Wait for the keyvault availability +MAX_WAIT=100 +WAIT_INTERVAL=1 +ELAPSED_TIME=0 +while true; do + # Check keyvault availability + set +e + az keyvault secret list --vault-name ${keyVaultName} + + # If it is available, exit the loop + if [ $? -eq 0 ]; then + break + fi + + # If not, wait and check again incrementing the time + ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL)) + + # If exceeded the maximum time, exit with error + if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then + exit 1 + fi + + # Wait for the next iteration + sleep $WAIT_INTERVAL +done +set -e + +# Get own private IP +PRIVATE_IP=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/privateIpAddress?api-version=2017-08-01&format=text") + + +# Store usernames and generate random passwords +DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN-NAME "$DOMAIN")" +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)" +DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")" +DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" +DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" +DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)" +LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" +LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)" +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,app")" +ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")" + +# Base command +INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/elastic/$OPENVIDU_VERSION/install_ov_master_node.sh)" + +# Common arguments +COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=azure" + "--deployment-type=elastic" + "--node-role='master-node'" + "--openvidu-pro-license=$OPENVIDU_PRO_LICENSE" + "--private-ip=$PRIVATE_IP" + "--domain-name=$DOMAIN" + "--enabled-modules='$ENABLED_MODULES'" + "--rtc-engine=$OPENVIDU_RTC_ENGINE" + "--redis-password=$REDIS_PASSWORD" + "--mongo-admin-user=$MONGO_ADMIN_USERNAME" + "--mongo-admin-password=$MONGO_ADMIN_PASSWORD" + "--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY" + "--minio-access-key=$MINIO_ACCESS_KEY" + "--minio-secret-key=$MINIO_SECRET_KEY" + "--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME" + "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" + "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" + "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" +) + + +# Turn with TLS +if [[ "${turnDomainName}" != '' ]]; then + LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}") + COMMON_ARGS+=( + "--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME" + ) +fi + +# Certificate arguments +if [[ "${certificateType}" == "selfsigned" ]]; then + CERT_ARGS=( + "--certificate-type=selfsigned" + ) +elif [[ "${certificateType}" == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT-EMAIL "${letsEncryptEmail}") + CERT_ARGS=( + "--certificate-type=letsencrypt" + "--letsencrypt-email=$LETSENCRYPT_EMAIL" + ) +else + # Download owncert files + mkdir -p /tmp/owncert + wget -O /tmp/owncert/fullchain.pem ${ownPublicCertificate} + wget -O /tmp/owncert/privkey.pem ${ownPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem) + OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem) + + CERT_ARGS=( + "--certificate-type=owncert" + "--owncert-public-key=$OWN_CERT_CRT" + "--owncert-private-key=$OWN_CERT_KEY" + ) + + # Turn with TLS and own certificate + if [[ "${turnDomainName}" != '' ]]; then + # Download owncert files + mkdir -p /tmp/owncert-turn + wget -O /tmp/owncert-turn/fullchain.pem ${turnOwnPublicCertificate} + wget -O /tmp/owncert-turn/privkey.pem ${turnOwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem) + OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem) + + CERT_ARGS+=( + "--turn-owncert-private-key=$OWN_CERT_KEY_TURN" + "--turn-owncert-public-key=$OWN_CERT_CRT_TURN" + ) + fi +fi + +# Construct the final command with all arguments +FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${COMMON_ARGS[@]}") $(printf "%s " "${CERT_ARGS[@]}")" + +# Install OpenVidu +exec bash -c "$FINAL_COMMAND" +''' + +var after_installScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Configure Domain +if [[ "${domainName}" == '' ]]; then + DOMAIN=${fqdn} +else + DOMAIN=${domainName} +fi + +# Generate URLs +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 DASHBOARD-URL --value $DASHBOARD_URL +az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL +az keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL + +az keyvault secret show --vault-name ${keyVaultName} --name MINIO-URL + +if [[ $? -ne 0 ]]; then + echo "Error updating keyvault" +fi +''' + +var update_config_from_secretScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Installation directory +INSTALL_DIR="/opt/openvidu" +CLUSTER_CONFIG_DIR="${INSTALL_DIR}/config/cluster" +MASTER_NODE_CONFIG_DIR="${INSTALL_DIR}/config/node" + +# Replace DOMAIN_NAME +export DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv) +if [[ -n "$DOMAIN" ]]; then + sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${CLUSTER_CONFIG_DIR}/openvidu.env" +else + exit 1 +fi + +# Replace LIVEKIT_TURN_DOMAIN_NAME +export LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv) +if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${CLUSTER_CONFIG_DIR}/openvidu.env" +fi + +if [[ ${certificateType} == "letsencrypt" ]]; then + export LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv) + sed -i "s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/" "${CLUSTER_CONFIG_DIR}/openvidu.env" +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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv) +export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) +export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv) +export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv) +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/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env" + +# Update URLs in secret +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 DASHBOARD-URL --value $DASHBOARD_URL +az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL +az keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL +''' + +var update_secret_from_configScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Installation directory +INSTALL_DIR="/opt/openvidu" +CLUSTER_CONFIG_DIR="${INSTALL_DIR}/config/cluster" +MASTER_NODE_CONFIG_DIR="${INSTALL_DIR}/config/node" + +if [[ ${certificateType} == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL="$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL "${CLUSTER_CONFIG_DIR}/openvidu.env")" + az keyvault secret set --vault-name ${keyVaultName} --name "LETSENCRYPT-EMAIL" --value $LETSENCRYPT_EMAIL +fi + +# 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")" +LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_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")" +DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +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 LIVEKIT-TURN-DOMAIN-NAME --value $LIVEKIT_TURN_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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD +az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES +''' + +var get_value_from_configScriptMaster = ''' +#!/bin/bash +set -e + +# Function to get the value of a given key from the environment file +get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi +} + +# Check if the correct number of arguments are supplied +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Get the key and file path from the arguments +key="$1" +file_path="$2" + +# Get and print the value +get_value "$key" "$file_path" +''' + +var store_secretScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Modes: save, generate +# save mode: save the secret in the secret manager +# generate mode: generate a random password and save it in the secret manager +MODE="$1" + +if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${3:-}" + LENGTH="${4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${LENGTH})" + RANDOM_PASSWORD="${PREFIX}${RANDOM_PASSWORD}" + az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null + if [[ $? -ne 0 ]]; then + echo "Error generating secret" + fi + echo "$RANDOM_PASSWORD" +elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null + if [[ $? -ne 0 ]]; then + echo "Error generating secret" + fi + echo "$SECRET_VALUE" +else + exit 1 +fi +''' + +var check_app_readyScriptMaster = ''' +#!/bin/bash +set -e +while true; do + HTTP_STATUS=$(curl -Ik http://localhost:7880/twirp/health | head -n1 | awk '{print $2}') + if [ $HTTP_STATUS == 200 ]; then + break + 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 installScriptMaster = reduce( + items(stringInterpolationParamsMaster), + { value: installScriptTemplateMaster }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var after_installScriptMaster = reduce( + items(stringInterpolationParamsMaster), + { value: after_installScriptTemplateMaster }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var update_config_from_secretScriptMaster = reduce( + items(stringInterpolationParamsMaster), + { value: update_config_from_secretScriptTemplateMaster }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var update_secret_from_configScriptMaster = reduce( + items(stringInterpolationParamsMaster), + { value: update_secret_from_configScriptTemplateMaster }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var store_secretScriptMaster = reduce( + items(stringInterpolationParamsMaster), + { value: store_secretScriptTemplateMaster }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var base64installMaster = base64(installScriptMaster) +var base64after_installMaster = base64(after_installScriptMaster) +var base64update_config_from_secretMaster = base64(update_config_from_secretScriptMaster) +var base64update_secret_from_configMaster = base64(update_secret_from_configScriptMaster) +var base64get_value_from_configMaster = base64(get_value_from_configScriptMaster) +var base64store_secretMaster = base64(store_secretScriptMaster) +var base64check_app_readyMaster = base64(check_app_readyScriptMaster) +var base64restartMaster = base64(restartScriptMaster) + +var userDataParamsMasterNode = { + base64install: base64installMaster + base64after_install: base64after_installMaster + base64update_config_from_secret: base64update_config_from_secretMaster + base64update_secret_from_config: base64update_secret_from_configMaster + base64get_value_from_config: base64get_value_from_configMaster + base64store_secret: base64store_secretMaster + base64check_app_ready: base64check_app_readyMaster + base64restart: base64restartMaster + keyVaultName: keyVaultName + storageAccountName: storageAccount.name +} + +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 + +# 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 + +# Install azure cli +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + +az login --identity --allow-no-subscriptions + +apt-get update && apt-get install -y + +export HOME="/root" + +# Install OpenVidu +/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; 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 + +set +e +az storage blob upload --account-name ${storageAccountName} --container-name automation-locks --name lock.txt --file /dev/null --auth-mode key +set -e + +az keyvault secret set --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --value "true" + +# Wait for the app +sleep 150 +/usr/local/bin/check_app_ready.sh +''' + +var userDataMasterNode = reduce( + items(userDataParamsMasterNode), + { value: userDataTemplateMasterNode }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +resource openviduMasterNode 'Microsoft.Compute/virtualMachines@2023-09-01' = { + name: masterNodeVMSettings.vmName + location: location + identity: { type: 'SystemAssigned' } + properties: { + hardwareProfile: { + vmSize: masterNodeInstanceType + } + storageProfile: { + osDisk: { + createOption: 'FromImage' + managedDisk: { + storageAccountType: masterNodeVMSettings.osDiskType + } + diskSizeGB: 100 + } + imageReference: masterNodeVMSettings.ubuntuOSVersion + } + networkProfile: { + networkInterfaces: [ + { + id: netInterfaceMasterNode.id + } + ] + } + osProfile: { + computerName: masterNodeVMSettings.vmName + adminUsername: adminUsername + adminPassword: adminSshKey + linuxConfiguration: masterNodeVMSettings.linuxConfiguration + } + userData: base64(userDataMasterNode) + } +} + +/*------------------------------------------- MEDIA NODES -------------------------------------------*/ + +var stringInterpolationParamsMedia = { + privateIPMasterNode: netInterfaceMasterNode.properties.ipConfigurations[0].properties.privateIPAddress + keyVaultName: keyVaultName +} + +var installScriptTemplateMedia = ''' +#!/bin/bash -x +set -e +DOMAIN= + +# Install dependencies +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") + +WAIT_INTERVAL=1 +MAX_WAIT=200 +ELAPSED_TIME=0 +set +e +while true; do + # get secret value + FINISH_MASTER_NODE=$(az keyvault secret show --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --query value -o tsv) + + # Check if the secret has been generated + if [ "$FINISH_MASTER_NODE" == "true" ]; then + break + fi + + ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL)) + + # Check if the maximum waiting time has been reached + if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then + exit 1 + fi + + sleep $WAIT_INTERVAL +done +set -e + +# Get current shared secret +DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv) +OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv) +REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv) +ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv) +OPENVIDU_VERSION="$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-VERSION --query value -o tsv)" + +# Get Master Node private IP +MASTER_NODE_IP=${privateIPMasterNode} + +# Base command +INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/elastic/$OPENVIDU_VERSION/install_ov_media_node.sh)" + +# Common arguments +COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=azure" + "--deployment-type=elastic" + "--node-role='media-node'" + "--master-node-private-ip=$MASTER_NODE_IP" + "--private-ip=$PRIVATE_IP" + "--enabled-modules='$ENABLED_MODULES'" + "--redis-password=$REDIS_PASSWORD" +) +# Construct the final command with all arguments +FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${COMMON_ARGS[@]}")" + +# Install OpenVidu +exec bash -c "$FINAL_COMMAND" +''' + +var stopMediaNodeParams = { + subscriptionId: subscription().subscriptionId + resourceGroupName: resourceGroup().name + vmScaleSetName: '${stackName}-mediaNodeScaleSet' + storageAccountName: storageAccount.name +} + +var stop_media_nodesScriptMediaTemplate = ''' +#!/bin/bash +set -e + +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=SIGINT openvidu || true + docker container kill --signal=SIGINT ingress || true + docker container kill --signal=SIGINT egress || true + + # Wait for running containers to not be openvidu, ingress or egress + while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do + echo "Waiting for containers to stop..." + sleep 5 + done +fi + +az login --identity + +RESOURCE_GROUP_NAME=${resourceGroupName} +VM_SCALE_SET_NAME=${vmScaleSetName} +SUBSCRIPTION_ID=${subscriptionId} +BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId') +INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}') +RESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Compute/virtualMachineScaleSets/$VM_SCALE_SET_NAME +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}" + +az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID +''' + +var userDataMediaNodeTemplate = ''' +#!/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 + +# stop_media_nodes.sh +echo ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh +chmod +x /usr/local/bin/stop_media_node.sh + +apt-get update && apt-get install -y +apt-get install -y jq + +# Install azure cli +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + +az login --identity + +# Protect from scale in actions +RESOURCE_GROUP_NAME=${resourceGroupName} +VM_SCALE_SET_NAME=${vmScaleSetName} +BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId') +INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}') +az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-id $INSTANCE_ID --protect-from-scale-in true + +export HOME="/root" + +# Install OpenVidu +/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + +# Start OpenVidu +systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } +#/usr/local/bin/set_as_unhealthy.sh +''' + +var installScriptMedia = reduce( + items(stringInterpolationParamsMedia), + { value: installScriptTemplateMedia }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var base64installMedia = base64(installScriptMedia) + +var stop_media_nodesScriptMedia = reduce( + items(stopMediaNodeParams), + { value: stop_media_nodesScriptMediaTemplate }, + (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } +).value + +var base64stopMediaNode = base64(stop_media_nodesScriptMedia) + +var userDataParamsMedia = { + base64install: base64installMedia + base64stop: base64stopMediaNode + resourceGroupName: resourceGroup().name + vmScaleSetName: '${stackName}-mediaNodeScaleSet' +} + +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: storageAccount.name + } + identity: { type: 'SystemAssigned' } + sku: { + name: mediaNodeInstanceType + tier: 'Standard' + capacity: initialNumberOfMediaNodes + } + properties: { + overprovision: true + upgradePolicy: { + mode: 'Automatic' + } + singlePlacementGroup: true + platformFaultDomainCount: 1 + virtualMachineProfile: { + storageProfile: { + osDisk: { + createOption: 'FromImage' + managedDisk: { + storageAccountType: mediaNodeVMSettings.osDiskType + } + diskSizeGB: 50 + } + imageReference: mediaNodeVMSettings.ubuntuOSVersion + } + osProfile: { + computerNamePrefix: mediaNodeVMSettings.vmName + adminUsername: adminUsername + adminPassword: adminSshKey + linuxConfiguration: mediaNodeVMSettings.linuxConfiguration + } + networkProfile: { + networkInterfaceConfigurations: [ + { + name: '${stackName}-mediaNodeNetInterface' + properties: { + primary: true + ipConfigurations: [ + { + name: 'ipconfigMediaNode' + properties: { + subnet: { + id: vnet_OV.properties.subnets[0].id + } + applicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + publicIPAddressConfiguration: { + name: 'publicIPAddressMediaNode' + properties: { + publicIPAddressVersion: 'IPv4' + } + } + } + } + ] + networkSecurityGroup: { + id: openviduMediaNodeNSG.id + } + } + } + ] + } + userData: base64userDataMediaNode + } + } +} + +resource openviduAutoScaleSettings 'Microsoft.Insights/autoscaleSettings@2022-10-01' = { + name: '${stackName}-autoscaleSettings' + location: resourceGroup().location + properties: { + profiles: [ + { + name: 'openvidu-medianode-autoscale' + capacity: { + minimum: string(minNumberOfMediaNodes) + maximum: string(maxNumberOfMediaNodes) + default: string(initialNumberOfMediaNodes) + } + rules: [ + { + metricTrigger: { + metricName: 'Percentage CPU' + metricNamespace: 'Microsoft.Compute/virtualMachineScaleSets' + metricResourceUri: openviduScaleSetMediaNode.id + statistic: 'Average' + operator: 'GreaterThan' + threshold: scaleTargetCPU + timeAggregation: 'Average' + timeWindow: 'PT5M' + timeGrain: 'PT1M' + } + scaleAction: { + direction: 'Increase' + type: 'ChangeCount' + value: '1' + cooldown: 'PT5M' + } + } + { + metricTrigger: { + metricName: 'Percentage CPU' + metricNamespace: 'Microsoft.Compute/virtualMachineScaleSets' + metricResourceUri: openviduScaleSetMediaNode.id + statistic: 'Average' + operator: 'LessThan' + threshold: scaleTargetCPU + timeAggregation: 'Average' + timeWindow: 'PT5M' + timeGrain: 'PT1M' + } + scaleAction: { + direction: 'Decrease' + type: 'ChangeCount' + value: '1' + cooldown: 'PT5M' + } + } + ] + } + ] + enabled: true + targetResourceUri: openviduScaleSetMediaNode.id + } +} + +/*------------------------------------------- SCALE IN ------------------------------------------*/ + +resource roleAssignmentMasterNode 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('roleAssignmentForMasterNode${openviduMasterNode.name}') + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + principalId: openviduMasterNode.identity.principalId + } +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('roleAssignmentForScaleSet${openviduScaleSetMediaNode.name}') + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + principalId: openviduScaleSetMediaNode.identity.principalId + } +} + +@description('Automation Account Name to create a runbook inside it for scale in') +param automationAccountName string + +module webhookModule '../../shared/webhookdeployment.json' = { + params: { + automationAccountName: automationAccountName + runbookName: 'scaleInRunbook' + webhookName: 'webhookForScaleIn' + WebhookExpiryTime: '2035-03-30T00:00:00Z' + _artifactsLocation: 'https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1' //Change when we upload this to s3 or blob + } + name: 'WebhookDeployment' +} + +resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = { + name: 'actiongrouptest' + 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 -------------------------------------------*/ + +resource publicIP_OV 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (isEmptyIp == true) { + name: '${stackName}-publicIP' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAddressVersion: 'IPv4' + publicIPAllocationMethod: 'Static' + dnsSettings: { + domainNameLabel: isEmptyDomain ? toLower('${stackName}') : null + fqdn: isEmptyDomain ? null : domainName + } + } +} + +resource publicIP_OV_ifNotEmpty 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (!isEmptyIp == true) { + name: publicIpAddressResourceName +} + +resource vnet_OV 'Microsoft.Network/virtualNetworks@2023-11-01' = { + name: networkSettings.vNetName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + networkSettings.vNetAddressPrefix + ] + } + subnets: [ + { + name: 'subnetForMediaNodes' + properties: { + addressPrefix: networkSettings.subnetAddressPrefixMedia + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroup: { + id: openviduMediaNodeNSG.id + } + } + } + ] + } +} + +resource subnetMasterNode 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = { + parent: vnet_OV + name: 'firstSubnetForMasterNodes' + properties: { + addressPrefix: networkSettings.subnetAddressPrefixMaster1 + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } +} + +resource netInterfaceMasterNode 'Microsoft.Network/networkInterfaces@2023-11-01' = { + name: '${stackName}-masterNoderNetInterface' + location: location + properties: { + ipConfigurations: [ + { + name: 'primaryIPConfig' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: subnetMasterNode.id + } + applicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + publicIPAddress: { + id: isEmptyIp ? publicIP_OV.id : publicIP_OV_ifNotEmpty.id + properties: { + deleteOption: 'Delete' + } + } + } + } + ] + networkSecurityGroup: { + id: openviduMasterNodeNSG.id + } + } +} + +resource openviduMasterNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-01' = { + name: '${stackName}-masterNoderNSG' + location: location + properties: { + securityRules: [ + { + name: 'SSH' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + access: 'Allow' + priority: 100 + direction: 'Inbound' + } + } + { + name: 'HTTP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '80' + access: 'Allow' + priority: 110 + direction: 'Inbound' + } + } + { + name: 'HTTPS' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '443' + access: 'Allow' + priority: 120 + direction: 'Inbound' + } + } + { + name: 'RTMP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '1935' + access: 'Allow' + priority: 130 + direction: 'Inbound' + } + } + { + name: 'MinIO' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '9000' + access: 'Allow' + priority: 140 + direction: 'Inbound' + } + } + ] + } +} + +resource openviduMasterNodeASG 'Microsoft.Network/applicationSecurityGroups@2024-03-01' = { + name: '${stackName}-masterNodeASG' + location: location +} + +resource mediaToMasterRedisIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_REDIS_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '7000' + access: 'Allow' + priority: 150 + direction: 'Inbound' + } +} + +resource mediaToMasterMinioIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_MINIO_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '9100' + access: 'Allow' + priority: 160 + direction: 'Inbound' + } +} + +resource mediaToMasterMongoIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_MONGO_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '20000' + access: 'Allow' + priority: 170 + direction: 'Inbound' + } +} + +resource mediaToMasterLokiIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_LOKI_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '3100' + access: 'Allow' + priority: 180 + direction: 'Inbound' + } +} + +resource mediaToMasterMimirIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_MIMIR_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '9009' + access: 'Allow' + priority: 190 + direction: 'Inbound' + } +} + +resource mediaToMasterV2CompatibilityWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_V2COMPATIBILITY_WEBHOOK_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '4443' + access: 'Allow' + priority: 200 + direction: 'Inbound' + } +} + +resource mediaToMasterDefaultAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '6080' + access: 'Allow' + priority: 210 + direction: 'Inbound' + } +} + +resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-01' = { + name: '${stackName}-mediaNoderNSG' + location: location + properties: { + securityRules: [ + { + name: 'SSH' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + access: 'Allow' + priority: 100 + direction: 'Inbound' + } + } + { + name: 'TURN' + properties: { + protocol: 'Udp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '443' + access: 'Allow' + priority: 110 + direction: 'Inbound' + } + } + { + name: 'WebRTC_over_TCP' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '7881' + access: 'Allow' + priority: 120 + direction: 'Inbound' + } + } + { + name: 'WebRTC_using_WHIP' + properties: { + protocol: 'Udp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '7885' + access: 'Allow' + priority: 130 + direction: 'Inbound' + } + } + { + name: 'WebRTC_traffic_UDP' + properties: { + protocol: 'Udp' + sourceAddressPrefix: '*' + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '50000' + '60000' + ] + access: 'Allow' + priority: 140 + direction: 'Inbound' + } + } + ] + } +} + +resource openviduMediaNodeASG 'Microsoft.Network/applicationSecurityGroups@2024-03-01' = { + name: '${stackName}-mediaNodeASG' + location: location +} + +resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMediaNodeNSG + name: 'masterNode_to_mediaNode_RTMP_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + destinationPortRange: '1935' + access: 'Allow' + priority: 150 + 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: 160 + 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: 170 + direction: 'Inbound' + } +} + +resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMediaNodeNSG + name: 'masterNode_to_mediaNode_HTTP_WHIP_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + destinationPortRange: '8080' + access: 'Allow' + priority: 180 + direction: 'Inbound' + } +} + +/*------------------------------------------- STORAGE ACCOUNT ----------------------------------------*/ + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: 'lockstorage${uniqueString(resourceGroup().id)}' + location: resourceGroup().location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Cool' + supportsHttpsTrafficOnly: true + } +} + +resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { + name: '${storageAccount.name}/default/automation-locks' + properties: { + publicAccess: 'None' + } +} + +/*------------------------------------------- OUTPUTS -------------------------------------------*/ + +output ipValidationStatus string = isValidIP ? 'IP address is valid' : 'IP address not valid' + +output domainValidationStatus string = isDomainValid ? 'Domain is valid' : 'Domain is not valid' + +output ownCertValidationStatus string = (certificateType == 'owncert' && ownPrivateCertificate != '' && ownPublicCertificate != '') + ? 'owncert selected and valid' + : 'You need to fill \'Own Public Certificate\' and \'Own Private Certificate\'' +output letsEncryptValidationStatus string = (certificateType == 'letsencrypt' && letsEncryptEmail != '') + ? 'letsEncrypt selected and valid' + : 'You need to fill \'Lets Encrypt Email\'' diff --git a/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.json b/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.json new file mode 100644 index 00000000..06ea7435 --- /dev/null +++ b/openvidu-deployment/pro/elastic/azure/cf-openvidu-elastic.json @@ -0,0 +1,1604 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "14975259616382894965" + } + }, + "parameters": { + "stackName": { + "type": "string", + "metadata": { + "description": "Stack name" + } + }, + "certificateType": { + "type": "string", + "defaultValue": "selfsigned", + "allowedValues": [ + "selfsigned", + "owncert", + "letsencrypt" + ], + "metadata": { + "description": "[selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate.\n[owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter)\nand an Elastic IP, you can use this option to use your own certificate.\n[letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter)\nand an Elastic IP, you can use this option to generate a Let's Encrypt certificate.\n" + } + }, + "publicIpAddress": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP" + } + }, + "publicIpAddressResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the PublicIPAddress resource in your azure if you have a resource of publicIPAddress" + } + }, + "domainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Domain name for the OpenVidu Deployment. Blank will generate default domain" + } + }, + "ownPublicCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'owncert', this parameter will be used to specify the public certificate" + } + }, + "ownPrivateCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'owncert', this parameter will be used to specify the private certificate" + } + }, + "letsEncryptEmail": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" + } + }, + "turnDomainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls" + } + }, + "turnOwnPublicCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + } + }, + "turnOwnPrivateCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + } + }, + "openviduLicense": { + "type": "securestring", + "metadata": { + "description": "Visit https://openvidu.io/account" + } + }, + "rtcEngine": { + "type": "string", + "defaultValue": "pion", + "allowedValues": [ + "pion", + "mediasoup" + ], + "metadata": { + "description": "RTCEngine media engine to use" + } + }, + "masterNodeInstanceType": { + "type": "string", + "defaultValue": "Standard_B2s", + "allowedValues": [ + "Standard_B1s", + "Standard_B1ms", + "Standard_B2s", + "Standard_B2ms", + "Standard_B4ms", + "Standard_B8ms", + "Standard_D2_v3", + "Standard_D4_v3", + "Standard_D8_v3", + "Standard_D16_v3", + "Standard_D32_v3", + "Standard_D48_v3", + "Standard_D64_v3", + "Standard_D2_v4", + "Standard_D4_v4", + "Standard_D8_v4", + "Standard_D16_v4", + "Standard_D32_v4", + "Standard_D48_v4", + "Standard_D64_v4", + "Standard_D96_v4", + "Standard_D2_v5", + "Standard_D4_v5", + "Standard_D8_v5", + "Standard_D16_v5", + "Standard_D32_v5", + "Standard_D48_v5", + "Standard_D64_v5", + "Standard_D96_v5", + "Standard_F2", + "Standard_F4", + "Standard_F8", + "Standard_F16", + "Standard_F32", + "Standard_F64", + "Standard_F72", + "Standard_F2s_v2", + "Standard_F4s_v2", + "Standard_F8s_v2", + "Standard_F16s_v2", + "Standard_F32s_v2", + "Standard_F64s_v2", + "Standard_F72s_v2", + "Standard_E2_v3", + "Standard_E4_v3", + "Standard_E8_v3", + "Standard_E16_v3", + "Standard_E32_v3", + "Standard_E48_v3", + "Standard_E64_v3", + "Standard_E96_v3", + "Standard_E2_v4", + "Standard_E4_v4", + "Standard_E8_v4", + "Standard_E16_v4", + "Standard_E32_v4", + "Standard_E48_v4", + "Standard_E64_v4", + "Standard_E2_v5", + "Standard_E4_v5", + "Standard_E8_v5", + "Standard_E16_v5", + "Standard_E32_v5", + "Standard_E48_v5", + "Standard_E64_v5", + "Standard_E96_v5", + "Standard_M64", + "Standard_M128", + "Standard_M208ms_v2", + "Standard_M416ms_v2", + "Standard_L4s_v2", + "Standard_L8s_v2", + "Standard_L16s_v2", + "Standard_L32s_v2", + "Standard_L64s_v2", + "Standard_L80s_v2", + "Standard_NC6", + "Standard_NC12", + "Standard_NC24", + "Standard_NC24r", + "Standard_ND6s", + "Standard_ND12s", + "Standard_ND24s", + "Standard_ND24rs", + "Standard_NV6", + "Standard_NV12", + "Standard_NV24", + "Standard_H8", + "Standard_H16", + "Standard_H16r", + "Standard_H16mr", + "Standard_HB120rs_v2", + "Standard_HC44rs", + "Standard_DC2s", + "Standard_DC4s", + "Standard_DC2s_v2", + "Standard_DC4s_v2", + "Standard_DC8s_v2", + "Standard_DC16s_v2", + "Standard_DC32s_v2", + "Standard_A1_v2", + "Standard_A2_v2", + "Standard_A4_v2", + "Standard_A8_v2", + "Standard_A2m_v2", + "Standard_A4m_v2", + "Standard_A8m_v2" + ], + "metadata": { + "description": "Specifies the EC2 instance type for your OpenVidu Master Node" + } + }, + "mediaNodeInstanceType": { + "type": "string", + "defaultValue": "Standard_B2s", + "allowedValues": [ + "Standard_B1s", + "Standard_B1ms", + "Standard_B2s", + "Standard_B2ms", + "Standard_B4ms", + "Standard_B8ms", + "Standard_D2_v3", + "Standard_D4_v3", + "Standard_D8_v3", + "Standard_D16_v3", + "Standard_D32_v3", + "Standard_D48_v3", + "Standard_D64_v3", + "Standard_D2_v4", + "Standard_D4_v4", + "Standard_D8_v4", + "Standard_D16_v4", + "Standard_D32_v4", + "Standard_D48_v4", + "Standard_D64_v4", + "Standard_D96_v4", + "Standard_D2_v5", + "Standard_D4_v5", + "Standard_D8_v5", + "Standard_D16_v5", + "Standard_D32_v5", + "Standard_D48_v5", + "Standard_D64_v5", + "Standard_D96_v5", + "Standard_F2", + "Standard_F4", + "Standard_F8", + "Standard_F16", + "Standard_F32", + "Standard_F64", + "Standard_F72", + "Standard_F2s_v2", + "Standard_F4s_v2", + "Standard_F8s_v2", + "Standard_F16s_v2", + "Standard_F32s_v2", + "Standard_F64s_v2", + "Standard_F72s_v2", + "Standard_E2_v3", + "Standard_E4_v3", + "Standard_E8_v3", + "Standard_E16_v3", + "Standard_E32_v3", + "Standard_E48_v3", + "Standard_E64_v3", + "Standard_E96_v3", + "Standard_E2_v4", + "Standard_E4_v4", + "Standard_E8_v4", + "Standard_E16_v4", + "Standard_E32_v4", + "Standard_E48_v4", + "Standard_E64_v4", + "Standard_E2_v5", + "Standard_E4_v5", + "Standard_E8_v5", + "Standard_E16_v5", + "Standard_E32_v5", + "Standard_E48_v5", + "Standard_E64_v5", + "Standard_E96_v5", + "Standard_M64", + "Standard_M128", + "Standard_M208ms_v2", + "Standard_M416ms_v2", + "Standard_L4s_v2", + "Standard_L8s_v2", + "Standard_L16s_v2", + "Standard_L32s_v2", + "Standard_L64s_v2", + "Standard_L80s_v2", + "Standard_NC6", + "Standard_NC12", + "Standard_NC24", + "Standard_NC24r", + "Standard_ND6s", + "Standard_ND12s", + "Standard_ND24s", + "Standard_ND24rs", + "Standard_NV6", + "Standard_NV12", + "Standard_NV24", + "Standard_H8", + "Standard_H16", + "Standard_H16r", + "Standard_H16mr", + "Standard_HB120rs_v2", + "Standard_HC44rs", + "Standard_DC2s", + "Standard_DC4s", + "Standard_DC2s_v2", + "Standard_DC4s_v2", + "Standard_DC8s_v2", + "Standard_DC16s_v2", + "Standard_DC32s_v2", + "Standard_A1_v2", + "Standard_A2_v2", + "Standard_A4_v2", + "Standard_A8_v2", + "Standard_A2m_v2", + "Standard_A4m_v2", + "Standard_A8m_v2" + ], + "metadata": { + "description": "Specifies the EC2 instance type for your OpenVidu Media Nodes" + } + }, + "adminUsername": { + "type": "string", + "metadata": { + "description": "Username for the Virtual Machine." + } + }, + "adminSshKey": { + "type": "securestring", + "metadata": { + "description": "SSH Key or password for the Virtual Machine" + } + }, + "initialNumberOfMediaNodes": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Number of initial media nodes to deploy" + } + }, + "minNumberOfMediaNodes": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of media nodes to deploy" + } + }, + "maxNumberOfMediaNodes": { + "type": "int", + "defaultValue": 5, + "metadata": { + "description": "Maximum number of media nodes to deploy" + } + }, + "scaleTargetCPU": { + "type": "int", + "defaultValue": 50, + "metadata": { + "description": "Target CPU percentage to scale up or down" + } + }, + "datetime": { + "type": "string", + "defaultValue": "[utcNow('u')]" + }, + "automationAccountName": { + "type": "string", + "metadata": { + "description": "Automation Account Name to create a runbook inside it for scale in" + } + } + }, + "variables": { + "copy": [ + { + "name": "allPartsValid", + "count": "[length(variables('domainParts'))]", + "input": "[and(and(and(and(and(greaterOrEquals(length(variables('domainParts')[copyIndex('allPartsValid')]), 1), lessOrEquals(length(variables('domainParts')[copyIndex('allPartsValid')]), 63)), not(empty(variables('domainParts')[copyIndex('allPartsValid')]))), equals(variables('domainParts')[copyIndex('allPartsValid')], toLower(variables('domainParts')[copyIndex('allPartsValid')]))), not(contains(variables('domainParts')[copyIndex('allPartsValid')], '--'))), empty(replace(variables('domainParts')[copyIndex('allPartsValid')], '[a-z0-9-]', '')))]" + } + ], + "isEmptyIp": "[equals(parameters('publicIpAddress'), '')]", + "ipSegments": "[split(parameters('publicIpAddress'), '.')]", + "isFourSegments": "[equals(length(variables('ipSegments')), 4)]", + "seg1valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[0]), 0), lessOrEquals(int(variables('ipSegments')[0]), 255)))]", + "seg2valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[1]), 0), lessOrEquals(int(variables('ipSegments')[1]), 255)))]", + "seg3valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[2]), 0), lessOrEquals(int(variables('ipSegments')[2]), 255)))]", + "seg4valid": "[if(variables('isEmptyIp'), true(), and(greaterOrEquals(int(variables('ipSegments')[3]), 0), lessOrEquals(int(variables('ipSegments')[3]), 255)))]", + "isValidIP": "[and(and(and(and(and(not(variables('isEmptyIp')), variables('isFourSegments')), variables('seg1valid')), variables('seg2valid')), variables('seg3valid')), variables('seg4valid'))]", + "isEmptyDomain": "[equals(parameters('domainName'), '')]", + "domainParts": "[split(parameters('domainName'), '.')]", + "validNumberParts": "[greaterOrEquals(length(variables('domainParts')), 2)]", + "isDomainValid": "[and(and(not(variables('isEmptyDomain')), variables('validNumberParts')), not(contains(variables('allPartsValid'), false())))]", + "masterNodeVMSettings": { + "vmName": "[format('{0}-VN-MasterNode', parameters('stackName'))]", + "osDiskType": "StandardSSD_LRS", + "ubuntuOSVersion": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest" + }, + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", + "keyData": "[parameters('adminSshKey')]" + } + ] + } + } + }, + "mediaNodeVMSettings": { + "vmName": "[format('{0}-VM-MediaNode', parameters('stackName'))]", + "osDiskType": "StandardSSD_LRS", + "ubuntuOSVersion": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest" + }, + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", + "keyData": "[parameters('adminSshKey')]" + } + ] + } + } + }, + "networkSettings": { + "vNetAddressPrefix": "10.0.0.0/16", + "subnetAddressPrefixMaster1": "10.0.1.0/24", + "subnetAddressPrefixMedia": "10.0.0.0/24", + "vNetName": "[format('{0}-virtualNetwork', parameters('stackName'))]" + }, + "keyVaultName": "[format('{0}-keyvault', parameters('stackName'))]", + "location": "[resourceGroup().location]", + "tenantId": "[subscription().tenantId]", + "deploymentUser": "[deployer().objectId]", + "installScriptTemplateMaster": "#!/bin/bash -x\nOPENVIDU_VERSION=main\nDOMAIN=\n\n# Assume azure cli is installed\n\napt-get update && apt-get install -y \\\n curl \\\n unzip \\\n jq \\\n wget\n\n# Configure Domain\nif [[ \"${domainName}\" == '' ]]; then\n DOMAIN=${fqdn}\nelse\n DOMAIN=${domainName}\nfi\n\n# Wait for the keyvault availability\nMAX_WAIT=100\nWAIT_INTERVAL=1\nELAPSED_TIME=0\nwhile true; do\n # Check keyvault availability\n set +e\n az keyvault secret list --vault-name ${keyVaultName}\n\n # If it is available, exit the loop\n if [ $? -eq 0 ]; then\n break\n fi\n\n # If not, wait and check again incrementing the time\n ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))\n\n # If exceeded the maximum time, exit with error\n if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then\n exit 1\n fi\n\n # Wait for the next iteration\n sleep $WAIT_INTERVAL\ndone\nset -e\n\n# Get own private IP\nPRIVATE_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\")\n\n\n# Store usernames and generate random passwords\nDOMAIN=\"$(/usr/local/bin/store_secret.sh save DOMAIN-NAME \"$DOMAIN\")\"\nOPENVIDU_PRO_LICENSE=\"$(/usr/local/bin/store_secret.sh save OPENVIDU-PRO-LICENSE \"${openviduLicense}\")\"\nOPENVIDU_RTC_ENGINE=\"$(/usr/local/bin/store_secret.sh save OPENVIDU-RTC-ENGINE \"${rtcEngine}\")\"\nREDIS_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate REDIS-PASSWORD)\"\nMONGO_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save MONGO-ADMIN-USERNAME \"mongoadmin\")\"\nMONGO_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate MONGO-ADMIN-PASSWORD)\"\nMONGO_REPLICA_SET_KEY=\"$(/usr/local/bin/store_secret.sh generate MONGO-REPLICA-SET-KEY)\"\nMINIO_ACCESS_KEY=\"$(/usr/local/bin/store_secret.sh save MINIO-ACCESS-KEY \"minioadmin\")\"\nMINIO_SECRET_KEY=\"$(/usr/local/bin/store_secret.sh generate MINIO-SECRET-KEY)\"\nDASHBOARD_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-USERNAME \"dashboardadmin\")\"\nDASHBOARD_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)\"\nGRAFANA_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME \"grafanaadmin\")\"\nGRAFANA_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)\"\nDEFAULT_APP_USERNAME=\"$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME \"calluser\")\"\nDEFAULT_APP_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)\"\nDEFAULT_APP_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME \"calladmin\")\"\nDEFAULT_APP_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)\"\nLIVEKIT_API_KEY=\"$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY \"API\" 12)\"\nLIVEKIT_API_SECRET=\"$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)\"\nOPENVIDU_VERSION=\"$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION \"${OPENVIDU_VERSION}\")\"\nENABLED_MODULES=\"$(/usr/local/bin/store_secret.sh save ENABLED-MODULES \"observability,app\")\"\nALL_SECRETS_GENERATED=\"$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED \"true\")\"\n\n# Base command\nINSTALL_COMMAND=\"sh <(curl -fsSL http://get.openvidu.io/pro/elastic/$OPENVIDU_VERSION/install_ov_master_node.sh)\"\n\n# Common arguments\nCOMMON_ARGS=(\n \"--no-tty\"\n \"--install\"\n \"--environment=azure\"\n \"--deployment-type=elastic\"\n \"--node-role='master-node'\"\n \"--openvidu-pro-license=$OPENVIDU_PRO_LICENSE\"\n \"--private-ip=$PRIVATE_IP\"\n \"--domain-name=$DOMAIN\"\n \"--enabled-modules='$ENABLED_MODULES'\"\n \"--rtc-engine=$OPENVIDU_RTC_ENGINE\"\n \"--redis-password=$REDIS_PASSWORD\"\n \"--mongo-admin-user=$MONGO_ADMIN_USERNAME\"\n \"--mongo-admin-password=$MONGO_ADMIN_PASSWORD\"\n \"--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY\"\n \"--minio-access-key=$MINIO_ACCESS_KEY\"\n \"--minio-secret-key=$MINIO_SECRET_KEY\"\n \"--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME\"\n \"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD\"\n \"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME\"\n \"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD\"\n \"--default-app-user=$DEFAULT_APP_USERNAME\"\n \"--default-app-password=$DEFAULT_APP_PASSWORD\"\n \"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME\"\n \"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD\"\n \"--livekit-api-key=$LIVEKIT_API_KEY\"\n \"--livekit-api-secret=$LIVEKIT_API_SECRET\"\n)\n\n\n# Turn with TLS\nif [[ \"${turnDomainName}\" != '' ]]; then\n LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME \"${turnDomainName}\")\n COMMON_ARGS+=(\n \"--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME\"\n )\nfi\n\n# Certificate arguments\nif [[ \"${certificateType}\" == \"selfsigned\" ]]; then\n CERT_ARGS=(\n \"--certificate-type=selfsigned\"\n )\nelif [[ \"${certificateType}\" == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT-EMAIL \"${letsEncryptEmail}\")\n CERT_ARGS=(\n \"--certificate-type=letsencrypt\"\n \"--letsencrypt-email=$LETSENCRYPT_EMAIL\"\n )\nelse\n # Download owncert files\n mkdir -p /tmp/owncert\n wget -O /tmp/owncert/fullchain.pem ${ownPublicCertificate}\n wget -O /tmp/owncert/privkey.pem ${ownPrivateCertificate}\n\n # Convert to base64\n OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem)\n OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem)\n\n CERT_ARGS=(\n \"--certificate-type=owncert\"\n \"--owncert-public-key=$OWN_CERT_CRT\"\n \"--owncert-private-key=$OWN_CERT_KEY\"\n )\n\n # Turn with TLS and own certificate\n if [[ \"${turnDomainName}\" != '' ]]; then\n # Download owncert files\n mkdir -p /tmp/owncert-turn\n wget -O /tmp/owncert-turn/fullchain.pem ${turnOwnPublicCertificate}\n wget -O /tmp/owncert-turn/privkey.pem ${turnOwnPrivateCertificate}\n\n # Convert to base64\n OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem)\n OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem)\n\n CERT_ARGS+=(\n \"--turn-owncert-private-key=$OWN_CERT_KEY_TURN\"\n \"--turn-owncert-public-key=$OWN_CERT_CRT_TURN\"\n )\n fi\nfi\n\n# Construct the final command with all arguments\nFINAL_COMMAND=\"$INSTALL_COMMAND $(printf \"%s \" \"${COMMON_ARGS[@]}\") $(printf \"%s \" \"${CERT_ARGS[@]}\")\"\n\n# Install OpenVidu\nexec bash -c \"$FINAL_COMMAND\"\n", + "after_installScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Configure Domain\nif [[ \"${domainName}\" == '' ]]; then\n DOMAIN=${fqdn}\nelse\n DOMAIN=${domainName}\nfi\n\n# Generate URLs\nDASHBOARD_URL=\"https://${DOMAIN}/dashboard/\"\nGRAFANA_URL=\"https://${DOMAIN}/grafana/\"\nMINIO_URL=\"https://${DOMAIN}/minio-console/\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL\n\naz keyvault secret show --vault-name ${keyVaultName} --name MINIO-URL\n\nif [[ $? -ne 0 ]]; then\n echo \"Error updating keyvault\"\nfi\n", + "update_config_from_secretScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Installation directory\nINSTALL_DIR=\"/opt/openvidu\"\nCLUSTER_CONFIG_DIR=\"${INSTALL_DIR}/config/cluster\"\nMASTER_NODE_CONFIG_DIR=\"${INSTALL_DIR}/config/node\"\n\n# Replace DOMAIN_NAME\nexport DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nif [[ -n \"$DOMAIN\" ]]; then\n sed -i \"s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nelse\n exit 1\nfi\n\n# Replace LIVEKIT_TURN_DOMAIN_NAME\nexport LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv)\nif [[ -n \"$LIVEKIT_TURN_DOMAIN_NAME\" ]]; then\n sed -i \"s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nfi\n\nif [[ ${certificateType} == \"letsencrypt\" ]]; then\n export LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv)\n sed -i \"s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nfi\n\n# Get the rest of the values\nexport REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)\nexport OPENVIDU_RTC_ENGINE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --query value -o tsv)\nexport OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)\nexport MONGO_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --query value -o tsv)\nexport MONGO_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --query value -o tsv)\nexport MONGO_REPLICA_SET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --query value -o tsv)\nexport DASHBOARD_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --query value -o tsv)\nexport DASHBOARD_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --query value -o tsv)\nexport MINIO_ACCESS_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --query value -o tsv)\nexport MINIO_SECRET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --query value -o tsv)\nexport GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --query value -o tsv)\nexport GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)\nexport LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)\nexport LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)\nexport DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)\nexport DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)\nexport DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)\nexport DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)\nexport ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)\n\n# Replace rest of the values\nsed -i \"s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/\" \"${MASTER_NODE_CONFIG_DIR}/master_node.env\"\nsed -i \"s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$DASHBOARD_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$DASHBOARD_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$MINIO_SECRET_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\n\n# Update URLs in secret\nDASHBOARD_URL=\"https://${DOMAIN}/dashboard/\"\nGRAFANA_URL=\"https://${DOMAIN}/grafana/\"\nMINIO_URL=\"https://${DOMAIN}/minio-console/\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL\n", + "update_secret_from_configScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Installation directory\nINSTALL_DIR=\"/opt/openvidu\"\nCLUSTER_CONFIG_DIR=\"${INSTALL_DIR}/config/cluster\"\nMASTER_NODE_CONFIG_DIR=\"${INSTALL_DIR}/config/node\"\n\nif [[ ${certificateType} == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=\"$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\n az keyvault secret set --vault-name ${keyVaultName} --name \"LETSENCRYPT-EMAIL\" --value $LETSENCRYPT_EMAIL\nfi\n\n# Get current values of the config\nREDIS_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD \"${MASTER_NODE_CONFIG_DIR}/master_node.env\")\"\nDOMAIN_NAME=\"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_TURN_DOMAIN_NAME=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nOPENVIDU_RTC_ENGINE=\"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nOPENVIDU_PRO_LICENSE=\"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMONGO_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMONGO_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMONGO_REPLICA_SET_KEY=\"$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMINIO_ACCESS_KEY=\"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMINIO_SECRET_KEY=\"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nDASHBOARD_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nDASHBOARD_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nGRAFANA_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nGRAFANA_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_API_KEY=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_API_SECRET=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nDEFAULT_APP_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh CALL_USER \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nDEFAULT_APP_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nDEFAULT_APP_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nDEFAULT_APP_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nENABLED_MODULES=\"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name REDIS-PASSWORD --value $REDIS_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN_NAME\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --value $LIVEKIT_TURN_DOMAIN_NAME\naz keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --value $OPENVIDU_RTC_ENGINE\naz keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --value $OPENVIDU_PRO_LICENSE\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --value $MONGO_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --value $MONGO_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --value $MONGO_REPLICA_SET_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --value $MINIO_ACCESS_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --value $MINIO_SECRET_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --value $DASHBOARD_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --value $DASHBOARD_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --value $GRAFANA_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES\n", + "get_value_from_configScriptMaster": "#!/bin/bash\nset -e\n\n# Function to get the value of a given key from the environment file\nget_value() {\n local key=\"$1\"\n local file_path=\"$2\"\n\n # Use grep to find the line with the key, ignoring lines starting with #\n # Use awk to split on '=' and print the second field, which is the value\n local value=$(grep -E \"^\\s*$key\\s*=\" \"$file_path\" | awk -F= '{print $2}' | sed 's/#.*//; s/^\\s*//; s/\\s*$//')\n\n # If the value is empty, return \"none\"\n if [ -z \"$value\" ]; then\n echo \"none\"\n else\n echo \"$value\"\n fi\n}\n\n# Check if the correct number of arguments are supplied\nif [ \"$#\" -ne 2 ]; then\n echo \"Usage: $0 \"\n exit 1\nfi\n\n# Get the key and file path from the arguments\nkey=\"$1\"\nfile_path=\"$2\"\n\n# Get and print the value\nget_value \"$key\" \"$file_path\"\n", + "store_secretScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Modes: save, generate\n# save mode: save the secret in the secret manager\n# generate mode: generate a random password and save it in the secret manager\nMODE=\"$1\"\n\nif [[ \"$MODE\" == \"generate\" ]]; then\n SECRET_KEY_NAME=\"$2\"\n PREFIX=\"${3:-}\"\n LENGTH=\"${4:-44}\"\n RANDOM_PASSWORD=\"$(openssl rand -base64 64 | tr -d '+/=\\n' | cut -c -${LENGTH})\"\n RANDOM_PASSWORD=\"${PREFIX}${RANDOM_PASSWORD}\"\n az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null\n if [[ $? -ne 0 ]]; then\n echo \"Error generating secret\"\n fi\n echo \"$RANDOM_PASSWORD\"\nelif [[ \"$MODE\" == \"save\" ]]; then\n SECRET_KEY_NAME=\"$2\"\n SECRET_VALUE=\"$3\"\n az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null\n if [[ $? -ne 0 ]]; then\n echo \"Error generating secret\"\n fi\n echo \"$SECRET_VALUE\"\nelse\n exit 1\nfi\n", + "check_app_readyScriptMaster": "#!/bin/bash\nset -e\nwhile true; do\n HTTP_STATUS=$(curl -Ik http://localhost:7880/twirp/health | head -n1 | awk '{print $2}')\n if [ $HTTP_STATUS == 200 ]; then\n break\n fi\n sleep 5\ndone\n", + "restartScriptMaster": "#!/bin/bash\nset -e\n# Stop all services\nsystemctl stop openvidu\n\n# Update config from secret\n/usr/local/bin/update_config_from_secret.sh\n\n# Start all services\nsystemctl start openvidu\n", + "base64get_value_from_configMaster": "[base64(variables('get_value_from_configScriptMaster'))]", + "base64check_app_readyMaster": "[base64(variables('check_app_readyScriptMaster'))]", + "base64restartMaster": "[base64(variables('restartScriptMaster'))]", + "userDataTemplateMasterNode": "#!/bin/bash -x\nset -eu -o pipefail\n\n# Introduce the scripts in the instance\n# install.sh\necho ${base64install} | base64 -d > /usr/local/bin/install.sh\nchmod +x /usr/local/bin/install.sh\n\n# after_install.sh\necho ${base64after_install} | base64 -d > /usr/local/bin/after_install.sh\nchmod +x /usr/local/bin/after_install.sh\n\n# update_config_from_secret.sh\necho ${base64update_config_from_secret} | base64 -d > /usr/local/bin/update_config_from_secret.sh\nchmod +x /usr/local/bin/update_config_from_secret.sh\n\n# update_secret_from_config.sh\necho ${base64update_secret_from_config} | base64 -d > /usr/local/bin/update_secret_from_config.sh\nchmod +x /usr/local/bin/update_secret_from_config.sh\n\n# get_value_from_config.sh\necho ${base64get_value_from_config} | base64 -d > /usr/local/bin/get_value_from_config.sh\nchmod +x /usr/local/bin/get_value_from_config.sh\n\n# store_secret.sh\necho ${base64store_secret} | base64 -d > /usr/local/bin/store_secret.sh\nchmod +x /usr/local/bin/store_secret.sh\n\n# check_app_ready.sh\necho ${base64check_app_ready} | base64 -d > /usr/local/bin/check_app_ready.sh\nchmod +x /usr/local/bin/check_app_ready.sh\n\n# restart.sh\necho ${base64restart} | base64 -d > /usr/local/bin/restart.sh\nchmod +x /usr/local/bin/restart.sh\n\n# Install azure cli\ncurl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash\n\naz login --identity --allow-no-subscriptions\n\napt-get update && apt-get install -y\n\nexport HOME=\"/root\"\n\n# Install OpenVidu\n/usr/local/bin/install.sh || { echo \"[OpenVidu] error installing OpenVidu\"; exit 1; }\n\n# Start OpenVidu\nsystemctl start openvidu || { echo \"[OpenVidu] error starting OpenVidu\"; exit 1; }\n\n# Update shared secret\n/usr/local/bin/after_install.sh || { echo \"[OpenVidu] error updating shared secret\"; exit 1; }\n\n# Launch on reboot\necho \"@reboot /usr/local/bin/restart.sh >> /var/log/openvidu-restart.log\" 2>&1 | crontab\n\nset +e\naz storage blob upload --account-name ${storageAccountName} --container-name automation-locks --name lock.txt --file /dev/null --auth-mode key\nset -e\n\naz keyvault secret set --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --value \"true\"\n\n# Wait for the app\nsleep 150\n/usr/local/bin/check_app_ready.sh\n", + "installScriptTemplateMedia": "#!/bin/bash -x\nset -e\nDOMAIN=\n\n# Install dependencies\napt-get update && apt-get install -y \\\n curl \\\n unzip \\\n jq \\\n wget\n\n# Get own private IP\nPRIVATE_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\")\n\nWAIT_INTERVAL=1\nMAX_WAIT=200\nELAPSED_TIME=0\nset +e\nwhile true; do\n # get secret value\n FINISH_MASTER_NODE=$(az keyvault secret show --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --query value -o tsv)\n\n # Check if the secret has been generated\n if [ \"$FINISH_MASTER_NODE\" == \"true\" ]; then\n break\n fi\n\n ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))\n\n # Check if the maximum waiting time has been reached\n if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then\n exit 1\n fi\n\n sleep $WAIT_INTERVAL\ndone\nset -e\n\n# Get current shared secret\nDOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nOPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)\nREDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)\nENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)\nOPENVIDU_VERSION=\"$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-VERSION --query value -o tsv)\"\n\n# Get Master Node private IP\nMASTER_NODE_IP=${privateIPMasterNode}\n\n# Base command\nINSTALL_COMMAND=\"sh <(curl -fsSL http://get.openvidu.io/pro/elastic/$OPENVIDU_VERSION/install_ov_media_node.sh)\"\n\n# Common arguments\nCOMMON_ARGS=(\n \"--no-tty\"\n \"--install\"\n \"--environment=azure\"\n \"--deployment-type=elastic\"\n \"--node-role='media-node'\"\n \"--master-node-private-ip=$MASTER_NODE_IP\"\n \"--private-ip=$PRIVATE_IP\"\n \"--enabled-modules='$ENABLED_MODULES'\"\n \"--redis-password=$REDIS_PASSWORD\"\n)\n# Construct the final command with all arguments\nFINAL_COMMAND=\"$INSTALL_COMMAND $(printf \"%s \" \"${COMMON_ARGS[@]}\")\"\n\n# Install OpenVidu\nexec bash -c \"$FINAL_COMMAND\"\n", + "stopMediaNodeParams": { + "subscriptionId": "[subscription().subscriptionId]", + "resourceGroupName": "[resourceGroup().name]", + "vmScaleSetName": "[format('{0}-mediaNodeScaleSet', parameters('stackName'))]", + "storageAccountName": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]" + }, + "stop_media_nodesScriptMediaTemplate": "#!/bin/bash\nset -e\n\nif ! (set -o noclobber ; echo > /tmp/global.lock) ; then\n exit 1 # the global.lock already exists\nfi\n\n# Execute if docker is installed\nif [ -x \"$(command -v docker)\" ]; then\n\n echo \"Stopping media node services and waiting for termination...\"\n docker container kill --signal=SIGINT openvidu || true\n docker container kill --signal=SIGINT ingress || true\n docker container kill --signal=SIGINT egress || true\n\n # Wait for running containers to not be openvidu, ingress or egress\n while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == \"true\" ] || \\\n [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == \"true\" ] || \\\n [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == \"true\" ]; do\n echo \"Waiting for containers to stop...\"\n sleep 5\n done\nfi\n\naz login --identity\n\nRESOURCE_GROUP_NAME=${resourceGroupName}\nVM_SCALE_SET_NAME=${vmScaleSetName}\nSUBSCRIPTION_ID=${subscriptionId}\nBEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy \"*\" \"http://169.254.169.254/metadata/instance?api-version=2021-02-01\" | jq -r '.compute.resourceId')\nINSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')\nRESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Compute/virtualMachineScaleSets/$VM_SCALE_SET_NAME\nTIMESTAMP=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n\naz tag update --resource-id $RESOURCE_ID --operation replace --tags \"STATUS\"=\"HEALTHY\" \"InstanceDeleteTime\"=\"$TIMESTAMP\" \"storageAccount\"=\"${storageAccountName}\"\n\naz vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID\n", + "userDataMediaNodeTemplate": "#!/bin/bash -x\nset -eu -o pipefail\n\n# Introduce the scripts in the instance\n# install.sh\necho ${base64install} | base64 -d > /usr/local/bin/install.sh\nchmod +x /usr/local/bin/install.sh\n\n# stop_media_nodes.sh\necho ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh\nchmod +x /usr/local/bin/stop_media_node.sh\n\napt-get update && apt-get install -y\napt-get install -y jq\n\n# Install azure cli\ncurl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash\n\naz login --identity\n\n# Protect from scale in actions\nRESOURCE_GROUP_NAME=${resourceGroupName}\nVM_SCALE_SET_NAME=${vmScaleSetName}\nBEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy \"*\" \"http://169.254.169.254/metadata/instance?api-version=2021-02-01\" | jq -r '.compute.resourceId')\nINSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')\naz vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-id $INSTANCE_ID --protect-from-scale-in true\n\nexport HOME=\"/root\"\n\n# Install OpenVidu\n/usr/local/bin/install.sh || { echo \"[OpenVidu] error installing OpenVidu\"; exit 1; }\n\n# Start OpenVidu\nsystemctl start openvidu || { echo \"[OpenVidu] error starting OpenVidu\"; exit 1; }\n#/usr/local/bin/set_as_unhealthy.sh\n", + "stop_media_nodesScriptMedia": "[reduce(items(variables('stopMediaNodeParams')), createObject('value', variables('stop_media_nodesScriptMediaTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "base64stopMediaNode": "[base64(variables('stop_media_nodesScriptMedia'))]" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-07-01", + "name": "[variables('keyVaultName')]", + "location": "[variables('location')]", + "properties": { + "enabledForDeployment": true, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true, + "tenantId": "[variables('tenantId')]", + "enableSoftDelete": false, + "accessPolicies": [ + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachines', variables('masterNodeVMSettings').vmName), '2023-09-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "set", + "list" + ] + } + }, + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName'))), '2024-07-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get" + ] + } + }, + { + "objectId": "[variables('deploymentUser')]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "list", + "set", + "delete", + "recover", + "backup", + "restore" + ] + } + } + ], + "sku": { + "name": "standard", + "family": "A" + }, + "networkAcls": { + "defaultAction": "Allow", + "bypass": "AzureServices" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', variables('masterNodeVMSettings').vmName)]", + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[variables('masterNodeVMSettings').vmName]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('masterNodeInstanceType')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('masterNodeVMSettings').osDiskType]" + }, + "diskSizeGB": 100 + }, + "imageReference": "[variables('masterNodeVMSettings').ubuntuOSVersion]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNoderNetInterface', parameters('stackName')))]" + } + ] + }, + "osProfile": { + "computerName": "[variables('masterNodeVMSettings').vmName]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('masterNodeVMSettings').linuxConfiguration]" + }, + "userData": "[base64(reduce(items(createObject('base64install', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'openviduLicense', parameters('openviduLicense'), 'rtcEngine', parameters('rtcEngine'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64after_install', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'openviduLicense', parameters('openviduLicense'), 'rtcEngine', parameters('rtcEngine'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('after_installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64update_config_from_secret', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'openviduLicense', parameters('openviduLicense'), 'rtcEngine', parameters('rtcEngine'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('update_config_from_secretScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64update_secret_from_config', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'openviduLicense', parameters('openviduLicense'), 'rtcEngine', parameters('rtcEngine'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('update_secret_from_configScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64get_value_from_config', variables('base64get_value_from_configMaster'), 'base64store_secret', base64(reduce(items(createObject('domainName', parameters('domainName'), 'fqdn', if(variables('isEmptyIp'), reference(resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), '2023-11-01').dnsSettings.fqdn, parameters('domainName')), 'turnDomainName', parameters('turnDomainName'), 'certificateType', parameters('certificateType'), 'letsEncryptEmail', parameters('letsEncryptEmail'), 'ownPublicCertificate', parameters('ownPublicCertificate'), 'ownPrivateCertificate', parameters('ownPrivateCertificate'), 'turnOwnPublicCertificate', parameters('turnOwnPublicCertificate'), 'turnOwnPrivateCertificate', parameters('turnOwnPrivateCertificate'), 'openviduLicense', parameters('openviduLicense'), 'rtcEngine', parameters('rtcEngine'), 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('store_secretScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64check_app_ready', variables('base64check_app_readyMaster'), 'base64restart', variables('base64restartMaster'), 'keyVaultName', variables('keyVaultName'), 'storageAccountName', format('lockstorage{0}', uniqueString(resourceGroup().id)))), createObject('value', variables('userDataTemplateMasterNode')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value)]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNoderNetInterface', parameters('stackName')))]", + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName')))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('lockstorage{0}', uniqueString(resourceGroup().id)))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachineScaleSets", + "apiVersion": "2024-07-01", + "name": "[format('{0}-mediaNodeScaleSet', parameters('stackName'))]", + "location": "[variables('location')]", + "tags": { + "InstanceDeleteTime": "[parameters('datetime')]", + "storageAccount": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]" + }, + "identity": { + "type": "SystemAssigned" + }, + "sku": { + "name": "[parameters('mediaNodeInstanceType')]", + "tier": "Standard", + "capacity": "[parameters('initialNumberOfMediaNodes')]" + }, + "properties": { + "overprovision": true, + "upgradePolicy": { + "mode": "Automatic" + }, + "singlePlacementGroup": true, + "platformFaultDomainCount": 1, + "virtualMachineProfile": { + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('mediaNodeVMSettings').osDiskType]" + }, + "diskSizeGB": 50 + }, + "imageReference": "[variables('mediaNodeVMSettings').ubuntuOSVersion]" + }, + "osProfile": { + "computerNamePrefix": "[variables('mediaNodeVMSettings').vmName]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('mediaNodeVMSettings').linuxConfiguration]" + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "[format('{0}-mediaNodeNetInterface', parameters('stackName'))]", + "properties": { + "primary": true, + "ipConfigurations": [ + { + "name": "ipconfigMediaNode", + "properties": { + "subnet": { + "id": "[reference(resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName), '2023-11-01').subnets[0].id]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "publicIPAddressConfiguration": { + "name": "publicIPAddressMediaNode", + "properties": { + "publicIPAddressVersion": "IPv4" + } + } + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + } + } + } + ] + }, + "userData": "[base64(reduce(items(createObject('base64install', base64(reduce(items(createObject('privateIPMasterNode', reference(resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNoderNetInterface', parameters('stackName'))), '2023-11-01').ipConfigurations[0].properties.privateIPAddress, 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('installScriptTemplateMedia')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64stop', variables('base64stopMediaNode'), 'resourceGroupName', resourceGroup().name, 'vmScaleSetName', format('{0}-mediaNodeScaleSet', parameters('stackName')))), createObject('value', variables('userDataMediaNodeTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value)]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNoderNetInterface', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('lockstorage{0}', uniqueString(resourceGroup().id)))]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName)]" + ] + }, + { + "type": "Microsoft.Insights/autoscalesettings", + "apiVersion": "2022-10-01", + "name": "[format('{0}-autoscaleSettings', parameters('stackName'))]", + "location": "[resourceGroup().location]", + "properties": { + "profiles": [ + { + "name": "openvidu-medianode-autoscale", + "capacity": { + "minimum": "[string(parameters('minNumberOfMediaNodes'))]", + "maximum": "[string(parameters('maxNumberOfMediaNodes'))]", + "default": "[string(parameters('initialNumberOfMediaNodes'))]" + }, + "rules": [ + { + "metricTrigger": { + "metricName": "Percentage CPU", + "metricNamespace": "Microsoft.Compute/virtualMachineScaleSets", + "metricResourceUri": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]", + "statistic": "Average", + "operator": "GreaterThan", + "threshold": "[parameters('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": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]", + "statistic": "Average", + "operator": "LessThan", + "threshold": "[parameters('scaleTargetCPU')]", + "timeAggregation": "Average", + "timeWindow": "PT5M", + "timeGrain": "PT1M" + }, + "scaleAction": { + "direction": "Decrease", + "type": "ChangeCount", + "value": "1", + "cooldown": "PT5M" + } + } + ] + } + ], + "enabled": true, + "targetResourceUri": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAssignmentForMasterNode{0}', variables('masterNodeVMSettings').vmName))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Compute/virtualMachines', variables('masterNodeVMSettings').vmName), '2023-09-01', 'full').identity.principalId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', variables('masterNodeVMSettings').vmName)]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAssignmentForScaleSet{0}', format('{0}-mediaNodeScaleSet', parameters('stackName'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName'))), '2024-07-01', 'full').identity.principalId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Insights/actionGroups", + "apiVersion": "2023-01-01", + "name": "actiongrouptest", + "location": "global", + "properties": { + "groupShortName": "scaleinag", + "enabled": true, + "automationRunbookReceivers": [ + { + "name": "scalein", + "useCommonAlertSchema": false, + "automationAccountId": "[reference(resourceId('Microsoft.Resources/deployments', 'WebhookDeployment'), '2022-09-01').outputs.automationAccountId.value]", + "runbookName": "scaleInRunbook", + "webhookResourceId": "[reference(resourceId('Microsoft.Resources/deployments', 'WebhookDeployment'), '2022-09-01').outputs.webhookId.value]", + "isGlobalRunbook": false, + "serviceUri": "[reference(resourceId('Microsoft.Resources/deployments', 'WebhookDeployment'), '2022-09-01').outputs.webhookUri.value]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'WebhookDeployment')]" + ] + }, + { + "type": "Microsoft.Insights/activityLogAlerts", + "apiVersion": "2020-10-01", + "name": "ScaleInAlertRule", + "location": "global", + "properties": { + "scopes": [ + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ], + "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": "[resourceId('Microsoft.Insights/actionGroups', 'actiongrouptest')]" + } + ] + }, + "enabled": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', 'actiongrouptest')]", + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "condition": "[equals(variables('isEmptyIp'), true())]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2023-11-01", + "name": "[format('{0}-publicIP', parameters('stackName'))]", + "location": "[variables('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static", + "dnsSettings": { + "domainNameLabel": "[if(variables('isEmptyDomain'), toLower(format('{0}', parameters('stackName'))), null())]", + "fqdn": "[if(variables('isEmptyDomain'), null(), parameters('domainName'))]" + } + } + }, + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2023-11-01", + "name": "[variables('networkSettings').vNetName]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('networkSettings').vNetAddressPrefix]" + ] + }, + "subnets": [ + { + "name": "subnetForMediaNodes", + "properties": { + "addressPrefix": "[variables('networkSettings').subnetAddressPrefixMedia]", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]", + "properties": { + "addressPrefix": "[variables('networkSettings').subnetAddressPrefixMaster1]", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName)]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNoderNetInterface', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "primaryIPConfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "publicIPAddress": { + "id": "[if(variables('isEmptyIp'), resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName'))), resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIpAddressResourceName')))]", + "properties": { + "deleteOption": "Delete" + } + } + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIP', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNoderNSG', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "securityRules": [ + { + "name": "SSH", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "22", + "access": "Allow", + "priority": 100, + "direction": "Inbound" + } + }, + { + "name": "HTTP", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "80", + "access": "Allow", + "priority": 110, + "direction": "Inbound" + } + }, + { + "name": "HTTPS", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "443", + "access": "Allow", + "priority": 120, + "direction": "Inbound" + } + }, + { + "name": "RTMP", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "1935", + "access": "Allow", + "priority": 130, + "direction": "Inbound" + } + }, + { + "name": "MinIO", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "9000", + "access": "Allow", + "priority": 140, + "direction": "Inbound" + } + } + ] + } + }, + { + "type": "Microsoft.Network/applicationSecurityGroups", + "apiVersion": "2024-03-01", + "name": "[format('{0}-masterNodeASG', parameters('stackName'))]", + "location": "[variables('location')]" + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_REDIS_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7000", + "access": "Allow", + "priority": 150, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_MINIO_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9100", + "access": "Allow", + "priority": 160, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_MONGO_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "20000", + "access": "Allow", + "priority": 170, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_LOKI_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "3100", + "access": "Allow", + "priority": 180, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_MIMIR_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9009", + "access": "Allow", + "priority": 190, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_V2COMPATIBILITY_WEBHOOK_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "4443", + "access": "Allow", + "priority": 200, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNoderNSG', parameters('stackName')), 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "6080", + "access": "Allow", + "priority": 210, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNoderNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}-mediaNoderNSG', parameters('stackName'))]", + "location": "[variables('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": "*", + "destinationPortRanges": [ + "50000", + "60000" + ], + "access": "Allow", + "priority": 140, + "direction": "Inbound" + } + } + ] + } + }, + { + "type": "Microsoft.Network/applicationSecurityGroups", + "apiVersion": "2024-03-01", + "name": "[format('{0}-mediaNodeASG', parameters('stackName'))]", + "location": "[variables('location')]" + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNoderNSG', parameters('stackName')), 'masterNode_to_mediaNode_RTMP_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "1935", + "access": "Allow", + "priority": 150, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNoderNSG', parameters('stackName')), 'masterNode_to_mediaNode_TURN_TLS_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "5349", + "access": "Allow", + "priority": 160, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNoderNSG', parameters('stackName')), 'masterNode_to_mediaNode_SERVER_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7880", + "access": "Allow", + "priority": 170, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNoderNSG', parameters('stackName')), 'masterNode_to_mediaNode_HTTP_WHIP_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "8080", + "access": "Allow", + "priority": 180, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNoderNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "accessTier": "Cool", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/default/automation-locks', format('lockstorage{0}', uniqueString(resourceGroup().id)))]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', format('lockstorage{0}', uniqueString(resourceGroup().id)))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "WebhookDeployment", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "runbookName": { + "value": "scaleInRunbook" + }, + "webhookName": { + "value": "webhookForScaleIn" + }, + "WebhookExpiryTime": { + "value": "2035-03-30T00:00:00Z" + }, + "_artifactsLocation": { + "value": "https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "automationAccountName": { + "type": "String", + "metadata": { + "description": "Automation account name" + } + }, + "webhookName": { + "type": "String", + "metadata": { + "description": "Webhook Name" + } + }, + "runbookName": { + "type": "String", + "metadata": { + "description": "Runbook Name for which webhook will be created" + } + }, + "WebhookExpiryTime": { + "type": "String", + "metadata": { + "description": "Webhook Expiry time" + } + }, + "_artifactsLocation": { + "defaultValue": "https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1", + "type": "String", + "metadata": { + "description": "URI to artifacts location" + } + } + }, + "resources": [ + { + "type": "Microsoft.Automation/automationAccounts", + "apiVersion": "2020-01-13-preview", + "name": "[parameters('automationAccountName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "sku": { + "name": "Basic" + } + }, + "resources": [ + { + "type": "runbooks", + "apiVersion": "2018-06-30", + "name": "[parameters('runbookName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[parameters('automationAccountName')]" + ], + "properties": { + "runbookType": "PowerShell72", + "logProgress": "true", + "description": "Scale In Runbook", + "publishContentLink": { + "uri": "[parameters('_artifactsLocation')]", + "version": "1.0.0.0" + } + } + }, + { + "type": "webhooks", + "apiVersion": "2018-06-30", + "name": "[parameters('webhookName')]", + "dependsOn": [ + "[parameters('automationAccountName')]", + "[parameters('runbookName')]" + ], + "properties": { + "isEnabled": true, + "expiryTime": "[parameters('WebhookExpiryTime')]", + "runbook": { + "name": "[parameters('runbookName')]" + } + } + } + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAutomationContributorAssignmentAutomationAccount{0}', parameters('automationAccountName')))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName')), '2023-11-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + } + ], + "outputs": { + "webhookUri": { + "type": "String", + "value": "[reference(parameters('webhookName')).uri]" + }, + "automationAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + }, + "webhookId": { + "type": "string", + "value": "[resourceId('Microsoft.Automation/automationAccounts/webhooks', parameters('automationAccountName'), parameters('webhookName'))]" + } + } + } + } + } + ], + "outputs": { + "ipValidationStatus": { + "type": "string", + "value": "[if(variables('isValidIP'), 'IP address is valid', 'IP address not valid')]" + }, + "domainValidationStatus": { + "type": "string", + "value": "[if(variables('isDomainValid'), 'Domain is valid', 'Domain is not valid')]" + }, + "ownCertValidationStatus": { + "type": "string", + "value": "[if(and(and(equals(parameters('certificateType'), 'owncert'), not(equals(parameters('ownPrivateCertificate'), ''))), not(equals(parameters('ownPublicCertificate'), ''))), 'owncert selected and valid', 'You need to fill ''Own Public Certificate'' and ''Own Private Certificate''')]" + }, + "letsEncryptValidationStatus": { + "type": "string", + "value": "[if(and(equals(parameters('certificateType'), 'letsencrypt'), not(equals(parameters('letsEncryptEmail'), ''))), 'letsEncrypt selected and valid', 'You need to fill ''Lets Encrypt Email''')]" + } + } +} \ No newline at end of file diff --git a/openvidu-deployment/pro/elastic/azure/createUiDefinition.json b/openvidu-deployment/pro/elastic/azure/createUiDefinition.json new file mode 100644 index 00000000..e69de29b diff --git a/openvidu-deployment/pro/elastic/install_ov_master_node.sh b/openvidu-deployment/pro/elastic/install_ov_master_node.sh new file mode 100644 index 00000000..e8e835f6 --- /dev/null +++ b/openvidu-deployment/pro/elastic/install_ov_master_node.sh @@ -0,0 +1,173 @@ +#!/bin/sh +# Docker & Docker Compose will need to be installed on the machine +set -eu +export DOCKER_VERSION="${DOCKER_VERSION:-27.5.1}" +export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.32.4}" +export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" +export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" +export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.2.7-debian-12-r0}" +export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-02-08T19-14-21Z}" +export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.4}" +export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.2-alpine}" +export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}" +export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}" +export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}" +export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}" +export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}" +export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}" +export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.0}" +export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.1.0}" +export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.3.2}" +export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.3.2}" +export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.15.0}" +export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.5.1}" + +wait_for_docker() { + echo "Waiting for Docker to start..." + + # Set a countdown (in seconds) + COUNTDOWN=60 + + while [ "$COUNTDOWN" -gt 0 ]; do + if docker info >/dev/null 2>&1; then + echo "Docker started successfully." + break + else + # Reduce the countdown by 1 each iteration. + COUNTDOWN=$(( COUNTDOWN - 1 )) + + if [ "$COUNTDOWN" -eq 0 ]; then + echo "ERROR: Docker did not start within the allocated time." + break + fi + + sleep 1 + fi + done +} + +# Check if executing as root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if ! command -v docker > /dev/null 2>&1 +then + curl -fsSL https://get.docker.com -o /tmp/get-docker.sh + sh /tmp/get-docker.sh --version "${DOCKER_VERSION}" || { echo "Can't install Docker automatically. Install it manually and run this script again"; exit 1; } +else + echo "Docker already installed. Check you have the latest version for best compatibility" +fi + +if ! command -v docker-compose > /dev/null 2>&1 +then + TIME_LIMIT_SECONDS=20 + START_TIME=$(awk 'BEGIN{srand(); print srand()}') + while true + do + CURRENT_TIME=$(awk 'BEGIN{srand(); print srand()}') + if [ $((CURRENT_TIME-START_TIME)) -gt $TIME_LIMIT_SECONDS ]; then + echo "Error downloading docker-compose. Could not download it in $TIME_LIMIT_SECONDS seconds" + rm -rf /usr/local/bin/docker-compose + exit 1 + fi + STATUS_RECEIVED=$(curl --retry 5 --retry-max-time 40 --write-out "%{http_code}\n" -L "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose) + CURL_EXIT_CODE=$? + if [ $CURL_EXIT_CODE -ne 0 ]; then + echo "Error downloading docker-compose. curl failed with exit code $CURL_EXIT_CODE. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + if [ "${STATUS_RECEIVED}" -ne "200" ]; then + echo "Error downloading docker-compose. Received HTTP status code $STATUS_RECEIVED. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + echo "Success downloading docker-compose" + chmod 755 /usr/local/bin/docker-compose + break + done + + # Create a symbolic link to docker-compose in the Docker CLI plugins directory + # so docker compose can be used also + mkdir -p /usr/local/lib/docker/cli-plugins + ln -s /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose +else + echo "Docker Compose already installed. Check you have the latest version for best compatibility" +fi + +# Restart Docker and wait for it to start +systemctl enable docker +systemctl stop docker +systemctl start docker +wait_for_docker + +# Create random temp directory +TMP_DIR=$(mktemp -d) +docker pull "${INSTALLER_IMAGE}" + +# Generate installation scripts +COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \ + -e OPENVIDU_VERSION=$OPENVIDU_VERSION \ + -e CADDY_SERVER_IMAGE=$CADDY_SERVER_IMAGE \ + -e CADDY_SERVER_PRO_IMAGE=$CADDY_SERVER_PRO_IMAGE \ + -e MINIO_SERVER_IMAGE=$MINIO_SERVER_IMAGE \ + -e MINIO_CLIENT_IMAGE=$MINIO_CLIENT_IMAGE \ + -e MONGO_SERVER_IMAGE=$MONGO_SERVER_IMAGE \ + -e REDIS_SERVER_IMAGE=$REDIS_SERVER_IMAGE \ + -e BUSYBOX_IMAGE=$BUSYBOX_IMAGE \ + -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ + -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ + -e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \ + -e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \ + -e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \ + -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ + -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ + -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ + -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ + -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ + -e PROMTAIL_IMAGE=$PROMTAIL_IMAGE \ + -e LOKI_IMAGE=$LOKI_IMAGE \ + -e MIMIR_IMAGE=$MIMIR_IMAGE \ + -e GRAFANA_IMAGE=$GRAFANA_IMAGE \ + ${INSTALLER_IMAGE} \ + --deployment-type=elastic \ + --node-role=master-node \ + --install \ + $*" + +INTERACTIVE_MODE=true +for arg in "$@"; do + if [ "$arg" = "--no-tty" ]; then + INTERACTIVE_MODE=false; + break + fi +done + +if [ "$INTERACTIVE_MODE" = true ]; then + docker run -it ${COMMON_DOCKER_OPTIONS} > /dev/tty +else + docker run -i ${COMMON_DOCKER_OPTIONS} +fi + +cd "$TMP_DIR/installation-scripts/openvidu/" +chmod +x install_ov_master_node.sh +./install_ov_master_node.sh + +cat finish-message.txt + +# Warn about private IP being setup correctly +echo +echo "ATTENTION!!! This is the private IP of the 'Master Node'. Make sure this IP is reachable from all the 'Media Nodes'" +cat private-ip.txt +echo "If this is not your private IP, reinstall the 'Master Node' with the correct '--private-ip' parameter" + +echo diff --git a/openvidu-deployment/pro/elastic/install_ov_media_node.sh b/openvidu-deployment/pro/elastic/install_ov_media_node.sh new file mode 100644 index 00000000..72204c53 --- /dev/null +++ b/openvidu-deployment/pro/elastic/install_ov_media_node.sh @@ -0,0 +1,172 @@ +#!/bin/sh +# Docker & Docker Compose will need to be installed on the machine +set -eu +export DOCKER_VERSION="${DOCKER_VERSION:-27.5.1}" +export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.32.4}" +export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" +export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" +export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.2.7-debian-12-r0}" +export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-02-08T19-14-21Z}" +export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.4}" +export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.2-alpine}" +export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}" +export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}" +export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}" +export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}" +export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}" +export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}" +export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.0}" +export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.1.0}" +export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.3.2}" +export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.3.2}" +export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.15.0}" +export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.5.1}" + +wait_for_docker() { + echo "Waiting for Docker to start..." + + # Set a countdown (in seconds) + COUNTDOWN=60 + + while [ "$COUNTDOWN" -gt 0 ]; do + if docker info >/dev/null 2>&1; then + echo "Docker started successfully." + break + else + # Reduce the countdown by 1 each iteration. + COUNTDOWN=$(( COUNTDOWN - 1 )) + + if [ "$COUNTDOWN" -eq 0 ]; then + echo "ERROR: Docker did not start within the allocated time." + break + fi + + sleep 1 + fi + done +} + +# Check if executing as root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if ! command -v docker > /dev/null 2>&1 +then + curl -fsSL https://get.docker.com -o /tmp/get-docker.sh + sh /tmp/get-docker.sh --version "${DOCKER_VERSION}" || { echo "Can't install Docker automatically. Install it manually and run this script again"; exit 1; } +else + echo "Docker already installed. Check you have the latest version for best compatibility" +fi + +if ! command -v docker-compose > /dev/null 2>&1 +then + TIME_LIMIT_SECONDS=20 + START_TIME=$(awk 'BEGIN{srand(); print srand()}') + while true + do + CURRENT_TIME=$(awk 'BEGIN{srand(); print srand()}') + if [ $((CURRENT_TIME-START_TIME)) -gt $TIME_LIMIT_SECONDS ]; then + echo "Error downloading docker-compose. Could not download it in $TIME_LIMIT_SECONDS seconds" + rm -rf /usr/local/bin/docker-compose + exit 1 + fi + STATUS_RECEIVED=$(curl --retry 5 --retry-max-time 40 --write-out "%{http_code}\n" -L "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose) + CURL_EXIT_CODE=$? + if [ $CURL_EXIT_CODE -ne 0 ]; then + echo "Error downloading docker-compose. curl failed with exit code $CURL_EXIT_CODE. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + if [ "${STATUS_RECEIVED}" -ne "200" ]; then + echo "Error downloading docker-compose. Received HTTP status code $STATUS_RECEIVED. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + echo "Success downloading docker-compose" + chmod 755 /usr/local/bin/docker-compose + break + done + + # Create a symbolic link to docker-compose in the Docker CLI plugins directory + # so docker compose can be used also + mkdir -p /usr/local/lib/docker/cli-plugins + ln -s /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose +else + echo "Docker Compose already installed. Check you have the latest version for best compatibility" +fi + +# Restart Docker and wait for it to start +systemctl enable docker +systemctl stop docker +systemctl start docker +wait_for_docker + +# Create random temp directory +TMP_DIR=$(mktemp -d) +docker pull "${INSTALLER_IMAGE}" + +# Generate installation scripts +COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \ + -e OPENVIDU_VERSION=$OPENVIDU_VERSION \ + -e CADDY_SERVER_IMAGE=$CADDY_SERVER_IMAGE \ + -e CADDY_SERVER_PRO_IMAGE=$CADDY_SERVER_PRO_IMAGE \ + -e MINIO_SERVER_IMAGE=$MINIO_SERVER_IMAGE \ + -e MINIO_CLIENT_IMAGE=$MINIO_CLIENT_IMAGE \ + -e MONGO_SERVER_IMAGE=$MONGO_SERVER_IMAGE \ + -e REDIS_SERVER_IMAGE=$REDIS_SERVER_IMAGE \ + -e BUSYBOX_IMAGE=$BUSYBOX_IMAGE \ + -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ + -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ + -e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \ + -e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \ + -e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \ + -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ + -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ + -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ + -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ + -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ + -e PROMTAIL_IMAGE=$PROMTAIL_IMAGE \ + -e LOKI_IMAGE=$LOKI_IMAGE \ + -e MIMIR_IMAGE=$MIMIR_IMAGE \ + -e GRAFANA_IMAGE=$GRAFANA_IMAGE \ + ${INSTALLER_IMAGE} \ + --deployment-type=elastic \ + --node-role=media-node \ + --install \ + $*" + +INTERACTIVE_MODE=true +for arg in "$@"; do + if [ "$arg" = "--no-tty" ]; then + INTERACTIVE_MODE=false; + break + fi +done + +if [ "$INTERACTIVE_MODE" = true ]; then + docker run -it ${COMMON_DOCKER_OPTIONS} > /dev/tty +else + docker run -i ${COMMON_DOCKER_OPTIONS} +fi + +cd "$TMP_DIR/installation-scripts/openvidu/" +chmod +x install_ov_media_node.sh +./install_ov_media_node.sh + +cat finish-message.txt + +# Warn about private IP being setup correctly +echo +echo "ATTENTION!!! This is the private IP of this 'Media Node'. Make sure this IP is reachable from the 'Master Node'" +cat private-ip.txt +echo "If this is not your private IP, reinstall the 'Media Node' with the correct '--private-ip' parameter" +echo diff --git a/openvidu-deployment/pro/ha/aws/cf-openvidu-ha-internal-tls.yaml b/openvidu-deployment/pro/ha/aws/cf-openvidu-ha-internal-tls.yaml new file mode 100644 index 00000000..3d0db46a --- /dev/null +++ b/openvidu-deployment/pro/ha/aws/cf-openvidu-ha-internal-tls.yaml @@ -0,0 +1,2394 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: OpenVidu Pro - High Availability + +Parameters: + DomainName: + Type: String + Description: Domain name for the OpenVidu High Availability cluster + AllowedPattern: ^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + ConstraintDescription: The domain name does not have a valid domain name format + + TurnDomainName: + Description: '(Optional) Domain name for the TURN server with TLS.' + Type: String + Default: '' + + OpenViduLicense: + Description: "Visit https://openvidu.io/account" + Type: String + AllowedPattern: ^(?!\s*$).+$ + NoEcho: true + ConstraintDescription: OpenVidu Pro License is mandatory + + RTCEngine: + Description: "RTCEngine media engine to use" + Type: String + AllowedValues: + - pion + - mediasoup + Default: pion + + MasterNodeInstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu Master Node" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + MediaNodeInstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu Media Nodes" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: Name of an existing EC2 KeyPair to enable SSH access to the instances + AllowedPattern: ^.+$ + ConstraintDescription: must be the name of an existing EC2 KeyPair. + + AmiId: + Type: AWS::SSM::Parameter::Value + Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id + Description: AMI ID for the EC2 instances + + InitialNumberOfMediaNodes: + Type: Number + Default: 1 + Description: Number of initial media nodes to deploy + + MinNumberOfMediaNodes: + Type: Number + Default: 1 + Description: Minimum number of media nodes to deploy + + MaxNumberOfMediaNodes: + Type: Number + Default: 5 + Description: Maximum number of media nodes to deploy + + ScaleTargetCPU: + Type: Number + Default: 50 + Description: Target CPU percentage to scale up or down + + S3AppDataBucketName: + Type: String + Description: Name of the S3 bucket to store data and recordings. If empty, a bucket will be created + + S3ClusterDataBucketName: + Type: String + Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created + + OpenViduVPC: + Description: "Dedicated VPC for OpenVidu cluster" + Type: AWS::EC2::VPC::Id + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a VPC ID + + OpenViduMasterNodeSubnets: + Description: "Subnets for OpenVidu Master Node" + Type: List + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a list of subnet IDs + + OpenViduMediaNodeSubnets: + Description: "Subnets for OpenVidu Media Nodes" + Type: List + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a list of subnet IDs + + MasterNodesDiskSize: + Description: Size of the disk in GB + Type: Number + Default: 100 + MinValue: 50 + ConstraintDescription: The disk size must be at least 50 GB + +Metadata: + 'AWS::CloudFormation::Interface': + ParameterGroups: + - Label: + default: Domain and Load Blancer configuration + Parameters: + - DomainName + - Label: + default: OpenVidu High Availability configuration + Parameters: + - OpenViduLicense + - RTCEngine + - Label: + default: EC2 Instance configuration + Parameters: + - MasterNodeInstanceType + - MediaNodeInstanceType + - KeyName + - AmiId + - Label: + default: Media Nodes Autoscaling Group configuration + Parameters: + - InitialNumberOfMediaNodes + - MinNumberOfMediaNodes + - MaxNumberOfMediaNodes + - ScaleTargetCPU + - Label: + default: S3 bucket for application data, cluster data and recordings + Parameters: + - S3AppDataBucketName + - S3ClusterDataBucketName + - Label: + default: VPC configuration + Parameters: + - OpenViduVPC + - OpenViduMasterNodeSubnets + - OpenViduMediaNodeSubnets + - Label: + default: Volumes configuration + Parameters: + - MasterNodesDiskSize + - Label: + default: (Optional) TURN server configuration with TLS + Parameters: + - TurnDomainName + +Conditions: + TurnTLSIsEnabled: !Not [!Equals [!Ref TurnDomainName, ""]] + CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] + CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""] + +Resources: + + OpenViduSharedInfo: + Type: AWS::SecretsManager::Secret + UpdateReplacePolicy: Retain + DeletionPolicy: Delete + Properties: + Name: !Sub openvidu-ha-${AWS::Region}-${AWS::StackName} + Description: Secret for OpenVidu High Availability to store deployment info and seed secret + # All the values are initialized by one master node and shared with the rest of the nodes + SecretString: | + { + "ALL_SECRETS_GENERATED": "false", + "DOMAIN_NAME": "none", + "LIVEKIT_TURN_DOMAIN_NAME": "none", + "OPENVIDU_PRO_LICENSE": "none", + "OPENVIDU_RTC_ENGINE": "none", + "REDIS_PASSWORD": "none", + "MONGO_ADMIN_USERNAME": "none", + "MONGO_ADMIN_PASSWORD": "none", + "MONGO_REPLICA_SET_KEY": "none", + "MINIO_URL": "none", + "MINIO_ACCESS_KEY": "none", + "MINIO_SECRET_KEY": "none", + "DASHBOARD_URL": "none", + "DASHBOARD_ADMIN_USERNAME": "none", + "DASHBOARD_ADMIN_PASSWORD": "none", + "GRAFANA_URL": "none", + "GRAFANA_ADMIN_USERNAME": "none", + "GRAFANA_ADMIN_PASSWORD": "none", + "DEFAULT_APP_USERNAME": "none", + "DEFAULT_APP_PASSWORD": "none", + "DEFAULT_APP_ADMIN_USERNAME": "none", + "DEFAULT_APP_ADMIN_PASSWORD": "none", + "LIVEKIT_API_KEY": "none", + "LIVEKIT_API_SECRET": "none", + "ENABLED_MODULES": "none", + "MASTER_NODE_1_PRIVATE_IP": "none", + "MASTER_NODE_2_PRIVATE_IP": "none", + "MASTER_NODE_3_PRIVATE_IP": "none", + "MASTER_NODE_4_PRIVATE_IP": "none", + "OPENVIDU_VERSION": "none" + } + + S3AppDataBucketResource: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-appdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateRecordingsBucket + + S3ClusterDataBucketResource: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-clusterdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateClusterDataBucket + + # ------------------------- + # Preprocess subnets to allocate Volumes and ENIs across Availability Zones + # For OpenVidu Master Nodes + # ------------------------- + SubnetProcessorFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub 'SubnetProcessor-${AWS::Region}-${AWS::StackName}' + Handler: index.lambda_handler + Role: !GetAtt LambdaExecutionRole.Arn + Code: + ZipFile: | + import cfnresponse + import boto3 + + def lambda_handler(event, context): + try: + # Process event data + subnets = event['ResourceProperties']['Subnets'] + ec2 = boto3.client('ec2') + + # Ensure we have at least four subnets by cycling through the available subnets + subnets = (subnets * 4)[:4] # Repeat the list to have at least 4 elements and then take the first 4 + + # Prepare the response + responseData = { + 'Subnet1': subnets[0], + 'Subnet2': subnets[1], + 'Subnet3': subnets[2], + 'Subnet4': subnets[3], + } + cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) + + except Exception as e: + cfnresponse.send(event, context, cfnresponse.FAILED, {'Message': str(e)}) + + + Runtime: python3.12 + Timeout: 120 + + SubnetProcessor: + Type: Custom::SubnetProcessor + Properties: + ServiceToken: !GetAtt SubnetProcessorFunction.Arn + Subnets: !Ref OpenViduMasterNodeSubnets + + LambdaLogGroup: + UpdateReplacePolicy: Retain + DeletionPolicy: Delete + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}' + RetentionInDays: 7 + + LambdaExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: 'sts:AssumeRole' + Policies: + - PolicyName: LambdaLogsPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}:*' + - Effect: Allow + Action: + - ec2:DescribeSubnets + Resource: '*' + + OpenViduMasterNodeRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: "/" + Policies: + - PolicyName: !Sub openvidu-master-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - autoscaling:SetInstanceHealth + Resource: '*' + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-id': !Ref 'AWS::StackId' + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:UpdateSecret + Resource: !Ref OpenViduSharedInfo + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + - Fn::If: + - CreateClusterDataBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3ClusterDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName}/* + - Fn::If: + - CreateClusterDataBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3ClusterDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-master-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduMediaNodeRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: / + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore + Policies: + - PolicyName: !Sub openvidu-media-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref OpenViduSharedInfo + - Effect: Allow + Action: + - autoscaling:SetInstanceHealth + - autoscaling:CompleteLifecycleAction + - autoscaling:RecordLifecycleActionHeartbeat + Resource: '*' + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-media-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduMasterInstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + InstanceProfileName: !Sub OpenViduMasterInstanceProfile-${AWS::Region}-${AWS::StackName} + Roles: + - !Ref OpenViduMasterNodeRole + + OpenViduMediaInstanceProfile: + Type: AWS::IAM::InstanceProfile + DependsOn: + - MasterNodesWaitCondition4 + Properties: + InstanceProfileName: !Sub OpenViduMediaInstanceProfile-${AWS::Region}-${AWS::StackName} + Roles: + - !Ref OpenViduMediaNodeRole + + OpenViduMasterLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Metadata: + Comment: Launch template for OpenVidu Master Node + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash -x + set -e + OPENVIDU_VERSION=main + DOMAIN= + YQ_VERSION=v4.44.5 + + # Install dependencies + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Singal to notify instance is waiting + SIGNAL_NAME="$1" + + # Token for IMDSv2 + TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" + + # Subnets IDs + SUBNETS=( + "${SubnetProcessor.Subnet1}" + "${SubnetProcessor.Subnet2}" + "${SubnetProcessor.Subnet3}" + "${SubnetProcessor.Subnet4}" + ) + + MAC_ADDRESS="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254//latest/meta-data/mac)" + SUBNET_ID="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s "http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS/subnet-id")" + + # Check master node number + MASTER_NODE_NUM=1 + for subnet in "${!SUBNETS[@]}"; do + if [[ "$subnet" == "$SUBNET_ID" ]]; then + + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none') + + # Check if current master node is reacheable with ping + ACUTAL_MASTER_NODE_IP=$(echo "$SHARED_SECRET" | jq -r ".MASTER_NODE_${!MASTER_NODE_NUM}_PRIVATE_IP") + if [[ "$ACUTAL_MASTER_NODE_IP" == "none" ]]; then + break + fi + fi + MASTER_NODE_NUM=$((MASTER_NODE_NUM + 1)) + done + + # Get own private IP + PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)" + + if [[ "$PRIVATE_IP" == "" ]]; then + echo "Error: Private IP not found" + exit 1 + fi + + # Store current private IP + /usr/local/bin/store_secret.sh save MASTER_NODE_${!MASTER_NODE_NUM}_PRIVATE_IP "${!PRIVATE_IP}" + + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') + + # If the private IP is the same as the first master node, generate the secrets + if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "false" ]]; then + DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN_NAME "${DomainName}")" + if [[ -n "${TurnDomainName}" ]]; then + LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")" + fi + OPENVIDU_PRO_LICENSE="$(/usr/local/bin/store_secret.sh save OPENVIDU_PRO_LICENSE "${OpenViduLicense}")" + OPENVIDU_RTC_ENGINE="$(/usr/local/bin/store_secret.sh save OPENVIDU_RTC_ENGINE "${RTCEngine}")" + # Store version so media nodes can use it to install the same version + /usr/local/bin/store_secret.sh save OPENVIDU_VERSION "${!OPENVIDU_VERSION}" + + # Store usernames and generate random passwords + REDIS_PASSWORD="$(/usr/local/bin/store_secret.sh generate REDIS_PASSWORD)" + MONGO_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save MONGO_ADMIN_USERNAME "mongoadmin")" + MONGO_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MONGO_ADMIN_PASSWORD)" + MONGO_REPLICA_SET_KEY="$(/usr/local/bin/store_secret.sh generate MONGO_REPLICA_SET_KEY)" + MINIO_ACCESS_KEY="$(/usr/local/bin/store_secret.sh save MINIO_ACCESS_KEY "minioadmin")" + MINIO_SECRET_KEY="$(/usr/local/bin/store_secret.sh generate MINIO_SECRET_KEY)" + DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD_ADMIN_USERNAME "dashboardadmin")" + DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" + GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")" + GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)" + DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")" + DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" + DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" + DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)" + LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" + LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)" + ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")" + ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")" + fi + + # Fetch the shared secret again + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') + if [[ "${!ALL_SECRETS_GENERATED}" == "false" ]]; then + echo "Error: Secrets not generated" + exit 1 + fi + + # sending the signal call + cfn-signal -e $? --stack ${AWS::StackId} --resource "$SIGNAL_NAME" --region ${AWS::Region} + + # Wait for all master nodes to store their private IPs + while true; do + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none') + + MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') + MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') + MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') + MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') + + # Check if all master nodes have stored their private IPs + if [[ "$MASTER_NODE_1_PRIVATE_IP" != "none" ]] && + [[ "$MASTER_NODE_2_PRIVATE_IP" != "none" ]] && + [[ "$MASTER_NODE_3_PRIVATE_IP" != "none" ]] && + [[ "$MASTER_NODE_4_PRIVATE_IP" != "none" ]]; then + break + fi + sleep 5 + done + + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') + MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') + MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') + MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') + 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=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME') + LIVEKIT_TURN_DOMAIN_NAME=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_TURN_DOMAIN_NAME') + OPENVIDU_PRO_LICENSE=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_PRO_LICENSE') + OPENVIDU_RTC_ENGINE=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_RTC_ENGINE') + REDIS_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.REDIS_PASSWORD') + MONGO_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.MONGO_ADMIN_USERNAME') + MONGO_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.MONGO_ADMIN_PASSWORD') + MONGO_REPLICA_SET_KEY=$(echo "$SHARED_SECRET" | jq -r '.MONGO_REPLICA_SET_KEY') + MINIO_ACCESS_KEY=$(echo "$SHARED_SECRET" | jq -r '.MINIO_ACCESS_KEY') + MINIO_SECRET_KEY=$(echo "$SHARED_SECRET" | jq -r '.MINIO_SECRET_KEY') + DASHBOARD_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_USERNAME') + DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD') + GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME') + GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD') + DEFAULT_APP_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_USERNAME') + DEFAULT_APP_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_PASSWORD') + DEFAULT_APP_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_USERNAME') + DEFAULT_APP_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_PASSWORD') + LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY') + LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET') + ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES') + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_master_node.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--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" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" + "--certificate-type='letsencrypt'" + "--letsencrypt-email='openvidu@gmail.com'" + ) + + if [[ "${!LIVEKIT_TURN_DOMAIN_NAME}" != "none" ]]; then + COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'") + fi + + # Construct the final command + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + + mode: '000755' + owner: root + group: root + '/usr/local/bin/config_s3.sh': + content: !Sub + - | + #!/bin/bash + set -e + + # Install dir and config dir + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + + # Config S3 bucket + EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com" + EXTERNAL_S3_REGION="${AWS::Region}" + EXTERNAL_S3_PATH_STYLE_ACCESS="false" + EXTERNAL_S3_BUCKET_APP_DATA=${S3AppDataBucketResourceName} + EXTERNAL_S3_BUCKET_CLUSTER_DATA=${S3ClusterDataBucketResourceName} + sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_BUCKET_CLUSTER_DATA=.*|EXTERNAL_S3_BUCKET_CLUSTER_DATA=$EXTERNAL_S3_BUCKET_CLUSTER_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + - S3AppDataBucketResourceName: !If + - CreateRecordingsBucket + - !Ref S3AppDataBucketResource + - !Ref S3AppDataBucketName + S3ClusterDataBucketResourceName: !If + - CreateClusterDataBucket + - !Ref S3ClusterDataBucketResource + - !Ref S3ClusterDataBucketName + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/after_install.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Save access URLs + DOMAIN=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME') + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_config_from_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" + + # Replace DOMAIN_NAME + export DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME) + if [[ -n "$DOMAIN" ]]; then + sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + else + exit 1 + fi + + # Replace LIVEKIT_TURN_DOMAIN_NAME + export LIVEKIT_TURN_DOMAIN_NAME=$(echo $SHARED_SECRET | jq -r .LIVEKIT_TURN_DOMAIN_NAME) + if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + fi + + # Replace rest of the values + sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD)/" "${!MASTER_NODE_CONFIG_DIR}/master_node.env" + sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_RTC_ENGINE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_PRO_LICENSE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_ACCESS_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_SECRET_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + + # Update URLs in secret + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_secret_from_config.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" + + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"REDIS_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${!MASTER_NODE_CONFIG_DIR}/master_node.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_TURN_DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_RTC_ENGINE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_PRO_LICENSE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_ACCESS_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_SECRET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/get_value_from_config.sh': + content: | + #!/bin/bash + set -e + + # Function to get the value of a given key from the environment file + get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi + } + + # Check if the correct number of arguments are supplied + if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Get the key and file path from the arguments + key="$1" + file_path="$2" + + # Get and print the value + get_value "$key" "$file_path" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/store_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Modes: save, generate + # save mode: save the secret in the secret manager + # generate mode: generate a random password and save it in the secret manager + MODE="$1" + SHARED_SECRET="$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --query SecretString --output text)" + if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${!3:-}" + LENGTH="${!4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${!LENGTH})" + RANDOM_PASSWORD="${!PREFIX}${!RANDOM_PASSWORD}" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$RANDOM_PASSWORD"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$RANDOM_PASSWORD" + elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$SECRET_VALUE"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$SECRET_VALUE" + else + exit 1 + fi + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/restart.sh': + content: | + #!/bin/bash + set -e + # Stop all services + systemctl stop openvidu + + # Update config from secret + /usr/local/bin/update_config_from_secret.sh + + # Start all services + systemctl start openvidu + mode: "000755" + owner: "root" + group: "root" + Properties: + LaunchTemplateName: !Sub 'openvidu-ha-master-${AWS::Region}-${AWS::StackName}' + LaunchTemplateData: + # Enable IMDSv2 + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + IamInstanceProfile: + Name: !Ref OpenViduMasterInstanceProfile + ImageId: !Ref AmiId + InstanceType: !Ref MasterNodeInstanceType + KeyName: !Ref KeyName + SecurityGroupIds: + - !Ref OpenViduMasterNodeSG + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: !Ref MasterNodesDiskSize + VolumeType: gp3 + DeleteOnTermination: true + + OpenViduMasterNode1: + Type: AWS::EC2::Instance + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 1 + SubnetId: !GetAtt SubnetProcessor.Subnet1 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition1" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + + MasterNodesWaitCondition1: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode2: + Type: AWS::EC2::Instance + DependsOn: MasterNodesWaitCondition1 + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 2 + SubnetId: !GetAtt SubnetProcessor.Subnet2 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition2" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + + MasterNodesWaitCondition2: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode3: + Type: AWS::EC2::Instance + DependsOn: MasterNodesWaitCondition2 + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 3 + SubnetId: !GetAtt SubnetProcessor.Subnet3 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition3" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + systemctl enable crond.service + systemctl start crond.service + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + + MasterNodesWaitCondition3: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode4: + Type: AWS::EC2::Instance + DependsOn: MasterNodesWaitCondition3 + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 4 + SubnetId: !GetAtt SubnetProcessor.Subnet4 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition4" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + systemctl enable crond.service + systemctl start crond.service + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + MasterNodesWaitCondition4: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMediaNodeLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Metadata: + Comment: Launch template for OpenVidu Media Node + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash + set -e + YQ_VERSION=v4.44.5 + + # Install dependencies + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Token for IMDSv2 + TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" + + # Get own private IP + PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)" + + SHARED_SECRET="$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none')" + if [[ "$SHARED_SECRET" == "none" ]]; then + echo "Error: Shared secret not found" + exit 1 + fi + + # Get OpenVidu Media Nodes version to deploy + OPENVIDU_VERSION=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_VERSION') + if [[ "$OPENVIDU_VERSION" == "none" ]]; then + echo "OpenVidu version not found" + exit 1 + fi + + ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') + if [[ "$ALL_SECRETS_GENERATED" == "none" ]]; then + echo "Error: Secrets not generated" + exit 1 + fi + MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') + MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') + MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') + MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') + MASTER_NODE_PRIVATE_IP_LIST="$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP" + REDIS_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.REDIS_PASSWORD') + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--deployment-type='ha'" + "--node-role='media-node'" + "--master-node-private-ip-list=$MASTER_NODE_PRIVATE_IP_LIST" + "--private-ip=$PRIVATE_IP" + "--redis-password=$REDIS_PASSWORD" + ) + + # Construct the final command with all arguments + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + mode: '000755' + owner: root + group: root + '/usr/local/bin/set_as_unhealthy.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own instance ID + INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + + # Set instance as unhealthy + aws autoscaling set-instance-health \ + --region ${AWS::Region} \ + --instance-id "$INSTANCE_ID" \ + --health-status Unhealthy + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/stop_media_node.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own instance ID + INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + ASG_NAME=openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} + + # Execute if docker is installed + if [ -x "$(command -v docker)" ]; then + echo "Stopping media node services and waiting for termination..." + docker container kill --signal=SIGQUIT openvidu || true + docker container kill --signal=SIGQUIT ingress || true + docker container kill --signal=SIGQUIT egress || true + TIME_PASSED=0 + HEARTBEAT_MAX=1800 + # Wait for running containers to not be openvidu, ingress or egress + while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do + echo "Waiting for containers to stop..." + sleep 5 + TIME_PASSED=$((TIME_PASSED+5)) + if [ $TIME_PASSED -ge $HEARTBEAT_MAX ]; then + echo "Increase lifecycle hook timeout to continue waiting for termination" + # Increase lifecycle hook timeout + aws autoscaling record-lifecycle-action-heartbeat \ + --region ${AWS::Region} \ + --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ + --auto-scaling-group-name "$ASG_NAME" \ + --instance-id "$INSTANCE_ID" + TIME_PASSED=0 + fi + done + fi + + aws autoscaling complete-lifecycle-action \ + --region ${AWS::Region} \ + --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ + --auto-scaling-group-name "$ASG_NAME" \ + --lifecycle-action-result CONTINUE \ + --instance-id "$INSTANCE_ID" + mode: "000755" + owner: "root" + group: "root" + Properties: + LaunchTemplateName: !Sub 'openvidu-ha-media-${AWS::Region}-${AWS::StackName}' + LaunchTemplateData: + # Enable IMDSv2 by default + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + IamInstanceProfile: + Arn: !GetAtt OpenViduMediaInstanceProfile.Arn + SecurityGroupIds: + - !GetAtt OpenViduMediaNodeSG.GroupId + ImageId: !Ref AmiId + KeyName: !Ref KeyName + InstanceType: !Ref MediaNodeInstanceType + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenViduMediaNodeLaunchTemplate + + export HOME="/root" + + # Install OpenVidu + /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } + + # Wait for the app + # /usr/local/bin/check_app_ready.sh + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeType: gp3 + DeleteOnTermination: true + VolumeSize: 50 + + OpenViduMediaNodeASG: + DependsOn: + - OpenViduMediaInstanceProfile + - OpenViduMasterInstanceProfile + - StopMediaNodeCloudWatchEventRule + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMediaNodeLaunchTemplate + Version: !GetAtt OpenViduMediaNodeLaunchTemplate.DefaultVersionNumber + MinSize: !Ref MinNumberOfMediaNodes + MaxSize: !Ref MaxNumberOfMediaNodes + DesiredCapacity: !Ref InitialNumberOfMediaNodes + VPCZoneIdentifier: !Ref OpenViduMediaNodeSubnets + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Media Node + PropagateAtLaunch: true + + StopMediaNodeLifecycleHook: + Type: 'AWS::AutoScaling::LifecycleHook' + Properties: + LifecycleHookName: !Sub StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} + AutoScalingGroupName: !Ref OpenViduMediaNodeASG + LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' + DefaultResult: 'CONTINUE' + HeartbeatTimeout: 3600 + + StopMediaNodeDocumentRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - ssm.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ssm:DescribeInstanceInformation + - ssm:ListCommands + - ssm:ListCommandInvocations + Resource: "*" + - Effect: Allow + Action: + - ssm:SendCommand + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunShellScript + - Action: + - ssm:SendCommand + Resource: !Sub arn:${AWS::Partition}:ec2:*:*:instance/* + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' + Effect: Allow + PolicyName: SSM-Automation-Policy + + StopMediaNodeAutomationDocument: + Type: AWS::SSM::Document + Properties: + Name: + Fn::Join: + # Generate a not too long and unique document name + # Getting a unique identifier from the stack id + - '' + - - 'StopMediaNodeAutomationDocument-' + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + DocumentType: Automation + Content: + schemaVersion: '0.3' + assumeRole: "{{automationAssumeRole}}" + description: This document stops the OpenVidu services in a Media Node and + terminates the instance. It stop the OpenVidu services without interrupting + the running sessions, ingress and egress running in the Media Node. + Also it sends a CONTINUE signal to the Auto Scaling Group to continue the + instance termination when the services are stopped. + parameters: + InstanceId: + type: String + automationAssumeRole: + type: String + default: !GetAtt StopMediaNodeDocumentRole.Arn + description: "(Required) The ARN of the role that allows Automation to + perform the actions." + mainSteps: + - name: RunCommand + action: aws:runCommand + inputs: + DocumentName: AWS-RunShellScript + InstanceIds: + - "{{ InstanceId }}" + Parameters: + # 24 hours as a timeout to wait for the instance to stop all services + executionTimeout: "60" + commands: + - nohup /usr/local/bin/stop_media_node.sh > /var/log/stop_media_node.log 2>&1 & + + + StopMediaNodeCloudWatchEventRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - events.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ssm:StartAutomationExecution + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT + PolicyName: !Sub StopMediaNodeCloudWatchEventPolicy-${AWS::Region}-${AWS::StackName} + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - iam:PassRole + Resource: !GetAtt StopMediaNodeDocumentRole.Arn + PolicyName: Pass-Role-SSM-Automation-Policy + + StopMediaNodeCloudWatchEventRule: + Type: AWS::Events::Rule + Properties: + Description: Rule to trigger the StopMediaNodeAutomationDocument when an instance is terminated + EventPattern: + source: + - aws.autoscaling + detail-type: + - EC2 Instance-terminate Lifecycle Action + detail: + AutoScalingGroupName: + - !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} + Name: + Fn::Join: + # Generate a not too long and unique rule name + # Getting a unique identifier from the stack id + - '' + - - StopMediaNodeCloudWatchEventRule- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Targets: + - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT + RoleArn: !GetAtt StopMediaNodeCloudWatchEventRole.Arn + Id: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - StopMediaNodeCloudWatchEventRule- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + InputTransformer: + InputPathsMap: + instanceid: "$.detail.EC2InstanceId" + InputTemplate: | + { + "InstanceId": [ ] + } + + OpenViduMediaNodeASGScalingPolicy: + Type: AWS::AutoScaling::ScalingPolicy + Properties: + AutoScalingGroupName: !Ref OpenViduMediaNodeASG + PolicyType: TargetTrackingScaling + EstimatedInstanceWarmup: 120 + TargetTrackingConfiguration: + PredefinedMetricSpecification: + PredefinedMetricType: ASGAverageCPUUtilization + TargetValue: !Ref ScaleTargetCPU + + OpenViduMasterNodeSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OpenVidu Master Node + GroupName: !Sub openvidu-ha-master-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + + OpenViduLoadBalancerToMasterHTTPIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 80 + ToPort: 80 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduLoadBalancerToMasterIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 443 + ToPort: 443 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduLoadBalancerToMasterHealthCheckIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7880 + ToPort: 7880 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + + OpenViduMasterToMasterRedisIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7000 + ToPort: 7001 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterRedisIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7000 + ToPort: 7001 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterMinioIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9100 + ToPort: 9100 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterMinioConsoleSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9101 + ToPort: 9101 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterMinioIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9100 + ToPort: 9100 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterMongoIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 20000 + ToPort: 20000 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterMongoIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 20000 + ToPort: 20000 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterMimirGrpcIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9095 + ToPort: 9095 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterMimirGossipIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7946 + ToPort: 7946 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterHTTPMimirSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9009 + ToPort: 9009 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterLokiGrpcIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9096 + ToPort: 9096 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterLokiGossipIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7947 + ToPort: 7947 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterDashboardsIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 5000 + ToPort: 5000 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterGrafanaIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 3000 + ToPort: 3000 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterHTTPLokiSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 3100 + ToPort: 3100 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterV2CompatibilityIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 4443 + ToPort: 4443 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterV2CompatibilityWebhookIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 4443 + ToPort: 4443 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMasterToMasterDefaultAppIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 6080 + ToPort: 6080 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterDefaultAppWebhookIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 6080 + ToPort: 6080 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OpenVidu Media Node + GroupName: !Sub openvidu-ha-media-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIpv6: ::/0 + + OpenViduLoadBalancerToMasterNodeRTMPIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 1945 + ToPort: 1945 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduMasterNodeToMediaNodeRTMPIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduLoadBalancerToMediaNodeIngressHealthCheckSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 9092 + ToPort: 9092 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduMasterNodeTurnTLSToMediaNodeIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Condition: TurnTLSIsEnabled + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 5349 + ToPort: 5349 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + + OpenViduMasterToMediaNodeServerIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 7880 + ToPort: 7880 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMediaNodeClientIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduLoadBalancerSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for the Load Balancer + GroupName: !Sub openvidu-ha-lb-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIpv6: ::/0 + + OpenViduTurnTLSLoadBalancerSG: + Type: AWS::EC2::SecurityGroup + Condition: TurnTLSIsEnabled + Properties: + GroupDescription: Security group for the Load Balancer for TURN with TLS + GroupName: !Sub openvidu-ha-turn-tls-lb-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + + LoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + DependsOn: + - MasterNodesWaitCondition4 + Properties: + Name: + Fn::Join: + # Generate a not too long and unique load balancer name + # Getting a unique identifier from the stack id + - '' + - - OpenViduHA- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Subnets: !Ref OpenViduMasterNodeSubnets + SecurityGroups: + - !Ref OpenViduLoadBalancerSG + Type: network + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Load Balancer + + TurnTLSLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Condition: TurnTLSIsEnabled + Properties: + Name: + Fn::Join: + # Generate a not too long and unique load balancer name + # Getting a unique identifier from the stack id + - '' + - - OpenViduHA-TurnTLS- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Subnets: !Ref OpenViduMasterNodeSubnets + SecurityGroups: + - !Ref OpenViduLoadBalancerSG + Type: network + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN with TLS Load Balancer + + OpenViduMasterNodeHTTPListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMasterNodeTGHTTP + LoadBalancerArn: !Ref LoadBalancer + Port: 80 + Protocol: TCP + + OpenViduMasterNodeListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMasterNodeTG + LoadBalancerArn: !Ref LoadBalancer + Port: 443 + Protocol: TCP + + OpenViduRTMPMasterNodeListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMasterNodeRTMPTG + LoadBalancerArn: !Ref LoadBalancer + Port: 1935 + Protocol: TCP + + OpenViduTurnTLSMasterNodeListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Condition: TurnTLSIsEnabled + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMasterNodeTurnTLSTG + LoadBalancerArn: !Ref TurnTLSLoadBalancer + Port: 443 + Protocol: TCP + + OpenViduMasterNodeTGHTTP: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OpenVidu-HTTP- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + TargetType: instance + Targets: + - Id: !Ref OpenViduMasterNode1 + - Id: !Ref OpenViduMasterNode2 + - Id: !Ref OpenViduMasterNode3 + - Id: !Ref OpenViduMasterNode4 + VpcId: !Ref OpenViduVPC + Port: 80 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: /health/caddy + HealthCheckProtocol: HTTP + HealthCheckPort: '7880' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group + + OpenViduMasterNodeTG: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OpenVidu- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + TargetType: instance + Targets: + - Id: !Ref OpenViduMasterNode1 + - Id: !Ref OpenViduMasterNode2 + - Id: !Ref OpenViduMasterNode3 + - Id: !Ref OpenViduMasterNode4 + VpcId: !Ref OpenViduVPC + Port: 443 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: /health/caddy + HealthCheckProtocol: HTTP + HealthCheckPort: '7880' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group + + OpenViduMasterNodeRTMPTG: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OVRTMP- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + VpcId: !Ref OpenViduVPC + TargetType: instance + Targets: + - Id: !Ref OpenViduMasterNode1 + - Id: !Ref OpenViduMasterNode2 + - Id: !Ref OpenViduMasterNode3 + - Id: !Ref OpenViduMasterNode4 + Port: 1945 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: /health/caddy + HealthCheckProtocol: HTTP + # Ingress health check port + HealthCheckPort: '7880' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - RTMP Target Group + + OpenViduMasterNodeTurnTLSTG: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: TurnTLSIsEnabled + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OVTurnTLS- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + TargetType: instance + Targets: + - Id: !Ref OpenViduMasterNode1 + - Id: !Ref OpenViduMasterNode2 + - Id: !Ref OpenViduMasterNode3 + - Id: !Ref OpenViduMasterNode4 + VpcId: !Ref OpenViduVPC + Port: 443 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: /health/caddy + HealthCheckProtocol: HTTP + HealthCheckPort: '7880' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN TLS Target Group + +Outputs: + ServicesAndCredentials: + Description: Services and credentials + Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/home?region=${AWS::Region}#!/secret?name=openvidu-ha-${AWS::Region}-${AWS::StackName} diff --git a/openvidu-deployment/pro/ha/aws/cf-openvidu-ha.yaml b/openvidu-deployment/pro/ha/aws/cf-openvidu-ha.yaml new file mode 100644 index 00000000..34ef55c8 --- /dev/null +++ b/openvidu-deployment/pro/ha/aws/cf-openvidu-ha.yaml @@ -0,0 +1,2341 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: OpenVidu Pro - High Availability + +Parameters: + DomainName: + Type: String + Description: Domain name for the OpenVidu High Availability cluster + AllowedPattern: ^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + ConstraintDescription: The domain name does not have a valid domain name format + + OpenViduCertificateARN: + Description: 'Amazon certificate arn resource to load into the LoadBalancer' + Type: String + AllowedPattern: '.+' + ConstraintDescription: The Load Balancer domain name must be defined + + TurnDomainName: + Description: '(Optional) Domain name for the TURN server with TLS.' + Type: String + Default: '' + + TurnCertificateARN: + Description: '(Optional) Amazon certificate arn resource to load into the TURN LoadBalancer' + Type: String + Default: '' + + OpenViduLicense: + Description: "Visit https://openvidu.io/account" + Type: String + AllowedPattern: ^(?!\s*$).+$ + NoEcho: true + ConstraintDescription: OpenVidu Pro License is mandatory + + RTCEngine: + Description: "RTCEngine media engine to use" + Type: String + AllowedValues: + - pion + - mediasoup + Default: pion + + MasterNodeInstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu Master Node" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + MediaNodeInstanceType: + Description: "Specifies the EC2 instance type for your OpenVidu Media Nodes" + Type: String + Default: c6a.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - t3.medium + - t3.large + - t3.xlarge + - t3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.8xlarge + - m5.12xlarge + - m5.16xlarge + - m5.24xlarge + - m6i.large + - m6i.xlarge + - m6i.2xlarge + - m6i.4xlarge + - m6i.8xlarge + - m6i.12xlarge + - m6i.16xlarge + - m6i.24xlarge + - m6i.32xlarge + - m6i.metal + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + - c6a.12xlarge + - c6a.16xlarge + - c6a.24xlarge + - c6a.32xlarge + - c6a.48xlarge + - c6a.metal + - c6i.large + - c6i.xlarge + - c6i.2xlarge + - c6i.4xlarge + - c6i.8xlarge + - c6i.12xlarge + - c6i.16xlarge + - c6i.24xlarge + - c6i.32xlarge + - c6i.metal + - c7a.medium + - c7a.large + - c7a.xlarge + - c7a.2xlarge + - c7a.4xlarge + - c7a.8xlarge + - c7a.12xlarge + - c7a.16xlarge + - c7a.24xlarge + - c7a.32xlarge + - c7a.48xlarge + - c7a.metal-48xl + - c7i.large + - c7i.xlarge + - c7i.2xlarge + - c7i.4xlarge + - c7i.8xlarge + - c7i.12xlarge + - c7i.16xlarge + - c7i.24xlarge + - c7i.48xlarge + - c7i.metal-24xl + - c7i.metal-48xl + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - m5n.large + - m5n.xlarge + - m5n.2xlarge + - m5n.4xlarge + - m5n.8xlarge + - m5n.12xlarge + - m5n.16xlarge + - m5n.24xlarge + - m6in.large + - m6in.xlarge + - m6in.2xlarge + - m6in.4xlarge + - m6in.8xlarge + - m6in.12xlarge + - m6in.16xlarge + - m6in.24xlarge + - m6in.32xlarge + - r5n.large + - r5n.xlarge + - r5n.2xlarge + - r5n.4xlarge + - r5n.8xlarge + - r5n.12xlarge + - r5n.16xlarge + - r5n.24xlarge + ConstraintDescription: "Must be a valid EC2 instance type" + + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: Name of an existing EC2 KeyPair to enable SSH access to the instances + AllowedPattern: ^.+$ + ConstraintDescription: must be the name of an existing EC2 KeyPair. + + AmiId: + Type: AWS::SSM::Parameter::Value + Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id + Description: AMI ID for the EC2 instances + + InitialNumberOfMediaNodes: + Type: Number + Default: 1 + Description: Number of initial media nodes to deploy + + MinNumberOfMediaNodes: + Type: Number + Default: 1 + Description: Minimum number of media nodes to deploy + + MaxNumberOfMediaNodes: + Type: Number + Default: 5 + Description: Maximum number of media nodes to deploy + + ScaleTargetCPU: + Type: Number + Default: 50 + Description: Target CPU percentage to scale up or down + + S3AppDataBucketName: + Type: String + Description: Name of the S3 bucket to store data and recordings. If empty, a bucket will be created + + S3ClusterDataBucketName: + Type: String + Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created + + OpenViduVPC: + Description: "Dedicated VPC for OpenVidu cluster" + Type: AWS::EC2::VPC::Id + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a VPC ID + + OpenViduMasterNodeSubnets: + Description: "Subnets for OpenVidu Master Node" + Type: List + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a list of subnet IDs + + OpenViduMediaNodeSubnets: + Description: "Subnets for OpenVidu Media Nodes" + Type: List + AllowedPattern: ^.+$ + ConstraintDescription: You must specify a list of subnet IDs + + MasterNodesDiskSize: + Description: Size of the disk in GB + Type: Number + Default: 100 + MinValue: 50 + ConstraintDescription: The disk size must be at least 50 GB + +Metadata: + 'AWS::CloudFormation::Interface': + ParameterGroups: + - Label: + default: Domain and Load Blancer configuration + Parameters: + - DomainName + - OpenViduCertificateARN + - Label: + default: OpenVidu High Availability configuration + Parameters: + - OpenViduLicense + - RTCEngine + - Label: + default: EC2 Instance configuration + Parameters: + - MasterNodeInstanceType + - MediaNodeInstanceType + - KeyName + - AmiId + - Label: + default: Media Nodes Autoscaling Group configuration + Parameters: + - InitialNumberOfMediaNodes + - MinNumberOfMediaNodes + - MaxNumberOfMediaNodes + - ScaleTargetCPU + - Label: + default: S3 bucket for application data, cluster data and recordings + Parameters: + - S3AppDataBucketName + - S3ClusterDataBucketName + - Label: + default: VPC configuration + Parameters: + - OpenViduVPC + - OpenViduMasterNodeSubnets + - OpenViduMediaNodeSubnets + - Label: + default: Volumes configuration + Parameters: + - MasterNodesDiskSize + - Label: + default: (Optional) TURN server configuration with TLS + Parameters: + - TurnDomainName + - TurnCertificateARN + +Conditions: + TurnTLSIsEnabled: !Or [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]] + CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] + CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""] + +Resources: + + OpenViduSharedInfo: + Type: AWS::SecretsManager::Secret + UpdateReplacePolicy: Retain + DeletionPolicy: Delete + Properties: + Name: !Sub openvidu-ha-${AWS::Region}-${AWS::StackName} + Description: Secret for OpenVidu High Availability to store deployment info and seed secret + # All the values are initialized by one master node and shared with the rest of the nodes + SecretString: | + { + "ALL_SECRETS_GENERATED": "false", + "DOMAIN_NAME": "none", + "LIVEKIT_TURN_DOMAIN_NAME": "none", + "OPENVIDU_PRO_LICENSE": "none", + "OPENVIDU_RTC_ENGINE": "none", + "REDIS_PASSWORD": "none", + "MONGO_ADMIN_USERNAME": "none", + "MONGO_ADMIN_PASSWORD": "none", + "MONGO_REPLICA_SET_KEY": "none", + "MINIO_URL": "none", + "MINIO_ACCESS_KEY": "none", + "MINIO_SECRET_KEY": "none", + "DASHBOARD_URL": "none", + "DASHBOARD_ADMIN_USERNAME": "none", + "DASHBOARD_ADMIN_PASSWORD": "none", + "GRAFANA_URL": "none", + "GRAFANA_ADMIN_USERNAME": "none", + "GRAFANA_ADMIN_PASSWORD": "none", + "DEFAULT_APP_USERNAME": "none", + "DEFAULT_APP_PASSWORD": "none", + "DEFAULT_APP_ADMIN_USERNAME": "none", + "DEFAULT_APP_ADMIN_PASSWORD": "none", + "LIVEKIT_API_KEY": "none", + "LIVEKIT_API_SECRET": "none", + "ENABLED_MODULES": "none", + "MASTER_NODE_1_PRIVATE_IP": "none", + "MASTER_NODE_2_PRIVATE_IP": "none", + "MASTER_NODE_3_PRIVATE_IP": "none", + "MASTER_NODE_4_PRIVATE_IP": "none", + "OPENVIDU_VERSION": "none" + } + + S3AppDataBucketResource: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-appdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateRecordingsBucket + + S3ClusterDataBucketResource: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-clusterdata', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateClusterDataBucket + + # ------------------------- + # Preprocess subnets to allocate Volumes and ENIs across Availability Zones + # For OpenVidu Master Nodes + # ------------------------- + SubnetProcessorFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub 'SubnetProcessor-${AWS::Region}-${AWS::StackName}' + Handler: index.lambda_handler + Role: !GetAtt LambdaExecutionRole.Arn + Code: + ZipFile: | + import cfnresponse + import boto3 + + def lambda_handler(event, context): + try: + # Process event data + subnets = event['ResourceProperties']['Subnets'] + ec2 = boto3.client('ec2') + + # Ensure we have at least four subnets by cycling through the available subnets + subnets = (subnets * 4)[:4] # Repeat the list to have at least 4 elements and then take the first 4 + + # Prepare the response + responseData = { + 'Subnet1': subnets[0], + 'Subnet2': subnets[1], + 'Subnet3': subnets[2], + 'Subnet4': subnets[3], + } + cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) + + except Exception as e: + cfnresponse.send(event, context, cfnresponse.FAILED, {'Message': str(e)}) + + + Runtime: python3.12 + Timeout: 120 + + SubnetProcessor: + Type: Custom::SubnetProcessor + Properties: + ServiceToken: !GetAtt SubnetProcessorFunction.Arn + Subnets: !Ref OpenViduMasterNodeSubnets + + LambdaLogGroup: + UpdateReplacePolicy: Retain + DeletionPolicy: Delete + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}' + RetentionInDays: 7 + + LambdaExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: 'sts:AssumeRole' + Policies: + - PolicyName: LambdaLogsPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/SubnetProcessor-${AWS::Region}-${AWS::StackName}:*' + - Effect: Allow + Action: + - ec2:DescribeSubnets + Resource: '*' + + OpenViduMasterNodeRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: "/" + Policies: + - PolicyName: !Sub openvidu-master-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - autoscaling:SetInstanceHealth + Resource: '*' + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-id': !Ref 'AWS::StackId' + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:UpdateSecret + Resource: !Ref OpenViduSharedInfo + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + - Fn::If: + - CreateClusterDataBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3ClusterDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName}/* + - Fn::If: + - CreateClusterDataBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3ClusterDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3ClusterDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-master-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduMediaNodeRole: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - 'sts:AssumeRole' + Path: / + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore + Policies: + - PolicyName: !Sub openvidu-media-policy-${AWS::Region}-${AWS::StackName} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref OpenViduSharedInfo + - Effect: Allow + Action: + - autoscaling:SetInstanceHealth + - autoscaling:CompleteLifecycleAction + - autoscaling:RecordLifecycleActionHeartbeat + Resource: '*' + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub ${S3AppDataBucketResource.Arn}/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:GetObject + - s3:PutObject + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName}/* + - Fn::If: + - CreateRecordingsBucket + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt S3AppDataBucketResource.Arn + - Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !Sub arn:${AWS::Partition}:s3:::${S3AppDataBucketName} + RoleName: + Fn::Join: + # Generate a not too long and unique role name + # Getting a unique identifier from the stack id + - '' + - - openvidu-media-role- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + + OpenViduMasterInstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + InstanceProfileName: !Sub OpenViduMasterInstanceProfile-${AWS::Region}-${AWS::StackName} + Roles: + - !Ref OpenViduMasterNodeRole + + OpenViduMediaInstanceProfile: + Type: AWS::IAM::InstanceProfile + DependsOn: + - MasterNodesWaitCondition4 + Properties: + InstanceProfileName: !Sub OpenViduMediaInstanceProfile-${AWS::Region}-${AWS::StackName} + Roles: + - !Ref OpenViduMediaNodeRole + + OpenViduMasterLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Metadata: + Comment: Launch template for OpenVidu Master Node + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash -x + set -e + OPENVIDU_VERSION=main + DOMAIN= + YQ_VERSION=v4.44.5 + + # Install dependencies + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Singal to notify instance is waiting + SIGNAL_NAME="$1" + + # Token for IMDSv2 + TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" + + # Subnets IDs + SUBNETS=( + "${SubnetProcessor.Subnet1}" + "${SubnetProcessor.Subnet2}" + "${SubnetProcessor.Subnet3}" + "${SubnetProcessor.Subnet4}" + ) + + MAC_ADDRESS="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254//latest/meta-data/mac)" + SUBNET_ID="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s "http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC_ADDRESS/subnet-id")" + + # Check master node number + MASTER_NODE_NUM=1 + for subnet in "${!SUBNETS[@]}"; do + if [[ "$subnet" == "$SUBNET_ID" ]]; then + + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none') + + # Check if current master node is reacheable with ping + ACUTAL_MASTER_NODE_IP=$(echo "$SHARED_SECRET" | jq -r ".MASTER_NODE_${!MASTER_NODE_NUM}_PRIVATE_IP") + if [[ "$ACUTAL_MASTER_NODE_IP" == "none" ]]; then + break + fi + fi + MASTER_NODE_NUM=$((MASTER_NODE_NUM + 1)) + done + + # Get own private IP + PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)" + + if [[ "$PRIVATE_IP" == "" ]]; then + echo "Error: Private IP not found" + exit 1 + fi + + # Store current private IP + /usr/local/bin/store_secret.sh save MASTER_NODE_${!MASTER_NODE_NUM}_PRIVATE_IP "${!PRIVATE_IP}" + + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') + + # If the private IP is the same as the first master node, generate the secrets + if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "false" ]]; then + DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN_NAME "${DomainName}")" + if [[ -n "${TurnDomainName}" ]]; then + LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")" + fi + OPENVIDU_PRO_LICENSE="$(/usr/local/bin/store_secret.sh save OPENVIDU_PRO_LICENSE "${OpenViduLicense}")" + OPENVIDU_RTC_ENGINE="$(/usr/local/bin/store_secret.sh save OPENVIDU_RTC_ENGINE "${RTCEngine}")" + # Store version so media nodes can use it to install the same version + /usr/local/bin/store_secret.sh save OPENVIDU_VERSION "${!OPENVIDU_VERSION}" + + # Store usernames and generate random passwords + REDIS_PASSWORD="$(/usr/local/bin/store_secret.sh generate REDIS_PASSWORD)" + MONGO_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save MONGO_ADMIN_USERNAME "mongoadmin")" + MONGO_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate MONGO_ADMIN_PASSWORD)" + MONGO_REPLICA_SET_KEY="$(/usr/local/bin/store_secret.sh generate MONGO_REPLICA_SET_KEY)" + MINIO_ACCESS_KEY="$(/usr/local/bin/store_secret.sh save MINIO_ACCESS_KEY "minioadmin")" + MINIO_SECRET_KEY="$(/usr/local/bin/store_secret.sh generate MINIO_SECRET_KEY)" + DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD_ADMIN_USERNAME "dashboardadmin")" + DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" + GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")" + GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)" + DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")" + DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" + DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" + DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)" + LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" + LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)" + ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")" + ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")" + fi + + # Fetch the shared secret again + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') + if [[ "${!ALL_SECRETS_GENERATED}" == "false" ]]; then + echo "Error: Secrets not generated" + exit 1 + fi + + # sending the signal call + cfn-signal -e $? --stack ${AWS::StackId} --resource "$SIGNAL_NAME" --region ${AWS::Region} + + # Wait for all master nodes to store their private IPs + while true; do + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none') + + MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') + MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') + MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') + MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') + + # Check if all master nodes have stored their private IPs + if [[ "$MASTER_NODE_1_PRIVATE_IP" != "none" ]] && + [[ "$MASTER_NODE_2_PRIVATE_IP" != "none" ]] && + [[ "$MASTER_NODE_3_PRIVATE_IP" != "none" ]] && + [[ "$MASTER_NODE_4_PRIVATE_IP" != "none" ]]; then + break + fi + sleep 5 + done + + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') + MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') + MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') + MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') + 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=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME') + LIVEKIT_TURN_DOMAIN_NAME=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_TURN_DOMAIN_NAME') + OPENVIDU_PRO_LICENSE=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_PRO_LICENSE') + OPENVIDU_RTC_ENGINE=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_RTC_ENGINE') + REDIS_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.REDIS_PASSWORD') + MONGO_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.MONGO_ADMIN_USERNAME') + MONGO_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.MONGO_ADMIN_PASSWORD') + MONGO_REPLICA_SET_KEY=$(echo "$SHARED_SECRET" | jq -r '.MONGO_REPLICA_SET_KEY') + MINIO_ACCESS_KEY=$(echo "$SHARED_SECRET" | jq -r '.MINIO_ACCESS_KEY') + MINIO_SECRET_KEY=$(echo "$SHARED_SECRET" | jq -r '.MINIO_SECRET_KEY') + DASHBOARD_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_USERNAME') + DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD') + GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME') + GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD') + DEFAULT_APP_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_USERNAME') + DEFAULT_APP_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_PASSWORD') + DEFAULT_APP_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_USERNAME') + DEFAULT_APP_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_PASSWORD') + LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY') + LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET') + ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES') + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_master_node.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--deployment-type='ha'" + "--node-role='master-node'" + "--external-load-balancer" + "--master-node-private-ip-list='$MASTER_NODE_PRIVATE_IP_LIST'" + "--openvidu-pro-license='$OPENVIDU_PRO_LICENSE'" + "--domain-name='$DOMAIN'" + "--enabled-modules='$ENABLED_MODULES'" + "--rtc-engine=$OPENVIDU_RTC_ENGINE" + "--redis-password=$REDIS_PASSWORD" + "--mongo-admin-user=$MONGO_ADMIN_USERNAME" + "--mongo-admin-password=$MONGO_ADMIN_PASSWORD" + "--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY" + "--minio-access-key=$MINIO_ACCESS_KEY" + "--minio-secret-key=$MINIO_SECRET_KEY" + "--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME" + "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" + "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" + "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" + ) + + if [[ "${!LIVEKIT_TURN_DOMAIN_NAME}" != "none" ]]; then + COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'") + fi + + # Construct the final command + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + + mode: '000755' + owner: root + group: root + '/usr/local/bin/config_s3.sh': + content: !Sub + - | + #!/bin/bash + set -e + + # Install dir and config dir + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + + # Config S3 bucket + EXTERNAL_S3_ENDPOINT="https://s3.${AWS::Region}.amazonaws.com" + EXTERNAL_S3_REGION="${AWS::Region}" + EXTERNAL_S3_PATH_STYLE_ACCESS="false" + EXTERNAL_S3_BUCKET_APP_DATA=${S3AppDataBucketResourceName} + EXTERNAL_S3_BUCKET_CLUSTER_DATA=${S3ClusterDataBucketResourceName} + sed -i "s|EXTERNAL_S3_ENDPOINT=.*|EXTERNAL_S3_ENDPOINT=$EXTERNAL_S3_ENDPOINT|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_REGION=.*|EXTERNAL_S3_REGION=$EXTERNAL_S3_REGION|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_PATH_STYLE_ACCESS=.*|EXTERNAL_S3_PATH_STYLE_ACCESS=$EXTERNAL_S3_PATH_STYLE_ACCESS|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_BUCKET_APP_DATA=.*|EXTERNAL_S3_BUCKET_APP_DATA=$EXTERNAL_S3_BUCKET_APP_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s|EXTERNAL_S3_BUCKET_CLUSTER_DATA=.*|EXTERNAL_S3_BUCKET_CLUSTER_DATA=$EXTERNAL_S3_BUCKET_CLUSTER_DATA|" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + - S3AppDataBucketResourceName: !If + - CreateRecordingsBucket + - !Ref S3AppDataBucketResource + - !Ref S3AppDataBucketName + S3ClusterDataBucketResourceName: !If + - CreateClusterDataBucket + - !Ref S3ClusterDataBucketResource + - !Ref S3ClusterDataBucketName + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/after_install.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Save access URLs + DOMAIN=$(echo "$SHARED_SECRET" | jq -r '.DOMAIN_NAME') + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_config_from_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" + + # Replace DOMAIN_NAME + export DOMAIN=$(echo $SHARED_SECRET | jq -r .DOMAIN_NAME) + if [[ -n "$DOMAIN" ]]; then + sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + else + exit 1 + fi + + # Replace LIVEKIT_TURN_DOMAIN_NAME + export LIVEKIT_TURN_DOMAIN_NAME=$(echo $SHARED_SECRET | jq -r .LIVEKIT_TURN_DOMAIN_NAME) + if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + fi + + # Replace rest of the values + sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(echo $SHARED_SECRET | jq -r .REDIS_PASSWORD)/" "${!MASTER_NODE_CONFIG_DIR}/master_node.env" + sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_RTC_ENGINE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$(echo $SHARED_SECRET | jq -r .OPENVIDU_PRO_LICENSE)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .MONGO_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .DASHBOARD_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_ACCESS_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$(echo $SHARED_SECRET | jq -r .MINIO_SECRET_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env" + sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" + + # Update URLs in secret + DASHBOARD_URL="https://${!DOMAIN}/dashboard/" + GRAFANA_URL="https://${!DOMAIN}/grafana/" + MINIO_URL="https://${!DOMAIN}/minio-console/" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$DOMAIN"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_URL": "'"$DASHBOARD_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_URL": "'"$GRAFANA_URL"'" }')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_URL": "'"$MINIO_URL"'" }')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/update_secret_from_config.sh': + content: !Sub | + #!/bin/bash + set -e + # Get current shared secret + SHARED_SECRET=$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text) + + # Installation directory + INSTALL_DIR="/opt/openvidu" + CLUSTER_CONFIG_DIR="${!INSTALL_DIR}/config/cluster" + MASTER_NODE_CONFIG_DIR="${!INSTALL_DIR}/config/node" + + + # Update shared secret + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"REDIS_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${!MASTER_NODE_CONFIG_DIR}/master_node.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_TURN_DOMAIN_NAME": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_RTC_ENGINE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"OPENVIDU_PRO_LICENSE": "'"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MONGO_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_ACCESS_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MINIO_SECRET_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DASHBOARD_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')" + + # Update shared secret + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --secret-string "$SHARED_SECRET" + + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/get_value_from_config.sh': + content: | + #!/bin/bash + set -e + + # Function to get the value of a given key from the environment file + get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi + } + + # Check if the correct number of arguments are supplied + if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 + fi + + # Get the key and file path from the arguments + key="$1" + file_path="$2" + + # Get and print the value + get_value "$key" "$file_path" + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/store_secret.sh': + content: !Sub | + #!/bin/bash + set -e + # Modes: save, generate + # save mode: save the secret in the secret manager + # generate mode: generate a random password and save it in the secret manager + MODE="$1" + SHARED_SECRET="$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --query SecretString --output text)" + if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${!3:-}" + LENGTH="${!4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${!LENGTH})" + RANDOM_PASSWORD="${!PREFIX}${!RANDOM_PASSWORD}" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$RANDOM_PASSWORD"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$RANDOM_PASSWORD" + elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"'"$SECRET_KEY_NAME"'": "'"$SECRET_VALUE"'"}')" + aws secretsmanager update-secret \ + --region ${AWS::Region} \ + --secret-id ${OpenViduSharedInfo} \ + --secret-string "$SHARED_SECRET" > /dev/null 2>&1 + echo "$SECRET_VALUE" + else + exit 1 + fi + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/restart.sh': + content: | + #!/bin/bash + set -e + # Stop all services + systemctl stop openvidu + + # Update config from secret + /usr/local/bin/update_config_from_secret.sh + + # Start all services + systemctl start openvidu + mode: "000755" + owner: "root" + group: "root" + Properties: + LaunchTemplateName: !Sub 'openvidu-ha-master-${AWS::Region}-${AWS::StackName}' + LaunchTemplateData: + # Enable IMDSv2 + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + IamInstanceProfile: + Name: !Ref OpenViduMasterInstanceProfile + ImageId: !Ref AmiId + InstanceType: !Ref MasterNodeInstanceType + KeyName: !Ref KeyName + SecurityGroupIds: + - !Ref OpenViduMasterNodeSG + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: !Ref MasterNodesDiskSize + VolumeType: gp3 + DeleteOnTermination: true + + OpenViduMasterNode1: + Type: AWS::EC2::Instance + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 1 + SubnetId: !GetAtt SubnetProcessor.Subnet1 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition1" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + + MasterNodesWaitCondition1: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode2: + Type: AWS::EC2::Instance + DependsOn: MasterNodesWaitCondition1 + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 2 + SubnetId: !GetAtt SubnetProcessor.Subnet2 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition2" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + + MasterNodesWaitCondition2: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode3: + Type: AWS::EC2::Instance + DependsOn: MasterNodesWaitCondition2 + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 3 + SubnetId: !GetAtt SubnetProcessor.Subnet3 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition3" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + systemctl enable crond.service + systemctl start crond.service + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + + MasterNodesWaitCondition3: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMasterNode4: + Type: AWS::EC2::Instance + DependsOn: MasterNodesWaitCondition3 + Properties: + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMasterLaunchTemplate + Version: !GetAtt OpenViduMasterLaunchTemplate.LatestVersionNumber + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Node 4 + SubnetId: !GetAtt SubnetProcessor.Subnet4 + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource OpenViduMasterLaunchTemplate + + # Install OpenVidu + /usr/local/bin/install.sh "MasterNodesWaitCondition4" || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + + # Config S3 bucket + /usr/local/bin/config_s3.sh || { echo "[OpenVidu] error configuring S3 bucket"; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } + + # Update shared secret + /usr/local/bin/after_install.sh || { echo "[OpenVidu] error updating shared secret"; exit 1; } + + # Launch on reboot + systemctl enable crond.service + systemctl start crond.service + echo "@reboot /usr/local/bin/restart.sh &> /var/log/openvidu-restart.log" | crontab + + MasterNodesWaitCondition4: + Type: 'AWS::CloudFormation::WaitCondition' + CreationPolicy: + ResourceSignal: + Timeout: PT10M + Count: '1' + + OpenViduMediaNodeLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Metadata: + Comment: Launch template for OpenVidu Media Node + AWS::CloudFormation::Init: + config: + files: + '/usr/local/bin/install.sh': + content: !Sub | + #!/bin/bash + set -e + YQ_VERSION=v4.44.5 + + # Install dependencies + apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + wget https://github.com/mikefarah/yq/releases/download/${!YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\ + tar xz && mv yq_linux_amd64 /usr/bin/yq + + # Install aws-cli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -qq awscliv2.zip + ./aws/install + rm -rf awscliv2.zip aws + + # Token for IMDSv2 + TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" + + # Get own private IP + PRIVATE_IP="$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/local-ipv4)" + + SHARED_SECRET="$(aws secretsmanager get-secret-value \ + --region ${AWS::Region} \ + --secret-id openvidu-ha-${AWS::Region}-${AWS::StackName} \ + --query SecretString --output text || echo 'none')" + if [[ "$SHARED_SECRET" == "none" ]]; then + echo "Error: Shared secret not found" + exit 1 + fi + + # Get OpenVidu Media Nodes version to deploy + OPENVIDU_VERSION=$(echo "$SHARED_SECRET" | jq -r '.OPENVIDU_VERSION') + if [[ "$OPENVIDU_VERSION" == "none" ]]; then + echo "OpenVidu version not found" + exit 1 + fi + + ALL_SECRETS_GENERATED=$(echo "$SHARED_SECRET" | jq -r '.ALL_SECRETS_GENERATED') + if [[ "$ALL_SECRETS_GENERATED" == "none" ]]; then + echo "Error: Secrets not generated" + exit 1 + fi + MASTER_NODE_1_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_1_PRIVATE_IP') + MASTER_NODE_2_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_2_PRIVATE_IP') + MASTER_NODE_3_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_3_PRIVATE_IP') + MASTER_NODE_4_PRIVATE_IP=$(echo "$SHARED_SECRET" | jq -r '.MASTER_NODE_4_PRIVATE_IP') + MASTER_NODE_PRIVATE_IP_LIST="$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP" + REDIS_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.REDIS_PASSWORD') + + # Base command + INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh)" + + # Common arguments + COMMON_ARGS=( + "--no-tty" + "--install" + "--environment=aws" + "--deployment-type='ha'" + "--node-role='media-node'" + "--master-node-private-ip-list=$MASTER_NODE_PRIVATE_IP_LIST" + "--private-ip=$PRIVATE_IP" + "--redis-password=$REDIS_PASSWORD" + ) + + # Construct the final command with all arguments + FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${!COMMON_ARGS[@]}")" + + # Install OpenVidu + exec bash -c "$FINAL_COMMAND" + mode: '000755' + owner: root + group: root + '/usr/local/bin/set_as_unhealthy.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own instance ID + INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + + # Set instance as unhealthy + aws autoscaling set-instance-health \ + --region ${AWS::Region} \ + --instance-id "$INSTANCE_ID" \ + --health-status Unhealthy + mode: "000755" + owner: "root" + group: "root" + '/usr/local/bin/stop_media_node.sh': + content: !Sub | + #!/bin/bash + set -e + # Token for IMDSv2 + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + + # Get own instance ID + INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + ASG_NAME=openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} + + # Execute if docker is installed + if [ -x "$(command -v docker)" ]; then + echo "Stopping media node services and waiting for termination..." + docker container kill --signal=SIGQUIT openvidu || true + docker container kill --signal=SIGQUIT ingress || true + docker container kill --signal=SIGQUIT egress || true + TIME_PASSED=0 + HEARTBEAT_MAX=1800 + # Wait for running containers to not be openvidu, ingress or egress + while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do + echo "Waiting for containers to stop..." + sleep 5 + TIME_PASSED=$((TIME_PASSED+5)) + if [ $TIME_PASSED -ge $HEARTBEAT_MAX ]; then + echo "Increase lifecycle hook timeout to continue waiting for termination" + # Increase lifecycle hook timeout + aws autoscaling record-lifecycle-action-heartbeat \ + --region ${AWS::Region} \ + --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ + --auto-scaling-group-name "$ASG_NAME" \ + --instance-id "$INSTANCE_ID" + TIME_PASSED=0 + fi + done + fi + + aws autoscaling complete-lifecycle-action \ + --region ${AWS::Region} \ + --lifecycle-hook-name StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} \ + --auto-scaling-group-name "$ASG_NAME" \ + --lifecycle-action-result CONTINUE \ + --instance-id "$INSTANCE_ID" + mode: "000755" + owner: "root" + group: "root" + Properties: + LaunchTemplateName: !Sub 'openvidu-ha-media-${AWS::Region}-${AWS::StackName}' + LaunchTemplateData: + # Enable IMDSv2 by default + MetadataOptions: + HttpEndpoint: enabled + HttpPutResponseHopLimit: 1 + HttpTokens: required + IamInstanceProfile: + Arn: !GetAtt OpenViduMediaInstanceProfile.Arn + SecurityGroupIds: + - !GetAtt OpenViduMediaNodeSG.GroupId + ImageId: !Ref AmiId + KeyName: !Ref KeyName + InstanceType: !Ref MediaNodeInstanceType + UserData: + Fn::Base64: !Sub | + #!/bin/bash + set -eu -o pipefail + + apt-get update && apt-get install -y \ + python3-pip \ + ec2-instance-connect + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + + cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenViduMediaNodeLaunchTemplate + + export HOME="/root" + + # Install OpenVidu + /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } + + # Start OpenVidu + systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/set_as_unhealthy.sh; exit 1; } + + # Wait for the app + # /usr/local/bin/check_app_ready.sh + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeType: gp3 + DeleteOnTermination: true + VolumeSize: 50 + + OpenViduMediaNodeASG: + DependsOn: + - OpenViduMediaInstanceProfile + - OpenViduMasterInstanceProfile + - StopMediaNodeCloudWatchEventRule + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + AutoScalingGroupName: !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} + LaunchTemplate: + LaunchTemplateId: !Ref OpenViduMediaNodeLaunchTemplate + Version: !GetAtt OpenViduMediaNodeLaunchTemplate.DefaultVersionNumber + TargetGroupARNs: + Fn::If: + - TurnTLSIsEnabled + - - !Ref OpenViduMediaNodeRTMPTG + - !Ref OpenViduMediaNodeTurnTLSTG + - - !Ref OpenViduMediaNodeRTMPTG + MinSize: !Ref MinNumberOfMediaNodes + MaxSize: !Ref MaxNumberOfMediaNodes + DesiredCapacity: !Ref InitialNumberOfMediaNodes + VPCZoneIdentifier: !Ref OpenViduMediaNodeSubnets + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Media Node + PropagateAtLaunch: true + + StopMediaNodeLifecycleHook: + Type: 'AWS::AutoScaling::LifecycleHook' + Properties: + LifecycleHookName: !Sub StopMediaNodeLifecycleHook-${AWS::Region}-${AWS::StackName} + AutoScalingGroupName: !Ref OpenViduMediaNodeASG + LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' + DefaultResult: 'CONTINUE' + HeartbeatTimeout: 3600 + + StopMediaNodeDocumentRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - ssm.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ssm:DescribeInstanceInformation + - ssm:ListCommands + - ssm:ListCommandInvocations + Resource: "*" + - Effect: Allow + Action: + - ssm:SendCommand + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunShellScript + - Action: + - ssm:SendCommand + Resource: !Sub arn:${AWS::Partition}:ec2:*:*:instance/* + Condition: + StringEquals: + 'aws:ResourceTag/aws:cloudformation:stack-name': !Ref 'AWS::StackName' + Effect: Allow + PolicyName: SSM-Automation-Policy + + StopMediaNodeAutomationDocument: + Type: AWS::SSM::Document + Properties: + Name: + Fn::Join: + # Generate a not too long and unique document name + # Getting a unique identifier from the stack id + - '' + - - 'StopMediaNodeAutomationDocument-' + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + DocumentType: Automation + Content: + schemaVersion: '0.3' + assumeRole: "{{automationAssumeRole}}" + description: This document stops the OpenVidu services in a Media Node and + terminates the instance. It stop the OpenVidu services without interrupting + the running sessions, ingress and egress running in the Media Node. + Also it sends a CONTINUE signal to the Auto Scaling Group to continue the + instance termination when the services are stopped. + parameters: + InstanceId: + type: String + automationAssumeRole: + type: String + default: !GetAtt StopMediaNodeDocumentRole.Arn + description: "(Required) The ARN of the role that allows Automation to + perform the actions." + mainSteps: + - name: RunCommand + action: aws:runCommand + inputs: + DocumentName: AWS-RunShellScript + InstanceIds: + - "{{ InstanceId }}" + Parameters: + # 24 hours as a timeout to wait for the instance to stop all services + executionTimeout: "60" + commands: + - nohup /usr/local/bin/stop_media_node.sh > /var/log/stop_media_node.log 2>&1 & + + + StopMediaNodeCloudWatchEventRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - events.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - ssm:StartAutomationExecution + Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT + PolicyName: !Sub StopMediaNodeCloudWatchEventPolicy-${AWS::Region}-${AWS::StackName} + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - iam:PassRole + Resource: !GetAtt StopMediaNodeDocumentRole.Arn + PolicyName: Pass-Role-SSM-Automation-Policy + + StopMediaNodeCloudWatchEventRule: + Type: AWS::Events::Rule + Properties: + Description: Rule to trigger the StopMediaNodeAutomationDocument when an instance is terminated + EventPattern: + source: + - aws.autoscaling + detail-type: + - EC2 Instance-terminate Lifecycle Action + detail: + AutoScalingGroupName: + - !Sub openvidu-ha-media-asg-${AWS::Region}-${AWS::StackName} + Name: + Fn::Join: + # Generate a not too long and unique rule name + # Getting a unique identifier from the stack id + - '' + - - StopMediaNodeCloudWatchEventRule- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Targets: + - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${StopMediaNodeAutomationDocument}:$DEFAULT + RoleArn: !GetAtt StopMediaNodeCloudWatchEventRole.Arn + Id: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - StopMediaNodeCloudWatchEventRule- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + InputTransformer: + InputPathsMap: + instanceid: "$.detail.EC2InstanceId" + InputTemplate: | + { + "InstanceId": [ ] + } + + OpenViduMediaNodeASGScalingPolicy: + Type: AWS::AutoScaling::ScalingPolicy + Properties: + AutoScalingGroupName: !Ref OpenViduMediaNodeASG + PolicyType: TargetTrackingScaling + EstimatedInstanceWarmup: 120 + TargetTrackingConfiguration: + PredefinedMetricSpecification: + PredefinedMetricType: ASGAverageCPUUtilization + TargetValue: !Ref ScaleTargetCPU + + OpenViduMasterNodeSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OpenVidu Master Node + GroupName: !Sub openvidu-ha-master-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + + OpenViduLoadBalancerToMasterIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7880 + ToPort: 7880 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduMasterToMasterRedisIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7000 + ToPort: 7001 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterRedisIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7000 + ToPort: 7001 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterMinioIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9100 + ToPort: 9100 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterMinioConsoleSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9101 + ToPort: 9101 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterMinioIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9100 + ToPort: 9100 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterMongoIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 20000 + ToPort: 20000 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterMongoIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 20000 + ToPort: 20000 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterMimirGrpcIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9095 + ToPort: 9095 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterMimirGossipIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7946 + ToPort: 7946 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterHTTPMimirSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9009 + ToPort: 9009 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterLokiGrpcIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 9096 + ToPort: 9096 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterLokiGossipIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 7947 + ToPort: 7947 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterDashboardsIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 5000 + ToPort: 5000 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMasterGrafanaIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 3000 + ToPort: 3000 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterHTTPLokiSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 3100 + ToPort: 3100 + SourceSecurityGroupId: !Ref OpenViduMediaNodeSG + + OpenViduMasterToMasterV2CompatibilityIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 4443 + ToPort: 4443 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterV2CompatibilityWebhookIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 4443 + ToPort: 4443 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMasterToMasterDefaultAppIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMasterNodeSG + IpProtocol: tcp + FromPort: 6080 + ToPort: 6080 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMediaNodeToMasterDefaultAppWebhookIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt OpenViduMasterNodeSG.GroupId + IpProtocol: tcp + FromPort: 6080 + ToPort: 6080 + SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId + + OpenViduMediaNodeSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OpenVidu Media Node + GroupName: !Sub openvidu-ha-media-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 7885 + ToPort: 7885 + CidrIpv6: ::/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: 50000 + ToPort: 60000 + CidrIpv6: ::/0 + + OpenViduLoadBalancerToMediaNodeRTMPIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 1945 + ToPort: 1945 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduLoadBalancerToMediaNodeIngressHealthCheckSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 9092 + ToPort: 9092 + SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG + + OpenViduLoadBalancerTurnTLSToMediaNodeIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Condition: TurnTLSIsEnabled + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 5349 + ToPort: 5349 + SourceSecurityGroupId: !Ref OpenViduTurnTLSLoadBalancerSG + + OpenViduLoadBalancerTurnTLSToMediaNodeHealthCheckSG: + Type: AWS::EC2::SecurityGroupIngress + Condition: TurnTLSIsEnabled + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 7880 + ToPort: 7880 + SourceSecurityGroupId: !Ref OpenViduTurnTLSLoadBalancerSG + + + OpenViduMasterToMediaNodeServerIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 7880 + ToPort: 7880 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduMasterToMediaNodeClientIngressSG: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref OpenViduMediaNodeSG + IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !Ref OpenViduMasterNodeSG + + OpenViduLoadBalancerSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for the Load Balancer + GroupName: !Sub openvidu-ha-lb-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 1935 + ToPort: 1935 + CidrIpv6: ::/0 + + OpenViduTurnTLSLoadBalancerSG: + Type: AWS::EC2::SecurityGroup + Condition: TurnTLSIsEnabled + Properties: + GroupDescription: Security group for the Load Balancer for TURN with TLS + GroupName: !Sub openvidu-ha-turn-tls-lb-sg-${AWS::Region}-${AWS::StackName} + VpcId: !Ref OpenViduVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIpv6: ::/0 + + LoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + DependsOn: + - MasterNodesWaitCondition4 + Properties: + Name: + Fn::Join: + # Generate a not too long and unique load balancer name + # Getting a unique identifier from the stack id + - '' + - - OpenViduHA- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Subnets: !Ref OpenViduMasterNodeSubnets + SecurityGroups: + - !Ref OpenViduLoadBalancerSG + Type: network + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Load Balancer + + TurnTLSLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Condition: TurnTLSIsEnabled + Properties: + Name: + Fn::Join: + # Generate a not too long and unique load balancer name + # Getting a unique identifier from the stack id + - '' + - - OpenViduHA-TurnTLS- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + Subnets: !Ref OpenViduMediaNodeSubnets + SecurityGroups: + - !Ref OpenViduTurnTLSLoadBalancerSG + Type: network + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN with TLS Load Balancer + + OpenViduMasterNodeListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMasterNodeTG + LoadBalancerArn: !Ref LoadBalancer + Port: 443 + Protocol: TLS + Certificates: + - CertificateArn: !Ref OpenViduCertificateARN + + OpenViduRTMPMediaNodeListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMediaNodeRTMPTG + LoadBalancerArn: !Ref LoadBalancer + Port: 1935 + Protocol: TLS + Certificates: + - CertificateArn: !Ref OpenViduCertificateARN + + OpenViduTurnTLSMediaNodeListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Condition: TurnTLSIsEnabled + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref OpenViduMediaNodeTurnTLSTG + LoadBalancerArn: !Ref TurnTLSLoadBalancer + Port: 443 + Protocol: TLS + Certificates: + - CertificateArn: !Ref TurnCertificateARN + + OpenViduMasterNodeTG: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OpenVidu- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + TargetType: instance + Targets: + - Id: !Ref OpenViduMasterNode1 + - Id: !Ref OpenViduMasterNode2 + - Id: !Ref OpenViduMasterNode3 + - Id: !Ref OpenViduMasterNode4 + VpcId: !Ref OpenViduVPC + Port: 7880 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: /health/caddy + HealthCheckProtocol: HTTP + HealthCheckPort: '7880' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group + + OpenViduMediaNodeRTMPTG: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OVRTMP- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + VpcId: !Ref OpenViduVPC + Port: 1945 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: / + HealthCheckProtocol: HTTP + # Ingress health check port + HealthCheckPort: '9092' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - RTMP Target Group + + OpenViduMediaNodeTurnTLSTG: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: TurnTLSIsEnabled + Properties: + Name: + Fn::Join: + # Generate a not too long and unique target id + # Getting a unique identifier from the stack id + - '' + - - OVTurnTLS- + - !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] + VpcId: !Ref OpenViduVPC + Port: 5349 + Protocol: TCP + Matcher: + HttpCode: '200' + HealthCheckIntervalSeconds: 10 + HealthCheckPath: / + HealthCheckProtocol: HTTP + HealthCheckPort: '7880' + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + UnhealthyThresholdCount: 4 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN TLS Target Group + +Outputs: + ServicesAndCredentials: + Description: Services and credentials + Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/home?region=${AWS::Region}#!/secret?name=openvidu-ha-${AWS::Region}-${AWS::StackName} diff --git a/openvidu-deployment/pro/ha/azure/cf-openvidu-ha.bicep b/openvidu-deployment/pro/ha/azure/cf-openvidu-ha.bicep new file mode 100644 index 00000000..fa66cf2d --- /dev/null +++ b/openvidu-deployment/pro/ha/azure/cf-openvidu-ha.bicep @@ -0,0 +1,2945 @@ +@description('Stack name') +param stackName string + +@description(''' +[selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate. +[owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter) +and an Elastic IP, you can use this option to use your own certificate. +[letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter) +and an Elastic IP, you can use this option to generate a Let's Encrypt certificate. +''') +@allowed([ + 'selfsigned' + 'owncert' + 'letsencrypt' +]) +param certificateType string = 'selfsigned' + +@description('Domain name for the OpenVidu Deployment. Blank will generate default domain') +param domainName string + +@description('If certificate type is \'owncert\', this parameter will be used to specify the public certificate') +param ownPublicCertificate string = '' + +@description('If certificate type is \'owncert\', this parameter will be used to specify the private certificate') +param ownPrivateCertificate string = '' + +@description('If certificate type is \'letsencrypt\', this email will be used for Let\'s Encrypt notifications') +param letsEncryptEmail string = '' + +@description('Name of the PublicIPAddress resource in Azure when using certificateType \'owncert\' or \'letsencrypt\'') +param publicIpAddressResourceName string = '' + +@description('(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls') +param turnDomainName string = '' + +@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') +param turnOwnPublicCertificate string = '' + +@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') +param turnOwnPrivateCertificate string = '' + +@description('Name of the PublicIPAddress resource in Azure when using TURN server with TLS') +param turnPublicIpAddressResourceName string = '' + +@description('Visit https://openvidu.io/account') +@secure() +param openviduLicense string + +@description('RTCEngine media engine to use') +@allowed([ + 'pion' + 'mediasoup' +]) +param rtcEngine string = 'pion' + +@description('Specifies the EC2 instance type for your OpenVidu Master Node') +@allowed([ + 'Standard_B1s' + 'Standard_B1ms' + 'Standard_B2s' + 'Standard_B2ms' + 'Standard_B4ms' + 'Standard_B8ms' + 'Standard_D2_v3' + 'Standard_D4_v3' + 'Standard_D8_v3' + 'Standard_D16_v3' + 'Standard_D32_v3' + 'Standard_D48_v3' + 'Standard_D64_v3' + 'Standard_D2_v4' + 'Standard_D4_v4' + 'Standard_D8_v4' + 'Standard_D16_v4' + 'Standard_D32_v4' + 'Standard_D48_v4' + 'Standard_D64_v4' + 'Standard_D96_v4' + 'Standard_D2_v5' + 'Standard_D4_v5' + 'Standard_D8_v5' + 'Standard_D16_v5' + 'Standard_D32_v5' + 'Standard_D48_v5' + 'Standard_D64_v5' + 'Standard_D96_v5' + 'Standard_F2' + 'Standard_F4' + 'Standard_F8' + 'Standard_F16' + 'Standard_F32' + 'Standard_F64' + 'Standard_F72' + 'Standard_F2s_v2' + 'Standard_F4s_v2' + 'Standard_F8s_v2' + 'Standard_F16s_v2' + 'Standard_F32s_v2' + 'Standard_F64s_v2' + 'Standard_F72s_v2' + 'Standard_E2_v3' + 'Standard_E4_v3' + 'Standard_E8_v3' + 'Standard_E16_v3' + 'Standard_E32_v3' + 'Standard_E48_v3' + 'Standard_E64_v3' + 'Standard_E96_v3' + 'Standard_E2_v4' + 'Standard_E4_v4' + 'Standard_E8_v4' + 'Standard_E16_v4' + 'Standard_E32_v4' + 'Standard_E48_v4' + 'Standard_E64_v4' + 'Standard_E2_v5' + 'Standard_E4_v5' + 'Standard_E8_v5' + 'Standard_E16_v5' + 'Standard_E32_v5' + 'Standard_E48_v5' + 'Standard_E64_v5' + 'Standard_E96_v5' + 'Standard_M64' + 'Standard_M128' + 'Standard_M208ms_v2' + 'Standard_M416ms_v2' + 'Standard_L4s_v2' + 'Standard_L8s_v2' + 'Standard_L16s_v2' + 'Standard_L32s_v2' + 'Standard_L64s_v2' + 'Standard_L80s_v2' + 'Standard_NC6' + 'Standard_NC12' + 'Standard_NC24' + 'Standard_NC24r' + 'Standard_ND6s' + 'Standard_ND12s' + 'Standard_ND24s' + 'Standard_ND24rs' + 'Standard_NV6' + 'Standard_NV12' + 'Standard_NV24' + 'Standard_H8' + 'Standard_H16' + 'Standard_H16r' + 'Standard_H16mr' + 'Standard_HB120rs_v2' + 'Standard_HC44rs' + 'Standard_DC2s' + 'Standard_DC4s' + 'Standard_DC2s_v2' + 'Standard_DC4s_v2' + 'Standard_DC8s_v2' + 'Standard_DC16s_v2' + 'Standard_DC32s_v2' + 'Standard_A1_v2' + 'Standard_A2_v2' + 'Standard_A4_v2' + 'Standard_A8_v2' + 'Standard_A2m_v2' + 'Standard_A4m_v2' + 'Standard_A8m_v2' +]) +param masterNodeInstanceType string = 'Standard_B2s' + +@description('Size of the disk in GB') +param masterNodesDiskSize int = 100 + +@description('Specifies the EC2 instance type for your OpenVidu Media Nodes') +@allowed([ + 'Standard_B1s' + 'Standard_B1ms' + 'Standard_B2s' + 'Standard_B2ms' + 'Standard_B4ms' + 'Standard_B8ms' + 'Standard_D2_v3' + 'Standard_D4_v3' + 'Standard_D8_v3' + 'Standard_D16_v3' + 'Standard_D32_v3' + 'Standard_D48_v3' + 'Standard_D64_v3' + 'Standard_D2_v4' + 'Standard_D4_v4' + 'Standard_D8_v4' + 'Standard_D16_v4' + 'Standard_D32_v4' + 'Standard_D48_v4' + 'Standard_D64_v4' + 'Standard_D96_v4' + 'Standard_D2_v5' + 'Standard_D4_v5' + 'Standard_D8_v5' + 'Standard_D16_v5' + 'Standard_D32_v5' + 'Standard_D48_v5' + 'Standard_D64_v5' + 'Standard_D96_v5' + 'Standard_F2' + 'Standard_F4' + 'Standard_F8' + 'Standard_F16' + 'Standard_F32' + 'Standard_F64' + 'Standard_F72' + 'Standard_F2s_v2' + 'Standard_F4s_v2' + 'Standard_F8s_v2' + 'Standard_F16s_v2' + 'Standard_F32s_v2' + 'Standard_F64s_v2' + 'Standard_F72s_v2' + 'Standard_E2_v3' + 'Standard_E4_v3' + 'Standard_E8_v3' + 'Standard_E16_v3' + 'Standard_E32_v3' + 'Standard_E48_v3' + 'Standard_E64_v3' + 'Standard_E96_v3' + 'Standard_E2_v4' + 'Standard_E4_v4' + 'Standard_E8_v4' + 'Standard_E16_v4' + 'Standard_E32_v4' + 'Standard_E48_v4' + 'Standard_E64_v4' + 'Standard_E2_v5' + 'Standard_E4_v5' + 'Standard_E8_v5' + 'Standard_E16_v5' + 'Standard_E32_v5' + 'Standard_E48_v5' + 'Standard_E64_v5' + 'Standard_E96_v5' + 'Standard_M64' + 'Standard_M128' + 'Standard_M208ms_v2' + 'Standard_M416ms_v2' + 'Standard_L4s_v2' + 'Standard_L8s_v2' + 'Standard_L16s_v2' + 'Standard_L32s_v2' + 'Standard_L64s_v2' + 'Standard_L80s_v2' + 'Standard_NC6' + 'Standard_NC12' + 'Standard_NC24' + 'Standard_NC24r' + 'Standard_ND6s' + 'Standard_ND12s' + 'Standard_ND24s' + 'Standard_ND24rs' + 'Standard_NV6' + 'Standard_NV12' + 'Standard_NV24' + 'Standard_H8' + 'Standard_H16' + 'Standard_H16r' + 'Standard_H16mr' + 'Standard_HB120rs_v2' + 'Standard_HC44rs' + 'Standard_DC2s' + 'Standard_DC4s' + 'Standard_DC2s_v2' + 'Standard_DC4s_v2' + 'Standard_DC8s_v2' + 'Standard_DC16s_v2' + 'Standard_DC32s_v2' + 'Standard_A1_v2' + 'Standard_A2_v2' + 'Standard_A4_v2' + 'Standard_A8_v2' + 'Standard_A2m_v2' + 'Standard_A4m_v2' + 'Standard_A8m_v2' +]) +param mediaNodeInstanceType string = 'Standard_B2s' + +@description('Username for the Virtual Machine.') +param adminUsername string + +@description('SSH Key for the Virtual Machine.') +@secure() +param adminSshKey string + +@description('Number of initial media nodes to deploy') +param initialNumberOfMediaNodes int = 1 + +@description('Minimum number of media nodes to deploy') +param minNumberOfMediaNodes int = 1 + +@description('Maximum number of media nodes to deploy') +param maxNumberOfMediaNodes int = 5 + +@description('Target CPU percentage to scale up or down') +param scaleTargetCPU int = 50 + +/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ + +var masterNodeVMSettings = { + osDiskType: 'StandardSSD_LRS' + osDiskSize: masterNodesDiskSize + ubuntuOSVersion: { + publisher: 'Canonical' + offer: '0001-com-ubuntu-server-jammy' + sku: '22_04-lts-gen2' + version: 'latest' + } + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${adminUsername}/.ssh/authorized_keys' + keyData: adminSshKey + } + ] + } + } +} + +var mediaNodeVMSettings = { + vmName: '${stackName}-VN-MediaNode' + osDiskType: 'StandardSSD_LRS' + ubuntuOSVersion: { + publisher: 'Canonical' + offer: '0001-com-ubuntu-server-jammy' + sku: '22_04-lts-gen2' + version: 'latest' + } + linuxConfiguration: { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${adminUsername}/.ssh/authorized_keys' + keyData: adminSshKey + } + ] + } + } +} + +var turnTLSIsEnabled = turnDomainName != '' + +var fqdn = domainName + +var keyVaultName = '${stackName}-keyvault' + +var location = resourceGroup().location + +var tenantId = subscription().tenantId + +var deploymentUser = az.deployer().objectId + +/*------------------------------------------- KEY VAULT -------------------------------------------*/ + +resource openviduSharedInfo 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: keyVaultName + location: location + properties: { + enabledForDeployment: true + enabledForDiskEncryption: false + enabledForTemplateDeployment: true + tenantId: tenantId + enableSoftDelete: false + accessPolicies: [ + { + objectId: openviduMasterNode1.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get', 'set', 'list'] + } + } + { + objectId: openviduMasterNode2.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get', 'set', 'list'] + } + } + { + objectId: openviduMasterNode3.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get', 'set', 'list'] + } + } + { + objectId: openviduMasterNode4.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get', 'set', 'list'] + } + } + { + objectId: openviduScaleSetMediaNode.identity.principalId + tenantId: tenantId + permissions: { + secrets: ['get'] + } + } + { + objectId: deploymentUser + tenantId: tenantId + permissions: { + secrets: ['get', 'list', 'set', 'delete', 'recover', 'backup', 'restore'] + } + } + ] + sku: { + name: 'standard' + family: 'A' + } + networkAcls: { + defaultAction: 'Allow' + bypass: 'AzureServices' + } + } +} + +/*------------------------------------------- MASTER NODE -------------------------------------------*/ + +var stringInterpolationParamsMaster1 = { + domainName: domainName + turnDomainName: turnDomainName + certificateType: certificateType + letsEncryptEmail: letsEncryptEmail + ownPublicCertificate: ownPublicCertificate + ownPrivateCertificate: ownPrivateCertificate + turnOwnPublicCertificate: turnOwnPublicCertificate + turnOwnPrivateCertificate: turnOwnPrivateCertificate + fqdn: fqdn + openviduLicense: openviduLicense + rtcEngine: rtcEngine + keyVaultName: keyVaultName + masterNodeNum: '1' +} + +var stringInterpolationParamsMaster2 = { + domainName: domainName + turnDomainName: turnDomainName + certificateType: certificateType + letsEncryptEmail: letsEncryptEmail + ownPublicCertificate: ownPublicCertificate + ownPrivateCertificate: ownPrivateCertificate + turnOwnPublicCertificate: turnOwnPublicCertificate + turnOwnPrivateCertificate: turnOwnPrivateCertificate + fqdn: fqdn + openviduLicense: openviduLicense + rtcEngine: rtcEngine + keyVaultName: keyVaultName + masterNodeNum: '2' +} + +var stringInterpolationParamsMaster3 = { + domainName: domainName + turnDomainName: turnDomainName + certificateType: certificateType + letsEncryptEmail: letsEncryptEmail + ownPublicCertificate: ownPublicCertificate + ownPrivateCertificate: ownPrivateCertificate + turnOwnPublicCertificate: turnOwnPublicCertificate + turnOwnPrivateCertificate: turnOwnPrivateCertificate + fqdn: fqdn + openviduLicense: openviduLicense + rtcEngine: rtcEngine + keyVaultName: keyVaultName + masterNodeNum: '3' +} + +var stringInterpolationParamsMaster4 = { + domainName: domainName + turnDomainName: turnDomainName + certificateType: certificateType + letsEncryptEmail: letsEncryptEmail + ownPublicCertificate: ownPublicCertificate + ownPrivateCertificate: ownPrivateCertificate + turnOwnPublicCertificate: turnOwnPublicCertificate + turnOwnPrivateCertificate: turnOwnPrivateCertificate + fqdn: fqdn + openviduLicense: openviduLicense + rtcEngine: rtcEngine + keyVaultName: keyVaultName + masterNodeNum: '4' +} + +var installScriptTemplateMaster = ''' +#!/bin/bash -x +set -e +OPENVIDU_VERSION=main +DOMAIN= + +# Assume azure cli is installed + +apt-get update && apt-get install -y \ + curl \ + unzip \ + jq \ + wget + +# Configure Domain +if [[ "${domainName}" == '' ]]; then + DOMAIN=${fqdn} +else + DOMAIN=${domainName} +fi + +# Wait for the keyvault availability +MAX_WAIT=100 +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 + DOMAIN="$(/usr/local/bin/store_secret.sh save DOMAIN-NAME "${domainName}")" + if [[ -n "${turnDomainName}" ]]; then + LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}")" + fi + if [[ "${certificateType}" == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT-EMAIL "${letsEncryptEmail}") + 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)" + DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")" + DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" + DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" + DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)" + LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" + LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)" + 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,app")" + ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")" +fi + +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 + sleep 5 +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) +if [[ -n "${turnDomainName}" ]]; then + LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv) +fi +if [[ "${certificateType}" == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv) +fi +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) +DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv) +DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) +DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv) +DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv) +ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv) + + +# Base command +INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_master_node.sh)" + +# 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" + "--default-app-user=$DEFAULT_APP_USERNAME" + "--default-app-password=$DEFAULT_APP_PASSWORD" + "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" + "--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD" + "--livekit-api-key=$LIVEKIT_API_KEY" + "--livekit-api-secret=$LIVEKIT_API_SECRET" +) + +if [[ $LIVEKIT_TURN_DOMAIN_NAME != "" ]]; then + COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME}") +fi + +# Certificate arguments +if [[ "${certificateType}" == "selfsigned" ]]; then + CERT_ARGS=( + "--certificate-type=selfsigned" + ) +elif [[ "${certificateType}" == "letsencrypt" ]]; then + CERT_ARGS=( + "--certificate-type=letsencrypt" + "--letsencrypt-email=$LETSENCRYPT_EMAIL" + ) +else + # Download owncert files + mkdir -p /tmp/owncert + wget -O /tmp/owncert/fullchain.pem ${ownPublicCertificate} + wget -O /tmp/owncert/privkey.pem ${ownPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem) + OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem) + + CERT_ARGS=( + "--certificate-type=owncert" + "--owncert-public-key=$OWN_CERT_CRT" + "--owncert-private-key=$OWN_CERT_KEY" + ) + + # Turn with TLS and own certificate + if [[ "${turnDomainName}" != '' ]]; then + # Download owncert files + mkdir -p /tmp/owncert-turn + wget -O /tmp/owncert-turn/fullchain.pem ${turnOwnPublicCertificate} + wget -O /tmp/owncert-turn/privkey.pem ${turnOwnPrivateCertificate} + + # Convert to base64 + OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem) + OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem) + + CERT_ARGS+=( + "--turn-owncert-private-key=$OWN_CERT_KEY_TURN" + "--turn-owncert-public-key=$OWN_CERT_CRT_TURN" + ) + fi +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 --allow-no-subscriptions > /dev/null + +# Generate URLs +DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv) +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 DASHBOARD-URL --value $DASHBOARD_URL +az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL +az keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL + +az keyvault secret show --vault-name ${keyVaultName} --name MINIO-URL + +if [[ $? -ne 0 ]]; then + echo "Error updating keyvault" +fi +''' +var update_config_from_secretScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Installation directory +INSTALL_DIR="/opt/openvidu" +CLUSTER_CONFIG_DIR="${INSTALL_DIR}/config/cluster" +MASTER_NODE_CONFIG_DIR="${INSTALL_DIR}/config/node" + +# Replace DOMAIN_NAME +export DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv) +if [[ -n "$DOMAIN" ]]; then + sed -i "s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/" "${CLUSTER_CONFIG_DIR}/openvidu.env" +else + exit 1 +fi + +# Replace LIVEKIT_TURN_DOMAIN_NAME +export LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv) +if [[ -n "$LIVEKIT_TURN_DOMAIN_NAME" ]]; then + sed -i "s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/" "${CLUSTER_CONFIG_DIR}/openvidu.env" +fi + +if [[ ${certificateType} == "letsencrypt" ]]; then + export LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv) + sed -i "s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/" "${CLUSTER_CONFIG_DIR}/openvidu.env" +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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv) +export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) +export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv) +export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv) +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/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" +sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env" + +# Update URLs in secret +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 DASHBOARD-URL --value $DASHBOARD_URL +az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL +az keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL +''' + +var update_secret_from_configScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Installation directory +INSTALL_DIR="/opt/openvidu" +CLUSTER_CONFIG_DIR="${INSTALL_DIR}/config/cluster" +MASTER_NODE_CONFIG_DIR="${INSTALL_DIR}/config/node" + +if [[ ${certificateType} == "letsencrypt" ]]; then + LETSENCRYPT_EMAIL="$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL "${CLUSTER_CONFIG_DIR}/openvidu.env")" + az keyvault secret set --vault-name ${keyVaultName} --name "LETSENCRYPT-EMAIL" --value $LETSENCRYPT_EMAIL +fi + +# 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")" +LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_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")" +DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")" +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 LIVEKIT-TURN-DOMAIN-NAME --value $LIVEKIT_TURN_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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME +az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD +az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES +''' + +var get_value_from_configScriptMaster = ''' +#!/bin/bash +set -e + +# Function to get the value of a given key from the environment file +get_value() { + local key="$1" + local file_path="$2" + + # Use grep to find the line with the key, ignoring lines starting with # + # Use awk to split on '=' and print the second field, which is the value + local value=$(grep -E "^\s*$key\s*=" "$file_path" | awk -F= '{print $2}' | sed 's/#.*//; s/^\s*//; s/\s*$//') + + # If the value is empty, return "none" + if [ -z "$value" ]; then + echo "none" + else + echo "$value" + fi +} + +# Check if the correct number of arguments are supplied +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Get the key and file path from the arguments +key="$1" +file_path="$2" + +# Get and print the value +get_value "$key" "$file_path" +''' + +var store_secretScriptTemplateMaster = ''' +#!/bin/bash +set -e + +az login --identity --allow-no-subscriptions > /dev/null + +# Modes: save, generate +# save mode: save the secret in the secret manager +# generate mode: generate a random password and save it in the secret manager +MODE="$1" + +if [[ "$MODE" == "generate" ]]; then + SECRET_KEY_NAME="$2" + PREFIX="${3:-}" + LENGTH="${4:-44}" + RANDOM_PASSWORD="$(openssl rand -base64 64 | tr -d '+/=\n' | cut -c -${LENGTH})" + RANDOM_PASSWORD="${PREFIX}${RANDOM_PASSWORD}" + az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null + if [[ $? -ne 0 ]]; then + echo "Error generating secret" + fi + echo "$RANDOM_PASSWORD" +elif [[ "$MODE" == "save" ]]; then + SECRET_KEY_NAME="$2" + SECRET_VALUE="$3" + az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null + if [[ $? -ne 0 ]]; then + echo "Error generating secret" + fi + echo "$SECRET_VALUE" +else + exit 1 +fi +''' + +var check_app_readyScriptMaster = ''' +#!/bin/bash +set -e +while true; do + HTTP_STATUS=$(curl -Ik http://localhost:7880/twirp/health | head -n1 | awk '{print $2}') + if [ $HTTP_STATUS == 200 ]; then + break + 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 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 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 base64installMaster1 = base64(installScriptMaster1) +var base64installMaster2 = base64(installScriptMaster2) +var base64installMaster3 = base64(installScriptMaster3) +var base64installMaster4 = base64(installScriptMaster4) +var base64after_installMaster = base64(after_installScriptMaster) +var base64update_config_from_secretMaster = base64(update_config_from_secretScriptMaster) +var base64update_secret_from_configMaster = base64(update_secret_from_configScriptMaster) +var base64get_value_from_configMaster = base64(get_value_from_configScriptMaster) +var base64store_secretMaster = base64(store_secretScriptMaster) +var base64check_app_readyMaster = base64(check_app_readyScriptMaster) +var base64restartMaster = base64(restartScriptMaster) + +var userDataParamsMasterNode1 = { + base64install: base64installMaster1 + base64after_install: base64after_installMaster + base64update_config_from_secret: base64update_config_from_secretMaster + base64update_secret_from_config: base64update_secret_from_configMaster + base64get_value_from_config: base64get_value_from_configMaster + base64store_secret: base64store_secretMaster + base64check_app_ready: base64check_app_readyMaster + base64restart: base64restartMaster + keyVaultName: keyVaultName + masterNodeNum: '1' +} + +var userDataParamsMasterNode2 = { + base64install: base64installMaster2 + base64after_install: base64after_installMaster + base64update_config_from_secret: base64update_config_from_secretMaster + base64update_secret_from_config: base64update_secret_from_configMaster + base64get_value_from_config: base64get_value_from_configMaster + base64store_secret: base64store_secretMaster + base64check_app_ready: base64check_app_readyMaster + base64restart: base64restartMaster + keyVaultName: keyVaultName + masterNodeNum: '2' +} + +var userDataParamsMasterNode3 = { + base64install: base64installMaster3 + base64after_install: base64after_installMaster + base64update_config_from_secret: base64update_config_from_secretMaster + base64update_secret_from_config: base64update_secret_from_configMaster + base64get_value_from_config: base64get_value_from_configMaster + base64store_secret: base64store_secretMaster + base64check_app_ready: base64check_app_readyMaster + base64restart: base64restartMaster + keyVaultName: keyVaultName + masterNodeNum: '3' +} + +var userDataParamsMasterNode4 = { + base64install: base64installMaster4 + base64after_install: base64after_installMaster + base64update_config_from_secret: base64update_config_from_secretMaster + base64update_secret_from_config: base64update_secret_from_configMaster + base64get_value_from_config: base64get_value_from_configMaster + base64store_secret: base64store_secretMaster + base64check_app_ready: base64check_app_readyMaster + base64restart: base64restartMaster + keyVaultName: keyVaultName + masterNodeNum: '4' + storageAccountName: storageAccount.name +} + +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 + +# 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 + +# Install azure cli +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + +az login --identity --allow-no-subscriptions + +apt-get update && apt-get install -y + +export HOME="/root" + +# Install OpenVidu +/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; 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 + +MASTER_NODE_NUM=${masterNodeNum} +if [[ $MASTER_NODE_NUM -eq 4 ]]; then + set +e + az storage blob upload --account-name ${storageAccountName} --container-name automation-locks --name lock.txt --file /dev/null --auth-mode key + set -e + az keyvault secret set --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --value "true" +fi + +# Wait for the app +sleep 150 +/usr/local/bin/check_app_ready.sh +''' + +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: 'SystemAssigned' } + 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 + adminPassword: adminSshKey + linuxConfiguration: masterNodeVMSettings.linuxConfiguration + } + userData: base64(userDataMasterNode1) + } +} + +resource openviduMasterNode2 'Microsoft.Compute/virtualMachines@2023-09-01' = { + name: '${stackName}-VM-MasterNode2' + location: location + identity: { type: 'SystemAssigned' } + 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 + adminPassword: adminSshKey + linuxConfiguration: masterNodeVMSettings.linuxConfiguration + } + userData: base64(userDataMasterNode2) + } + dependsOn: [openviduMasterNode1] +} + +resource openviduMasterNode3 'Microsoft.Compute/virtualMachines@2023-09-01' = { + name: '${stackName}-VM-MasterNode3' + location: location + identity: { type: 'SystemAssigned' } + 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 + adminPassword: adminSshKey + linuxConfiguration: masterNodeVMSettings.linuxConfiguration + } + userData: base64(userDataMasterNode3) + } + dependsOn: [openviduMasterNode2] +} + +resource openviduMasterNode4 'Microsoft.Compute/virtualMachines@2023-09-01' = { + name: '${stackName}-VM-MasterNode4' + location: location + identity: { type: 'SystemAssigned' } + 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 + adminPassword: adminSshKey + linuxConfiguration: masterNodeVMSettings.linuxConfiguration + } + userData: base64(userDataMasterNode4) + } + dependsOn: [openviduMasterNode3] +} + +/*------------------------------------------- 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 +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") + +WAIT_INTERVAL=1 +MAX_WAIT=10000 +ELAPSED_TIME=0 +set +e +while true; do + # get secret value + FINISH_MASTER_NODE=$(az keyvault secret show --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --query value -o tsv) + + # Check if all master nodes finished + if [ "$FINISH_MASTER_NODE" == "true" ]; then + break + fi + + ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL)) + + # Check if the maximum waiting time has been reached + if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then + 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) +ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv) +OPENVIDU_VERSION=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-VERSION --query value -o tsv) + +# Base command +INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh)" + +# 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" +"--enabled-modules='$ENABLED_MODULES'" +"--redis-password=$REDIS_PASSWORD" +) + +# Construct the final command with all arguments +FINAL_COMMAND="$INSTALL_COMMAND $(printf "%s " "${COMMON_ARGS[@]}")" + +# Install OpenVidu +exec bash -c "$FINAL_COMMAND" +''' + +var stopMediaNodeParams = { + subscriptionId: subscription().subscriptionId + resourceGroupName: resourceGroup().name + vmScaleSetName: '${stackName}-mediaNodeScaleSet' + storageAccountName: storageAccount.name +} + +var stop_media_nodesScriptMediaTemplate = ''' +#!/bin/bash +set -e + +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=SIGINT openvidu || true + docker container kill --signal=SIGINT ingress || true + docker container kill --signal=SIGINT egress || true + + # Wait for running containers to not be openvidu, ingress or egress + while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ + [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do + echo "Waiting for containers to stop..." + sleep 5 + done +fi + +az login --identity + +RESOURCE_GROUP_NAME=${resourceGroupName} +VM_SCALE_SET_NAME=${vmScaleSetName} +SUBSCRIPTION_ID=${subscriptionId} +BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId') +INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}') +RESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Compute/virtualMachineScaleSets/$VM_SCALE_SET_NAME + +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}" + +az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID +''' + +var userDataMediaNodeTemplate = ''' +#!/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 + +# stop_media_nodes.sh +echo ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh +chmod +x /usr/local/bin/stop_media_node.sh + +apt-get update && apt-get install -y +apt-get install -y jq + +# Install azure cli +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + +az login --identity + +# Protect from scale in actions +RESOURCE_GROUP_NAME=${resourceGroupName} +VM_SCALE_SET_NAME=${vmScaleSetName} +BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId') +INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}') +az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-id $INSTANCE_ID --protect-from-scale-in true + +export HOME="/root" + +# Install OpenVidu +/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; } + +# Start OpenVidu +systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; } +''' + +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 base64installMedia = base64(installScriptMedia) +var base64stopMediaNode = base64(stop_media_nodesScriptMedia) + +var userDataParamsMedia = { + base64install: base64installMedia + base64stop: base64stopMediaNode +} + +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: storageAccount.name + } + identity: { type: 'SystemAssigned' } + sku: { + name: mediaNodeInstanceType + tier: 'Standard' + capacity: initialNumberOfMediaNodes + } + properties: { + overprovision: true + upgradePolicy: { + mode: 'Automatic' + } + singlePlacementGroup: true + platformFaultDomainCount: 1 + virtualMachineProfile: { + storageProfile: { + osDisk: { + createOption: 'FromImage' + managedDisk: { + storageAccountType: mediaNodeVMSettings.osDiskType + } + diskSizeGB: 50 + } + imageReference: mediaNodeVMSettings.ubuntuOSVersion + } + osProfile: { + computerNamePrefix: mediaNodeVMSettings.vmName + adminUsername: adminUsername + adminPassword: adminSshKey + linuxConfiguration: mediaNodeVMSettings.linuxConfiguration + } + networkProfile: { + networkInterfaceConfigurations: [ + { + name: '${stackName}-mediaNodeNetInterface' + properties: { + primary: true + ipConfigurations: [ + { + name: 'ipconfigMediaNode' + properties: { + subnet: { + id: vnet_OV.properties.subnets[0].id + } + applicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + publicIPAddressConfiguration: { + name: 'publicIPAddressMediaNode' + properties: { + publicIPAddressVersion: 'IPv4' + } + } + } + } + ] + networkSecurityGroup: { + id: openviduMediaNodeNSG.id + } + } + } + ] + } + userData: base64userDataMediaNode + } + } +} + +resource openviduAutoScaleSettingsMediaNode 'Microsoft.Insights/autoscaleSettings@2022-10-01' = { + name: '${stackName}-autoscaleSettings' + location: resourceGroup().location + properties: { + profiles: [ + { + name: 'openvidu-medianode-autoscale' + capacity: { + minimum: string(minNumberOfMediaNodes) + maximum: string(maxNumberOfMediaNodes) + default: string(initialNumberOfMediaNodes) + } + rules: [ + { + metricTrigger: { + metricName: 'Percentage CPU' + metricNamespace: 'Microsoft.Compute/virtualMachineScaleSets' + metricResourceUri: openviduScaleSetMediaNode.id + statistic: 'Average' + operator: 'GreaterThan' + threshold: scaleTargetCPU + timeAggregation: 'Average' + timeWindow: 'PT5M' + timeGrain: 'PT1M' + } + scaleAction: { + direction: 'Increase' + type: 'ChangeCount' + value: '1' + cooldown: 'PT5M' + } + } + { + metricTrigger: { + metricName: 'Percentage CPU' + metricNamespace: 'Microsoft.Compute/virtualMachineScaleSets' + metricResourceUri: openviduScaleSetMediaNode.id + statistic: 'Average' + operator: 'LessThan' + threshold: scaleTargetCPU + timeAggregation: 'Average' + timeWindow: 'PT5M' + timeGrain: 'PT1M' + } + scaleAction: { + direction: 'Decrease' + type: 'ChangeCount' + value: '1' + cooldown: 'PT5M' + } + } + ] + } + ] + enabled: true + targetResourceUri: openviduScaleSetMediaNode.id + } +} + +/*------------------------------------------- SCALE IN ------------------------------------------*/ + +resource roleAssignmentMasterNode 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('roleAssignmentForMasterNode${openviduMasterNode4.name}') + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + principalId: openviduMasterNode4.identity.principalId + } +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('roleAssignmentForScaleSet${openviduScaleSetMediaNode.name}') + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + principalId: openviduScaleSetMediaNode.identity.principalId + } +} + +@description('Automation Account Name to create a runbook inside it for scale in') +param automationAccountName string + +module webhookModule '../../shared/webhookdeployment.json' = { + params: { + automationAccountName: automationAccountName + runbookName: 'scaleInRunbook' + webhookName: 'webhookForScaleIn' + WebhookExpiryTime: '2035-03-30T00:00:00Z' + _artifactsLocation: 'https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1' //Change when we upload this to s3 or blob + } + name: 'WebhookDeployment' +} + +resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = { + name: 'actiongrouptest' + 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 isEmptyIp = publicIpAddressResourceName == '' +var turnIsEmptyIp = turnPublicIpAddressResourceName == '' +var lbName = '${stackName}-loadBalancer' +var lbFrontEndName = 'LoadBalancerFrontEnd' +var lbBackendPoolNameMasterNode = 'LoadBalancerBackEndMasterNode' + +resource publicIPAddressLoadBalancer 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (isEmptyIp == true) { + name: '${stackName}-publicIPAddressLoadBalancer' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAddressVersion: 'IPv4' + publicIPAllocationMethod: 'Static' + } +} + +resource publicIP_LoadBalancer_ifNotEmpty 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (!isEmptyIp == true) { + name: publicIpAddressResourceName +} + +resource publicIPAddressTurnTLSLoadBalancer 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (turnTLSIsEnabled == true) { + name: '${stackName}-publicIPAddressTurnTLSLoadBalancer' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAddressVersion: 'IPv4' + publicIPAllocationMethod: 'Static' + } +} + +resource publicIP_TurnTLSLoadBalancer_ifNotEmpty 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (!turnIsEmptyIp && turnTLSIsEnabled == true) { + name: publicIpAddressResourceName +} + +resource LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = { + name: lbName + location: location + sku: { + name: 'Standard' + } + properties: { + frontendIPConfigurations: [ + { + name: lbFrontEndName + properties: { + publicIPAddress: { + id: isEmptyIp ? publicIPAddressLoadBalancer.id : publicIP_LoadBalancer_ifNotEmpty.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: 1935 + 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: [] + } +} + +var tlbName = '${stackName}-loadBalancer' +var tlbFrontEndName = 'LoadBalancerFrontEnd' + +resource TurnTLSLoadbalancer 'Microsoft.Network/loadBalancers@2024-05-01' = if (turnTLSIsEnabled == true) { + name: tlbName + location: location + sku: { + name: 'Standard' + } + properties: { + frontendIPConfigurations: [ + { + name: tlbFrontEndName + properties: { + privateIPAllocationMethod: 'Dynamic' + privateIPAddressVersion: 'IPv4' + publicIPAddress: { + id: turnIsEmptyIp ? publicIPAddressLoadBalancer.id : publicIP_TurnTLSLoadBalancer_ifNotEmpty.id + } + } + } + ] + backendAddressPools: [ + { + name: lbBackendPoolNameMasterNode + } + ] + loadBalancingRules: [ + { + name: 'TURNTLSRuleforMasterNode' + properties: { + frontendIPConfiguration: { + id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', tlbName, tlbFrontEndName) + } + backendAddressPool: { + id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', tlbName, lbBackendPoolNameMasterNode) + } + frontendPort: 443 + backendPort: 443 + enableFloatingIP: false + protocol: 'Tcp' + enableTcpReset: true + loadDistribution: 'Default' + disableOutboundSnat: true + probe: { + id: resourceId('Microsoft.Network/loadBalancers/probes', tlbName, 'probeForHTTPSRuleMasterNode') + } + } + } + ] + probes: [ + { + name: 'probeForTURNTLSRuleMasterNode' + properties: { + protocol: 'Http' + requestPath: '/' + port: 443 + probeThreshold: 3 + intervalInSeconds: 10 + numberOfProbes: 5 + } + } + ] + } +} + +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 + } + } + } + ] + } +} + +resource subnetMasterNode1 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = { + parent: vnet_OV + name: 'firstSubnetForMasterNodes' + properties: { + addressPrefix: networkSettings.subnetAddressPrefixMaster1 + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + natGateway: { + id: natGateway.id + } + } +} + +resource subnetMasterNode2 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = { + parent: vnet_OV + name: 'secondSubnetForMasterNodes' + properties: { + addressPrefix: networkSettings.subnetAddressPrefixMaster2 + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + natGateway: { + id: natGateway.id + } + } +} + +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' + } +} + +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 masterToMasterDefaultApp 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'masterNode_to_masterNode_DEFAULTAPP_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '6080' + access: 'Allow' + priority: 290 + direction: 'Inbound' + } +} + +resource mediaToMasterDefaultAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { + parent: openviduMasterNodeNSG + name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS' + properties: { + protocol: 'Tcp' + sourceApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMasterNodeASG.id + } + ] + destinationPortRange: '6080' + access: 'Allow' + priority: 300 + 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: '*' + destinationPortRanges: [ + '50000' + '60000' + ] + access: 'Allow' + priority: 140 + 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: 150 + 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: 160 + direction: 'Inbound' + } +} + +resource loadBalancerToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = if (turnTLSIsEnabled == true) { + parent: openviduMediaNodeNSG + name: 'loadbalancer_to_mediaNode_TURN_TLS_INGRESS' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: 'AzureLoadBalancer' + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + destinationPortRange: '5349' + access: 'Allow' + priority: 170 + direction: 'Inbound' + } +} + +resource loadBalancerToMediaTurnTlsHealthCheckIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = if (turnTLSIsEnabled == true) { + parent: openviduMediaNodeNSG + name: 'masterNode_to_mediaNode_TURN_TLSHEALTHCHECK_INGRESS' + properties: { + protocol: 'Tcp' + sourceAddressPrefix: 'AzureLoadBalancer' + sourcePortRange: '*' + destinationApplicationSecurityGroups: [ + { + id: openviduMediaNodeASG.id + } + ] + destinationPortRange: '7880' + access: 'Allow' + priority: 180 + 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: 190 + 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: 200 + direction: 'Inbound' + } +} + +/*------------------------------------------- STORAGE ACCOUNT ----------------------------------------*/ + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: 'lockstorage${uniqueString(resourceGroup().id)}' + location: resourceGroup().location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Cool' + supportsHttpsTrafficOnly: true + } +} + +resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { + name: '${storageAccount.name}/default/automation-locks' + properties: { + publicAccess: 'None' + } +} + +/*------------------------------------------- OUTPUTS -------------------------------------------*/ diff --git a/openvidu-deployment/pro/ha/azure/cf-openvidu-ha.json b/openvidu-deployment/pro/ha/azure/cf-openvidu-ha.json new file mode 100644 index 00000000..1d5ae095 --- /dev/null +++ b/openvidu-deployment/pro/ha/azure/cf-openvidu-ha.json @@ -0,0 +1,2597 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "2980729101824584018" + } + }, + "parameters": { + "stackName": { + "type": "string", + "metadata": { + "description": "Stack name" + } + }, + "certificateType": { + "type": "string", + "defaultValue": "selfsigned", + "allowedValues": [ + "selfsigned", + "owncert", + "letsencrypt" + ], + "metadata": { + "description": "[selfsigned] Not recommended for production use. If you don't have a FQDN, (DomainName parameter) you can use this option to generate a self-signed certificate.\n[owncert] Valid for productions environments. If you have a FQDN, (DomainName parameter)\nand an Elastic IP, you can use this option to use your own certificate.\n[letsencrypt] Valid for production environments. If you have a FQDN, (DomainName parameter)\nand an Elastic IP, you can use this option to generate a Let's Encrypt certificate.\n" + } + }, + "domainName": { + "type": "string", + "metadata": { + "description": "Domain name for the OpenVidu Deployment. Blank will generate default domain" + } + }, + "ownPublicCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'owncert', this parameter will be used to specify the public certificate" + } + }, + "ownPrivateCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'owncert', this parameter will be used to specify the private certificate" + } + }, + "letsEncryptEmail": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" + } + }, + "publicIpAddressResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the PublicIPAddress resource in Azure when using certificateType 'owncert' or 'letsencrypt'" + } + }, + "turnDomainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls" + } + }, + "turnOwnPublicCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + } + }, + "turnOwnPrivateCertificate": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified." + } + }, + "turnPublicIpAddressResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the PublicIPAddress resource in Azure when using TURN server with TLS" + } + }, + "openviduLicense": { + "type": "securestring", + "metadata": { + "description": "Visit https://openvidu.io/account" + } + }, + "rtcEngine": { + "type": "string", + "defaultValue": "pion", + "allowedValues": [ + "pion", + "mediasoup" + ], + "metadata": { + "description": "RTCEngine media engine to use" + } + }, + "masterNodeInstanceType": { + "type": "string", + "defaultValue": "Standard_B2s", + "allowedValues": [ + "Standard_B1s", + "Standard_B1ms", + "Standard_B2s", + "Standard_B2ms", + "Standard_B4ms", + "Standard_B8ms", + "Standard_D2_v3", + "Standard_D4_v3", + "Standard_D8_v3", + "Standard_D16_v3", + "Standard_D32_v3", + "Standard_D48_v3", + "Standard_D64_v3", + "Standard_D2_v4", + "Standard_D4_v4", + "Standard_D8_v4", + "Standard_D16_v4", + "Standard_D32_v4", + "Standard_D48_v4", + "Standard_D64_v4", + "Standard_D96_v4", + "Standard_D2_v5", + "Standard_D4_v5", + "Standard_D8_v5", + "Standard_D16_v5", + "Standard_D32_v5", + "Standard_D48_v5", + "Standard_D64_v5", + "Standard_D96_v5", + "Standard_F2", + "Standard_F4", + "Standard_F8", + "Standard_F16", + "Standard_F32", + "Standard_F64", + "Standard_F72", + "Standard_F2s_v2", + "Standard_F4s_v2", + "Standard_F8s_v2", + "Standard_F16s_v2", + "Standard_F32s_v2", + "Standard_F64s_v2", + "Standard_F72s_v2", + "Standard_E2_v3", + "Standard_E4_v3", + "Standard_E8_v3", + "Standard_E16_v3", + "Standard_E32_v3", + "Standard_E48_v3", + "Standard_E64_v3", + "Standard_E96_v3", + "Standard_E2_v4", + "Standard_E4_v4", + "Standard_E8_v4", + "Standard_E16_v4", + "Standard_E32_v4", + "Standard_E48_v4", + "Standard_E64_v4", + "Standard_E2_v5", + "Standard_E4_v5", + "Standard_E8_v5", + "Standard_E16_v5", + "Standard_E32_v5", + "Standard_E48_v5", + "Standard_E64_v5", + "Standard_E96_v5", + "Standard_M64", + "Standard_M128", + "Standard_M208ms_v2", + "Standard_M416ms_v2", + "Standard_L4s_v2", + "Standard_L8s_v2", + "Standard_L16s_v2", + "Standard_L32s_v2", + "Standard_L64s_v2", + "Standard_L80s_v2", + "Standard_NC6", + "Standard_NC12", + "Standard_NC24", + "Standard_NC24r", + "Standard_ND6s", + "Standard_ND12s", + "Standard_ND24s", + "Standard_ND24rs", + "Standard_NV6", + "Standard_NV12", + "Standard_NV24", + "Standard_H8", + "Standard_H16", + "Standard_H16r", + "Standard_H16mr", + "Standard_HB120rs_v2", + "Standard_HC44rs", + "Standard_DC2s", + "Standard_DC4s", + "Standard_DC2s_v2", + "Standard_DC4s_v2", + "Standard_DC8s_v2", + "Standard_DC16s_v2", + "Standard_DC32s_v2", + "Standard_A1_v2", + "Standard_A2_v2", + "Standard_A4_v2", + "Standard_A8_v2", + "Standard_A2m_v2", + "Standard_A4m_v2", + "Standard_A8m_v2" + ], + "metadata": { + "description": "Specifies the EC2 instance type for your OpenVidu Master Node" + } + }, + "masterNodesDiskSize": { + "type": "int", + "defaultValue": 100, + "metadata": { + "description": "Size of the disk in GB" + } + }, + "mediaNodeInstanceType": { + "type": "string", + "defaultValue": "Standard_B2s", + "allowedValues": [ + "Standard_B1s", + "Standard_B1ms", + "Standard_B2s", + "Standard_B2ms", + "Standard_B4ms", + "Standard_B8ms", + "Standard_D2_v3", + "Standard_D4_v3", + "Standard_D8_v3", + "Standard_D16_v3", + "Standard_D32_v3", + "Standard_D48_v3", + "Standard_D64_v3", + "Standard_D2_v4", + "Standard_D4_v4", + "Standard_D8_v4", + "Standard_D16_v4", + "Standard_D32_v4", + "Standard_D48_v4", + "Standard_D64_v4", + "Standard_D96_v4", + "Standard_D2_v5", + "Standard_D4_v5", + "Standard_D8_v5", + "Standard_D16_v5", + "Standard_D32_v5", + "Standard_D48_v5", + "Standard_D64_v5", + "Standard_D96_v5", + "Standard_F2", + "Standard_F4", + "Standard_F8", + "Standard_F16", + "Standard_F32", + "Standard_F64", + "Standard_F72", + "Standard_F2s_v2", + "Standard_F4s_v2", + "Standard_F8s_v2", + "Standard_F16s_v2", + "Standard_F32s_v2", + "Standard_F64s_v2", + "Standard_F72s_v2", + "Standard_E2_v3", + "Standard_E4_v3", + "Standard_E8_v3", + "Standard_E16_v3", + "Standard_E32_v3", + "Standard_E48_v3", + "Standard_E64_v3", + "Standard_E96_v3", + "Standard_E2_v4", + "Standard_E4_v4", + "Standard_E8_v4", + "Standard_E16_v4", + "Standard_E32_v4", + "Standard_E48_v4", + "Standard_E64_v4", + "Standard_E2_v5", + "Standard_E4_v5", + "Standard_E8_v5", + "Standard_E16_v5", + "Standard_E32_v5", + "Standard_E48_v5", + "Standard_E64_v5", + "Standard_E96_v5", + "Standard_M64", + "Standard_M128", + "Standard_M208ms_v2", + "Standard_M416ms_v2", + "Standard_L4s_v2", + "Standard_L8s_v2", + "Standard_L16s_v2", + "Standard_L32s_v2", + "Standard_L64s_v2", + "Standard_L80s_v2", + "Standard_NC6", + "Standard_NC12", + "Standard_NC24", + "Standard_NC24r", + "Standard_ND6s", + "Standard_ND12s", + "Standard_ND24s", + "Standard_ND24rs", + "Standard_NV6", + "Standard_NV12", + "Standard_NV24", + "Standard_H8", + "Standard_H16", + "Standard_H16r", + "Standard_H16mr", + "Standard_HB120rs_v2", + "Standard_HC44rs", + "Standard_DC2s", + "Standard_DC4s", + "Standard_DC2s_v2", + "Standard_DC4s_v2", + "Standard_DC8s_v2", + "Standard_DC16s_v2", + "Standard_DC32s_v2", + "Standard_A1_v2", + "Standard_A2_v2", + "Standard_A4_v2", + "Standard_A8_v2", + "Standard_A2m_v2", + "Standard_A4m_v2", + "Standard_A8m_v2" + ], + "metadata": { + "description": "Specifies the EC2 instance type for your OpenVidu Media Nodes" + } + }, + "adminUsername": { + "type": "string", + "metadata": { + "description": "Username for the Virtual Machine." + } + }, + "adminSshKey": { + "type": "securestring", + "metadata": { + "description": "SSH Key for the Virtual Machine." + } + }, + "initialNumberOfMediaNodes": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Number of initial media nodes to deploy" + } + }, + "minNumberOfMediaNodes": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of media nodes to deploy" + } + }, + "maxNumberOfMediaNodes": { + "type": "int", + "defaultValue": 5, + "metadata": { + "description": "Maximum number of media nodes to deploy" + } + }, + "scaleTargetCPU": { + "type": "int", + "defaultValue": 50, + "metadata": { + "description": "Target CPU percentage to scale up or down" + } + }, + "datetime": { + "type": "string", + "defaultValue": "[utcNow('u')]" + }, + "automationAccountName": { + "type": "string", + "metadata": { + "description": "Automation Account Name to create a runbook inside it for scale in" + } + } + }, + "variables": { + "masterNodeVMSettings": { + "osDiskType": "StandardSSD_LRS", + "osDiskSize": "[parameters('masterNodesDiskSize')]", + "ubuntuOSVersion": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest" + }, + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", + "keyData": "[parameters('adminSshKey')]" + } + ] + } + } + }, + "mediaNodeVMSettings": { + "vmName": "[format('{0}-VN-MediaNode', parameters('stackName'))]", + "osDiskType": "StandardSSD_LRS", + "ubuntuOSVersion": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest" + }, + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", + "keyData": "[parameters('adminSshKey')]" + } + ] + } + } + }, + "turnTLSIsEnabled": "[not(equals(parameters('turnDomainName'), ''))]", + "fqdn": "[parameters('domainName')]", + "keyVaultName": "[format('{0}-keyvault', parameters('stackName'))]", + "location": "[resourceGroup().location]", + "tenantId": "[subscription().tenantId]", + "deploymentUser": "[deployer().objectId]", + "stringInterpolationParamsMaster1": { + "domainName": "[parameters('domainName')]", + "turnDomainName": "[parameters('turnDomainName')]", + "certificateType": "[parameters('certificateType')]", + "letsEncryptEmail": "[parameters('letsEncryptEmail')]", + "ownPublicCertificate": "[parameters('ownPublicCertificate')]", + "ownPrivateCertificate": "[parameters('ownPrivateCertificate')]", + "turnOwnPublicCertificate": "[parameters('turnOwnPublicCertificate')]", + "turnOwnPrivateCertificate": "[parameters('turnOwnPrivateCertificate')]", + "fqdn": "[variables('fqdn')]", + "openviduLicense": "[parameters('openviduLicense')]", + "rtcEngine": "[parameters('rtcEngine')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "1" + }, + "stringInterpolationParamsMaster2": { + "domainName": "[parameters('domainName')]", + "turnDomainName": "[parameters('turnDomainName')]", + "certificateType": "[parameters('certificateType')]", + "letsEncryptEmail": "[parameters('letsEncryptEmail')]", + "ownPublicCertificate": "[parameters('ownPublicCertificate')]", + "ownPrivateCertificate": "[parameters('ownPrivateCertificate')]", + "turnOwnPublicCertificate": "[parameters('turnOwnPublicCertificate')]", + "turnOwnPrivateCertificate": "[parameters('turnOwnPrivateCertificate')]", + "fqdn": "[variables('fqdn')]", + "openviduLicense": "[parameters('openviduLicense')]", + "rtcEngine": "[parameters('rtcEngine')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "2" + }, + "stringInterpolationParamsMaster3": { + "domainName": "[parameters('domainName')]", + "turnDomainName": "[parameters('turnDomainName')]", + "certificateType": "[parameters('certificateType')]", + "letsEncryptEmail": "[parameters('letsEncryptEmail')]", + "ownPublicCertificate": "[parameters('ownPublicCertificate')]", + "ownPrivateCertificate": "[parameters('ownPrivateCertificate')]", + "turnOwnPublicCertificate": "[parameters('turnOwnPublicCertificate')]", + "turnOwnPrivateCertificate": "[parameters('turnOwnPrivateCertificate')]", + "fqdn": "[variables('fqdn')]", + "openviduLicense": "[parameters('openviduLicense')]", + "rtcEngine": "[parameters('rtcEngine')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "3" + }, + "stringInterpolationParamsMaster4": { + "domainName": "[parameters('domainName')]", + "turnDomainName": "[parameters('turnDomainName')]", + "certificateType": "[parameters('certificateType')]", + "letsEncryptEmail": "[parameters('letsEncryptEmail')]", + "ownPublicCertificate": "[parameters('ownPublicCertificate')]", + "ownPrivateCertificate": "[parameters('ownPrivateCertificate')]", + "turnOwnPublicCertificate": "[parameters('turnOwnPublicCertificate')]", + "turnOwnPrivateCertificate": "[parameters('turnOwnPrivateCertificate')]", + "fqdn": "[variables('fqdn')]", + "openviduLicense": "[parameters('openviduLicense')]", + "rtcEngine": "[parameters('rtcEngine')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "4" + }, + "installScriptTemplateMaster": "#!/bin/bash -x\nset -e\nOPENVIDU_VERSION=main\nDOMAIN=\n\n# Assume azure cli is installed\n\napt-get update && apt-get install -y \\\n curl \\\n unzip \\\n jq \\\n wget\n\n# Configure Domain\nif [[ \"${domainName}\" == '' ]]; then\n DOMAIN=${fqdn}\nelse\n DOMAIN=${domainName}\nfi\n\n# Wait for the keyvault availability\nMAX_WAIT=100\nWAIT_INTERVAL=1\nELAPSED_TIME=0\nset +e\nwhile true; do\n # Check keyvault availability\n az keyvault secret list --vault-name ${keyVaultName} \n\n # If it is available, exit the loop\n if [ $? -eq 0 ]; then\n break\n fi\n\n # If not, wait and check again incrementing the time\n ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))\n\n # If exceeded the maximum time, exit with error\n if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then\n exit 1\n fi\n\n # Esperar antes de la próxima comprobación\n sleep $WAIT_INTERVAL\ndone\nset -e\n\nMASTER_NODE_NUM=${masterNodeNum}\n\n# Get own private IP\nPRIVATE_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\")\n\n# Store current private IP\nPRIVATE_IP=\"$(/usr/local/bin/store_secret.sh save MASTER-NODE-${masterNodeNum}-PRIVATE-IP $PRIVATE_IP)\"\n\n\nif [[ $MASTER_NODE_NUM -eq 1 ]] && [[ \"$ALL_SECRETS_GENERATED\" == \"\" || \"$ALL_SECRETS_GENERATED\" == \"false\" ]]; then\n DOMAIN=\"$(/usr/local/bin/store_secret.sh save DOMAIN-NAME \"${domainName}\")\"\n if [[ -n \"${turnDomainName}\" ]]; then\n LIVEKIT_TURN_DOMAIN_NAME=\"$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME \"${turnDomainName}\")\"\n fi\n if [[ \"${certificateType}\" == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=$(/usr/local/bin/store_secret.sh save LETSENCRYPT-EMAIL \"${letsEncryptEmail}\")\n fi\n\n # Store usernames and generate random passwords\n OPENVIDU_PRO_LICENSE=\"$(/usr/local/bin/store_secret.sh save OPENVIDU-PRO-LICENSE \"${openviduLicense}\")\"\n OPENVIDU_RTC_ENGINE=\"$(/usr/local/bin/store_secret.sh save OPENVIDU-RTC-ENGINE \"${rtcEngine}\")\"\n REDIS_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate REDIS-PASSWORD)\"\n MONGO_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save MONGO-ADMIN-USERNAME \"mongoadmin\")\"\n MONGO_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate MONGO-ADMIN-PASSWORD)\"\n MONGO_REPLICA_SET_KEY=\"$(/usr/local/bin/store_secret.sh generate MONGO-REPLICA-SET-KEY)\"\n MINIO_ACCESS_KEY=\"$(/usr/local/bin/store_secret.sh save MINIO-ACCESS-KEY \"minioadmin\")\"\n MINIO_SECRET_KEY=\"$(/usr/local/bin/store_secret.sh generate MINIO-SECRET-KEY)\"\n DASHBOARD_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-USERNAME \"dashboardadmin\")\"\n DASHBOARD_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)\"\n GRAFANA_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME \"grafanaadmin\")\"\n GRAFANA_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)\"\n DEFAULT_APP_USERNAME=\"$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME \"calluser\")\"\n DEFAULT_APP_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)\"\n DEFAULT_APP_ADMIN_USERNAME=\"$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME \"calladmin\")\"\n DEFAULT_APP_ADMIN_PASSWORD=\"$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)\"\n LIVEKIT_API_KEY=\"$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY \"API\" 12)\"\n LIVEKIT_API_SECRET=\"$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)\"\n OPENVIDU_VERSION=\"$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION \"${OPENVIDU_VERSION}\")\"\n ENABLED_MODULES=\"$(/usr/local/bin/store_secret.sh save ENABLED-MODULES \"observability,app\")\"\n ALL_SECRETS_GENERATED=\"$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED \"true\")\"\nfi\n\nwhile true; do\n MASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv) || true\n MASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv) || true\n MASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv) || true\n MASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv) || true\n # Check if all master nodes have stored their private IPs\n if [[ \"$MASTER_NODE_1_PRIVATE_IP\" != \"\" ]] &&\n [[ \"$MASTER_NODE_2_PRIVATE_IP\" != \"\" ]] &&\n [[ \"$MASTER_NODE_3_PRIVATE_IP\" != \"\" ]] &&\n [[ \"$MASTER_NODE_4_PRIVATE_IP\" != \"\" ]]; then\n break\n fi\n sleep 5\ndone\n\n\n# Fetch the values in the keyvault\nMASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_PRIVATE_IP_LIST=\"$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP\"\n\nDOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nif [[ -n \"${turnDomainName}\" ]]; then\n LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv)\nfi\nif [[ \"${certificateType}\" == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv)\nfi\nOPENVIDU_RTC_ENGINE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --query value -o tsv)\nOPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)\nREDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)\nMONGO_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --query value -o tsv)\nMONGO_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --query value -o tsv)\nMONGO_REPLICA_SET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --query value -o tsv)\nDASHBOARD_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --query value -o tsv)\nDASHBOARD_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --query value -o tsv)\nMINIO_ACCESS_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --query value -o tsv)\nMINIO_SECRET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --query value -o tsv)\nGRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --query value -o tsv)\nGRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)\nLIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)\nLIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)\nDEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)\nDEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)\nDEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)\nDEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)\nENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)\n\n\n# Base command\nINSTALL_COMMAND=\"sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_master_node.sh)\"\n\n# Common arguments\nCOMMON_ARGS=(\n \"--no-tty\"\n \"--install\"\n \"--environment=azure\"\n \"--deployment-type='ha'\"\n \"--node-role='master-node'\"\n \"--external-load-balancer\"\n \"--internal-tls-termination\"\n \"--master-node-private-ip-list='$MASTER_NODE_PRIVATE_IP_LIST'\"\n \"--openvidu-pro-license='$OPENVIDU_PRO_LICENSE'\"\n \"--domain-name='$DOMAIN'\"\n \"--enabled-modules='$ENABLED_MODULES'\"\n \"--rtc-engine=$OPENVIDU_RTC_ENGINE\"\n \"--redis-password=$REDIS_PASSWORD\"\n \"--mongo-admin-user=$MONGO_ADMIN_USERNAME\"\n \"--mongo-admin-password=$MONGO_ADMIN_PASSWORD\"\n \"--mongo-replica-set-key=$MONGO_REPLICA_SET_KEY\"\n \"--minio-access-key=$MINIO_ACCESS_KEY\"\n \"--minio-secret-key=$MINIO_SECRET_KEY\"\n \"--dashboard-admin-user=$DASHBOARD_ADMIN_USERNAME\"\n \"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD\"\n \"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME\"\n \"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD\"\n \"--default-app-user=$DEFAULT_APP_USERNAME\"\n \"--default-app-password=$DEFAULT_APP_PASSWORD\"\n \"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME\"\n \"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD\"\n \"--livekit-api-key=$LIVEKIT_API_KEY\"\n \"--livekit-api-secret=$LIVEKIT_API_SECRET\"\n)\n\nif [[ $LIVEKIT_TURN_DOMAIN_NAME != \"\" ]]; then\n COMMON_ARGS+=(\"--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME}\")\nfi\n\n# Certificate arguments\nif [[ \"${certificateType}\" == \"selfsigned\" ]]; then\n CERT_ARGS=(\n \"--certificate-type=selfsigned\"\n )\nelif [[ \"${certificateType}\" == \"letsencrypt\" ]]; then\n CERT_ARGS=(\n \"--certificate-type=letsencrypt\"\n \"--letsencrypt-email=$LETSENCRYPT_EMAIL\"\n )\nelse\n # Download owncert files\n mkdir -p /tmp/owncert\n wget -O /tmp/owncert/fullchain.pem ${ownPublicCertificate}\n wget -O /tmp/owncert/privkey.pem ${ownPrivateCertificate}\n\n # Convert to base64\n OWN_CERT_CRT=$(base64 -w 0 /tmp/owncert/fullchain.pem)\n OWN_CERT_KEY=$(base64 -w 0 /tmp/owncert/privkey.pem)\n\n CERT_ARGS=(\n \"--certificate-type=owncert\"\n \"--owncert-public-key=$OWN_CERT_CRT\"\n \"--owncert-private-key=$OWN_CERT_KEY\"\n )\n\n # Turn with TLS and own certificate\n if [[ \"${turnDomainName}\" != '' ]]; then\n # Download owncert files\n mkdir -p /tmp/owncert-turn\n wget -O /tmp/owncert-turn/fullchain.pem ${turnOwnPublicCertificate}\n wget -O /tmp/owncert-turn/privkey.pem ${turnOwnPrivateCertificate}\n\n # Convert to base64\n OWN_CERT_CRT_TURN=$(base64 -w 0 /tmp/owncert-turn/fullchain.pem)\n OWN_CERT_KEY_TURN=$(base64 -w 0 /tmp/owncert-turn/privkey.pem)\n\n CERT_ARGS+=(\n \"--turn-owncert-private-key=$OWN_CERT_KEY_TURN\"\n \"--turn-owncert-public-key=$OWN_CERT_CRT_TURN\"\n )\n fi\nfi\n\n# Construct the final command\nFINAL_COMMAND=\"$INSTALL_COMMAND $(printf \"%s \" \"${COMMON_ARGS[@]}\") $(printf \"%s \" \"${CERT_ARGS[@]}\")\"\n\n# Install OpenVidu\nexec bash -c \"$FINAL_COMMAND\" \n", + "after_installScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Generate URLs\nDOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nDASHBOARD_URL=\"https://${DOMAIN}/dashboard/\"\nGRAFANA_URL=\"https://${DOMAIN}/grafana/\"\nMINIO_URL=\"https://${DOMAIN}/minio-console/\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL\n\naz keyvault secret show --vault-name ${keyVaultName} --name MINIO-URL\n\nif [[ $? -ne 0 ]]; then\n echo \"Error updating keyvault\"\nfi\n", + "update_config_from_secretScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Installation directory\nINSTALL_DIR=\"/opt/openvidu\"\nCLUSTER_CONFIG_DIR=\"${INSTALL_DIR}/config/cluster\"\nMASTER_NODE_CONFIG_DIR=\"${INSTALL_DIR}/config/node\"\n\n# Replace DOMAIN_NAME\nexport DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)\nif [[ -n \"$DOMAIN\" ]]; then\n sed -i \"s/DOMAIN_NAME=.*/DOMAIN_NAME=$DOMAIN/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nelse\n exit 1\nfi\n\n# Replace LIVEKIT_TURN_DOMAIN_NAME\nexport LIVEKIT_TURN_DOMAIN_NAME=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --query value -o tsv)\nif [[ -n \"$LIVEKIT_TURN_DOMAIN_NAME\" ]]; then\n sed -i \"s/LIVEKIT_TURN_DOMAIN_NAME=.*/LIVEKIT_TURN_DOMAIN_NAME=$LIVEKIT_TURN_DOMAIN_NAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nfi\n\nif [[ ${certificateType} == \"letsencrypt\" ]]; then\n export LETSENCRYPT_EMAIL=$(az keyvault secret show --vault-name ${keyVaultName} --name LETSENCRYPT-EMAIL --query value -o tsv)\n sed -i \"s/LETSENCRYPT_EMAIL=.*/LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nfi\n\n# Get the rest of the values\nexport REDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)\nexport OPENVIDU_RTC_ENGINE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --query value -o tsv)\nexport OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)\nexport MONGO_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --query value -o tsv)\nexport MONGO_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --query value -o tsv)\nexport MONGO_REPLICA_SET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --query value -o tsv)\nexport DASHBOARD_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --query value -o tsv)\nexport DASHBOARD_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --query value -o tsv)\nexport MINIO_ACCESS_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --query value -o tsv)\nexport MINIO_SECRET_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --query value -o tsv)\nexport GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --query value -o tsv)\nexport GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)\nexport LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)\nexport LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)\nexport DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)\nexport DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)\nexport DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)\nexport DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)\nexport ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)\n\n# Replace rest of the values\nsed -i \"s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/\" \"${MASTER_NODE_CONFIG_DIR}/master_node.env\"\nsed -i \"s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/DASHBOARD_ADMIN_USERNAME=.*/DASHBOARD_ADMIN_USERNAME=$DASHBOARD_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/DASHBOARD_ADMIN_PASSWORD=.*/DASHBOARD_ADMIN_PASSWORD=$DASHBOARD_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MINIO_ACCESS_KEY=.*/MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/MINIO_SECRET_KEY=.*/MINIO_SECRET_KEY=$MINIO_SECRET_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\nsed -i \"s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/\" \"${CLUSTER_CONFIG_DIR}/master_node/app.env\"\nsed -i \"s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/\" \"${CLUSTER_CONFIG_DIR}/openvidu.env\"\n\n# Update URLs in secret\nDASHBOARD_URL=\"https://${DOMAIN}/dashboard/\"\nGRAFANA_URL=\"https://${DOMAIN}/grafana/\"\nMINIO_URL=\"https://${DOMAIN}/minio-console/\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-URL --value $DASHBOARD_URL\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-URL --value $GRAFANA_URL\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-URL --value $MINIO_URL\n", + "update_secret_from_configScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Installation directory\nINSTALL_DIR=\"/opt/openvidu\"\nCLUSTER_CONFIG_DIR=\"${INSTALL_DIR}/config/cluster\"\nMASTER_NODE_CONFIG_DIR=\"${INSTALL_DIR}/config/node\"\n\nif [[ ${certificateType} == \"letsencrypt\" ]]; then\n LETSENCRYPT_EMAIL=\"$(/usr/local/bin/get_value_from_config.sh LETSENCRYPT_EMAIL \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\n az keyvault secret set --vault-name ${keyVaultName} --name \"LETSENCRYPT-EMAIL\" --value $LETSENCRYPT_EMAIL\nfi\n\n# Get current values of the config\nREDIS_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD \"${MASTER_NODE_CONFIG_DIR}/master_node.env\")\"\nDOMAIN_NAME=\"$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_TURN_DOMAIN_NAME=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nOPENVIDU_RTC_ENGINE=\"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nOPENVIDU_PRO_LICENSE=\"$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMONGO_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMONGO_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMONGO_REPLICA_SET_KEY=\"$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMINIO_ACCESS_KEY=\"$(/usr/local/bin/get_value_from_config.sh MINIO_ACCESS_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nMINIO_SECRET_KEY=\"$(/usr/local/bin/get_value_from_config.sh MINIO_SECRET_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nDASHBOARD_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_USERNAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nDASHBOARD_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh DASHBOARD_ADMIN_PASSWORD \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nGRAFANA_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_USERNAME \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nGRAFANA_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_API_KEY=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nLIVEKIT_API_SECRET=\"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\nDEFAULT_APP_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh CALL_USER \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nDEFAULT_APP_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nDEFAULT_APP_ADMIN_USERNAME=\"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nDEFAULT_APP_ADMIN_PASSWORD=\"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET \"${CLUSTER_CONFIG_DIR}/master_node/app.env\")\"\nENABLED_MODULES=\"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES \"${CLUSTER_CONFIG_DIR}/openvidu.env\")\"\n\n# Update shared secret\naz keyvault secret set --vault-name ${keyVaultName} --name REDIS-PASSWORD --value $REDIS_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name DOMAIN-NAME --value $DOMAIN_NAME\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-TURN-DOMAIN-NAME --value $LIVEKIT_TURN_DOMAIN_NAME\naz keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-RTC-ENGINE --value $OPENVIDU_RTC_ENGINE\naz keyvault secret set --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --value $OPENVIDU_PRO_LICENSE\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-USERNAME --value $MONGO_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-ADMIN-PASSWORD --value $MONGO_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name MONGO-REPLICA-SET-KEY --value $MONGO_REPLICA_SET_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-ACCESS-KEY --value $MINIO_ACCESS_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name MINIO-SECRET-KEY --value $MINIO_SECRET_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-USERNAME --value $DASHBOARD_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DASHBOARD-ADMIN-PASSWORD --value $DASHBOARD_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAME --value $GRAFANA_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY\naz keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME\naz keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD\naz keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES\n", + "get_value_from_configScriptMaster": "#!/bin/bash\nset -e\n\n# Function to get the value of a given key from the environment file\nget_value() {\n local key=\"$1\"\n local file_path=\"$2\"\n\n # Use grep to find the line with the key, ignoring lines starting with #\n # Use awk to split on '=' and print the second field, which is the value\n local value=$(grep -E \"^\\s*$key\\s*=\" \"$file_path\" | awk -F= '{print $2}' | sed 's/#.*//; s/^\\s*//; s/\\s*$//')\n\n # If the value is empty, return \"none\"\n if [ -z \"$value\" ]; then\n echo \"none\"\n else\n echo \"$value\"\n fi\n}\n\n# Check if the correct number of arguments are supplied\nif [ \"$#\" -ne 2 ]; then\n echo \"Usage: $0 \"\n exit 1\nfi\n\n# Get the key and file path from the arguments\nkey=\"$1\"\nfile_path=\"$2\"\n\n# Get and print the value\nget_value \"$key\" \"$file_path\"\n", + "store_secretScriptTemplateMaster": "#!/bin/bash\nset -e\n\naz login --identity --allow-no-subscriptions > /dev/null\n\n# Modes: save, generate\n# save mode: save the secret in the secret manager\n# generate mode: generate a random password and save it in the secret manager\nMODE=\"$1\"\n\nif [[ \"$MODE\" == \"generate\" ]]; then\n SECRET_KEY_NAME=\"$2\"\n PREFIX=\"${3:-}\"\n LENGTH=\"${4:-44}\"\n RANDOM_PASSWORD=\"$(openssl rand -base64 64 | tr -d '+/=\\n' | cut -c -${LENGTH})\"\n RANDOM_PASSWORD=\"${PREFIX}${RANDOM_PASSWORD}\"\n az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $RANDOM_PASSWORD > /dev/null\n if [[ $? -ne 0 ]]; then\n echo \"Error generating secret\"\n fi\n echo \"$RANDOM_PASSWORD\"\nelif [[ \"$MODE\" == \"save\" ]]; then\n SECRET_KEY_NAME=\"$2\"\n SECRET_VALUE=\"$3\"\n az keyvault secret set --vault-name ${keyVaultName} --name $SECRET_KEY_NAME --value $SECRET_VALUE > /dev/null\n if [[ $? -ne 0 ]]; then\n echo \"Error generating secret\"\n fi\n echo \"$SECRET_VALUE\"\nelse\n exit 1\nfi\n", + "check_app_readyScriptMaster": "#!/bin/bash\nset -e\nwhile true; do\n HTTP_STATUS=$(curl -Ik http://localhost:7880/twirp/health | head -n1 | awk '{print $2}')\n if [ $HTTP_STATUS == 200 ]; then\n break\n fi\n sleep 5\ndone\n", + "restartScriptMaster": "#!/bin/bash\nset -e\n# Stop all services\nsystemctl stop openvidu\n\n# Update config from secret\n/usr/local/bin/update_config_from_secret.sh\n\n# Start all services\nsystemctl start openvidu\n", + "installScriptMaster1": "[reduce(items(variables('stringInterpolationParamsMaster1')), createObject('value', variables('installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "installScriptMaster2": "[reduce(items(variables('stringInterpolationParamsMaster2')), createObject('value', variables('installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "installScriptMaster3": "[reduce(items(variables('stringInterpolationParamsMaster3')), createObject('value', variables('installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "installScriptMaster4": "[reduce(items(variables('stringInterpolationParamsMaster4')), createObject('value', variables('installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "after_installScriptMaster": "[reduce(items(variables('stringInterpolationParamsMaster1')), createObject('value', variables('after_installScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "update_config_from_secretScriptMaster": "[reduce(items(variables('stringInterpolationParamsMaster1')), createObject('value', variables('update_config_from_secretScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "update_secret_from_configScriptMaster": "[reduce(items(variables('stringInterpolationParamsMaster1')), createObject('value', variables('update_secret_from_configScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "store_secretScriptMaster": "[reduce(items(variables('stringInterpolationParamsMaster1')), createObject('value', variables('store_secretScriptTemplateMaster')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "base64installMaster1": "[base64(variables('installScriptMaster1'))]", + "base64installMaster2": "[base64(variables('installScriptMaster2'))]", + "base64installMaster3": "[base64(variables('installScriptMaster3'))]", + "base64installMaster4": "[base64(variables('installScriptMaster4'))]", + "base64after_installMaster": "[base64(variables('after_installScriptMaster'))]", + "base64update_config_from_secretMaster": "[base64(variables('update_config_from_secretScriptMaster'))]", + "base64update_secret_from_configMaster": "[base64(variables('update_secret_from_configScriptMaster'))]", + "base64get_value_from_configMaster": "[base64(variables('get_value_from_configScriptMaster'))]", + "base64store_secretMaster": "[base64(variables('store_secretScriptMaster'))]", + "base64check_app_readyMaster": "[base64(variables('check_app_readyScriptMaster'))]", + "base64restartMaster": "[base64(variables('restartScriptMaster'))]", + "userDataParamsMasterNode1": { + "base64install": "[variables('base64installMaster1')]", + "base64after_install": "[variables('base64after_installMaster')]", + "base64update_config_from_secret": "[variables('base64update_config_from_secretMaster')]", + "base64update_secret_from_config": "[variables('base64update_secret_from_configMaster')]", + "base64get_value_from_config": "[variables('base64get_value_from_configMaster')]", + "base64store_secret": "[variables('base64store_secretMaster')]", + "base64check_app_ready": "[variables('base64check_app_readyMaster')]", + "base64restart": "[variables('base64restartMaster')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "1" + }, + "userDataParamsMasterNode2": { + "base64install": "[variables('base64installMaster2')]", + "base64after_install": "[variables('base64after_installMaster')]", + "base64update_config_from_secret": "[variables('base64update_config_from_secretMaster')]", + "base64update_secret_from_config": "[variables('base64update_secret_from_configMaster')]", + "base64get_value_from_config": "[variables('base64get_value_from_configMaster')]", + "base64store_secret": "[variables('base64store_secretMaster')]", + "base64check_app_ready": "[variables('base64check_app_readyMaster')]", + "base64restart": "[variables('base64restartMaster')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "2" + }, + "userDataParamsMasterNode3": { + "base64install": "[variables('base64installMaster3')]", + "base64after_install": "[variables('base64after_installMaster')]", + "base64update_config_from_secret": "[variables('base64update_config_from_secretMaster')]", + "base64update_secret_from_config": "[variables('base64update_secret_from_configMaster')]", + "base64get_value_from_config": "[variables('base64get_value_from_configMaster')]", + "base64store_secret": "[variables('base64store_secretMaster')]", + "base64check_app_ready": "[variables('base64check_app_readyMaster')]", + "base64restart": "[variables('base64restartMaster')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "3" + }, + "userDataParamsMasterNode4": { + "base64install": "[variables('base64installMaster4')]", + "base64after_install": "[variables('base64after_installMaster')]", + "base64update_config_from_secret": "[variables('base64update_config_from_secretMaster')]", + "base64update_secret_from_config": "[variables('base64update_secret_from_configMaster')]", + "base64get_value_from_config": "[variables('base64get_value_from_configMaster')]", + "base64store_secret": "[variables('base64store_secretMaster')]", + "base64check_app_ready": "[variables('base64check_app_readyMaster')]", + "base64restart": "[variables('base64restartMaster')]", + "keyVaultName": "[variables('keyVaultName')]", + "masterNodeNum": "4", + "storageAccountName": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]" + }, + "userDataTemplateMasterNode": "#!/bin/bash -x\nset -eu -o pipefail\n\n# Introduce the scripts in the instance\n# install.sh\necho ${base64install} | base64 -d > /usr/local/bin/install.sh\nchmod +x /usr/local/bin/install.sh\n\n# after_install.sh\necho ${base64after_install} | base64 -d > /usr/local/bin/after_install.sh\nchmod +x /usr/local/bin/after_install.sh\n\n# update_config_from_secret.sh\necho ${base64update_config_from_secret} | base64 -d > /usr/local/bin/update_config_from_secret.sh\nchmod +x /usr/local/bin/update_config_from_secret.sh\n\n# update_secret_from_config.sh\necho ${base64update_secret_from_config} | base64 -d > /usr/local/bin/update_secret_from_config.sh\nchmod +x /usr/local/bin/update_secret_from_config.sh\n\n# get_value_from_config.sh\necho ${base64get_value_from_config} | base64 -d > /usr/local/bin/get_value_from_config.sh\nchmod +x /usr/local/bin/get_value_from_config.sh\n\n# store_secret.sh\necho ${base64store_secret} | base64 -d > /usr/local/bin/store_secret.sh\nchmod +x /usr/local/bin/store_secret.sh\n\n# check_app_ready.sh\necho ${base64check_app_ready} | base64 -d > /usr/local/bin/check_app_ready.sh\nchmod +x /usr/local/bin/check_app_ready.sh\n\n# restart.sh\necho ${base64restart} | base64 -d > /usr/local/bin/restart.sh\nchmod +x /usr/local/bin/restart.sh\n\n# Install azure cli\ncurl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash\n\naz login --identity --allow-no-subscriptions\n\napt-get update && apt-get install -y\n\nexport HOME=\"/root\"\n\n# Install OpenVidu\n/usr/local/bin/install.sh || { echo \"[OpenVidu] error installing OpenVidu\"; exit 1; }\n\n# Start OpenVidu\nsystemctl start openvidu || { echo \"[OpenVidu] error starting OpenVidu\"; exit 1; }\n\n# Update shared secret\n/usr/local/bin/after_install.sh || { echo \"[OpenVidu] error updating shared secret\"; exit 1; }\n\n# Launch on reboot\necho \"@reboot /usr/local/bin/restart.sh >> /var/log/openvidu-restart.log\" 2>&1 | crontab\n\nMASTER_NODE_NUM=${masterNodeNum}\nif [[ $MASTER_NODE_NUM -eq 4 ]]; then\n set +e\n az storage blob upload --account-name ${storageAccountName} --container-name automation-locks --name lock.txt --file /dev/null --auth-mode key\n set -e\n az keyvault secret set --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --value \"true\"\nfi\n\n# Wait for the app\nsleep 150\n/usr/local/bin/check_app_ready.sh\n", + "userDataMasterNode1": "[reduce(items(variables('userDataParamsMasterNode1')), createObject('value', variables('userDataTemplateMasterNode')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "userDataMasterNode2": "[reduce(items(variables('userDataParamsMasterNode2')), createObject('value', variables('userDataTemplateMasterNode')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "userDataMasterNode3": "[reduce(items(variables('userDataParamsMasterNode3')), createObject('value', variables('userDataTemplateMasterNode')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "userDataMasterNode4": "[reduce(items(variables('userDataParamsMasterNode4')), createObject('value', variables('userDataTemplateMasterNode')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "installScriptTemplateMedia": "#!/bin/bash -x\nDOMAIN=\n\n# Install dependencies\napt-get update && apt-get install -y \\\n curl \\\n unzip \\\n jq \\\n wget\n\n# Get own private IP\nPRIVATE_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\")\n\nWAIT_INTERVAL=1\nMAX_WAIT=10000\nELAPSED_TIME=0\nset +e\nwhile true; do\n # get secret value\n FINISH_MASTER_NODE=$(az keyvault secret show --vault-name ${keyVaultName} --name FINISH-MASTER-NODE --query value -o tsv)\n\n # Check if all master nodes finished\n if [ \"$FINISH_MASTER_NODE\" == \"true\" ]; then\n break\n fi\n\n ELAPSED_TIME=$((ELAPSED_TIME + WAIT_INTERVAL))\n\n # Check if the maximum waiting time has been reached\n if [ $ELAPSED_TIME -ge $MAX_WAIT ]; then\n exit 1\n fi\n\n sleep $WAIT_INTERVAL\ndone\nset -e\n\nMASTER_NODE_1_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-1-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_2_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-2-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_3_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-3-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_4_PRIVATE_IP=$(az keyvault secret show --vault-name ${keyVaultName} --name MASTER-NODE-4-PRIVATE-IP --query value -o tsv)\nMASTER_NODE_PRIVATE_IP_LIST=\"$MASTER_NODE_1_PRIVATE_IP,$MASTER_NODE_2_PRIVATE_IP,$MASTER_NODE_3_PRIVATE_IP,$MASTER_NODE_4_PRIVATE_IP\"\nREDIS_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name REDIS-PASSWORD --query value -o tsv)\nENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)\nOPENVIDU_VERSION=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-VERSION --query value -o tsv)\n\n# Base command\nINSTALL_COMMAND=\"sh <(curl -fsSL http://get.openvidu.io/pro/ha/$OPENVIDU_VERSION/install_ov_media_node.sh)\"\n\n# Common arguments\nCOMMON_ARGS=(\n\"--no-tty\"\n\"--install\"\n\"--environment=azure\"\n\"--deployment-type='ha'\"\n\"--node-role='media-node'\"\n\"--master-node-private-ip-list=$MASTER_NODE_PRIVATE_IP_LIST\"\n\"--private-ip=$PRIVATE_IP\"\n\"--enabled-modules='$ENABLED_MODULES'\"\n\"--redis-password=$REDIS_PASSWORD\"\n)\n\n# Construct the final command with all arguments\nFINAL_COMMAND=\"$INSTALL_COMMAND $(printf \"%s \" \"${COMMON_ARGS[@]}\")\"\n\n# Install OpenVidu\nexec bash -c \"$FINAL_COMMAND\"\n", + "stopMediaNodeParams": { + "subscriptionId": "[subscription().subscriptionId]", + "resourceGroupName": "[resourceGroup().name]", + "vmScaleSetName": "[format('{0}-mediaNodeScaleSet', parameters('stackName'))]", + "storageAccountName": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]" + }, + "stop_media_nodesScriptMediaTemplate": "#!/bin/bash\nset -e\n\nif ! (set -o noclobber ; echo > /tmp/global.lock) ; then\n exit 1 # the global.lock already exists\nfi\n\n# Execute if docker is installed\nif [ -x \"$(command -v docker)\" ]; then\n\n echo \"Stopping media node services and waiting for termination...\"\n docker container kill --signal=SIGINT openvidu || true\n docker container kill --signal=SIGINT ingress || true\n docker container kill --signal=SIGINT egress || true\n\n # Wait for running containers to not be openvidu, ingress or egress\n while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == \"true\" ] || \\\n [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == \"true\" ] || \\\n [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == \"true\" ]; do\n echo \"Waiting for containers to stop...\"\n sleep 5\n done\nfi\n\naz login --identity\n\nRESOURCE_GROUP_NAME=${resourceGroupName}\nVM_SCALE_SET_NAME=${vmScaleSetName}\nSUBSCRIPTION_ID=${subscriptionId}\nBEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy \"*\" \"http://169.254.169.254/metadata/instance?api-version=2021-02-01\" | jq -r '.compute.resourceId')\nINSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')\nRESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Compute/virtualMachineScaleSets/$VM_SCALE_SET_NAME\n\nTIMESTAMP=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\naz tag update --resource-id $RESOURCE_ID --operation replace --tags \"STATUS\"=\"HEALTHY\" \"InstanceDeleteTime\"=\"$TIMESTAMP\" \"storageAccount\"=\"${storageAccountName}\"\n\naz vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID\n", + "userDataMediaNodeTemplate": "#!/bin/bash -x\nset -eu -o pipefail\n\n# Introduce the scripts in the instance\n# install.sh\necho ${base64install} | base64 -d > /usr/local/bin/install.sh\nchmod +x /usr/local/bin/install.sh\n\n# stop_media_nodes.sh\necho ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh\nchmod +x /usr/local/bin/stop_media_node.sh\n\napt-get update && apt-get install -y \napt-get install -y jq\n\n# Install azure cli\ncurl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash\n\naz login --identity\n\n# Protect from scale in actions\nRESOURCE_GROUP_NAME=${resourceGroupName}\nVM_SCALE_SET_NAME=${vmScaleSetName}\nBEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy \"*\" \"http://169.254.169.254/metadata/instance?api-version=2021-02-01\" | jq -r '.compute.resourceId')\nINSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')\naz vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-id $INSTANCE_ID --protect-from-scale-in true\n\nexport HOME=\"/root\"\n\n# Install OpenVidu\n/usr/local/bin/install.sh || { echo \"[OpenVidu] error installing OpenVidu\"; exit 1; }\n\n# Start OpenVidu\nsystemctl start openvidu || { echo \"[OpenVidu] error starting OpenVidu\"; exit 1; }\n", + "stop_media_nodesScriptMedia": "[reduce(items(variables('stopMediaNodeParams')), createObject('value', variables('stop_media_nodesScriptMediaTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value]", + "base64stopMediaNode": "[base64(variables('stop_media_nodesScriptMedia'))]", + "isEmptyIp": "[equals(parameters('publicIpAddressResourceName'), '')]", + "turnIsEmptyIp": "[equals(parameters('turnPublicIpAddressResourceName'), '')]", + "lbName": "[format('{0}-loadBalancer', parameters('stackName'))]", + "lbFrontEndName": "LoadBalancerFrontEnd", + "lbBackendPoolNameMasterNode": "LoadBalancerBackEndMasterNode", + "tlbName": "[format('{0}-loadBalancer', parameters('stackName'))]", + "tlbFrontEndName": "LoadBalancerFrontEnd", + "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": "[format('{0}-virtualNetwork', parameters('stackName'))]" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-07-01", + "name": "[variables('keyVaultName')]", + "location": "[variables('location')]", + "properties": { + "enabledForDeployment": true, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true, + "tenantId": "[variables('tenantId')]", + "enableSoftDelete": false, + "accessPolicies": [ + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode1', parameters('stackName'))), '2023-09-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "set", + "list" + ] + } + }, + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode2', parameters('stackName'))), '2023-09-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "set", + "list" + ] + } + }, + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode3', parameters('stackName'))), '2023-09-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "set", + "list" + ] + } + }, + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode4', parameters('stackName'))), '2023-09-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "set", + "list" + ] + } + }, + { + "objectId": "[reference(resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName'))), '2024-07-01', 'full').identity.principalId]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get" + ] + } + }, + { + "objectId": "[variables('deploymentUser')]", + "tenantId": "[variables('tenantId')]", + "permissions": { + "secrets": [ + "get", + "list", + "set", + "delete", + "recover", + "backup", + "restore" + ] + } + } + ], + "sku": { + "name": "standard", + "family": "A" + }, + "networkAcls": { + "defaultAction": "Allow", + "bypass": "AzureServices" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode1', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode2', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode3', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode4', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[format('{0}-VM-MasterNode1', parameters('stackName'))]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('masterNodeInstanceType')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('masterNodeVMSettings').osDiskType]" + }, + "diskSizeGB": "[variables('masterNodeVMSettings').osDiskSize]" + }, + "imageReference": "[variables('masterNodeVMSettings').ubuntuOSVersion]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface1', parameters('stackName')))]" + } + ] + }, + "osProfile": { + "computerName": "[format('{0}-VM-MasterNode1', parameters('stackName'))]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('masterNodeVMSettings').linuxConfiguration]" + }, + "userData": "[base64(variables('userDataMasterNode1'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface1', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[format('{0}-VM-MasterNode2', parameters('stackName'))]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('masterNodeInstanceType')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('masterNodeVMSettings').osDiskType]" + }, + "diskSizeGB": "[variables('masterNodeVMSettings').osDiskSize]" + }, + "imageReference": "[variables('masterNodeVMSettings').ubuntuOSVersion]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface2', parameters('stackName')))]" + } + ] + }, + "osProfile": { + "computerName": "[format('{0}-VM-MasterNode2', parameters('stackName'))]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('masterNodeVMSettings').linuxConfiguration]" + }, + "userData": "[base64(variables('userDataMasterNode2'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface2', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode1', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[format('{0}-VM-MasterNode3', parameters('stackName'))]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('masterNodeInstanceType')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('masterNodeVMSettings').osDiskType]" + }, + "diskSizeGB": "[variables('masterNodeVMSettings').osDiskSize]" + }, + "imageReference": "[variables('masterNodeVMSettings').ubuntuOSVersion]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface3', parameters('stackName')))]" + } + ] + }, + "osProfile": { + "computerName": "[format('{0}-VM-MasterNode3', parameters('stackName'))]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('masterNodeVMSettings').linuxConfiguration]" + }, + "userData": "[base64(variables('userDataMasterNode3'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface3', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode2', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "[format('{0}-VM-MasterNode4', parameters('stackName'))]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('masterNodeInstanceType')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('masterNodeVMSettings').osDiskType]" + }, + "diskSizeGB": "[variables('masterNodeVMSettings').osDiskSize]" + }, + "imageReference": "[variables('masterNodeVMSettings').ubuntuOSVersion]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface4', parameters('stackName')))]" + } + ] + }, + "osProfile": { + "computerName": "[format('{0}-VM-MasterNode4', parameters('stackName'))]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('masterNodeVMSettings').linuxConfiguration]" + }, + "userData": "[base64(variables('userDataMasterNode4'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface4', parameters('stackName')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode3', parameters('stackName')))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('lockstorage{0}', uniqueString(resourceGroup().id)))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachineScaleSets", + "apiVersion": "2024-07-01", + "name": "[format('{0}-mediaNodeScaleSet', parameters('stackName'))]", + "location": "[variables('location')]", + "tags": { + "InstanceDeleteTime": "[parameters('datetime')]", + "storageAccount": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]" + }, + "identity": { + "type": "SystemAssigned" + }, + "sku": { + "name": "[parameters('mediaNodeInstanceType')]", + "tier": "Standard", + "capacity": "[parameters('initialNumberOfMediaNodes')]" + }, + "properties": { + "overprovision": true, + "upgradePolicy": { + "mode": "Automatic" + }, + "singlePlacementGroup": true, + "platformFaultDomainCount": 1, + "virtualMachineProfile": { + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "[variables('mediaNodeVMSettings').osDiskType]" + }, + "diskSizeGB": 50 + }, + "imageReference": "[variables('mediaNodeVMSettings').ubuntuOSVersion]" + }, + "osProfile": { + "computerNamePrefix": "[variables('mediaNodeVMSettings').vmName]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminSshKey')]", + "linuxConfiguration": "[variables('mediaNodeVMSettings').linuxConfiguration]" + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "[format('{0}-mediaNodeNetInterface', parameters('stackName'))]", + "properties": { + "primary": true, + "ipConfigurations": [ + { + "name": "ipconfigMediaNode", + "properties": { + "subnet": { + "id": "[reference(resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName), '2023-11-01').subnets[0].id]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "publicIPAddressConfiguration": { + "name": "publicIPAddressMediaNode", + "properties": { + "publicIPAddressVersion": "IPv4" + } + } + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + } + } + } + ] + }, + "userData": "[base64(reduce(items(createObject('base64install', base64(reduce(items(createObject('privateIPMasterNode1', reference(resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface1', parameters('stackName'))), '2023-11-01').ipConfigurations[0].properties.privateIPAddress, 'privateIPMasterNode2', reference(resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface2', parameters('stackName'))), '2023-11-01').ipConfigurations[0].properties.privateIPAddress, 'privateIPMasterNode3', reference(resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface3', parameters('stackName'))), '2023-11-01').ipConfigurations[0].properties.privateIPAddress, 'privateIPMasterNode4', reference(resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface4', parameters('stackName'))), '2023-11-01').ipConfigurations[0].properties.privateIPAddress, 'keyVaultName', variables('keyVaultName'))), createObject('value', variables('installScriptTemplateMedia')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value), 'base64stop', variables('base64stopMediaNode'))), createObject('value', variables('userDataMediaNodeTemplate')), lambda('curr', 'next', createObject('value', replace(lambdaVariables('curr').value, format('${{{0}}}', lambdaVariables('next').key), lambdaVariables('next').value)))).value)]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface1', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface2', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface3', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-masterNodeNetInterface4', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('lockstorage{0}', uniqueString(resourceGroup().id)))]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName)]" + ] + }, + { + "type": "Microsoft.Insights/autoscalesettings", + "apiVersion": "2022-10-01", + "name": "[format('{0}-autoscaleSettings', parameters('stackName'))]", + "location": "[resourceGroup().location]", + "properties": { + "profiles": [ + { + "name": "openvidu-medianode-autoscale", + "capacity": { + "minimum": "[string(parameters('minNumberOfMediaNodes'))]", + "maximum": "[string(parameters('maxNumberOfMediaNodes'))]", + "default": "[string(parameters('initialNumberOfMediaNodes'))]" + }, + "rules": [ + { + "metricTrigger": { + "metricName": "Percentage CPU", + "metricNamespace": "Microsoft.Compute/virtualMachineScaleSets", + "metricResourceUri": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]", + "statistic": "Average", + "operator": "GreaterThan", + "threshold": "[parameters('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": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]", + "statistic": "Average", + "operator": "LessThan", + "threshold": "[parameters('scaleTargetCPU')]", + "timeAggregation": "Average", + "timeWindow": "PT5M", + "timeGrain": "PT1M" + }, + "scaleAction": { + "direction": "Decrease", + "type": "ChangeCount", + "value": "1", + "cooldown": "PT5M" + } + } + ] + } + ], + "enabled": true, + "targetResourceUri": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAssignmentForMasterNode{0}', format('{0}-VM-MasterNode4', parameters('stackName'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode4', parameters('stackName'))), '2023-09-01', 'full').identity.principalId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}-VM-MasterNode4', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAssignmentForScaleSet{0}', format('{0}-mediaNodeScaleSet', parameters('stackName'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName'))), '2024-07-01', 'full').identity.principalId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Insights/actionGroups", + "apiVersion": "2023-01-01", + "name": "actiongrouptest", + "location": "global", + "properties": { + "groupShortName": "scaleinag", + "enabled": true, + "automationRunbookReceivers": [ + { + "name": "scalein", + "useCommonAlertSchema": false, + "automationAccountId": "[reference(resourceId('Microsoft.Resources/deployments', 'WebhookDeployment'), '2022-09-01').outputs.automationAccountId.value]", + "runbookName": "scaleInRunbook", + "webhookResourceId": "[reference(resourceId('Microsoft.Resources/deployments', 'WebhookDeployment'), '2022-09-01').outputs.webhookId.value]", + "isGlobalRunbook": false, + "serviceUri": "[reference(resourceId('Microsoft.Resources/deployments', 'WebhookDeployment'), '2022-09-01').outputs.webhookUri.value]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'WebhookDeployment')]" + ] + }, + { + "type": "Microsoft.Insights/activityLogAlerts", + "apiVersion": "2020-10-01", + "name": "ScaleInAlertRule", + "location": "global", + "properties": { + "scopes": [ + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ], + "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": "[resourceId('Microsoft.Insights/actionGroups', 'actiongrouptest')]" + } + ] + }, + "enabled": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/actionGroups', 'actiongrouptest')]", + "[resourceId('Microsoft.Compute/virtualMachineScaleSets', format('{0}-mediaNodeScaleSet', parameters('stackName')))]" + ] + }, + { + "condition": "[equals(variables('isEmptyIp'), true())]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "name": "[format('{0}-publicIPAddressLoadBalancer', parameters('stackName'))]", + "location": "[variables('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static" + } + }, + { + "condition": "[equals(variables('turnTLSIsEnabled'), true())]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "name": "[format('{0}-publicIPAddressTurnTLSLoadBalancer', parameters('stackName'))]", + "location": "[variables('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static" + } + }, + { + "type": "Microsoft.Network/loadBalancers", + "apiVersion": "2024-05-01", + "name": "[variables('lbName')]", + "location": "[variables('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "frontendIPConfigurations": [ + { + "name": "[variables('lbFrontEndName')]", + "properties": { + "publicIPAddress": { + "id": "[if(variables('isEmptyIp'), resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIPAddressLoadBalancer', parameters('stackName'))), resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIpAddressResourceName')))]" + } + } + } + ], + "backendAddressPools": [ + { + "name": "[variables('lbBackendPoolNameMasterNode')]" + } + ], + "loadBalancingRules": [ + { + "name": "HTTPSRuleforMasterNode", + "properties": { + "frontendIPConfiguration": { + "id": "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', variables('lbName'), variables('lbFrontEndName'))]" + }, + "backendAddressPool": { + "id": "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', variables('lbName'), variables('lbBackendPoolNameMasterNode'))]" + }, + "frontendPort": 443, + "backendPort": 443, + "enableFloatingIP": false, + "protocol": "Tcp", + "enableTcpReset": true, + "loadDistribution": "Default", + "disableOutboundSnat": true, + "probe": { + "id": "[resourceId('Microsoft.Network/loadBalancers/probes', variables('lbName'), 'probeForHTTPSRuleMasterNode')]" + } + } + }, + { + "name": "RTMPRuleforMasterNode", + "properties": { + "frontendIPConfiguration": { + "id": "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', variables('lbName'), variables('lbFrontEndName'))]" + }, + "backendAddressPool": { + "id": "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', variables('lbName'), variables('lbBackendPoolNameMasterNode'))]" + }, + "frontendPort": 1935, + "backendPort": 1945, + "enableFloatingIP": false, + "protocol": "Tcp", + "enableTcpReset": true, + "loadDistribution": "Default", + "disableOutboundSnat": true, + "probe": { + "id": "[resourceId('Microsoft.Network/loadBalancers/probes', variables('lbName'), 'probeForRTMPRuleMasterNode')]" + } + } + }, + { + "name": "HTTPRuleforMasterNode", + "properties": { + "frontendIPConfiguration": { + "id": "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', variables('lbName'), variables('lbFrontEndName'))]" + }, + "backendAddressPool": { + "id": "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', variables('lbName'), variables('lbBackendPoolNameMasterNode'))]" + }, + "frontendPort": 80, + "backendPort": 80, + "enableFloatingIP": false, + "protocol": "Tcp", + "enableTcpReset": true, + "loadDistribution": "Default", + "disableOutboundSnat": true, + "probe": { + "id": "[resourceId('Microsoft.Network/loadBalancers/probes', variables('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": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIPAddressLoadBalancer', parameters('stackName')))]" + ] + }, + { + "condition": "[equals(variables('turnTLSIsEnabled'), true())]", + "type": "Microsoft.Network/loadBalancers", + "apiVersion": "2024-05-01", + "name": "[variables('tlbName')]", + "location": "[variables('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "frontendIPConfigurations": [ + { + "name": "[variables('tlbFrontEndName')]", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "privateIPAddressVersion": "IPv4", + "publicIPAddress": { + "id": "[if(variables('turnIsEmptyIp'), resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIPAddressLoadBalancer', parameters('stackName'))), resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIpAddressResourceName')))]" + } + } + } + ], + "backendAddressPools": [ + { + "name": "[variables('lbBackendPoolNameMasterNode')]" + } + ], + "loadBalancingRules": [ + { + "name": "TURNTLSRuleforMasterNode", + "properties": { + "frontendIPConfiguration": { + "id": "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', variables('tlbName'), variables('tlbFrontEndName'))]" + }, + "backendAddressPool": { + "id": "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', variables('tlbName'), variables('lbBackendPoolNameMasterNode'))]" + }, + "frontendPort": 443, + "backendPort": 443, + "enableFloatingIP": false, + "protocol": "Tcp", + "enableTcpReset": true, + "loadDistribution": "Default", + "disableOutboundSnat": true, + "probe": { + "id": "[resourceId('Microsoft.Network/loadBalancers/probes', variables('tlbName'), 'probeForHTTPSRuleMasterNode')]" + } + } + } + ], + "probes": [ + { + "name": "probeForTURNTLSRuleMasterNode", + "properties": { + "protocol": "Http", + "requestPath": "/", + "port": 443, + "probeThreshold": 3, + "intervalInSeconds": 10, + "numberOfProbes": 5 + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIPAddressLoadBalancer', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/natGateways", + "apiVersion": "2021-05-01", + "name": "[format('{0}-natGateway', parameters('stackName'))]", + "location": "[variables('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "idleTimeoutInMinutes": 4, + "publicIpAddresses": [ + { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIPnatGateway', parameters('stackName')))]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', format('{0}-publicIPnatGateway', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2021-05-01", + "name": "[format('{0}-publicIPnatGateway', parameters('stackName'))]", + "location": "[variables('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static", + "idleTimeoutInMinutes": 4 + } + }, + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2023-11-01", + "name": "[variables('networkSettings').vNetName]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('networkSettings').vNetAddressPrefix]" + ] + }, + "subnets": [ + { + "name": "subnetForMediaNodes", + "properties": { + "addressPrefix": "[variables('networkSettings').subnetAddressPrefixMedia]", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]", + "properties": { + "addressPrefix": "[variables('networkSettings').subnetAddressPrefixMaster1]", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "natGateway": { + "id": "[resourceId('Microsoft.Network/natGateways', format('{0}-natGateway', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/natGateways', format('{0}-natGateway', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName)]" + ] + }, + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', variables('networkSettings').vNetName, 'secondSubnetForMasterNodes')]", + "properties": { + "addressPrefix": "[variables('networkSettings').subnetAddressPrefixMaster2]", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "natGateway": { + "id": "[resourceId('Microsoft.Network/natGateways', format('{0}-natGateway', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/natGateways', format('{0}-natGateway', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks', variables('networkSettings').vNetName)]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNodeNetInterface1', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "primaryIPConfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "loadBalancerBackendAddressPools": [ + { + "id": "[reference(resourceId('Microsoft.Network/loadBalancers', variables('lbName')), '2024-05-01').backendAddressPools[0].id]" + } + ] + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/loadBalancers', variables('lbName'))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNodeNetInterface2', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "primaryIPConfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'secondSubnetForMasterNodes')]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "loadBalancerBackendAddressPools": [ + { + "id": "[reference(resourceId('Microsoft.Network/loadBalancers', variables('lbName')), '2024-05-01').backendAddressPools[0].id]" + } + ] + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/loadBalancers', variables('lbName'))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'secondSubnetForMasterNodes')]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNodeNetInterface3', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "primaryIPConfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "loadBalancerBackendAddressPools": [ + { + "id": "[reference(resourceId('Microsoft.Network/loadBalancers', variables('lbName')), '2024-05-01').backendAddressPools[0].id]" + } + ] + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/loadBalancers', variables('lbName'))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'firstSubnetForMasterNodes')]" + ] + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNodeNetInterface4', parameters('stackName'))]", + "location": "[variables('location')]", + "properties": { + "ipConfigurations": [ + { + "name": "primaryIPConfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'secondSubnetForMasterNodes')]" + }, + "applicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "loadBalancerBackendAddressPools": [ + { + "id": "[reference(resourceId('Microsoft.Network/loadBalancers', variables('lbName')), '2024-05-01').backendAddressPools[0].id]" + } + ] + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/loadBalancers', variables('lbName'))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('networkSettings').vNetName, 'secondSubnetForMasterNodes')]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}-masterNodeNSG', parameters('stackName'))]", + "location": "[variables('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" + } + } + ] + } + }, + { + "type": "Microsoft.Network/applicationSecurityGroups", + "apiVersion": "2024-03-01", + "name": "[format('{0}-masterNodeASG', parameters('stackName'))]", + "location": "[variables('location')]" + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'loadBalancer_to_masterNode_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "443", + "access": "Allow", + "priority": 110, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_REDIS_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "7000", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7001", + "access": "Allow", + "priority": 120, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_REDIS_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "7000", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7001", + "access": "Allow", + "priority": 130, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_MINIO_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9100", + "access": "Allow", + "priority": 140, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_MINIO_CONSOLE_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9101", + "access": "Allow", + "priority": 150, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_MINIO_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9100", + "access": "Allow", + "priority": 160, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_MONGO_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "20000", + "access": "Allow", + "priority": 170, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_MONGO_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "20000", + "access": "Allow", + "priority": 180, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_MIMIRGRPC_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9095", + "access": "Allow", + "priority": 190, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_MIMIRGOSSIP_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7946", + "access": "Allow", + "priority": 200, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_MIMIR_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9009", + "access": "Allow", + "priority": 210, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_LOKIGRPC_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9096", + "access": "Allow", + "priority": 220, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_LOKIGOSSIP_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7947", + "access": "Allow", + "priority": 230, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_DASHBOARD_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "5000", + "access": "Allow", + "priority": 240, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_GRAFANA_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "3000", + "access": "Allow", + "priority": 250, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_LOKI_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "3100", + "access": "Allow", + "priority": 260, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_V2COMPATIBILITY_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "4443", + "access": "Allow", + "priority": 270, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_V2COMPATIBILITY_WEBHOOK_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "4443", + "access": "Allow", + "priority": 280, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'masterNode_to_masterNode_DEFAULTAPP_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "6080", + "access": "Allow", + "priority": 290, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-masterNodeNSG', parameters('stackName')), 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "6080", + "access": "Allow", + "priority": 300, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-masterNodeNSG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2023-11-01", + "name": "[format('{0}-mediaNodeNSG', parameters('stackName'))]", + "location": "[variables('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": "*", + "destinationPortRanges": [ + "50000", + "60000" + ], + "access": "Allow", + "priority": 140, + "direction": "Inbound" + } + } + ] + } + }, + { + "type": "Microsoft.Network/applicationSecurityGroups", + "apiVersion": "2024-03-01", + "name": "[format('{0}-mediaNodeASG', parameters('stackName'))]", + "location": "[variables('location')]" + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNodeNSG', parameters('stackName')), 'loadBalancer_to_mediaNode_RTMP_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "AzureLoadBalancer", + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "1945", + "access": "Allow", + "priority": 150, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNodeNSG', parameters('stackName')), 'loadBalancer_to_mediaNode_HEALTHCHECK_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "AzureLoadBalancer", + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "9092", + "access": "Allow", + "priority": 160, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "condition": "[equals(variables('turnTLSIsEnabled'), true())]", + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNodeNSG', parameters('stackName')), 'loadbalancer_to_mediaNode_TURN_TLS_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "AzureLoadBalancer", + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "5349", + "access": "Allow", + "priority": 170, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "condition": "[equals(variables('turnTLSIsEnabled'), true())]", + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNodeNSG', parameters('stackName')), 'masterNode_to_mediaNode_TURN_TLSHEALTHCHECK_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceAddressPrefix": "AzureLoadBalancer", + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7880", + "access": "Allow", + "priority": 180, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNodeNSG', parameters('stackName')), 'masterNode_to_mediaNode_SERVER_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "7880", + "access": "Allow", + "priority": 190, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2023-11-01", + "name": "[format('{0}/{1}', format('{0}-mediaNodeNSG', parameters('stackName')), 'masterNode_to_mediaNode_CLIENT_INGRESS')]", + "properties": { + "protocol": "Tcp", + "sourceApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]" + } + ], + "sourcePortRange": "*", + "destinationApplicationSecurityGroups": [ + { + "id": "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]" + } + ], + "destinationPortRange": "8080", + "access": "Allow", + "priority": 200, + "direction": "Inbound" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-masterNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/applicationSecurityGroups', format('{0}-mediaNodeASG', parameters('stackName')))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-mediaNodeNSG', parameters('stackName')))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "[format('lockstorage{0}', uniqueString(resourceGroup().id))]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "accessTier": "Cool", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/default/automation-locks', format('lockstorage{0}', uniqueString(resourceGroup().id)))]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', format('lockstorage{0}', uniqueString(resourceGroup().id)))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "WebhookDeployment", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "runbookName": { + "value": "scaleInRunbook" + }, + "webhookName": { + "value": "webhookForScaleIn" + }, + "WebhookExpiryTime": { + "value": "2035-03-30T00:00:00Z" + }, + "_artifactsLocation": { + "value": "https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "automationAccountName": { + "type": "String", + "metadata": { + "description": "Automation account name" + } + }, + "webhookName": { + "type": "String", + "metadata": { + "description": "Webhook Name" + } + }, + "runbookName": { + "type": "String", + "metadata": { + "description": "Runbook Name for which webhook will be created" + } + }, + "WebhookExpiryTime": { + "type": "String", + "metadata": { + "description": "Webhook Expiry time" + } + }, + "_artifactsLocation": { + "defaultValue": "https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1", + "type": "String", + "metadata": { + "description": "URI to artifacts location" + } + } + }, + "resources": [ + { + "type": "Microsoft.Automation/automationAccounts", + "apiVersion": "2020-01-13-preview", + "name": "[parameters('automationAccountName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "sku": { + "name": "Basic" + } + }, + "resources": [ + { + "type": "runbooks", + "apiVersion": "2018-06-30", + "name": "[parameters('runbookName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[parameters('automationAccountName')]" + ], + "properties": { + "runbookType": "PowerShell72", + "logProgress": "true", + "description": "Scale In Runbook", + "publishContentLink": { + "uri": "[parameters('_artifactsLocation')]", + "version": "1.0.0.0" + } + } + }, + { + "type": "webhooks", + "apiVersion": "2018-06-30", + "name": "[parameters('webhookName')]", + "dependsOn": [ + "[parameters('automationAccountName')]", + "[parameters('runbookName')]" + ], + "properties": { + "isEnabled": true, + "expiryTime": "[parameters('WebhookExpiryTime')]", + "runbook": { + "name": "[parameters('runbookName')]" + } + } + } + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAutomationContributorAssignmentAutomationAccount{0}', parameters('automationAccountName')))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName')), '2023-11-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + } + ], + "outputs": { + "webhookUri": { + "type": "String", + "value": "[reference(parameters('webhookName')).uri]" + }, + "automationAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + }, + "webhookId": { + "type": "string", + "value": "[resourceId('Microsoft.Automation/automationAccounts/webhooks', parameters('automationAccountName'), parameters('webhookName'))]" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/openvidu-deployment/pro/ha/azure/createUiDefinition.json b/openvidu-deployment/pro/ha/azure/createUiDefinition.json new file mode 100644 index 00000000..e69de29b diff --git a/openvidu-deployment/pro/ha/install_ov_master_node.sh b/openvidu-deployment/pro/ha/install_ov_master_node.sh new file mode 100644 index 00000000..a31a04fd --- /dev/null +++ b/openvidu-deployment/pro/ha/install_ov_master_node.sh @@ -0,0 +1,165 @@ +#!/bin/sh +# Docker & Docker Compose will need to be installed on the machine +set -eu +export DOCKER_VERSION="${DOCKER_VERSION:-27.5.1}" +export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.32.4}" +export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" +export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" +export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.2.7-debian-12-r0}" +export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-02-08T19-14-21Z}" +export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.4}" +export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.2-alpine}" +export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}" +export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}" +export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}" +export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}" +export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}" +export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}" +export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.0}" +export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.1.0}" +export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.3.2}" +export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.3.2}" +export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.15.0}" +export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.5.1}" + +wait_for_docker() { + echo "Waiting for Docker to start..." + + # Set a countdown (in seconds) + COUNTDOWN=60 + + while [ "$COUNTDOWN" -gt 0 ]; do + if docker info >/dev/null 2>&1; then + echo "Docker started successfully." + break + else + # Reduce the countdown by 1 each iteration. + COUNTDOWN=$(( COUNTDOWN - 1 )) + + if [ "$COUNTDOWN" -eq 0 ]; then + echo "ERROR: Docker did not start within the allocated time." + break + fi + + sleep 1 + fi + done +} + +# Check if executing as root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if ! command -v docker > /dev/null 2>&1 +then + curl -fsSL https://get.docker.com -o /tmp/get-docker.sh + sh /tmp/get-docker.sh --version "${DOCKER_VERSION}" || { echo "Can't install Docker automatically. Install it manually and run this script again"; exit 1; } +else + echo "Docker already installed. Check you have the latest version for best compatibility" +fi + +if ! command -v docker-compose > /dev/null 2>&1 +then + TIME_LIMIT_SECONDS=20 + START_TIME=$(awk 'BEGIN{srand(); print srand()}') + while true + do + CURRENT_TIME=$(awk 'BEGIN{srand(); print srand()}') + if [ $((CURRENT_TIME-START_TIME)) -gt $TIME_LIMIT_SECONDS ]; then + echo "Error downloading docker-compose. Could not download it in $TIME_LIMIT_SECONDS seconds" + rm -rf /usr/local/bin/docker-compose + exit 1 + fi + STATUS_RECEIVED=$(curl --retry 5 --retry-max-time 40 --write-out "%{http_code}\n" -L "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose) + CURL_EXIT_CODE=$? + if [ $CURL_EXIT_CODE -ne 0 ]; then + echo "Error downloading docker-compose. curl failed with exit code $CURL_EXIT_CODE. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + if [ "${STATUS_RECEIVED}" -ne "200" ]; then + echo "Error downloading docker-compose. Received HTTP status code $STATUS_RECEIVED. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + echo "Success downloading docker-compose" + chmod 755 /usr/local/bin/docker-compose + break + done + + # Create a symbolic link to docker-compose in the Docker CLI plugins directory + # so docker compose can be used also + mkdir -p /usr/local/lib/docker/cli-plugins + ln -s /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose +else + echo "Docker Compose already installed. Check you have the latest version for best compatibility" +fi + +# Restart Docker and wait for it to start +systemctl enable docker +systemctl stop docker +systemctl start docker +wait_for_docker + +# Create random temp directory +TMP_DIR=$(mktemp -d) +docker pull "${INSTALLER_IMAGE}" + +# Generate installation scripts +COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \ + -e OPENVIDU_VERSION=$OPENVIDU_VERSION \ + -e CADDY_SERVER_IMAGE=$CADDY_SERVER_IMAGE \ + -e CADDY_SERVER_PRO_IMAGE=$CADDY_SERVER_PRO_IMAGE \ + -e MINIO_SERVER_IMAGE=$MINIO_SERVER_IMAGE \ + -e MINIO_CLIENT_IMAGE=$MINIO_CLIENT_IMAGE \ + -e MONGO_SERVER_IMAGE=$MONGO_SERVER_IMAGE \ + -e REDIS_SERVER_IMAGE=$REDIS_SERVER_IMAGE \ + -e BUSYBOX_IMAGE=$BUSYBOX_IMAGE \ + -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ + -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ + -e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \ + -e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \ + -e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \ + -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ + -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ + -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ + -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ + -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ + -e PROMTAIL_IMAGE=$PROMTAIL_IMAGE \ + -e LOKI_IMAGE=$LOKI_IMAGE \ + -e MIMIR_IMAGE=$MIMIR_IMAGE \ + -e GRAFANA_IMAGE=$GRAFANA_IMAGE \ + ${INSTALLER_IMAGE} \ + --deployment-type=ha \ + --node-role=master-node \ + --install \ + $*" + +INTERACTIVE_MODE=true +for arg in "$@"; do + if [ "$arg" = "--no-tty" ]; then + INTERACTIVE_MODE=false; + break + fi +done + +if [ "$INTERACTIVE_MODE" = true ]; then + docker run -it ${COMMON_DOCKER_OPTIONS} > /dev/tty +else + docker run -i ${COMMON_DOCKER_OPTIONS} +fi + +cd "$TMP_DIR/installation-scripts/openvidu/" +chmod +x install_ov_master_node.sh +./install_ov_master_node.sh + +cat finish-message.txt diff --git a/openvidu-deployment/pro/ha/install_ov_media_node.sh b/openvidu-deployment/pro/ha/install_ov_media_node.sh new file mode 100644 index 00000000..275d435f --- /dev/null +++ b/openvidu-deployment/pro/ha/install_ov_media_node.sh @@ -0,0 +1,173 @@ +#!/bin/sh +# Docker & Docker Compose will need to be installed on the machine +set -eu +export DOCKER_VERSION="${DOCKER_VERSION:-27.5.1}" +export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.32.4}" +export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" +export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" +export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.2.7-debian-12-r0}" +export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-02-08T19-14-21Z}" +export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.4}" +export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.2-alpine}" +export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}" +export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}" +export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}" +export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}" +export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}" +export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}" +export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}" +export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}" +export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.0}" +export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.1.0}" +export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.3.2}" +export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.3.2}" +export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.15.0}" +export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.5.1}" + +wait_for_docker() { + echo "Waiting for Docker to start..." + + # Set a countdown (in seconds) + COUNTDOWN=60 + + while [ "$COUNTDOWN" -gt 0 ]; do + if docker info >/dev/null 2>&1; then + echo "Docker started successfully." + break + else + # Reduce the countdown by 1 each iteration. + COUNTDOWN=$(( COUNTDOWN - 1 )) + + if [ "$COUNTDOWN" -eq 0 ]; then + echo "ERROR: Docker did not start within the allocated time." + break + fi + + sleep 1 + fi + done +} + +# Check if executing as root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if ! command -v docker > /dev/null 2>&1 +then + curl -fsSL https://get.docker.com -o /tmp/get-docker.sh + sh /tmp/get-docker.sh --version "${DOCKER_VERSION}" || { echo "Can't install Docker automatically. Install it manually and run this script again"; exit 1; } +else + echo "Docker already installed. Check you have the latest version for best compatibility" +fi + +if ! command -v docker-compose > /dev/null 2>&1 +then + TIME_LIMIT_SECONDS=20 + START_TIME=$(awk 'BEGIN{srand(); print srand()}') + while true + do + CURRENT_TIME=$(awk 'BEGIN{srand(); print srand()}') + if [ $((CURRENT_TIME-START_TIME)) -gt $TIME_LIMIT_SECONDS ]; then + echo "Error downloading docker-compose. Could not download it in $TIME_LIMIT_SECONDS seconds" + rm -rf /usr/local/bin/docker-compose + exit 1 + fi + STATUS_RECEIVED=$(curl --retry 5 --retry-max-time 40 --write-out "%{http_code}\n" -L "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose) + CURL_EXIT_CODE=$? + if [ $CURL_EXIT_CODE -ne 0 ]; then + echo "Error downloading docker-compose. curl failed with exit code $CURL_EXIT_CODE. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + if [ "${STATUS_RECEIVED}" -ne "200" ]; then + echo "Error downloading docker-compose. Received HTTP status code $STATUS_RECEIVED. There are still $((TIME_LIMIT_SECONDS - (CURRENT_TIME - START_TIME))) seconds left to retry..." + rm -rf /usr/local/bin/docker-compose + sleep 2 + continue + fi + echo "Success downloading docker-compose" + chmod 755 /usr/local/bin/docker-compose + break + done + + # Create a symbolic link to docker-compose in the Docker CLI plugins directory + # so docker compose can be used also + mkdir -p /usr/local/lib/docker/cli-plugins + ln -s /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose +else + echo "Docker Compose already installed. Check you have the latest version for best compatibility" +fi + +# Restart Docker and wait for it to start +systemctl enable docker +systemctl stop docker +systemctl start docker +wait_for_docker + +# Create random temp directory +TMP_DIR=$(mktemp -d) +docker pull "${INSTALLER_IMAGE}" + +# Generate installation scripts +COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \ + -e OPENVIDU_VERSION=$OPENVIDU_VERSION \ + -e CADDY_SERVER_IMAGE=$CADDY_SERVER_IMAGE \ + -e CADDY_SERVER_PRO_IMAGE=$CADDY_SERVER_PRO_IMAGE \ + -e MINIO_SERVER_IMAGE=$MINIO_SERVER_IMAGE \ + -e MINIO_CLIENT_IMAGE=$MINIO_CLIENT_IMAGE \ + -e MONGO_SERVER_IMAGE=$MONGO_SERVER_IMAGE \ + -e REDIS_SERVER_IMAGE=$REDIS_SERVER_IMAGE \ + -e BUSYBOX_IMAGE=$BUSYBOX_IMAGE \ + -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ + -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ + -e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \ + -e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \ + -e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \ + -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ + -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ + -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ + -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ + -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ + -e PROMTAIL_IMAGE=$PROMTAIL_IMAGE \ + -e LOKI_IMAGE=$LOKI_IMAGE \ + -e MIMIR_IMAGE=$MIMIR_IMAGE \ + -e GRAFANA_IMAGE=$GRAFANA_IMAGE \ + ${INSTALLER_IMAGE} \ + --deployment-type=ha \ + --node-role=media-node \ + --install \ + $*" + +INTERACTIVE_MODE=true +for arg in "$@"; do + if [ "$arg" = "--no-tty" ]; then + INTERACTIVE_MODE=false; + break + fi +done + +if [ "$INTERACTIVE_MODE" = true ]; then + docker run -it ${COMMON_DOCKER_OPTIONS} > /dev/tty +else + docker run -i ${COMMON_DOCKER_OPTIONS} +fi + +cd "$TMP_DIR/installation-scripts/openvidu/" +chmod +x install_ov_media_node.sh +./install_ov_media_node.sh + +cat finish-message.txt + +# Warn about private IP being setup correctly +echo +echo "ATTENTION!!! This is the private IP of this 'Media Node'. Make sure this IP is reachable from all the 'Master Node'" +cat private-ip.txt +echo "If this is not your private IP, reinstall the 'Media Node' with the correct '--private-ip' parameter" + +echo diff --git a/openvidu-deployment/pro/shared/scaleInRunbook.ps1 b/openvidu-deployment/pro/shared/scaleInRunbook.ps1 new file mode 100644 index 00000000..87ec06bc --- /dev/null +++ b/openvidu-deployment/pro/shared/scaleInRunbook.ps1 @@ -0,0 +1,166 @@ +<# + .DESCRIPTION + A runbook that will scale in the Media Nodes gracefully in OpenVidu + + .NOTES + AUTHOR: Sergio Fernández Gómez + LAST EDIT: March 24, 2025 +#> +param +( + [Parameter (Mandatory=$false)] + [object] $WebhookData +) +$ErrorActionPreference = "stop" + +if (!($WebhookData)) { + Write-Error "This runbook is meant to be started from an Azure alert webhook only." + exit +} + +# Get the data object from WebhookData +$WebhookBody = (ConvertFrom-Json -InputObject $WebhookData.RequestBody) +# Get the info needed to identify the VM (depends on the payload schema) +$schemaId = $WebhookBody.schemaId + +# Check if the schemaId is the one we can manage +if (!($schemaId -eq "Microsoft.Insights/activityLogs")) { + Write-Error "The alert data schema - $schemaId - is not supported." + exit 1 +} + +# This is the Activity Log Alert schema +$AlertContext = [object] (($WebhookBody.data).context).activityLog +$ResourceGroupName = $AlertContext.resourceGroupName +$ResourceType = $AlertContext.resourceType +$SubscriptionId = $AlertContext.subscriptionId +$ResourceName = (($AlertContext.resourceId).Split("/"))[-1] +$status = ($WebhookBody.data).status + +# Check if the status is not activated to leave the runbook +if (!($status -eq "Activated")) { + Write-Error "No action taken. Alert status: $status" + exit 1 +} +# Determine code path depending on the resourceType +if (!($ResourceType -eq "Microsoft.Compute/virtualMachineScaleSets")) { + Write-Error "$ResourceType is not a supported resource type for this runbook." + exit 1 +} + +# Ensures you do not inherit an AzContext in your runbook +Disable-AzContextAutosave -Scope Process + +#Login into azure +try { + # Connect to Azure with system-assigned managed identity + $AzureContext = (Connect-AzAccount -Identity).context + # set and store context + $AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext +} +catch { + Write-Error -Message $_.Exception + throw $_.Exception +} + +################################################################################################################# +#Here the runbook is logged in azure and nothing else is done +################################################################################################################# + + +######################################## LOCK ########################################## +Import-Module Az.Storage +$VMSS = Get-AzVmss -ResourceGroupName $ResourceGroupName -VMScaleSetName $ResourceName +$StorageAccountName = $VMSS.Tags["storageAccount"] +$StorageAccountKey = (Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName)[0].Value +$Context = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey +#$blob = Get-AzureStorageBlob -Context $storageContext -Container $ContainerName -Blob $BlobName -ErrorAction Stop +#$leaseStatus = $blob.ICloudBlob.Properties.LeaseStatus; +#If($leaseStatus -eq "Locked") +#{ +# $blob.ICloudBlob.BreakLease() +# Write-Host "Successfully broken lease on '$BlobName' blob." +#} +$Lease = az storage blob lease acquire -b "lock.txt" -c "automation-locks" --account-name $StorageAccountName --account-key $StorageAccountKey + +if (-not $Lease) { + Write-Output "Lock is already held. Exiting." + exit 0 +} + +try +{ + ######################################## CHECKS ########################################## + + #Get the timestamp of the event that triggered the runbook + $EventTimestamp = $WebhookBody.data.context.activityLog.eventTimestamp + + $DateTag = [datetime]$VMSS.Tags["InstanceDeleteTime"] + $DateEventTimestamp = [datetime]$EventTimeStamp + + "Checking if the event was launched before the last instance was deleted" + if ($DateEventTimestamp -lt $DateTag) { + Write-Output "The event was launched before the last instance was deleted. Exiting..." + exit 1 + } + "Done checking" + + + # Get the instances and select the index 0 instance to check if runcommand is running on it and later invoke the run command + $InstancesInVMSS = Get-AzVmssVM -ResourceGroupName $ResourceGroupName -VMScaleSetName $ResourceName + $InstanceCount = $InstancesInVMSS.Count + + "Checking if theres more than 1 instance in the VMSS" + if ($InstanceCount -le 1) { + "There is only one instance in the VMSS. Exiting..." + exit 1 # Exit the script if there is only one instance + } + + + # Check the tags in the VMSS to see if there is a tag with value TERMINATING + "Checking TAG for TERMINATING" + if($VMSS.Tags.Values -contains "TERMINATING"){ + "Found 'TERMINATING' tag so this runbook will not execute." + exit 1 + } + + ######################################## MODIFIYING ########################################## + + $VMSS.Tags["STATUS"] = "TERMINATING" + "Terminating not found changing TAG" + Set-AzResource -ResourceId $VMSS.Id -Tag $VMSS.Tags -Force + "TAG updated" + + # If no VM has been selected previously, select the VM with instance_id 0 and tag it as TERMINATING instance + $InstanceId = $InstancesInVMSS[0].InstanceId + + "Checking if one Run Command is executing" + + # Iterate through each instance and check if RunCommand is still running + foreach ($Instance in $InstancesInVMSS) { + $runCommandStatus = Get-AzVmssVMRunCommand -ResourceGroupName $ResourceGroupName -VMScaleSetName $ResourceName -InstanceId $Instance.InstanceId + + # Check if the RunCommand is still running + if ($runCommandStatus.ProvisioningState -eq "Running") { + Write-Output "Instance $($Instance.InstanceId) is still running a command. Exiting..." + exit 1 # Exit the script if any instance is still running the command + } + } + "Done checking" + + "Sending RunCommand" + $Token = (Get-AzAccessToken).Token + $Uri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachineScaleSets/$ResourceName/virtualMachines/$InstanceId/runCommand?api-version=2021-11-01" + + $Body = @{ + commandId = 'RunShellScript' + script = @('sudo /usr/local/bin/stop_media_node.sh') + } | ConvertTo-Json -Depth 3 + + Invoke-RestMethod -Uri $Uri -Method POST -Headers @{ Authorization = "Bearer $Token" } -Body $Body -ContentType "application/json" + "RunCommand sent" +} +finally +{ + az storage blob lease release -b "lock.txt" -c "automation-locks" --account-name $StorageAccountName --account-key $StorageAccountKey --lease-id $Lease +} \ No newline at end of file diff --git a/openvidu-deployment/pro/shared/webhookdeployment.json b/openvidu-deployment/pro/shared/webhookdeployment.json new file mode 100644 index 00000000..57287227 --- /dev/null +++ b/openvidu-deployment/pro/shared/webhookdeployment.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "automationAccountName": { + "type": "String", + "metadata": { + "description": "Automation account name" + } + }, + "webhookName": { + "type": "String", + "metadata": { + "description": "Webhook Name" + } + }, + "runbookName": { + "type": "String", + "metadata": { + "description": "Runbook Name for which webhook will be created" + } + }, + "WebhookExpiryTime": { + "type": "String", + "metadata": { + "description": "Webhook Expiry time" + } + }, + "_artifactsLocation": { + "defaultValue": "https://raw.githubusercontent.com/Piwccle/AzureScaleIn/refs/heads/main/scaleInRunbook.ps1", + "type": "String", + "metadata": { + "description": "URI to artifacts location" + } + } + }, + "resources": [ + { + "type": "Microsoft.Automation/automationAccounts", + "apiVersion": "2020-01-13-preview", + "name": "[parameters('automationAccountName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "sku": { + "name": "Basic" + } + }, + "resources": [ + { + "type": "runbooks", + "apiVersion": "2018-06-30", + "name": "[parameters('runbookName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[parameters('automationAccountName')]" + ], + "properties": { + "runbookType": "PowerShell72", + "logProgress": "true", + "description": "Scale In Runbook", + "publishContentLink": { + "uri": "[parameters('_artifactsLocation')]", + "version": "1.0.0.0" + } + } + }, + { + "type": "webhooks", + "apiVersion": "2018-06-30", + "name": "[parameters('webhookName')]", + "dependsOn": [ + "[parameters('automationAccountName')]", + "[parameters('runbookName')]" + ], + "properties": { + "isEnabled": true, + "expiryTime": "[parameters('WebhookExpiryTime')]", + "runbook": { + "name": "[parameters('runbookName')]" + } + } + } + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('roleAutomationContributorAssignmentAutomationAccount{0}', parameters('automationAccountName')))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName')), '2023-11-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + } + ], + "outputs": { + "webhookUri": { + "type": "String", + "value": "[reference(parameters('webhookName')).uri]" + }, + "automationAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + }, + "webhookId": { + "type": "string", + "value": "[resourceId('Microsoft.Automation/automationAccounts/webhooks', parameters('automationAccountName'), parameters('webhookName'))]" + } + } +} \ No newline at end of file diff --git a/openvidu-deployment/update.sh b/openvidu-deployment/update.sh new file mode 100644 index 00000000..49c076f4 --- /dev/null +++ b/openvidu-deployment/update.sh @@ -0,0 +1,63 @@ +#!/bin/sh +set -eu +export INSTALL_PREFIX="${INSTALL_PREFIX:-/opt/openvidu}" +export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" +export REGISTRY="${REGISTRY:-docker.io}" +export UPDATER_IMAGE="${UPDATER_IMAGE:-${REGISTRY}/openvidu/openvidu-updater:${OPENVIDU_VERSION}}" + +# Check if executing as root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +# Check if docker is installed +if ! command -v docker > /dev/null 2>&1; then + echo "Docker is not installed. Please install Docker and try again." + exit 1 +fi + +# Check if file /opt/openvidu/deployment-info.yaml exists +if ! [ -f /opt/openvidu/deployment-info.yaml ]; then + echo "OpenVidu is not installed. Please install OpenVidu and try again." + exit 1 +fi + +# Stop OpenVidu service +echo "Stopping OpenVidu service..." +systemctl stop openvidu + +# Pull updater image +docker pull "${UPDATER_IMAGE}" + +# Temporary directory for post-update script +TMP_DIR=$(mktemp -d) + +# Generate installation scripts +COMMON_DOCKER_OPTIONS="--network=host \ + -v ${INSTALL_PREFIX}:${INSTALL_PREFIX} \ + -v ${TMP_DIR}:${TMP_DIR} \ + ${UPDATER_IMAGE} \ + --docker-registry=${REGISTRY} \ + --install-prefix=${INSTALL_PREFIX} \ + --post-update-script="${TMP_DIR}/post-update.sh" \ + $*" + +INTERACTIVE_MODE=true +for arg in "$@"; do + if [ "$arg" = "--no-tty" ]; then + INTERACTIVE_MODE=false; + break + fi +done + +if [ "$INTERACTIVE_MODE" = true ]; then + docker run -it ${COMMON_DOCKER_OPTIONS} > /dev/tty +else + docker run -i ${COMMON_DOCKER_OPTIONS} +fi + +if [ -f "${TMP_DIR}/post-update.sh" ]; then + chmod +x "${TMP_DIR}/post-update.sh" + "${TMP_DIR}/post-update.sh" +fi