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.
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
An Azure account with an active subscription. You can create an account for free.
To deploy the Bicep files, either the Azure CLI or Azure PowerShell installed:
Install the Azure CLI locally to run the commands. You need Azure CLI version 2.0.28 or later. Run az version to find your installed version and dependent libraries, and run az upgrade to upgrade.
Sign in to Azure by using the az login command.
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:
- Microsoft.Network virtualNetworks: Creates an Azure virtual network.
- Microsoft.Network virtualNetworks/subnets: Creates a subnet for the VMs.
- Microsoft.Compute virtualMachines: Creates the VMs.
- Microsoft.Compute availabilitySets: Creates an availability set.
- Microsoft.Network networkInterfaces: Creates network interfaces.
- Microsoft.Network loadBalancers: Creates an internal load balancer.
- Microsoft.Storage storageAccounts: Creates a storage account.
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
Save the Bicep file to your local computer as main.bicep.
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:
- Microsoft.Network virtualNetworks/subnets: Creates an AzureBastionSubnet subnet.
- Microsoft.Network bastionHosts: Creates the Bastion host.
- Microsoft.Network publicIPAddresses: Creates a public IP address for the Bastion host.
- Microsoft Network networkSecurityGroups: Controls the settings for network security groups.
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
Save the Bicep file to your local computer as bastion.bicep.
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
toparam 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'
- Line 2: Change
Save the bastion.bicep file.
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
In the portal, search for and select Virtual machines.
On the Virtual machines page, select BackendVM1.
At the top of the BackendVM1 page, select the dropdown arrow next to Connect, and then select Bastion.
On the Bastion page, enter the username and password that you created for the VM, and then select Connect.
Communicate between VMs
From the desktop of BackendVM1, open PowerShell.
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.
To allow ICMP inbound through Windows Firewall on this VM, enter the following command:
New-NetFirewallRule –DisplayName "Allow ICMPv4-In" –Protocol ICMPv4
Close the Bastion connection to BackendVM1.
Repeat the steps in Connect to a VM to connect to BackendVM0.
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
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: