├── images ├── Dashboard.png ├── MasterNSG.png ├── RunScript.png ├── SlaveNSG.png ├── TestHarness.png └── VmExtensions.png ├── templates ├── azuredeploy.parameters.json └── azuredeploy.json ├── scripts ├── deploy.sh └── run.ps1 ├── test └── bing-test.jmx ├── README.md └── cse ├── slave-node └── install-jmeter-with-chocolatey.ps1 └── master-node └── install-jmeter-with-chocolatey.ps1 /images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/HEAD/images/Dashboard.png -------------------------------------------------------------------------------- /images/MasterNSG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/HEAD/images/MasterNSG.png -------------------------------------------------------------------------------- /images/RunScript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/HEAD/images/RunScript.png -------------------------------------------------------------------------------- /images/SlaveNSG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/HEAD/images/SlaveNSG.png -------------------------------------------------------------------------------- /images/TestHarness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/HEAD/images/TestHarness.png -------------------------------------------------------------------------------- /images/VmExtensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/HEAD/images/VmExtensions.png -------------------------------------------------------------------------------- /templates/azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.1", 4 | "parameters": { 5 | "allowedAddress": { 6 | "value": "*" 7 | }, 8 | "location": { 9 | "value": "West Europe" 10 | }, 11 | "vmSize": { 12 | "value": "Standard_F4s_v2" 13 | }, 14 | "vmImagePublisher": { 15 | "value": "MicrosoftWindowsServer" 16 | }, 17 | "vmImageOffer": { 18 | "value": "WindowsServer" 19 | }, 20 | "vmImageSku": { 21 | "value": "2019-Datacenter-with-Containers" 22 | }, 23 | "adminUsername": { 24 | "value": "azadmin" 25 | }, 26 | "adminPassword": { 27 | "value": "N1cePassw0rd!" 28 | }, 29 | "workspaceName": { 30 | "value": "IndigoJMeterTestLogAnalytics" 31 | }, 32 | "masterScriptFilePath": { 33 | "value": "https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/cse/master-node" 34 | }, 35 | "masterScriptFileName": { 36 | "value": "install-jmeter-with-chocolatey.ps1" 37 | }, 38 | "slaveScriptFilePath": { 39 | "value": "https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/cse/slave-node" 40 | }, 41 | "slaveScriptFileName": { 42 | "value": "install-jmeter-with-chocolatey.ps1" 43 | }, 44 | "bastion": { 45 | "value": { 46 | "namePrefix": "Bastion", 47 | "subnetAddressPrefix": "10.0.1.0/24" 48 | } 49 | }, 50 | "virtualNetwork": { 51 | "value": { 52 | "namePrefix": "JmeterVnet", 53 | "addressPrefixes": ["10.0.0.0/16"], 54 | "subnetPrefix": "VmSubnet", 55 | "subnetAddressPrefix": "10.0.0.0/24" 56 | } 57 | }, 58 | "masterNodes": { 59 | "value": { 60 | "namePrefix": "MasterVm", 61 | "locations": [{ 62 | "region": "West Europe", 63 | "nameSuffix": "We" 64 | } 65 | ]} 66 | }, 67 | "slaveNodes": { 68 | "value": { 69 | "namePrefix": "SlaveVm", 70 | "locations": [{ 71 | "region": "Japan West", 72 | "nameSuffix": "Jw" 73 | }, 74 | { 75 | "region": "Brazil South", 76 | "nameSuffix": "Bs" 77 | }, 78 | { 79 | "region": "North Europe", 80 | "nameSuffix": "Ne" 81 | }, 82 | { 83 | "region": "West US", 84 | "nameSuffix": "Wu" 85 | } 86 | ]} 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # JMeter distributed testing leverages multiple systems to perform load testing. 4 | # Distributed testing can be used to sping up a large amount of concurrent virtual users 5 | # and generate traffic aginst websites and server applications. For more information, see 6 | # https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.html 7 | 8 | # Variables 9 | resourceGroupName="JMeterTestHarnessRG" 10 | location="WestEurope" 11 | deploy=1 12 | 13 | # ARM template and parameters files 14 | template="../templates/azuredeploy.json" 15 | parameters="../templates/azuredeploy.parameters.json" 16 | 17 | # SubscriptionId of the current subscription 18 | subscriptionId=$(az account show --query id --output tsv) 19 | 20 | # Check if the resource group already exists 21 | createResourceGroup() { 22 | rg=$1 23 | 24 | echo "Checking if [$rg] resource group actually exists in the [$subscriptionId] subscription..." 25 | 26 | if ! az group show --name "$rg" &>/dev/null; then 27 | echo "No [$rg] resource group actually exists in the [$subscriptionId] subscription" 28 | echo "Creating [$rg] resource group in the [$subscriptionId] subscription..." 29 | 30 | # Create the resource group 31 | if az group create --name "$rg" --location "$location" 1>/dev/null; then 32 | echo "[$rg] resource group successfully created in the [$subscriptionId] subscription" 33 | else 34 | echo "Failed to create [$rg] resource group in the [$subscriptionId] subscription" 35 | exit 1 36 | fi 37 | else 38 | echo "[$rg] resource group already exists in the [$subscriptionId] subscription" 39 | fi 40 | } 41 | 42 | # Validate the ARM template 43 | validateTemplate() { 44 | resourceGroup=$1 45 | template=$2 46 | parameters=$3 47 | arguments=$4 48 | 49 | echo "Validating [$template] ARM template..." 50 | 51 | if [[ -z $arguments ]]; then 52 | error=$(az group deployment validate \ 53 | --resource-group "$resourceGroup" \ 54 | --template-file "$template" \ 55 | --parameters "$parameters" \ 56 | --query error \ 57 | --output json) 58 | else 59 | error=$(az group deployment validate \ 60 | --resource-group "$resourceGroup" \ 61 | --template-file "$template" \ 62 | --parameters "$parameters" \ 63 | --arguments $arguments \ 64 | --query error \ 65 | --output json) 66 | fi 67 | 68 | if [[ -z $error ]]; then 69 | echo "[$template] ARM template successfully validated" 70 | else 71 | echo "Failed to validate the [$template] ARM template" 72 | echo "$error" 73 | exit 1 74 | fi 75 | } 76 | 77 | # Deploy ARM template 78 | deployTemplate() { 79 | resourceGroup=$1 80 | template=$2 81 | parameters=$3 82 | arguments=$4 83 | 84 | if [ $deploy != 1 ]; then 85 | return 86 | fi 87 | # Deploy the ARM template 88 | echo "Deploying ["$template"] ARM template..." 89 | 90 | if [[ -z $arguments ]]; then 91 | az group deployment create \ 92 | --resource-group $resourceGroup \ 93 | --template-file $template \ 94 | --parameters $parameters 1>/dev/null 95 | else 96 | az group deployment create \ 97 | --resource-group $resourceGroup \ 98 | --template-file $template \ 99 | --parameters $parameters \ 100 | --parameters $arguments 1>/dev/null 101 | fi 102 | 103 | az group deployment create \ 104 | --resource-group $resourceGroup \ 105 | --template-file $template \ 106 | --parameters $parameters 1>/dev/null 107 | 108 | if [[ $? == 0 ]]; then 109 | echo "["$template"] ARM template successfully provisioned" 110 | else 111 | echo "Failed to provision the ["$template"] ARM template" 112 | exit -1 113 | fi 114 | } 115 | 116 | # Create Resource Group 117 | createResourceGroup "$resourceGroupName" 118 | 119 | # Deploy JMeter Test Harness 120 | deployTemplate \ 121 | "$resourceGroupName" \ 122 | "$template" \ 123 | "$parameters" -------------------------------------------------------------------------------- /scripts/run.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(Position=0)][string]$JMeterTest, 3 | [Parameter(Position=1)][string]$KeyVaultResourceGroupName = "key-vault-resource-group", 4 | [Parameter(Position=2)][string]$KeyVaultName = "key-vault-name", 5 | [Parameter(Position=3)][string]$TenantName = "your-tenant.onmicrosoft.com", 6 | [Parameter(Position=4)][string]$Remote = "", 7 | [Parameter(Position=5)][ValidateRange(1, [int]::MaxValue)][int]$NumThreads = 20, 8 | [Parameter(Position=6)][ValidateRange(1, [int]::MaxValue)][int]$Duration = 600, 9 | [Parameter(Position=7)][ValidateRange(1, [int]::MaxValue)][int]$WarmupTime = 30, 10 | [Parameter(Position=8)][ValidateSet("Standard", ` 11 | "Hold", ` 12 | "DiskStore", ` 13 | "StrippedDiskStore", ` 14 | "Batch", "Statistical", ` 15 | "Stripped", ` 16 | "StrippedBatch", ` 17 | "Asynch", ` 18 | "StrippedAsynch")][string]$Mode = "Standard", 19 | [Parameter(Position=9)][bool]$UseAuthentication = $false, 20 | [Parameter(Position=10)][bool]$LogParameters = $true 21 | ) 22 | 23 | if ($LogParameters -eq $true) 24 | { 25 | $line = [System.String]::new("-", 80) 26 | Write-Host $line 27 | Write-Host "Parameters" 28 | Write-Host $line 29 | Write-Host 'JMeterTest:' $JMeterTest 30 | Write-Host 'KeyVaultResourceGroupName:' $KeyVaultResourceGroupName 31 | Write-Host 'KeyVaultName:' $KeyVaultName 32 | Write-Host 'TenantName:' $TenantName 33 | Write-Host 'Remote:' $Remote 34 | Write-Host 'NumThreads:' $NumThreads 35 | Write-Host 'WarmupTime:' $WarmupTime 36 | Write-Host 'Duration:' $Duration 37 | Write-Host 'Mode:' $Mode 38 | Write-Host 'UseAuthentication:' $UseAuthentication 39 | Write-Host $line 40 | } 41 | 42 | 43 | # 44 | # parameters validation 45 | # 46 | if ([System.String]::IsNullOrWhiteSpace($JMeterTest)) { 47 | Write-Host "ERROR: the -TestFileAndPath parameter cannot be null." 48 | exit -10 49 | } 50 | if (![System.String]::IsNullOrWhiteSpace($Remote)) { 51 | $prefix = "_" + $Remote -replace "\.", "" 52 | $prefix = $prefix -replace ",", "" 53 | $prefix = $prefix -replace " ", "_" 54 | } 55 | else { 56 | $prefix = "_local" 57 | } 58 | 59 | [System.IO.Directory]::SetCurrentDirectory((Get-Location).ToString()) 60 | $testFileFullPath = [System.IO.Path]::GetFullPath($JMeterTest) 61 | 62 | if (![System.IO.File]::Exists($testFileFullPath)) { 63 | Write-Host "ERROR:" $JMeterTest "file does not exists." 64 | exit -10 65 | } 66 | 67 | $testName = [System.IO.Path]::GetFileNameWithoutExtension($testFileFullPath) 68 | 69 | $folderName = $testName + $prefix 70 | 71 | # 72 | # A few variables 73 | # 74 | $jmeterClientIdSecretName="JMeterTestDriverClientId" 75 | $jmeterClientSecretSecretName="JMeterTestDriverClientSecret" 76 | $jmeterResourceUriSecretName="JMeterTestDriverResourceUri" 77 | 78 | # 79 | # Create the test_runs folder 80 | # 81 | $currentPath = (Get-Location).ToString() 82 | $rootFolderName = [System.IO.Path]::Combine($currentPath, "test_runs") 83 | 84 | if (![System.IO.Directory]::Exists($rootFolderName)) { 85 | New-Item -Path $rootFolderName -ItemType Directory 86 | } 87 | 88 | $folderName = $testName+$prefix 89 | 90 | $testFolderName = [System.IO.Path]::Combine($rootFolderName, $folderName) 91 | if (![System.IO.Directory]::Exists($testFolderName)) { 92 | New-Item -Path $testFolderName -ItemType Directory 93 | } 94 | 95 | $testRunFolderName = [System.IO.Path]::Combine($testFolderName, "test_" + (Get-Date -Format "yyyyMMdd_hhmmss").ToString()) 96 | $testRunOutputFolderName = [System.IO.Path]::Combine($testRunFolderName, "output") 97 | $testRunLogsFolderName = [System.IO.Path]::Combine($testRunFolderName, "logs") 98 | $testRunResultsFolderName = [System.IO.Path]::Combine($testRunFolderName, "results") 99 | 100 | if (![System.IO.Directory]::Exists($testRunFolderName)) { 101 | New-Item -Path $testRunFolderName -ItemType Directory 102 | New-Item -Path $testRunOutputFolderName -ItemType Directory 103 | New-Item -Path $testRunLogsFolderName -ItemType Directory 104 | New-Item -Path $testRunResultsFolderName -ItemType Directory 105 | } 106 | 107 | # 108 | # If Authentication is used, read the secrets from KV 109 | # 110 | if($UseAuthentication) { 111 | # Get the ClientID, ClientSecret and ResourceURI from Azure Key Vault 112 | $clientId = Get-AzKeyVaultSecret -vaultName "$KeyVaultName" -SecretName "$jmeterClientIdSecretName" -ErrorAction Stop 113 | $clientSecret = Get-AzKeyVaultSecret -vaultName "$KeyVaultName" -SecretName "$jmeterClientSecretSecretName" -ErrorAction Stop 114 | $resourceUri = Get-AzKeyVaultSecret -vaultName "$KeyVaultName" -SecretName "$jmeterResourceUriSecretName" -ErrorAction Stop 115 | 116 | if( !($clientId) -or !($clientSecret) -or !($resourceUri)) { 117 | Write-Host "ERROR: Missing secrets in" $KeyVaultName "Azure Key Vault. Secrets $jmeterClientIdSecretName, $jmeterClientSecretSecretName and $jmeterResourceUriSecretName are required!" 118 | exit -10 119 | } 120 | } 121 | 122 | # Define the common part of the JMeter command 123 | $jMeterFolder = (Get-ChildItem Env:JMETER_HOME).Value 124 | $jMeterPath = "$jMeterFolder\bin\jmeter" 125 | $parameters = @("-n", "-t", "`"$JMeterTest`"", "-l", "`"$testRunResultsFolderName\resultfile.jtl`"", "-e", "-o", "`"$testRunOutputFolderName`"", "-j", "`"$testRunLogsFolderName\jmeter.jtl`"", "-Jmode=$Mode") 126 | 127 | if (![System.String]::IsNullOrWhiteSpace($Remote)) { 128 | # Set general parameters 129 | $parameters += "-Gnum_threads=$($NumThreads)" 130 | $parameters += "-Gramp_time=$($WarmupTime)" 131 | $parameters += "-Gduration=$($Duration)" 132 | 133 | # Define additional part of JMeter command when running the test remotely on slave nodes 134 | $ip = Invoke-RestMethod -Uri 'https://api.ipify.org' 135 | $parameters += "-Djava.rmi.server.hostname=$ip" 136 | $parameters += "-R" 137 | $parameters += "`"$Remote`"" 138 | 139 | if ($UseAuthentication) { 140 | # Use authenication credentials from Key Vault 141 | $parameters += "-Gtenant_name=`"$($TenantName)`"" 142 | $parameters += "-Gclient_id=`"$($clientId.SecretValueText)`"" 143 | $parameters += "-Gclient_secret=`"$($clientSecret.SecretValueText)`"" 144 | $parameters += "-Gresource_uri=`"$($resourceUri.SecretValueText)`"" 145 | } 146 | } else { 147 | # Set general parameters 148 | $parameters += "-Jnum_threads=$($NumThreads)" 149 | $parameters += "-Jramp_time=$($WarmupTime)" 150 | $parameters += "-Jduration=$($Duration)" 151 | 152 | if ($UseAuthentication) { 153 | # Use authenication credentials from Key Vault 154 | $parameters += "-Jtenant_name=`"$($TenantName)`"" 155 | $parameters += "-Jclient_id=`"$($clientId.SecretValueText)`"" 156 | $parameters += "-Jclient_secret=`"$($clientSecret.SecretValueText)`"" 157 | $parameters += "-Jresource_uri=`"$($resourceUri.SecretValueText)`"" 158 | } 159 | } 160 | 161 | #Write-Host $jmeter $parameters 162 | & $jMeterPath $parameters -------------------------------------------------------------------------------- /test/bing-test.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This JMeter test calls the GET method of the Synthetic API 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | num_threads 13 | ${__P(num_threads, NUM_THREADS)} 14 | = 15 | 16 | 17 | ramp_time 18 | ${__P(ramp_time, RAMP_TIME)} 19 | = 20 | 21 | 22 | duration 23 | ${__P(duration, DURATION)} 24 | = 25 | 26 | 27 | hostname 28 | www.bing.com 29 | = 30 | 31 | 32 | portNumber 33 | 443 34 | = 35 | 36 | 37 | requestPath 38 | / 39 | = 40 | 41 | 42 | protocol 43 | https 44 | = 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ${hostname} 56 | ${portNumber} 57 | ${protocol} 58 | 59 | 60 | This element defines the default values for HTTP requests such as the protocol, host name and port number 61 | 6 62 | 63 | 64 | 65 | 66 | 67 | This thread group defines the number of concurrent users and duration of the test in terms of time or number of iterations 68 | continue 69 | 70 | false 71 | -1 72 | 73 | ${num_threads} 74 | ${ramp_time} 75 | true 76 | ${duration} 77 | 78 | true 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ${requestPath} 90 | GET 91 | true 92 | false 93 | true 94 | false 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Accept 104 | text/html 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | services: azure-resource-manager, virtual-machines, azure-bastion, azure-monitor, virtual-network 3 | author: paolosalvatori 4 | --- 5 | 6 | # JMeter Distributed Test Harness # 7 | Apache JMeter distributed testing leverages multiple systems to perform load testing against a target system, typically a web site or REST API. Distributed testing can be used to sping up a large amount of concurrent virtual users and generate traffic aginst websites and server applications. This project contains an [ARM template](https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/overview) that can be used to deploy an [Apache JMeter Distributed Test Harness](https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.html) composed of [Windows virtual machines in Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/overview) located in different regions. There are other projects available on GitHub that allow to build an Apache JMeter distributed test harness on Azure. These projects use, respectively, [Azure Kubernetes Servicer](https://docs.microsoft.com/en-us/azure/aks/intro-kubernetes) (AKS) and [Azure Container Instances](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-overview) (ACI) to build an Apache JMeter distributed test environment in a single virtual network and region: 8 | 9 | - [AKS based scalable Jmeter Test Framework with Grafana](https://github.com/petegrimsdale/aks_testing_fwk) 10 | - [Load Testing Pipeline with JMeter, ACI and Terraform](https://github.com/Azure-Samples/jmeter-aci-terraform) 11 | 12 | # Architecture # 13 | The following picture shows the architecture and network topology of the JMeter distributed test harness. 14 | 15 | ![Architecture](https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/images/TestHarness.png) 16 | 17 | JMeter master and slave nodes expose a Public IP using the [Java Remote Method Invocation](https://en.wikipedia.org/wiki/Java_remote_method_invocation) communication protocol over the public internet. In order to lock down security, [Network Security Groups](https://docs.microsoft.com/en-us/azure/virtual-network/security-overview) are used to allow inbound traffic on the TCP ports used by JMeter on master and slave nodes only from the virtual machines that are part of the topology. 18 | 19 | The following picture shows the Network Security Group of the master node. 20 | 21 | ![Master NSG](https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/images/MasterNSG.png) 22 | 23 | At point 1 you can note that the access via RDP is allowed only from a given public IP. You can restrict the RDP access to master and slave nodes by specifing a public IP as value of the **allowedAddress** parameter in the **azuredeploy.parameters.json** file. 24 | At point 2 and 3 you can see that the access to ports **1099** and **4000-4002** used by JMeter on the master node is restricted to the public IPs of the slave nodes. 25 | 26 | The following picture shows the Network Security Group of the slave node. 27 | 28 | ![Slave NSG](https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/images/SlaveNSG.png) 29 | 30 | At point 1 you can note that the access via RDP is allowed only from a given public IP. You can restrict the RDP access to master and slave nodes by specifing a public IP as value of the **allowedAddress** parameter in the **azuredeploy.parameters.json** file. 31 | At point 2 and 3 you can see that the access to ports **1099** and **4000-4002** used by JMeter on the master node is restricted to the public IPs of the master node. 32 | 33 | You can connect to master and slave nodes via RDP on port 3389. In addition, you can connect to the JMeter master virtual machine via [Azure Bastion](https://docs.microsoft.com/en-us/azure/bastion/bastion-overview) which provides secure and seamless RDP/SSH connectivity to your virtual machines directly in the Azure portal over SSL. You can customize the ARM template to disable the access to virtual machines via RDP by eliminating the corresponding rule in the Network Security Groups or you can eliminate Azure Bastion if you don't want to use this access type. 34 | 35 | A [Custom Script Extension for Windows](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows) downloads and executes a PowerShell script that performs the following tasks: 36 | 37 | - Automatically installs Apache JMeter on both the master and slave nodes via [Chocolatey](https://chocolatey.org/packages/jmeter) . - Customizes the JMeter properties file to disable RMI over SSL and set 4000 TCP port for client/server communications. 38 | - Downloads the [JMeter Backend Listener for Application Insights](https://github.com/adrianmo/jmeter-backend-azure) that can be used to send test results to Azure Application Insights. 39 | - Creates inbound rules in the Windows Firewall to allow traffic on ports 1099 and 4000-4002. 40 | - Creates a Windows Task on slave nodes to launch JMeter Server at the startup. 41 | - Automatically starts Jmeter Server on slave nodes. 42 | 43 | In addition, all the virtual machines in the topology are configured to collect diagnostics logs, Windows Logs, and performance counters to a Log Analytics workspace. The workspace makes use of the following solutions to keep track of the health of the virtual machines: 44 | 45 | - [Agent Health](https://docs.microsoft.com/en-us/azure/azure-monitor/insights/solution-agenthealth) 46 | - [Service Map](https://docs.microsoft.com/en-us/azure/azure-monitor/insights/service-map) 47 | - [Infrastructure Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/insights/vminsights-enable-overview) 48 | 49 | # Deployment # 50 | You can use the **azuredeploy.json** ARM template and parameters file included in this repository to deploy the JMeter test harness. Make sure to edit the **azuredeploy.parameters.json** file to customize the installation. In particular, you can customize the list of virtual machines by editing the **virtualMachines** parameter in the parameters file. You can also use the **deploy.sh** Bash script to deploy the ARM template. 51 | 52 | # Testing # 53 | You can connect to the JMeter master node with the credentials specified in the ARM template to run tests using the JMeter UI or command-line tool. For more information on how to run tests on remote nodes, see: 54 | 55 | - [Apache JMeter Distributed Testing Step-by-step](https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.html) 56 | - [Jmeter Remote Testing](https://jmeter.apache.org/usermanual/remote-test.html) 57 | 58 | You can also use the **run.ps1** PowerShell script to run tests on the master node or remote nodes. The script allows to specify the thread number, warmup time, and duration of the test. In order to use this data as parameters, the JMeter test file (.jmx) needs to use define corresponding parameters. As a sample, see the **bing-test.jmx** JMeter test in this repository. 59 | 60 | ![Run SCript](https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/images/RunScript.png) 61 | 62 | This script allows to save JMeter logs, results and dashboard on the local file system. 63 | 64 | ![JMeter Dashboard](https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/images/Dashboard.png) 65 | 66 | You can use **Windows PowerShell** or **Windows PowerShell ISE** to run commands. For example, the following command: 67 | 68 | ```powershell 69 | .\run.ps1 -JMeterTest .\bing-test.jmx -Duration 60 -WarmupTime 30 -NumThreads 30 -Remote "191.233.25.31, 40.74.104.255, 52.155.176.185" 70 | ``` 71 | 72 | generates the following JMeter command: 73 | 74 | ```batch 75 | C:\ProgramData\chocolatey\lib\jmeter\tools\apache-jmeter-5.2.1\bin\jmeter -n -t ".\bing-test.jmx" -l "C:\tests\test_runs\bing-test_1912332531_4074104255_52155176185\test_20200325_052525\results\resultfile.jtl" -e -o "C:\tests\test_runs\bing-test_1912332531_4074104255_52155176185\test_20200325_052525\output" -j "C:\tests\test_runs\bing-test_1912332531_4074104255_52155176185\test_20200325_052525\logs\jmeter.jtl" -Jmode=Stand 76 | ard -Gnum_threads=30 -Gramp_time=30 -Gduration=60 -Djava.rmi.server.hostname=51.124.79.211 -R "191.233.25.31, 40.74.104.255, 52.155.176.185" 77 | ``` 78 | 79 | # Possible Developments # 80 | This solution uses Public IPs to let master and slave nodes to communicate with each other. An alternative solution could be deploying master and slave nodes in different virtual networks located in different regions and use [global virtual network peering](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-peering-overview) to connect these virtual networks. Using this approach, the master node could communicate with slave nodes via private IP addresses. 81 | 82 | This topology has the following advantages over a topology that uses private IP addresses: 83 | 84 | - Reduced complexity 85 | - Lower total cost of ownership (TCO) 86 | - Extensibility to other cloud platforms. 87 | 88 | As an example of extensibility, you can provision slave nodes across multiple regions, across multiple Azure subscriptions and even on other cloud platforms like AWS or GCP. I personally tested this possibility by provisioning additional slave nodes on AWS. 89 | 90 | Last but not least, the ARM template can be easily changed to replace **virtual machines** with **virtual machine scale sets** (VMSS). 91 | -------------------------------------------------------------------------------- /cse/slave-node/install-jmeter-with-chocolatey.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(Position=0)][string]$appInsightsJMeterListernerUrl = "https://github.com/adrianmo/jmeter-backend-azure/releases/download/0.2.1/jmeter.backendlistener.azure-0.2.1.jar" 3 | ) 4 | 5 | try { 6 | # Install NuGet packace provider 7 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 8 | } 9 | catch { 10 | # Log error 11 | Write-Output "$(Get-TimeStamp) An error occurred while installing NuGet package provider" | Out-file $logFile -append 12 | $ErrorMessage = $_.Exception.Message 13 | Write-Output "$ErrorMessage" | Out-file $logFile -append 14 | throw $_.Exception 15 | } 16 | 17 | # Set Log Directory and File path 18 | $logDirectory = "C:\WindowsAzure\Logs" 19 | $logFile = "$logDirectory\InstallJMeter.log" 20 | 21 | try { 22 | # Create Log Directory if not exists 23 | if (![System.IO.Directory]::Exists($logDirectory)) { 24 | New-Item -Path $logDirectory -ItemType Directory -Force 25 | } 26 | 27 | # Create log file if it does not exist 28 | if (!(Test-Path $logfile)) { 29 | New-Item $logfile -ItemType file 30 | } 31 | } 32 | catch { 33 | # Log error 34 | Write-Output "$(Get-TimeStamp) An error occurred while creating the log folder or file" | Out-file $logFile -append 35 | $ErrorMessage = $_.Exception.Message 36 | Write-Output "$ErrorMessage" | Out-file $logFile -append 37 | throw $_.Exception 38 | } 39 | 40 | # Define Get-Timestamp function 41 | Function Get-TimeStamp { 42 | return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date) 43 | } 44 | 45 | # Trace parameters 46 | Write-Output "$(Get-TimeStamp) appInsightsJMeterListernerUrl=[$appInsightsJMeterListernerUrl]" | Out-file $logFile -append 47 | 48 | try { 49 | # Log Start 50 | Write-Output "$(Get-TimeStamp) Installing Chocolatey..." | Out-file $logFile -append 51 | 52 | # Install Chocolatey 53 | Set-ExecutionPolicy Bypass -Scope Process -Force 54 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 55 | Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression 56 | 57 | # Log success 58 | Write-Output "$(Get-TimeStamp) Chocolatey has been successfully installed" | Out-file $logFile -append 59 | } 60 | catch { 61 | # Log error 62 | Write-Output "$(Get-TimeStamp) An error occurred while installing Chocolatey" | Out-file $logFile -append 63 | $ErrorMessage = $_.Exception.Message 64 | Write-Output "$ErrorMessage" | Out-file $logFile -append 65 | throw $_.Exception 66 | } 67 | 68 | try { 69 | # Log Start 70 | Write-Output "$(Get-TimeStamp) Installing JMeter with Chocolatey..." | Out-file $logFile -append 71 | 72 | # Install JMeter with Chocolatey 73 | $command = "choco install jmeter --yes --force" 74 | Invoke-Expression $command 75 | $jmeterFolder = (Get-ItemProperty -Path ‘Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment’ -Name JMETER_HOME).JMETER_HOME 76 | 77 | # Log success 78 | Write-Output "$(Get-TimeStamp) JMeter has been successfully installed" | Out-file $logFile -append 79 | } 80 | catch { 81 | # Log error 82 | Write-Output "$(Get-TimeStamp) An error occurred while installing JMeter" | Out-file $logFile -append 83 | $ErrorMessage = $_.Exception.Message 84 | Write-Output "$ErrorMessage" | Out-file $logFile -append 85 | throw $_.Exception 86 | } 87 | 88 | try { 89 | # Log Start 90 | Write-Output "$(Get-TimeStamp) Add settings JMeter properties file" | Out-file $logFile -append 91 | 92 | # Append settings to the jmeter properties file 93 | $content = @" 94 | # Parameter that controls the RMI port used by RemoteSampleListenerImpl (The Controller) 95 | # Default value is 0 which means port is randomly assigned 96 | # You may need to open Firewall port on the Controller machine 97 | server.rmi.localport=4000 98 | 99 | # Set this if you don't want to use SSL for RMI 100 | server.rmi.ssl.disable=true 101 | "@ 102 | 103 | Add-Content -Path "$jmeterFolder\bin\jmeter.properties" $content 104 | 105 | # Log success 106 | Write-Output "$(Get-TimeStamp) Settings have been successfully added to JMeter properties file" | Out-file $logFile -append 107 | } 108 | catch { 109 | # Log error 110 | Write-Output "$(Get-TimeStamp) An error occurred while adding settings to the JMeter properties file" | Out-file $logFile -append 111 | $ErrorMessage = $_.Exception.Message 112 | Write-Output "$ErrorMessage" | Out-file $logFile -append 113 | throw $_.Exception 114 | } 115 | 116 | try { 117 | # Log Start 118 | Write-Output "$(Get-TimeStamp) Download JMeter listener for Application Insights" | Out-file $logFile -append 119 | 120 | # Download Application Insights listener for JMeter 121 | $source = "$appInsightsJMeterListernerUrl" 122 | $destination = "$jmeterFolder\lib\ext\jmeter.backendlistener.azure-0.2.0.jar" 123 | $client = New-Object System.Net.WebClient 124 | $client.DownloadFile($source, $destination) 125 | 126 | # Log success 127 | Write-Output "$(Get-TimeStamp) JMeter listener for Application Insights has been successfully downloaded" | Out-file $logFile -append 128 | } 129 | catch { 130 | # Log error 131 | Write-Output "$(Get-TimeStamp) An error occurred while downloading the JMeter listener for Application Insights" | Out-file $logFile -append 132 | $ErrorMessage = $_.Exception.Message 133 | Write-Output "$ErrorMessage" | Out-file $logFile -append 134 | throw $_.Exception 135 | } 136 | 137 | try { 138 | # Create Public IP 139 | $retryCount = 0 140 | $retryInterval = 3 141 | $maxRetryCount = 5 142 | $success = $false 143 | 144 | do { 145 | try { 146 | $ip = Invoke-RestMethod -Uri 'https://api.ipify.org' 147 | Write-Output "$(Get-TimeStamp) Public IP address is: $ip" | Out-file $logFile -append 148 | $success = $true 149 | } 150 | catch { 151 | Write-Output "Next attempt in 5 seconds" 152 | Start-sleep -Seconds $retryInterval 153 | } 154 | 155 | $retryCount++ 156 | 157 | } until ($retryCount -eq $maxRetryCount -or $success) 158 | 159 | if (-not($success)) { 160 | exit 161 | } 162 | 163 | # Create Script File 164 | $content = @" 165 | cd $jmeterFolder\bin 166 | jmeter-server -Dclient.rmi.localport=4000 -Djava.rmi.server.hostname=$ip 167 | "@ 168 | New-Item -Path C:\Scripts -ItemType Directory -Force 169 | New-Item -Path C:\Scripts\JMeterServer.bat -ItemType File -Force 170 | Set-Content C:\Scripts\JMeterServer.bat $content 171 | } 172 | catch { 173 | # Log error 174 | Write-Output "$(Get-TimeStamp) An error occurred while creating the JmeterServer.bat file" | Out-file $logFile -append 175 | $ErrorMessage = $_.Exception.Message 176 | Write-Output "$ErrorMessage" | Out-file $logFile -append 177 | throw $_.Exception 178 | } 179 | 180 | try { 181 | # Log Start 182 | Write-Output "$(Get-TimeStamp) Creating Windows Firewall Rules..." | Out-file $logFile -append 183 | 184 | # Create Firewall Inbound rule to allow traffic to the Java RMI Port 185 | New-NetFirewallRule -DisplayName 'Allow Java RMI Port' -Profile 'Any' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 1099 186 | 187 | # Create Firewall Inbound rule to allow traffic to the JMeter Port range 188 | New-NetFirewallRule -DisplayName 'Allow JMeter Port Range' -Profile 'Any' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 4000-4002 189 | 190 | # Log Success 191 | Write-Output "$(Get-TimeStamp) Windows Firewall Rules have been successfully created" | Out-file $logFile -append 192 | } 193 | catch { 194 | # Log error 195 | Write-Output "$(Get-TimeStamp) An error occurred while creating Windows Firewall Rules" | Out-file $logFile -append 196 | $ErrorMessage = $_.Exception.Message 197 | Write-Output "$ErrorMessage" | Out-file $logFile -append 198 | throw $_.Exception 199 | } 200 | 201 | try { 202 | # Log Start 203 | Write-Output "$(Get-TimeStamp) Configuring a task to start the JMeter server at each reboot" | Out-file $logFile -append 204 | 205 | # Define a task trigger to run the task at startup 206 | $Trigger1= New-ScheduledTaskTrigger -AtStartup 207 | 208 | # Deine another trigger to run the task in 30 seconds 209 | $Time = (Get-Date).AddMinutes(1) 210 | $Trigger2= New-ScheduledTaskTrigger -Once -At ($Time.TimeOfDay.ToString("hh\:mm")) 211 | 212 | 213 | # Define the user account running the task 214 | $User= "NT AUTHORITY\SYSTEM" 215 | 216 | # Specify what program to run and with its parameters 217 | $Action= New-ScheduledTaskAction -Execute "C:\Scripts\JMeterServer.bat" 218 | 219 | # Register the task 220 | Register-ScheduledTask -TaskName "RunJMeterServer" -Trigger $Trigger1, $Trigger2 -User $User -Action $Action -RunLevel Highest –Force 221 | 222 | # Log success 223 | Write-Output "$(Get-TimeStamp) The task has been successfully created" | Out-file $logFile -append 224 | } 225 | catch { 226 | # Log error 227 | Write-Output "$(Get-TimeStamp) An error occurred while setting the task" | Out-file $logFile -append 228 | $ErrorMessage = $_.Exception.Message 229 | Write-Output "$ErrorMessage" | Out-file $logFile -append 230 | throw $_.Exception 231 | } -------------------------------------------------------------------------------- /cse/master-node/install-jmeter-with-chocolatey.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(Position=0)][string]$appInsightsJMeterListernerUrl = "https://github.com/adrianmo/jmeter-backend-azure/releases/download/0.2.1/jmeter.backendlistener.azure-0.2.1.jar" 3 | ) 4 | 5 | try { 6 | # Install NuGet packace provider 7 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 8 | } 9 | catch { 10 | # Log error 11 | Write-Output "$(Get-TimeStamp) An error occurred while installing NuGet package provider" | Out-file $logFile -append 12 | $ErrorMessage = $_.Exception.Message 13 | Write-Output "$ErrorMessage" | Out-file $logFile -append 14 | throw $_.Exception 15 | } 16 | 17 | # Set Log Directory and File path 18 | $logDirectory = "C:\WindowsAzure\Logs" 19 | $logFile = "$logDirectory\InstalllJMeter.log" 20 | 21 | try { 22 | # Create Log Directory if not exists 23 | if (![System.IO.Directory]::Exists($logDirectory)) { 24 | New-Item -Path $logDirectory -ItemType Directory -Force 25 | } 26 | 27 | # Create log file if it does not exist 28 | if (!(Test-Path $logfile)) { 29 | New-Item $logfile -ItemType file 30 | } 31 | } 32 | catch { 33 | # Log error 34 | Write-Output "$(Get-TimeStamp) An error occurred while creating the log folder or file" | Out-file $logFile -append 35 | $ErrorMessage = $_.Exception.Message 36 | Write-Output "$ErrorMessage" | Out-file $logFile -append 37 | throw $_.Exception 38 | } 39 | 40 | # Define Get-Timestamp function 41 | Function Get-TimeStamp { 42 | return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date) 43 | } 44 | 45 | # Trace parameters 46 | Write-Output "$(Get-TimeStamp) appInsightsJMeterListernerUrl=[$appInsightsJMeterListernerUrl]" | Out-file $logFile -append 47 | 48 | try { 49 | # Log Start 50 | Write-Output "$(Get-TimeStamp) Installing Chocolatey..." | Out-file $logFile -append 51 | 52 | # Install Chocolatey 53 | Set-ExecutionPolicy Bypass -Scope Process -Force 54 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 55 | Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression 56 | 57 | # Log success 58 | Write-Output "$(Get-TimeStamp) Chocolatey has been successfully installed" | Out-file $logFile -append 59 | } 60 | catch { 61 | # Log error 62 | Write-Output "$(Get-TimeStamp) An error occurred while installing Chocolatey" | Out-file $logFile -append 63 | $ErrorMessage = $_.Exception.Message 64 | Write-Output "$ErrorMessage" | Out-file $logFile -append 65 | throw $_.Exception 66 | } 67 | 68 | try { 69 | # Log Start 70 | Write-Output "$(Get-TimeStamp) Installing JMeter with Chocolatey..." | Out-file $logFile -append 71 | 72 | # Install JMeter with Chocolatey 73 | $command = "choco install jmeter --yes --force" 74 | Invoke-Expression $command 75 | $jmeterFolder = (Get-ItemProperty -Path ‘Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment’ -Name JMETER_HOME).JMETER_HOME 76 | 77 | # Log success 78 | Write-Output "$(Get-TimeStamp) JMeter has been successfully installed" | Out-file $logFile -append 79 | } 80 | catch { 81 | # Log error 82 | Write-Output "$(Get-TimeStamp) An error occurred while installing JMeter" | Out-file $logFile -append 83 | $ErrorMessage = $_.Exception.Message 84 | Write-Output "$ErrorMessage" | Out-file $logFile -append 85 | throw $_.Exception 86 | } 87 | 88 | try { 89 | # Log Start 90 | Write-Output "$(Get-TimeStamp) Add settings JMeter properties file" | Out-file $logFile -append 91 | 92 | # Append settings to the jmeter properties file 93 | $content = @" 94 | # Parameter that controls the RMI port used by RemoteSampleListenerImpl (The Controller) 95 | # Default value is 0 which means port is randomly assigned 96 | # You may need to open Firewall port on the Controller machine 97 | client.rmi.localport=4000 98 | 99 | # Set this if you don't want to use SSL for RMI 100 | server.rmi.ssl.disable=true 101 | "@ 102 | 103 | Add-Content -Path "$jmeterFolder\bin\jmeter.properties" $content 104 | 105 | # Log success 106 | Write-Output "$(Get-TimeStamp) Settings have been successfully added to JMeter properties file" | Out-file $logFile -append 107 | } 108 | catch { 109 | # Log error 110 | Write-Output "$(Get-TimeStamp) An error occurred while adding settings to the JMeter properties file" | Out-file $logFile -append 111 | $ErrorMessage = $_.Exception.Message 112 | Write-Output "$ErrorMessage" | Out-file $logFile -append 113 | throw $_.Exception 114 | } 115 | 116 | try { 117 | # Log Start 118 | Write-Output "$(Get-TimeStamp) Download JMeter listener for Application Insights" | Out-file $logFile -append 119 | 120 | # Download Application Insights listener for JMeter 121 | $source = "$appInsightsJMeterListernerUrl" 122 | $destination = "$jmeterFolder\lib\ext\jmeter.backendlistener.azure-0.2.0.jar" 123 | $client = New-Object System.Net.WebClient 124 | $client.DownloadFile($source, $destination) 125 | 126 | # Log success 127 | Write-Output "$(Get-TimeStamp) JMeter listener for Application Insights has been successfully downloaded" | Out-file $logFile -append 128 | } 129 | catch { 130 | # Log error 131 | Write-Output "$(Get-TimeStamp) An error occurred while downloading the JMeter listener for Application Insights" | Out-file $logFile -append 132 | $ErrorMessage = $_.Exception.Message 133 | Write-Output "$ErrorMessage" | Out-file $logFile -append 134 | throw $_.Exception 135 | } 136 | 137 | try { 138 | # Log Start 139 | Write-Output "$(Get-TimeStamp) Creating Windows Firewall Rules..." | Out-file $logFile -append 140 | 141 | # Create Firewall Inbound rule to allow traffic to the Java RMI Port 142 | New-NetFirewallRule -DisplayName 'Allow Java RMI Port' -Profile 'Any' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 1099 143 | 144 | # Create Firewall Inbound rule to allow traffic to the JMeter Port range 145 | New-NetFirewallRule -DisplayName 'Allow JMeter Port Range' -Profile 'Any' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 4000-4002 146 | 147 | # Log Success 148 | Write-Output "$(Get-TimeStamp) Windows Firewall Rules have been successfully created" | Out-file $logFile -append 149 | } 150 | catch { 151 | # Log error 152 | Write-Output "$(Get-TimeStamp) An error occurred while creating Windows Firewall Rules" | Out-file $logFile -append 153 | $ErrorMessage = $_.Exception.Message 154 | Write-Output "$ErrorMessage" | Out-file $logFile -append 155 | throw $_.Exception 156 | } 157 | 158 | try { 159 | # Log Start 160 | Write-Output "$(Get-TimeStamp) Disabling Internet Explorer Enhanced Security Configuration..." | Out-file $logFile -append 161 | function Disable-InternetExplorerESC { 162 | $AdminKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" 163 | $UserKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}" 164 | Set-ItemProperty -Path $AdminKey -Name "IsInstalled" -Value 0 -Force 165 | Set-ItemProperty -Path $UserKey -Name "IsInstalled" -Value 0 -Force 166 | Stop-Process -Name Explorer -Force 167 | Write-Host "IE Enhanced Security Configuration (ESC) has been disabled." -ForegroundColor Green 168 | } 169 | function Enable-InternetExplorerESC { 170 | $AdminKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" 171 | $UserKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}" 172 | Set-ItemProperty -Path $AdminKey -Name "IsInstalled" -Value 1 -Force 173 | Set-ItemProperty -Path $UserKey -Name "IsInstalled" -Value 1 -Force 174 | Stop-Process -Name Explorer 175 | Write-Host "IE Enhanced Security Configuration (ESC) has been enabled." -ForegroundColor Green 176 | } 177 | function Disable-UserAccessControl { 178 | Set-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "ConsentPromptBehaviorAdmin" -Value 00000000 -Force 179 | Write-Host "User Access Control (UAC) has been disabled." -ForegroundColor Green 180 | } 181 | 182 | Disable-InternetExplorerESC 183 | 184 | # Log Success 185 | Write-Output "$(Get-TimeStamp) Internet Explorer Enhanced Security Configuration has been successfully disabled" | Out-file $logFile -append 186 | } 187 | catch { 188 | # Log error 189 | Write-Output "$(Get-TimeStamp) An error occurred while disabling Internet Explorer Enhanced Security Configuration" | Out-file $logFile -append 190 | $ErrorMessage = $_.Exception.Message 191 | Write-Output "$ErrorMessage" | Out-file $logFile -append 192 | } 193 | 194 | try { 195 | # Log Start 196 | Write-Output "$(Get-TimeStamp) Installing Az PowerShell module..." | Out-file $logFile -append 197 | 198 | # Install Az PowerShell module 199 | Install-Module -Name Az -AllowClobber -Scope AllUsers -Force 200 | 201 | # Log Success 202 | Write-Output "$(Get-TimeStamp) Az PowerShell module has been successfully disabled" | Out-file $logFile -append 203 | } 204 | catch { 205 | # Log error 206 | Write-Output "$(Get-TimeStamp) An error occurred while installing Az PowerShell module" | Out-file $logFile -append 207 | $ErrorMessage = $_.Exception.Message 208 | Write-Output "$ErrorMessage" | Out-file $logFile -append 209 | throw $_.Exception 210 | } -------------------------------------------------------------------------------- /templates/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "allowedAddress": { 6 | "type": "string", 7 | "defaultValue": "*", 8 | "metadata": { 9 | "description": "This parameter contains the public IP addresses that is allowed to connect to virtual machines via RDP." 10 | } 11 | }, 12 | "location": { 13 | "type": "string", 14 | "defaultValue": "[resourceGroup().location]", 15 | "metadata": { 16 | "description": "Location for the resources." 17 | } 18 | }, 19 | "vmSize": { 20 | "type": "string", 21 | "defaultValue": "Standard_B2s", 22 | "metadata": { 23 | "description": "Size for the Virtual Machine." 24 | } 25 | }, 26 | "vmImagePublisher": { 27 | "defaultValue": "MicrosoftWindowsServer", 28 | "type": "string", 29 | "metadata": { 30 | "description": "The publisher of the image reference used by the virtual machine" 31 | } 32 | }, 33 | "vmImageOffer": { 34 | "defaultValue": "WindowsServer", 35 | "type": "string", 36 | "metadata": { 37 | "description": "The offer of the image reference used by the virtual machine" 38 | } 39 | }, 40 | "vmImageSku": { 41 | "defaultValue": "2019-Datacenter-with-Containers", 42 | "type": "string", 43 | "metadata": { 44 | "description": "The sku of the image reference used by the virtual machine" 45 | } 46 | }, 47 | "vmImageVersion": { 48 | "defaultValue": "latest", 49 | "type": "string", 50 | "metadata": { 51 | "description": "The version of the image reference used by the virtual machine" 52 | } 53 | }, 54 | "adminUsername": { 55 | "type": "string", 56 | "metadata": { 57 | "description": "Username for the Virtual Machine." 58 | } 59 | }, 60 | "adminPassword": { 61 | "type": "securestring", 62 | "metadata": { 63 | "description": "Password for the Virtual Machine." 64 | } 65 | }, 66 | "workspaceName": { 67 | "type": "string", 68 | "defaultValue": "IndigoJMeterLogAnalytics", 69 | "metadata": { 70 | "description": "Name of the Log Analytics workspace" 71 | } 72 | }, 73 | "masterScriptFilePath": { 74 | "type": "string", 75 | "defaultValue": "https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/cse/master-node", 76 | "metadata": { 77 | "description": "The relative path of the script to download" 78 | } 79 | }, 80 | "masterScriptFileName": { 81 | "type": "string", 82 | "defaultValue": "install-jmeter-with-chocolatey.ps1", 83 | "metadata": { 84 | "description": "The name of the script to download" 85 | } 86 | }, 87 | "slaveScriptFilePath": { 88 | "type": "string", 89 | "defaultValue": "https://raw.githubusercontent.com/paolosalvatori/jmeter-distributed-test-harness/master/cse/slave-node", 90 | "metadata": { 91 | "description": "The relative path of the script to download" 92 | } 93 | }, 94 | "slaveScriptFileName": { 95 | "type": "string", 96 | "defaultValue": "install-jmeter-with-chocolatey.ps1", 97 | "metadata": { 98 | "description": "The name of the script to download" 99 | } 100 | }, 101 | "bastion": { 102 | "type": "object", 103 | "defaultValue": { 104 | "namePrefix": "Bastion", 105 | "subnetAddressPrefix": "10.0.1.0/24" 106 | }, 107 | "metadata": { 108 | "description": "This object contains data to build Azure Bastion resources" 109 | } 110 | }, 111 | "virtualNetwork": { 112 | "type": "object", 113 | "defaultValue": { 114 | "namePrefix": "JmeterVnet", 115 | "addressPrefixes": [ 116 | "10.0.0.0/16" 117 | ], 118 | "subnetPrefix": "VmSubnet", 119 | "subnetAddressPrefix": "10.0.0.0/24" 120 | }, 121 | "metadata": { 122 | "description": "This object contains data to build virtual networks and subnets" 123 | } 124 | }, 125 | "masterNodes": { 126 | "type": "object", 127 | "defaultValue": [ 128 | ], 129 | "metadata": { 130 | "description": "This object contains the name prefix and an array of the locations of the master nodes" 131 | } 132 | }, 133 | "slaveNodes": { 134 | "type": "object", 135 | "defaultValue": [ 136 | ], 137 | "metadata": { 138 | "description": "This object contains the name prefix and an array of the locations of the slave nodes" 139 | } 140 | } 141 | }, 142 | "variables": { 143 | "workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName'))]" 144 | }, 145 | "resources": [ 146 | { 147 | "apiVersion": "2019-09-01", 148 | "type": "Microsoft.Network/networkSecurityGroups", 149 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg')]", 150 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 151 | "tags": { 152 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg')]" 153 | }, 154 | "copy": { 155 | "name": "masterVirtualMachineNetworkSecurityGroupsCopy", 156 | "count": "[length(parameters('masterNodes').locations)]" 157 | }, 158 | "properties": { 159 | "securityRules": [ 160 | { 161 | "name": "AllowRDPInbound", 162 | "properties": { 163 | "description": "Allow RDP Connections", 164 | "priority": 100, 165 | "protocol": "Tcp", 166 | "access": "Allow", 167 | "direction": "Inbound", 168 | "sourceAddressPrefix": "[parameters('allowedAddress')]", 169 | "sourcePortRange": "*", 170 | "destinationAddressPrefix": "*", 171 | "destinationPortRange": "3389" 172 | } 173 | }, 174 | { 175 | "name": "AllowJavaRmiInbound", 176 | "properties": { 177 | "description": "Allow Java RMI Inbound Traffic", 178 | "priority": 120, 179 | "protocol": "Tcp", 180 | "access": "Allow", 181 | "direction": "Inbound", 182 | "copy": [ 183 | { 184 | "name": "sourceAddressPrefixes", 185 | "count": "[length(parameters('slaveNodes').locations)]", 186 | "input": "[reference(resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex('sourceAddressPrefixes')].nameSuffix, 'Ip')), '2019-09-01').ipAddress]" 187 | } 188 | ], 189 | "sourcePortRange": "*", 190 | "destinationAddressPrefix": "*", 191 | "destinationPortRange": "1099" 192 | } 193 | }, 194 | { 195 | "name": "AllowJMeterPortInbound", 196 | "properties": { 197 | "description": "Allow JMeter Inbound Traffic", 198 | "priority": 130, 199 | "protocol": "Tcp", 200 | "access": "Allow", 201 | "direction": "Inbound", 202 | "copy": [ 203 | { 204 | "name": "sourceAddressPrefixes", 205 | "count": "[length(parameters('slaveNodes').locations)]", 206 | "input": "[reference(resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex('sourceAddressPrefixes')].nameSuffix, 'Ip')), '2019-09-01').ipAddress]" 207 | } 208 | ], 209 | "sourcePortRange": "*", 210 | "destinationAddressPrefix": "*", 211 | "destinationPortRange": "4000-4002" 212 | } 213 | } 214 | ] 215 | } 216 | }, 217 | { 218 | "apiVersion": "2019-09-01", 219 | "type": "Microsoft.Network/networkSecurityGroups", 220 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nsg')]", 221 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 222 | "tags": { 223 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nsg')]" 224 | }, 225 | "copy": { 226 | "name": "slaveVirtualMachineNetworkSecurityGroupsCopy", 227 | "count": "[length(parameters('slaveNodes').locations)]" 228 | }, 229 | "properties": { 230 | "securityRules": [ 231 | { 232 | "name": "AllowRDPInbound", 233 | "properties": { 234 | "description": "Allow RDP Connections", 235 | "priority": 100, 236 | "protocol": "Tcp", 237 | "access": "Allow", 238 | "direction": "Inbound", 239 | "sourceAddressPrefix": "[parameters('allowedAddress')]", 240 | "sourcePortRange": "*", 241 | "destinationAddressPrefix": "*", 242 | "destinationPortRange": "3389" 243 | } 244 | }, 245 | { 246 | "name": "AllowJavaRmiInbound", 247 | "properties": { 248 | "description": "Allow Java RMI Inbound Traffic", 249 | "priority": 120, 250 | "protocol": "Tcp", 251 | "access": "Allow", 252 | "direction": "Inbound", 253 | "copy": [ 254 | { 255 | "name": "sourceAddressPrefixes", 256 | "count": "[length(parameters('masterNodes').locations)]", 257 | "input": "[reference(resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex('sourceAddressPrefixes')].nameSuffix, 'Ip')), '2019-09-01').ipAddress]" 258 | } 259 | ], 260 | "sourcePortRange": "*", 261 | "destinationAddressPrefix": "*", 262 | "destinationPortRange": "1099" 263 | } 264 | }, 265 | { 266 | "name": "AllowJMeterPortInbound", 267 | "properties": { 268 | "description": "Allow JMeter Inbound Traffic", 269 | "priority": 130, 270 | "protocol": "Tcp", 271 | "access": "Allow", 272 | "direction": "Inbound", 273 | "copy": [ 274 | { 275 | "name": "sourceAddressPrefixes", 276 | "count": "[length(parameters('masterNodes').locations)]", 277 | "input": "[reference(resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex('sourceAddressPrefixes')].nameSuffix, 'Ip')), '2019-09-01').ipAddress]" 278 | } 279 | ], 280 | "sourcePortRange": "*", 281 | "destinationAddressPrefix": "*", 282 | "destinationPortRange": "4000-4002" 283 | } 284 | } 285 | ] 286 | } 287 | }, 288 | { 289 | "apiVersion": "2019-09-01", 290 | "type": "Microsoft.Network/networkSecurityGroups", 291 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg')]", 292 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 293 | "tags": { 294 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg')]" 295 | }, 296 | "copy": { 297 | "name": "masterBastionNetworkSecurityGroupsCopy", 298 | "count": "[length(parameters('masterNodes').locations)]" 299 | }, 300 | "properties": { 301 | "securityRules": [ 302 | { 303 | "name": "BastionAllowHttpsInbound", 304 | "properties": { 305 | "description": "Allow inbound HTTPS traffic", 306 | "protocol": "Tcp", 307 | "sourcePortRange": "*", 308 | "sourceAddressPrefix": "*", 309 | "destinationPortRange": "443", 310 | "destinationAddressPrefix": "*", 311 | "access": "Allow", 312 | "priority": 100, 313 | "direction": "Inbound" 314 | } 315 | }, 316 | { 317 | "name": "BastionAllowGatewayManagerInbound", 318 | "properties": { 319 | "description": "Allow inbound HTTPS traffic from Application Gateway", 320 | "protocol": "Tcp", 321 | "sourcePortRange": "*", 322 | "sourceAddressPrefix": "GatewayManager", 323 | "destinationPortRange": "443", 324 | "destinationAddressPrefix": "*", 325 | "access": "Allow", 326 | "priority": 120, 327 | "direction": "Inbound" 328 | } 329 | }, 330 | { 331 | "name": "BastionDenyAllInbound", 332 | "properties": { 333 | "description": "Deny all inbound traffic", 334 | "protocol": "*", 335 | "sourcePortRange": "*", 336 | "destinationPortRange": "*", 337 | "sourceAddressPrefix": "*", 338 | "destinationAddressPrefix": "*", 339 | "access": "Deny", 340 | "priority": 900, 341 | "direction": "Inbound" 342 | } 343 | }, 344 | { 345 | "name": "BastionVnetAllowRdpAndSshOutbound", 346 | "properties": { 347 | "description": "Allow inbound RDP and SSH traffic", 348 | "protocol": "Tcp", 349 | "sourcePortRange": "*", 350 | "sourceAddressPrefix": "*", 351 | "destinationPortRanges": [ 352 | "22", 353 | "3389" 354 | ], 355 | "destinationAddressPrefix": "VirtualNetwork", 356 | "access": "Allow", 357 | "priority": 100, 358 | "direction": "Outbound" 359 | } 360 | }, 361 | { 362 | "name": "BastionAzureAllowHttpsOutbound", 363 | "properties": { 364 | "description": "Allow inbound RDP and SSH traffic", 365 | "protocol": "Tcp", 366 | "sourcePortRange": "*", 367 | "sourceAddressPrefix": "*", 368 | "destinationPortRange": "443", 369 | "destinationAddressPrefix": "AzureCloud", 370 | "access": "Allow", 371 | "priority": 120, 372 | "direction": "Outbound" 373 | } 374 | }, 375 | { 376 | "name": "BastionDenyAllOutbound", 377 | "properties": { 378 | "description": "Deny all inbound traffic", 379 | "protocol": "*", 380 | "sourcePortRange": "*", 381 | "sourceAddressPrefix": "*", 382 | "destinationPortRange": "*", 383 | "destinationAddressPrefix": "*", 384 | "access": "Deny", 385 | "priority": 900, 386 | "direction": "Outbound" 387 | } 388 | } 389 | ] 390 | } 391 | }, 392 | { 393 | "apiVersion": "2015-11-01-preview", 394 | "type": "Microsoft.OperationalInsights/workspaces", 395 | "name": "[parameters('workspaceName')]", 396 | "location": "[parameters('location')]", 397 | "properties": { 398 | "sku": { 399 | "name": "PerGB2018" 400 | } 401 | }, 402 | "resources": [ 403 | { 404 | "apiVersion": "2015-11-01-preview", 405 | "location": "[parameters('location')]", 406 | "name": "[concat('AgentHealthAssessment', '(', parameters('workspaceName'),')')]", 407 | "type": "Microsoft.OperationsManagement/solutions", 408 | "dependsOn": [ 409 | "[concat('Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]" 410 | ], 411 | "properties": { 412 | "workspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]" 413 | }, 414 | "plan": { 415 | "name": "[concat('AgentHealthAssessment', '(', parameters('workspaceName'),')')]", 416 | "publisher": "Microsoft", 417 | "product": "OMSGallery/AgentHealthAssessment", 418 | "promotionCode": "" 419 | } 420 | }, 421 | { 422 | "apiVersion": "2015-11-01-preview", 423 | "location": "[parameters('location')]", 424 | "name": "[concat('ServiceMap', '(', parameters('workspaceName'),')')]", 425 | "type": "Microsoft.OperationsManagement/solutions", 426 | "dependsOn": [ 427 | "[concat('Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]" 428 | ], 429 | "properties": { 430 | "workspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]" 431 | }, 432 | "plan": { 433 | "name": "[concat('ServiceMap', '(', parameters('workspaceName'),')')]", 434 | "publisher": "Microsoft", 435 | "product": "[concat('OMSGallery/', 'ServiceMap')]", 436 | "promotionCode": "" 437 | } 438 | }, 439 | { 440 | "apiVersion": "2015-11-01-preview", 441 | "location": "[parameters('location')]", 442 | "name": "[concat('InfrastructureInsights', '(', parameters('workspaceName'),')')]", 443 | "type": "Microsoft.OperationsManagement/solutions", 444 | "dependsOn": [ 445 | "[concat('Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]" 446 | ], 447 | "properties": { 448 | "workspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]" 449 | }, 450 | "plan": { 451 | "name": "[concat('InfrastructureInsights', '(', parameters('workspaceName'),')')]", 452 | "publisher": "Microsoft", 453 | "product": "[concat('OMSGallery/', 'InfrastructureInsights')]", 454 | "promotionCode": "" 455 | } 456 | }, 457 | { 458 | "apiVersion": "2015-11-01-preview", 459 | "type": "dataSources", 460 | "name": "WindowsEventLog-Application", 461 | "dependsOn": [ 462 | "[variables('workspaceId')]" 463 | ], 464 | "kind": "WindowsEvent", 465 | "properties": { 466 | "eventLogName": "Application", 467 | "eventTypes": [ 468 | { 469 | "eventType": "Error" 470 | }, 471 | { 472 | "eventType": "Warning" 473 | } 474 | ] 475 | } 476 | } 477 | ] 478 | }, 479 | { 480 | "apiVersion": "2019-09-01", 481 | "type": "Microsoft.Network/publicIPAddresses", 482 | "name": "[concat(concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix), 'Ip')]", 483 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 484 | "tags": { 485 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]" 486 | }, 487 | "copy": { 488 | "name": "masterPublicIPAddressesCopy", 489 | "count": "[length(parameters('masterNodes').locations)]" 490 | }, 491 | "sku": { 492 | "name": "Standard" 493 | }, 494 | "properties": { 495 | "publicIPAllocationMethod": "Static" 496 | } 497 | }, 498 | { 499 | "apiVersion": "2019-09-01", 500 | "type": "Microsoft.Network/publicIPAddresses", 501 | "name": "[concat(concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix), 'Ip')]", 502 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 503 | "tags": { 504 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix)]" 505 | }, 506 | "copy": { 507 | "name": "slavePublicIPAddressesCopy", 508 | "count": "[length(parameters('slaveNodes').locations)]" 509 | }, 510 | "sku": { 511 | "name": "Standard" 512 | }, 513 | "properties": { 514 | "publicIPAllocationMethod": "Static" 515 | } 516 | }, 517 | { 518 | "apiVersion": "2019-09-01", 519 | "type": "Microsoft.Network/publicIPAddresses", 520 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Ip')]", 521 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 522 | "tags": { 523 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Ip')]" 524 | }, 525 | "copy": { 526 | "name": "masterBastionPublicIPAddressesCopy", 527 | "count": "[length(parameters('masterNodes').locations)]" 528 | }, 529 | "sku": { 530 | "name": "Standard" 531 | }, 532 | "properties": { 533 | "publicIPAllocationMethod": "Static" 534 | } 535 | }, 536 | { 537 | "type": "Microsoft.Network/bastionHosts", 538 | "apiVersion": "2019-09-01", 539 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]", 540 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 541 | "tags": { 542 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]" 543 | }, 544 | "copy": { 545 | "name": "masterBastionHostsCopy", 546 | "count": "[length(parameters('masterNodes').locations)]" 547 | }, 548 | "dependsOn": [ 549 | "masterBastionPublicIPAddressesCopy", 550 | "masterVirtualNetworksCopy" 551 | ], 552 | "properties": { 553 | "ipConfigurations": [ 554 | { 555 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'IpConfiguration')]", 556 | "properties": { 557 | "subnet": { 558 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', concat(parameters('virtualNetwork').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix), 'AzureBastionSubnet')]" 559 | }, 560 | "publicIPAddress": { 561 | "id": "[resourceId('Microsoft.Network/publicIpAddresses', concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Ip'))]" 562 | } 563 | } 564 | } 565 | ] 566 | } 567 | }, 568 | { 569 | "type": "Microsoft.Network/bastionHosts/providers/diagnosticsettings", 570 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, '/Microsoft.Insights/service')]", 571 | "apiVersion": "2016-09-01", 572 | "location": "[resourceGroup().location]", 573 | "tags": { 574 | "name": "[concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]" 575 | }, 576 | "copy": { 577 | "name": "bastionHostDiagnosticSettingsCopy", 578 | "count": "[length(parameters('masterNodes').locations)]" 579 | }, 580 | "dependsOn": [ 581 | "[variables('workspaceId')]", 582 | "masterBastionHostsCopy" 583 | ], 584 | "properties": { 585 | "workspaceId": "[variables('workspaceId')]", 586 | "logs": [ 587 | { 588 | "category": "BastionAuditLogs", 589 | "enabled": true, 590 | "retentionPolicy": { 591 | "enabled": true, 592 | "days": 0 593 | } 594 | } 595 | ] 596 | } 597 | }, 598 | { 599 | "apiVersion": "2019-09-01", 600 | "type": "Microsoft.Network/virtualNetworks", 601 | "name": "[concat(parameters('virtualNetwork').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]", 602 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 603 | "copy": { 604 | "name": "masterVirtualNetworksCopy", 605 | "count": "[length(parameters('masterNodes').locations)]" 606 | }, 607 | "dependsOn": [ 608 | "masterVirtualMachineNetworkSecurityGroupsCopy", 609 | "masterBastionNetworkSecurityGroupsCopy" 610 | ], 611 | "properties": { 612 | "addressSpace": { 613 | "addressPrefixes": "[parameters('virtualNetwork').addressPrefixes]" 614 | }, 615 | "subnets": [ 616 | { 617 | "name": "[concat(parameters('virtualNetwork').subnetPrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]", 618 | "properties": { 619 | "addressPrefix": "[parameters('virtualNetwork').subnetAddressPrefix]", 620 | "networkSecurityGroup": { 621 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg'))]" 622 | } 623 | } 624 | }, 625 | { 626 | "name": "AzureBastionSubnet", 627 | "properties": { 628 | "addressPrefix": "[parameters('bastion').subnetAddressPrefix]", 629 | "networkSecurityGroup": { 630 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('bastion').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg'))]" 631 | } 632 | } 633 | } 634 | ] 635 | } 636 | }, 637 | { 638 | "apiVersion": "2019-09-01", 639 | "type": "Microsoft.Network/virtualNetworks", 640 | "name": "[concat(parameters('virtualNetwork').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix)]", 641 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 642 | "copy": { 643 | "name": "slaveVirtualNetworksCopy", 644 | "count": "[length(parameters('slaveNodes').locations)]" 645 | }, 646 | "dependsOn": [ 647 | "slaveVirtualMachineNetworkSecurityGroupsCopy" 648 | ], 649 | "properties": { 650 | "addressSpace": { 651 | "addressPrefixes": "[parameters('virtualNetwork').addressPrefixes]" 652 | }, 653 | "subnets": [ 654 | { 655 | "name": "[concat(parameters('virtualNetwork').subnetPrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix)]", 656 | "properties": { 657 | "addressPrefix": "[parameters('virtualNetwork').subnetAddressPrefix]", 658 | "networkSecurityGroup": { 659 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nsg'))]" 660 | } 661 | } 662 | } 663 | ] 664 | } 665 | }, 666 | { 667 | "apiVersion": "2018-08-01", 668 | "type": "Microsoft.Network/networkInterfaces", 669 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nic')]", 670 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 671 | "tags": { 672 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nic')]" 673 | }, 674 | "copy": { 675 | "name": "masterNetworkInterfacesCopy", 676 | "count": "[length(parameters('masterNodes').locations)]" 677 | }, 678 | "dependsOn": [ 679 | "masterPublicIPAddressesCopy", 680 | "masterVirtualNetworksCopy", 681 | "masterVirtualMachineNetworkSecurityGroupsCopy", 682 | "masterBastionNetworkSecurityGroupsCopy" 683 | ], 684 | "properties": { 685 | "ipConfigurations": [ 686 | { 687 | "name": "VmIpConfiguration", 688 | "properties": { 689 | "privateIPAllocationMethod": "Dynamic", 690 | "publicIPAddress": { 691 | "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Ip'))]" 692 | }, 693 | "subnet": { 694 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', concat(parameters('virtualNetwork').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix), concat(parameters('virtualNetwork').subnetPrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix))]" 695 | } 696 | } 697 | } 698 | ], 699 | "networkSecurityGroup": { 700 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nsg'))]" 701 | } 702 | } 703 | }, 704 | { 705 | "apiVersion": "2018-08-01", 706 | "type": "Microsoft.Network/networkInterfaces", 707 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nic')]", 708 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 709 | "tags": { 710 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nic')]" 711 | }, 712 | "copy": { 713 | "name": "slaveNetworkInterfacesCopy", 714 | "count": "[length(parameters('slaveNodes').locations)]" 715 | }, 716 | "dependsOn": [ 717 | "slavePublicIPAddressesCopy", 718 | "slaveVirtualNetworksCopy", 719 | "slaveVirtualMachineNetworkSecurityGroupsCopy" 720 | ], 721 | "properties": { 722 | "ipConfigurations": [ 723 | { 724 | "name": "VmIpConfiguration", 725 | "properties": { 726 | "privateIPAllocationMethod": "Dynamic", 727 | "publicIPAddress": { 728 | "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Ip'))]" 729 | }, 730 | "subnet": { 731 | "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', concat(parameters('virtualNetwork').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix), concat(parameters('virtualNetwork').subnetPrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix))]" 732 | } 733 | } 734 | } 735 | ], 736 | "networkSecurityGroup": { 737 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nsg'))]" 738 | } 739 | } 740 | }, 741 | { 742 | "apiVersion": "2019-07-01", 743 | "type": "Microsoft.Compute/virtualMachines", 744 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]", 745 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 746 | "copy": { 747 | "name": "masterVirtualMachinesCopy", 748 | "count": "[length(parameters('masterNodes').locations)]" 749 | }, 750 | "dependsOn": [ 751 | "[resourceId('Microsoft.Network/networkInterfaces', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, 'Nic'))]" 752 | ], 753 | "tags": { 754 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]" 755 | }, 756 | "properties": { 757 | "hardwareProfile": { 758 | "vmSize": "[parameters('vmSize')]" 759 | }, 760 | "osProfile": { 761 | "computerName": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix)]", 762 | "adminUsername": "[parameters('adminUsername')]", 763 | "adminPassword": "[parameters('adminPassword')]", 764 | "windowsConfiguration": { 765 | "provisionVmAgent": "true", 766 | "enableAutomaticUpdates": "true" 767 | } 768 | }, 769 | "storageProfile": { 770 | "imageReference": { 771 | "publisher": "[parameters('vmImagePublisher')]", 772 | "offer": "[parameters('vmImageOffer')]", 773 | "sku": "[parameters('vmImageSku')]", 774 | "version": "[parameters('vmImageVersion')]" 775 | }, 776 | "osDisk": { 777 | "createOption": "FromImage" 778 | }, 779 | "dataDisks": [ 780 | ] 781 | }, 782 | "networkProfile": { 783 | "networkInterfaces": [ 784 | { 785 | "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix), 'Nic'))]" 786 | } 787 | ] 788 | } 789 | }, 790 | "resources": [ 791 | ] 792 | }, 793 | { 794 | "apiVersion": "2019-07-01", 795 | "type": "Microsoft.Compute/virtualMachines", 796 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix)]", 797 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 798 | "copy": { 799 | "name": "slaveVirtualMachinesCopy", 800 | "count": "[length(parameters('slaveNodes').locations)]" 801 | }, 802 | "dependsOn": [ 803 | "[resourceId('Microsoft.Network/networkInterfaces', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, 'Nic'))]" 804 | ], 805 | "tags": { 806 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix)]" 807 | }, 808 | "properties": { 809 | "hardwareProfile": { 810 | "vmSize": "[parameters('vmSize')]" 811 | }, 812 | "osProfile": { 813 | "computerName": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix)]", 814 | "adminUsername": "[parameters('adminUsername')]", 815 | "adminPassword": "[parameters('adminPassword')]", 816 | "windowsConfiguration": { 817 | "provisionVmAgent": "true", 818 | "enableAutomaticUpdates": "true" 819 | } 820 | }, 821 | "storageProfile": { 822 | "imageReference": { 823 | "publisher": "[parameters('vmImagePublisher')]", 824 | "offer": "[parameters('vmImageOffer')]", 825 | "sku": "[parameters('vmImageSku')]", 826 | "version": "[parameters('vmImageVersion')]" 827 | }, 828 | "osDisk": { 829 | "createOption": "FromImage" 830 | }, 831 | "dataDisks": [ 832 | ] 833 | }, 834 | "networkProfile": { 835 | "networkInterfaces": [ 836 | { 837 | "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix), 'Nic'))]" 838 | } 839 | ] 840 | } 841 | }, 842 | "resources": [ 843 | ] 844 | }, 845 | { 846 | "apiVersion": "2019-03-01", 847 | "type": "Microsoft.Compute/virtualMachines/extensions", 848 | "name": "[concat(concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix), '/MicrosoftMonitoringAgent')]", 849 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 850 | "copy": { 851 | "name": "masterMicrosoftMonitoringAgentCopy", 852 | "count": "[length(parameters('masterNodes').locations)]" 853 | }, 854 | "dependsOn": [ 855 | "[resourceId('Microsoft.Compute/virtualMachines', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix))]" 856 | ], 857 | "properties": { 858 | "publisher": "Microsoft.EnterpriseCloud.Monitoring", 859 | "type": "MicrosoftMonitoringAgent", 860 | "typeHandlerVersion": "1.0", 861 | "autoUpgradeMinorVersion": true, 862 | "settings": { 863 | "workspaceId": "[reference(variables('workspaceId'), '2017-03-15-preview').customerId]" 864 | }, 865 | "protectedSettings": { 866 | "workspaceKey": "[listKeys(variables('workspaceId'), '2017-03-15-preview').primarySharedKey]" 867 | } 868 | } 869 | }, 870 | { 871 | "apiVersion": "2019-03-01", 872 | "type": "Microsoft.Compute/virtualMachines/extensions", 873 | "name": "[concat(concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix), '/MicrosoftMonitoringAgent')]", 874 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 875 | "copy": { 876 | "name": "slaveMicrosoftMonitoringAgentCopy", 877 | "count": "[length(parameters('slaveNodes').locations)]" 878 | }, 879 | "dependsOn": [ 880 | "[resourceId('Microsoft.Compute/virtualMachines', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix))]" 881 | ], 882 | "properties": { 883 | "publisher": "Microsoft.EnterpriseCloud.Monitoring", 884 | "type": "MicrosoftMonitoringAgent", 885 | "typeHandlerVersion": "1.0", 886 | "autoUpgradeMinorVersion": true, 887 | "settings": { 888 | "workspaceId": "[reference(variables('workspaceId'), '2017-03-15-preview').customerId]" 889 | }, 890 | "protectedSettings": { 891 | "workspaceKey": "[listKeys(variables('workspaceId'), '2017-03-15-preview').primarySharedKey]" 892 | } 893 | } 894 | }, 895 | { 896 | "apiVersion": "2019-03-01", 897 | "type": "Microsoft.Compute/virtualMachines/extensions", 898 | "name": "[concat(concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix), '/DependencyAgent')]", 899 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 900 | "copy": { 901 | "name": "masterDependencyAgentCopy", 902 | "count": "[length(parameters('masterNodes').locations)]" 903 | }, 904 | "dependsOn": [ 905 | "[resourceId('Microsoft.Compute/virtualMachines', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix))]" 906 | ], 907 | "properties": { 908 | "publisher": "Microsoft.Azure.Monitoring.DependencyAgent", 909 | "type": "DependencyAgentWindows", 910 | "typeHandlerVersion": "9.4", 911 | "autoUpgradeMinorVersion": true, 912 | "settings": { 913 | }, 914 | "protectedSettings": { 915 | } 916 | } 917 | }, 918 | { 919 | "apiVersion": "2019-03-01", 920 | "type": "Microsoft.Compute/virtualMachines/extensions", 921 | "name": "[concat(concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix), '/DependencyAgent')]", 922 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 923 | "copy": { 924 | "name": "slaveDependencyAgentCopy", 925 | "count": "[length(parameters('slaveNodes').locations)]" 926 | }, 927 | "dependsOn": [ 928 | "[resourceId('Microsoft.Compute/virtualMachines', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix))]" 929 | ], 930 | "properties": { 931 | "publisher": "Microsoft.Azure.Monitoring.DependencyAgent", 932 | "type": "DependencyAgentWindows", 933 | "typeHandlerVersion": "9.4", 934 | "autoUpgradeMinorVersion": true, 935 | "settings": { 936 | }, 937 | "protectedSettings": { 938 | } 939 | } 940 | }, 941 | { 942 | "apiVersion": "2019-03-01", 943 | "type": "Microsoft.Compute/virtualMachines/extensions", 944 | "name": "[concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix, '/InstallJMeter')]", 945 | "location": "[parameters('masterNodes').locations[copyIndex()].region]", 946 | "copy": { 947 | "name": "masterCustomScriptExtensionCopy", 948 | "count": "[length(parameters('masterNodes').locations)]" 949 | }, 950 | "dependsOn": [ 951 | "[resourceId('Microsoft.Compute/virtualMachines', concat(parameters('masterNodes').namePrefix, parameters('masterNodes').locations[copyIndex()].nameSuffix))]" 952 | ], 953 | "properties": { 954 | "publisher": "Microsoft.Compute", 955 | "type": "CustomScriptExtension", 956 | "typeHandlerVersion": "1.9", 957 | "autoUpgradeMinorVersion": true, 958 | "settings": { 959 | "fileUris": [ 960 | "[concat(parameters('masterScriptFilePath'), '/', parameters('masterScriptFileName'))]" 961 | ] 962 | }, 963 | "protectedSettings": { 964 | "commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -File ', parameters('masterScriptFileName'))]" 965 | } 966 | } 967 | }, 968 | { 969 | "apiVersion": "2019-03-01", 970 | "type": "Microsoft.Compute/virtualMachines/extensions", 971 | "name": "[concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix, '/InstallJMeter')]", 972 | "location": "[parameters('slaveNodes').locations[copyIndex()].region]", 973 | "copy": { 974 | "name": "slaveCustomScriptExtensionCopy", 975 | "count": "[length(parameters('slaveNodes').locations)]" 976 | }, 977 | "dependsOn": [ 978 | "[resourceId('Microsoft.Compute/virtualMachines', concat(parameters('slaveNodes').namePrefix, parameters('slaveNodes').locations[copyIndex()].nameSuffix))]" 979 | ], 980 | "properties": { 981 | "publisher": "Microsoft.Compute", 982 | "type": "CustomScriptExtension", 983 | "typeHandlerVersion": "1.9", 984 | "autoUpgradeMinorVersion": true, 985 | "settings": { 986 | "fileUris": [ 987 | "[concat(parameters('slaveScriptFilePath'), '/', parameters('slaveScriptFileName'))]" 988 | ] 989 | }, 990 | "protectedSettings": { 991 | "commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -File ', parameters('slaveScriptFileName'))]" 992 | } 993 | } 994 | } 995 | ], 996 | "outputs": { 997 | } 998 | } --------------------------------------------------------------------------------