Quickstart: Use Bicep templates to create a virtual network

This quickstart shows you how to create a virtual network with two virtual machines (VMs), and then deploy Azure Bastion on the virtual network, by using Bicep templates. You then securely connect to the VMs from the internet by using Bastion and start private communication between the VMs.

A virtual network is the fundamental building block for private networks in Azure. Azure Virtual Network enables Azure resources like VMs to securely communicate with each other and the internet.

Diagram of resources created in the virtual network quickstart.

Bicep is a domain-specific language (DSL) that uses declarative syntax to deploy Azure resources. It provides concise syntax, reliable type safety, and support for code reuse. Bicep offers the best authoring experience for your infrastructure-as-code solutions in Azure.

Prerequisites


Create the virtual network and VMs

This quickstart uses the Two VMs in VNET Bicep template from Azure Resource Manager Quickstart Templates to create the virtual network, resource subnet, and VMs. The Bicep template defines the following Azure resources:

Review the Bicep file:

@description('Admin username')
param adminUsername string

@description('Admin password')
@secure()
param adminPassword string

@description('Prefix to use for VM names')
param vmNamePrefix string = 'BackendVM'

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Size of the virtual machines')
param vmSize string = 'Standard_D2s_v3'

var availabilitySetName = 'AvSet'
var storageAccountType = 'Standard_LRS'
var storageAccountName = uniqueString(resourceGroup().id)
var virtualNetworkName = 'vNet'
var subnetName = 'backendSubnet'
var loadBalancerName = 'ilb'
var networkInterfaceName = 'nic'
var subnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName)
var numberOfInstances = 2

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageAccountType
  }
  kind: 'StorageV2'
}

resource availabilitySet 'Microsoft.Compute/availabilitySets@2023-09-01' = {
  name: availabilitySetName
  location: location
  sku: {
    name: 'Aligned'
  }
  properties: {
    platformUpdateDomainCount: 2
    platformFaultDomainCount: 2
  }
}

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
  name: virtualNetworkName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
    subnets: [
      {
        name: subnetName
        properties: {
          addressPrefix: '10.0.2.0/24'
        }
      }
    ]
  }
}

resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = [for i in range(0, numberOfInstances): {
  name: '${networkInterfaceName}${i}'
  location: location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: subnetRef
          }
          loadBalancerBackendAddressPools: [
            {
              id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', loadBalancerName, 'BackendPool1')
            }
          ]
        }
      }
    ]
  }
  dependsOn: [
    virtualNetwork
    loadBalancer
  ]
}]

resource loadBalancer 'Microsoft.Network/loadBalancers@2023-09-01' = {
  name: loadBalancerName
  location: location
  sku: {
    name: 'Standard'
  }
  properties: {
    frontendIPConfigurations: [
      {
        properties: {
          subnet: {
            id: subnetRef
          }
          privateIPAddress: '10.0.2.6'
          privateIPAllocationMethod: 'Static'
        }
        name: 'LoadBalancerFrontend'
      }
    ]
    backendAddressPools: [
      {
        name: 'BackendPool1'
      }
    ]
    loadBalancingRules: [
      {
        properties: {
          frontendIPConfiguration: {
            id: resourceId('Microsoft.Network/loadBalancers/frontendIpConfigurations', loadBalancerName, 'LoadBalancerFrontend')
          }
          backendAddressPool: {
            id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', loadBalancerName, 'BackendPool1')
          }
          probe: {
            id: resourceId('Microsoft.Network/loadBalancers/probes', loadBalancerName, 'lbprobe')
          }
          protocol: 'Tcp'
          frontendPort: 80
          backendPort: 80
          idleTimeoutInMinutes: 15
        }
        name: 'lbrule'
      }
    ]
    probes: [
      {
        properties: {
          protocol: 'Tcp'
          port: 80
          intervalInSeconds: 15
          numberOfProbes: 2
        }
        name: 'lbprobe'
      }
    ]
  }
  dependsOn: [
    virtualNetwork
  ]
}

resource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = [for i in range(0, numberOfInstances): {
  name: '${vmNamePrefix}${i}'
  location: location
  properties: {
    availabilitySet: {
      id: availabilitySet.id
    }
    hardwareProfile: {
      vmSize: vmSize
    }
    osProfile: {
      computerName: '${vmNamePrefix}${i}'
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    storageProfile: {
      imageReference: {
        publisher: 'MicrosoftWindowsServer'
        offer: 'WindowsServer'
        sku: '2019-Datacenter'
        version: 'latest'
      }
      osDisk: {
        createOption: 'FromImage'
      }
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: networkInterface[i].id
        }
      ]
    }
    diagnosticsProfile: {
      bootDiagnostics: {
        enabled: true
        storageUri: storageAccount.properties.primaryEndpoints.blob
      }
    }
  }
}]

output location string = location
output name string = loadBalancer.name
output resourceGroupName string = resourceGroup().name
output resourceId string = loadBalancer.id

Deploy the Bicep template

  1. Save the Bicep file to your local computer as main.bicep.

  2. Deploy the Bicep file by using either the Azure CLI or Azure PowerShell:

    az group create --name TestRG --location eastus
    az deployment group create --resource-group TestRG --template-file main.bicep
    

When the deployment finishes, a message indicates that the deployment succeeded.

Deploy Azure Bastion

Bastion uses your browser to connect to VMs in your virtual network over Secure Shell (SSH) or Remote Desktop Protocol (RDP) by using their private IP addresses. The VMs don't need public IP addresses, client software, or special configuration. For more information about Bastion, see What is Azure Bastion?.

Note

Hourly pricing starts from the moment that Bastion is deployed, regardless of outbound data usage. For more information, see Pricing and SKUs. If you're deploying Bastion as part of a tutorial or test, we recommend that you delete this resource after you finish using it.

Use the Azure Bastion as a Service Bicep template from Azure Resource Manager Quickstart Templates to deploy and configure Bastion in your virtual network. This Bicep template defines the following Azure resources:

Review the Bicep file:

@description('Name of new or existing vnet to which Azure Bastion should be deployed')
param vnetName string = 'vnet01'

@description('IP prefix for available addresses in vnet address space')
param vnetIpPrefix string = '10.1.0.0/16'

@description('Specify whether to provision new vnet or deploy to existing vnet')
@allowed([
  'new'
  'existing'
])
param vnetNewOrExisting string = 'new'

@description('Bastion subnet IP prefix MUST be within vnet IP prefix address space')
param bastionSubnetIpPrefix string = '10.1.1.0/26'

@description('Name of Azure Bastion resource')
param bastionHostName string

@description('Azure region for Bastion and virtual network')
param location string = resourceGroup().location

var publicIpAddressName = '${bastionHostName}-pip'
var bastionSubnetName = 'AzureBastionSubnet'

resource publicIp 'Microsoft.Network/publicIPAddresses@2022-01-01' = {
  name: publicIpAddressName
  location: location
  sku: {
    name: 'Standard'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

// if vnetNewOrExisting == 'new', create a new vnet and subnet
resource newVirtualNetwork 'Microsoft.Network/virtualNetworks@2022-01-01' = if (vnetNewOrExisting == 'new') {
  name: vnetName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        vnetIpPrefix
      ]
    }
    subnets: [
      {
        name: bastionSubnetName
        properties: {
          addressPrefix: bastionSubnetIpPrefix
        }
      }
    ]
  }
}

// if vnetNewOrExisting == 'existing', reference an existing vnet and create a new subnet under it
resource existingVirtualNetwork 'Microsoft.Network/virtualNetworks@2022-01-01' existing = if (vnetNewOrExisting == 'existing') {
  name: vnetName
}
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = if (vnetNewOrExisting == 'existing') {
  parent: existingVirtualNetwork
  name: bastionSubnetName
  properties: {
    addressPrefix: bastionSubnetIpPrefix
  }
}

resource bastionHost 'Microsoft.Network/bastionHosts@2022-01-01' = {
  name: bastionHostName
  location: location
  dependsOn: [
    newVirtualNetwork
    existingVirtualNetwork
  ]
  properties: {
    ipConfigurations: [
      {
        name: 'IpConf'
        properties: {
          subnet: {
            id: subnet.id
          }
          publicIPAddress: {
            id: publicIp.id
          }
        }
      }
    ]
  }
}

Deploy the Bicep template

  1. Save the Bicep file to your local computer as bastion.bicep.

  2. Use a text or code editor to make the following changes in the file:

    • Line 2: Change param vnetName string from 'vnet01' to 'VNet'.
    • Line 5: Change param vnetIpPrefix string from '10.1.0.0/16' to '10.0.0.0/16'.
    • Line 12: Change param vnetNewOrExisting string from 'new' to 'existing'.
    • Line 15: Change param bastionSubnetIpPrefix string from '10.1.1.0/26' to '10.0.1.0/26'.
    • Line 18: Change param bastionHostName string to param bastionHostName = 'VNet-bastion'.

    The first 18 lines of your Bicep file should now look like this example:

    @description('Name of new or existing vnet to which Azure Bastion should be deployed')
    param vnetName string = 'VNet'
    
    @description('IP prefix for available addresses in vnet address space')
    param vnetIpPrefix string = '10.0.0.0/16'
    
    @description('Specify whether to provision new vnet or deploy to existing vnet')
    @allowed([
      'new'
      'existing'
    ])
    param vnetNewOrExisting string = 'existing'
    
    @description('Bastion subnet IP prefix MUST be within vnet IP prefix address space')
    param bastionSubnetIpPrefix string = '10.0.1.0/26'
    
    @description('Name of Azure Bastion resource')
    param bastionHostName = 'VNet-bastion'
    
    
  3. Save the bastion.bicep file.

  4. Deploy the Bicep file by using either the Azure CLI or Azure PowerShell:

    az deployment group create --resource-group TestRG --template-file bastion.bicep
    

When the deployment finishes, a message indicates that the deployment succeeded.

Note

VMs in a virtual network with a Bastion host don't need public IP addresses. Bastion provides the public IP, and the VMs use private IPs to communicate within the network. You can remove the public IPs from any VMs in Bastion-hosted virtual networks. For more information, see Dissociate a public IP address from an Azure VM.

Review deployed resources

Use the Azure CLI, Azure PowerShell, or the Azure portal to review the deployed resources:

az resource list --resource-group TestRG

Connect to a VM

  1. In the portal, search for and select Virtual machines.

  2. On the Virtual machines page, select BackendVM1.

  3. At the top of the BackendVM1 page, select the dropdown arrow next to Connect, and then select Bastion.

    Screenshot of connecting to the first virtual machine with Azure Bastion.

  4. On the Bastion page, enter the username and password that you created for the VM, and then select Connect.

Communicate between VMs

  1. From the desktop of BackendVM1, open PowerShell.

  2. Enter ping BackendVM0. You get a reply similar to the following message:

    PS C:\Users\BackendVM1> ping BackendVM0
    
    Pinging BackendVM0.ovvzzdcazhbu5iczfvonhg2zrb.bx.internal.cloudapp.net with 32 bytes of data
    Request timed out.
    Request timed out.
    Request timed out.
    Request timed out.
    
    Ping statistics for 10.0.0.5:
        Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),
    

    The ping fails because it uses the Internet Control Message Protocol (ICMP). By default, ICMP isn't allowed through Windows Firewall.

  3. To allow ICMP inbound through Windows Firewall on this VM, enter the following command:

    New-NetFirewallRule –DisplayName "Allow ICMPv4-In" –Protocol ICMPv4
    
  4. Close the Bastion connection to BackendVM1.

  5. Repeat the steps in Connect to a VM to connect to BackendVM0.

  6. From PowerShell on BackendVM0, enter ping BackendVM1.

    This time you get a success reply similar to the following message, because you allowed ICMP through the firewall on BackendVM1.

    PS C:\Users\BackendVM0> ping BackendVM1
    
    Pinging BackendVM1.e5p2dibbrqtejhq04lqrusvd4g.bx.internal.cloudapp.net [10.0.0.4] with 32 bytes of data:
    Reply from 10.0.0.4: bytes=32 time=2ms TTL=128
    Reply from 10.0.0.4: bytes=32 time<1ms TTL=128
    Reply from 10.0.0.4: bytes=32 time<1ms TTL=128
    Reply from 10.0.0.4: bytes=32 time<1ms TTL=128
    
    Ping statistics for 10.0.0.4:
        Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
    Approximate round trip times in milli-seconds:
        Minimum = 0ms, Maximum = 2ms, Average = 0ms
    
  7. Close the Bastion connection to BackendVM0.

Clean up resources

When you finish with the virtual network, use the Azure CLI, Azure PowerShell, or the Azure portal to delete the resource group and all its resources:

az group delete --name TestRG

Next steps

In this quickstart, you created a virtual network that has two subnets: one that contains two VMs and the other for Bastion. You deployed Bastion, and you used it to connect to the VMs and start communication between the VMs. To learn more about virtual network settings, see Create, change, or delete a virtual network.

Private communication between VMs is unrestricted in a virtual network. To learn more about configuring various types of VM communications in a virtual network, continue to the next article: