Start/Stop Azure TRE
Once you've provisioned an Azure TRE instance it will begin to incur running costs of the underlying Azure services.
Within evaluation or development, you may want to "pause" the TRE environment during out of office hours or weekends, to reduce costs without having to completely destroy the environment. The following make
targets provide a simple way to start and stop both the Azure Firewall and Azure Application Gateway instances, considerably reducing the Azure TRE instance running costs.
Info
After running make all
underlying Azure TRE services are automatically started and billing will start.
Start Azure TRE
This will allocate the Azure Firewall settings with a public IP and start the Azure Application Gateway service, starting billing of both services.
make tre-start
Stop Azure TRE
This will deallocate the Azure Firewall public IP and stop the Azure Application Gateway service, stopping billing of both services.
make tre-stop
Automating stop
In certain situations, you might want to stop any TRE running on a schedule to reduce costs in a wider way. We have this procedure setup in our development subscriptions where each night we stop all our environments after which each developer would need to manually start their TRE when they need it again.
Requirements
We use Azure Automation to run this procedure.
Be sure to create a runbook with PowerShell 7.1 or PowerShell 7.2 enabled and an identity with contributor permissions on the subscription. Note that the script below uses a system managed identity and if you use something different then you might need to update the authentication part.
If you create a new Automation account, you will have the required modules preinstalled.
Finally, schedule it to run when it makes sense for you.
Stop Runbook Script
try {
"Logging in to Azure..."
Connect-AzAccount -Identity
}
catch {
Write-Error -Message $_.Exception
throw $_.Exception
}
$azContext = Get-AzContext
$azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile)
$token = $profileClient.AcquireAccessToken($azContext.Subscription.TenantId)
$authHeader = @{
'Content-Type' = 'application/json'
'Authorization' = 'Bearer ' + $token.AccessToken
}
# Get all resource groups that have the default Azure TRE project tag value
$ResourceGroups = Get-AzResourceGroup -Tag @{'project' = 'Azure Trusted Research Environment' }
foreach ($Group in $ResourceGroups) {
if ($Group.ResourceGroupName -like '*-ws-*') {
# Deal with the workspace resource groups separately (below)
continue
}
# Deallocate the Azure Firewall (expecting only one per TRE instance)
$Firewall = Get-AzFirewall -ResourceGroupName $Group.ResourceGroupName
if ($null -ne $Firewall) {
$Firewall.Deallocate()
Write-Output "Deallocating Firewall '$($Firewall.Name)'"
Set-AzFirewall -AzureFirewall $Firewall
}
# Stop the Application Gateway(s)
# Multiple Application Gateways may exist if the certs shared service is installed
$Gateways = Get-AzApplicationGateway -ResourceGroupName $Group.ResourceGroupName
foreach ($Gateway in $Gateways) {
Write-Output "Stopping Application Gateway '$($Gateway.Name)'"
Stop-AzApplicationGateway -ApplicationGateway $Gateway
}
# Stop the MySQL servers
$MySQLServers = Get-AzResource -ResourceGroupName $Group.ResourceGroupName -ResourceType "Microsoft.DBforMySQL/servers"
foreach ($Server in $MySQLServers) {
# Invoke the REST API
Write-Output "Stopping MySQL '$($Server.Name)'"
$restUri = 'https://management.azure.com/subscriptions/' + $azContext.Subscription.Id + '/resourceGroups/' + $Group.ResourceGroupName + '/providers/Microsoft.DBForMySQL/servers/' + $Server.Name + '/stop?api-version=2020-01-01'
$response = Invoke-RestMethod -Uri $restUri -Method POST -Headers $authHeader
}
# Deallocate all the virtual machine scale sets (resource processor)
$VMSS = Get-AzVMSS -ResourceGroupName $Group.ResourceGroupName
foreach ($item in $VMSS) {
Write-Output "Stopping VMSS '$($item.Name)'"
Stop-AzVmss -ResourceGroupName $item.ResourceGroupName -VMScaleSetName $item.Name -Force
}
# Deallocate all the VMs
$VM = Get-AzVM -ResourceGroupName $Group.ResourceGroupName
foreach ($item in $VM) {
Write-Output "Stopping VM '$($item.Name)'"
Stop-AzVm -ResourceGroupName $item.ResourceGroupName -Name $item.Name -Force
}
# Process all the workspace resource groups for this TRE instance
$WorkspaceResourceGroups = Get-AzResourceGroup -Name "$($Group.ResourceGroupName)-ws-*"
foreach ($wsrg in $WorkspaceResourceGroups) {
# Deallocate all the VMs
$VM = Get-AzVM -ResourceGroupName $wsrg.ResourceGroupName
foreach ($item in $VM) {
Write-Output "Stopping workspace VM '$($item.Name)'"
Stop-AzVm -ResourceGroupName $item.ResourceGroupName -Name $item.Name -Force
}
}
}
Automating start
To restart the TRE core services (Firewall, Application Gateway(s), Virtual Machine Scale Sets, Virtual Machines, and MySQL), you can use make tre-start
. Depending on your workflow, you might not be able to easily execute the make
target. Alternatively, you can create a second Runbook and execute it manually. The PowerShell code to start TRE core services is below:
try {
"Logging in to Azure..."
Connect-AzAccount -Identity
}
catch {
Write-Error -Message $_.Exception
throw $_.Exception
}
$azContext = Get-AzContext
$azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile)
$token = $profileClient.AcquireAccessToken($azContext.Subscription.TenantId)
$authHeader = @{
'Content-Type' = 'application/json'
'Authorization' = 'Bearer ' + $token.AccessToken
}
# Get all resource groups that have the default Azure TRE project tag value
$ResourceGroups = Get-AzResourceGroup -Tag @{'project' = 'Azure Trusted Research Environment' }
foreach ($Group in $ResourceGroups) {
if ($Group.ResourceGroupName -like '*-ws-*') {
# Don't deal with the workspace resource groups
continue
}
$azureTreId = $Group.Tags['tre_id']
Write-Output "Starting TRE core resources for '$azureTreId'"
# Allocate the Azure Firewall (expecting only one per TRE instance)
$Firewall = Get-AzFirewall -ResourceGroupName $Group.ResourceGroupName
if ($null -ne $Firewall) {
# Find the firewall's public IP and virtual network
$pip = Get-AzPublicIpAddress -ResourceGroupName $Group.ResourceGroupName -Name "pip-fw-$azureTreId"
$vnet = Get-AzVirtualNetwork -ResourceGroupName $Group.ResourceGroupName -Name "vnet-$azureTreId"
# Find the firewall's public management IP - note this will only be present for a firewall with a Basic SKU
$mgmtPip = Get-AzPublicIpAddress -ResourceGroupName "rg-$azureTreId" -Name "pip-fw-management-$azureTreId" -ErrorAction SilentlyContinue
$Firewall.Allocate($vnet, $pip, $mgmtPip)
Write-Output "Allocating Firewall '$($Firewall.Name)' with public IP '$($pip.Name)'"
Set-AzFirewall -AzureFirewall $Firewall
}
# Start the Application Gateway(s)
# Multiple Application Gateways may exist if the certs shared service is installed
$Gateways = Get-AzApplicationGateway -ResourceGroupName $Group.ResourceGroupName
foreach ($Gateway in $Gateways) {
Write-Output "Starting Application Gateway '$($Gateway.Name)'"
Start-AzApplicationGateway -ApplicationGateway $Gateway
}
# Start the MySQL servers
$MySQLServers = Get-AzResource -ResourceGroupName $Group.ResourceGroupName -ResourceType "Microsoft.DBforMySQL/servers"
foreach ($Server in $MySQLServers) {
# Invoke the REST API
Write-Output "Starting MySQL '$($Server.Name)'"
$restUri = 'https://management.azure.com/subscriptions/' + $azContext.Subscription.Id + '/resourceGroups/' + $Group.ResourceGroupName + '/providers/Microsoft.DBForMySQL/servers/' + $Server.Name + '/start?api-version=2020-01-01'
$response = Invoke-RestMethod -Uri $restUri -Method POST -Headers $authHeader
}
# Allocate all the virtual machine scale sets (resource processor)
$VMSS = Get-AzVMSS -ResourceGroupName $Group.ResourceGroupName
foreach ($item in $VMSS) {
Write-Output "Starting VMSS '$($item.Name)'"
Start-AzVmss -ResourceGroupName $item.ResourceGroupName -VMScaleSetName $item.Name
}
# Start VMs
$VM = Get-AzVM -ResourceGroupName $Group.ResourceGroupName
foreach ($item in $VM) {
Write-Output "Starting VM '$($item.Name)'"
Start-AzVm -ResourceGroupName $item.ResourceGroupName -Name $item.Name
}
}