While I was playing around with the Azure Resource Manager Copy Operation, I started thinking about what I could do with it. The first wild idea that popped into my head was, to use it to deploy multi-geo environments from a single ARM Template.

Alright, some of you might think that it’s not such great idea, and I can appreciate that. But I’m just too curious, so let’s give this a chance.

The Scenario

Imagine a service that needs to be deployed to multiple Data Centers. The Virtual Machine configuration for each environment is identical and generally requires to be deployed to at least five Microsoft Azure Data Centers.

The following Azure Resource Manager (ARM) Template, deploys CentOS 7.1 Virtual Machines to East US, West US, West Europe, East Asia and Southeast Asia Azure Data Centers. To accomplish this, I defined an array of Azure Regions, that is then used to populate the location property for each Resource.

For this to work, I had to create copies the Virtual Network (VNet), the Storage Account, the Public IP, the Network Interface (NIC) and the Virtual Machine in each region. Building on the ARM Template from my earlier post, I decided to use the vmCount as the control value for the Copy Operation applied of each Resource. This strategy worked out because the Template creates one Virtual Machine per region.

This ARM Template should be considered an experiment, because the Resource Group resides in a particular Azure Region and it contains Resources from across the world. As of writing this post, I can tell you that this is pretty cool, and that I’m curious to find out more about the long-term maintainability of this scenario.

Taking this a few steps further, the Custom Script for Linux Virtual Machine Extension could be used to download, unpack, install and configure a service. Then an Azure Traffic Manager instance could be created and configured to load balance traffic across the multi-geo environment.

Building the ARM Template

This step required some gymnastics, because I wasn’t familiar with the Resouce Dependencies for some of the Resources in the Template. After a couple of minutes, it was clear that I needed to add Copy Operations to all of the Resources. Using the copyIndex() and concat() functions, I named the Resources. Then I made sure that everything was referenced properly.

DeploymentTemplate.json

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "newStorageAccountName": {
            "type": "string",
            "metadata": {
                "description": "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed."
            }
        },
        "adminUsername": {
            "type": "string",
            "metadata": {
                "description": "User name for the Virtual Machine."
            }
        },
        "adminPassword": {
            "type": "securestring",
            "metadata": {
                "description": "Password for the Virtual Machine."
            }
        },
        "dnsPrefixNameForPublicIP": {
            "type": "string",
            "metadata": {
                "description": "Unique DNS Name for the Public IP used to access the Virtual Machine."
            }
        },
        "OSVersion": {
            "type": "string",
            "defaultValue": "7.1",
            "allowedValues": [
                "7.1"
            ]
        },
        "vmCount": {
            "type": "int",
            "defaultValue": 1
        },
        "virtualNetworkName": {
            "type": "string",
            "metadata": {
                "description": "Virtual Network Name"
            }
        },
        "vmNamePrefix": {
            "type": "string",
            "metadata": {
                "description": "Virtual Machine Name Prefix"
            }
        },
        "vmSize": {
            "type": "string",
            "defaultValue": "Standard_A1",
            "metadata": {
                "description": "Virtual Machine Size"
            }
        },
        "locations": {
            "type": "array",
            "defaultValue": [
                "East Asia",
                "Southeast Asia",
                "East US",
                "West US",
                "West Europe"
            ],
            "metadata": {
                "description": "Locations"
            }
        }
    },
    "variables": {
        "addressPrefix": "10.0.0.0/16",
        "dataDiskSize": "1023",
        "imageOffer": "CentOS",
        "imagePublisher": "OpenLogic",
        "nicName": "NIC",
        "OSDiskName": "osdisk",
        "publicIPAddressType": "Dynamic",
        "storageAccountType": "Standard_LRS",
        "subnetName": "Subnet",
        "subnetPrefix": "10.0.0.0/24",
        "vmStorageAccountContainerName": "vhds"
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "name": "[concat(parameters('newStorageAccountName'),copyIndex())]",
            "apiVersion": "2015-05-01-preview",
            "location": "[parameters('locations')[copyIndex()]]",
            "tags": {
                "displayName": "StorageAccount"
            },
            "properties": {
                "accountType": "[variables('storageAccountType')]"
            },
            "copy": {
                "name": "storageAccountCopy",
                "count": "[parameters('vmCount')]"
            }
        },
        {
            "apiVersion": "2015-05-01-preview",
            "type": "Microsoft.Network/publicIPAddresses",
            "name": "[concat(parameters('dnsPrefixNameForPublicIP'),copyIndex())]",
            "location": "[parameters('locations')[copyIndex()]]",
            "tags": {
                "displayName": "PublicIPAddress"
            },
            "properties": {
                "publicIPAllocationMethod": "[variables('publicIPAddressType')]",
                "dnsSettings": {
                    "domainNameLabel": "[concat(parameters('dnsPrefixNameForPublicIP'),copyIndex())]"
                }
            },
            "copy": {
                "name": "publicIpCopy",
                "count": "[parameters('vmCount')]"
            }
        },
        {
            "apiVersion": "2015-05-01-preview",
            "type": "Microsoft.Network/virtualNetworks",
            "name": "[concat(parameters('virtualNetworkName'),copyIndex())]",
            "location": "[parameters('locations')[copyIndex()]]",
            "tags": {
                "displayName": "VirtualNetwork"
            },
            "properties": {
                "addressSpace": {
                    "addressPrefixes": [
                        "[variables('addressPrefix')]"
                    ]
                },
                "subnets": [
                    {
                        "name": "[variables('subnetName')]",
                        "properties": {
                            "addressPrefix": "[variables('subnetPrefix')]"
                        }
                    }
                ]
            },
            "copy": {
                "name": "vnetCopy",
                "count": "[parameters('vmCount')]"
            }
        },
        {
            "apiVersion": "2015-05-01-preview",
            "type": "Microsoft.Network/networkInterfaces",
            "name": "[concat(variables('nicName'),copyIndex())]",
            "location": "[parameters('locations')[copyIndex()]]",
            "tags": {
                "displayName": "NetworkInterface"
            },
            "dependsOn": [
                "[concat('Microsoft.Network/publicIPAddresses/', parameters('dnsPrefixNameForPublicIP'), copyIndex())]",
                "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'), copyIndex())]"
            ],
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "[concat('ipconfig',copyIndex())]",
                        "properties": {
                            "privateIPAllocationMethod": "Dynamic",
                            "publicIPAddress": {
                                "id": "[resourceId('Microsoft.Network/publicIPAddresses',concat(parameters('dnsPrefixNameForPublicIP'),copyIndex()))]"
                            },
                            "subnet": {
                                "id": "[concat(resourceId('Microsoft.Network/virtualNetworks',parameters('virtualNetworkName')), copyIndex(),'/subnets/',variables('subnetName'))]"
                            }
                        }
                    }
                ]
            },
            "copy": {
                "name": "nicCopy",
                "count": "[parameters('vmCount')]"
            }
        },
        {
            "apiVersion": "2015-05-01-preview",
            "copy": {
                "name": "nodeCopy",
                "count": "[parameters('vmCount')]"
            },
            "dependsOn": [
                "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'), copyIndex())]",
                "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'), copyIndex())]"
            ],
            "location": "[parameters('locations')[copyIndex()]]",
            "name": "[concat(parameters('vmNamePrefix'),copyIndex())]",
            "properties": {
                "hardwareProfile": {
                    "vmSize": "[parameters('vmSize')]"
                },
                "osProfile": {
                    "computername": "[concat(parameters('vmNamePrefix'), copyIndex())]",
                    "adminUsername": "[parameters('adminUsername')]",
                    "adminPassword": "[parameters('adminPassword')]"
                },
                "storageProfile": {
                    "imageReference": {
                        "publisher": "[variables('imagePublisher')]",
                        "offer": "[variables('imageOffer')]",
                        "sku": "[parameters('OSVersion')]",
                        "version": "latest"
                    },
                    "osDisk": {
                        "name": "osdisk",
                        "vhd": {
                            "uri": "[concat('http://',parameters('newStorageAccountName'),copyIndex(),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',concat(variables('OSDiskName')),'.vhd')]"
                        },
                        "caching": "ReadWrite",
                        "createOption": "FromImage"
                    },
                    "dataDisks": [
                        {
                            "name": "datadisk1",
                            "diskSizeGB": "[variables('dataDiskSize')]",
                            "lun": 0,
                            "vhd": {
                                "Uri": "[concat('http://',parameters('newStorageAccountName'),copyIndex(),'.blob.core.windows.net/vhds/',concat(parameters('vmNamePrefix')),'dataDisk1' ,'.vhd')]"
                            },
                            "caching": "None",
                            "createOption": "Empty"
                        }
                    ]
                },
                "networkProfile": {
                    "networkInterfaces": [
                        {
                            "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('nicName') ,copyIndex()))]"
                        }
                    ]
                }
            },
            "tags": {
                "displayName": "VirtualMachine"
            },
            "type": "Microsoft.Compute/virtualMachines"
        }
    ]
}

Publishing the ARM Template

Prepare the Template Parameter File a.k.a DeploymentTemplate.param.dev.json

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "newStorageAccountName": {
            "value": "briseboiscentos"
        },
        "adminUsername": {
            "value": "briseboiscentos"
        },
        "dnsPrefixNameForPublicIP": {
            "value": "centosdns"
        },
        "virtualNetworkName": {
            "value": "centosvnet"
        },
        "vmNamePrefix": {
            "value": "briseboiscentos"
        },
        "vmSize": {
            "value": "Standard_A1"
        },
        "vmCount": {
            "value": 5
        },
        "locations": {
            "value": [
                "East Asia",
                "Southeast Asia",
                "East US",
                "West US",
                "West Europe"
            ]
        }
    }
}

Then deploy the Azure Resource Manager Template and Parameters using PowerShell.

New-AzureResourceGroup -Name 'centosbrisebois' `
                       -Location 'eastus' `
                       -TemplateFile 'DeploymentTemplate.json' `
                       -TemplateParameterFile 'DeploymentTemplate.param.dev.json' `
                       -Force -Verbose
# New-AzureResourceGroup VERBOSE Log

VERBOSE: 12:26:05 AM - Created resource group 'centosbrisebois' in location 'eastus'
VERBOSE: 12:26:06 AM - Template is valid.
VERBOSE: 12:26:07 AM - Create template deployment 'DeploymentTemplate'.
VERBOSE: 12:26:12 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos3' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet1' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet0' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet3' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet4' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos1' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos0' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet2' provisioning status is running
VERBOSE: 12:26:12 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos2' provisioning status is running
VERBOSE: 12:26:14 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns0' provisioning status is running
VERBOSE: 12:26:14 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos4' provisioning status is running
VERBOSE: 12:26:14 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns1' provisioning status is running
VERBOSE: 12:26:14 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns2' provisioning status is running
VERBOSE: 12:26:14 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns3' provisioning status is running
VERBOSE: 12:26:14 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns4' provisioning status is running
VERBOSE: 12:26:24 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns2' provisioning status is succeeded
VERBOSE: 12:26:24 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet3' provisioning status is succeeded
VERBOSE: 12:26:24 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet4' provisioning status is succeeded
VERBOSE: 12:26:24 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet2' provisioning status is succeeded
VERBOSE: 12:26:26 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns3' provisioning status is succeeded
VERBOSE: 12:26:26 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns4' provisioning status is succeeded
VERBOSE: 12:26:26 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet1' provisioning status is succeeded
VERBOSE: 12:26:26 AM - Resource Microsoft.Network/virtualNetworks 'centosvnet0' provisioning status is succeeded
VERBOSE: 12:26:28 AM - Resource Microsoft.Network/networkInterfaces 'NIC4' provisioning status is succeeded
VERBOSE: 12:26:28 AM - Resource Microsoft.Network/networkInterfaces 'NIC2' provisioning status is succeeded
VERBOSE: 12:26:28 AM - Resource Microsoft.Network/networkInterfaces 'NIC3' provisioning status is succeeded
VERBOSE: 12:26:28 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns0' provisioning status is succeeded
VERBOSE: 12:26:28 AM - Resource Microsoft.Network/publicIPAddresses 'centosdns1' provisioning status is succeeded
VERBOSE: 12:26:31 AM - Resource Microsoft.Network/networkInterfaces 'NIC1' provisioning status is succeeded
VERBOSE: 12:26:31 AM - Resource Microsoft.Network/networkInterfaces 'NIC0' provisioning status is succeeded
VERBOSE: 12:26:38 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos3' provisioning status is succeeded
VERBOSE: 12:26:40 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos3' provisioning status is running
VERBOSE: 12:27:04 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos2' provisioning status is running
VERBOSE: 12:27:04 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos1' provisioning status is succeeded
VERBOSE: 12:27:04 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos2' provisioning status is succeeded
VERBOSE: 12:27:09 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos1' provisioning status is running
VERBOSE: 12:27:09 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos4' provisioning status is succeeded
VERBOSE: 12:27:11 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos4' provisioning status is running
VERBOSE: 12:28:35 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos3' provisioning status is succeeded
VERBOSE: 12:28:57 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos2' provisioning status is succeeded
VERBOSE: 12:29:09 AM - Resource Microsoft.Storage/storageAccounts 'briseboiscentos0' provisioning status is succeeded
VERBOSE: 12:29:11 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos0' provisioning status is running
VERBOSE: 12:29:20 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos4' provisioning status is succeeded
VERBOSE: 12:31:10 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos0' provisioning status is succeeded
VERBOSE: 12:31:48 AM - Resource Microsoft.Compute/virtualMachines 'briseboiscentos1' provisioning status is succeeded

This deployment took about 5 minutes to complete across five Microsoft Azure Data Centers. I’m amazed by the fact that I was able to do this with a single Virtual Machine definition. This obviously comes with a price. As you may have noticed, the Azure Resource Manager Template can get complicated.

This experiment was quite interesting and will definitely fuel more experiments with the Azure Resource Manager. Please use the comment section below ask questions, share your experiences and to discuss best practices.

Helpful Azure Resource Manager Documentation

5 responses to Create Multi-Geo Environments using Azure Resource Manager

  1. 

    in your copy block, instead of using paramters(‘vMCount’) you can use the template length function of your locations array:
    length(parameters(‘locations’))

    Liked by 1 person

  2. 

    after creating a vnet in a resoure group in one region how will you use the same vnet for muktiple region’s?its throwing an error that it already exist in a resource group you cannot use that vnet.

    Like

  3. 

    Hello Alexandre,
    In the beginning of this post you wrote that “some of you might think that it’s not such great idea, and I can appreciate that”. Why is that? What could be alternative? What is ‘official’ recommendation from MSFT to implement multi-region deployment in ARM?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.