Introduction
In this guide, we’ll walk through deploying an Azure Virtual Machine (VM) using Bicep, Microsoft’s domain-specific language (DSL) for Azure Resource Manager (ARM) templates.
Bicep provides a more readable and concise syntax compared to raw ARM JSON templates while retaining the same functionality. If you’re working with Infrastructure as Code (IaC) in Azure, Bicep is the recommended alternative to ARM.
We’ll cover how to create a fully functional Azure VM, including:
✔ Resource Group creation
✔ Virtual Network (VNet) and Subnet setup
✔ Network Security Group (NSG) for security
✔ Public and Private IP configuration
✔ Network Interface (NIC) association
✔ Deploying a Windows Server VM
By the end of this tutorial, you will have an automated and repeatable method to provision Azure Virtual Machines using Bicep and Azure CLI. 🚀
Prerequisites
Before getting started, ensure you have:
✅ An active Azure subscription
✅ Azure CLI installed (az --version
)
✅ Bicep CLI installed (az bicep install
)
✅ VS Code with the Bicep extension (for syntax highlighting & validation)
Step 1: Set Up Your Bicep File
Create a new file called main.bicep
:
Then, open it in Visual Studio Code for editing.
Step 2: Define the Azure Resource Group
All Azure resources should be placed inside a Resource Group. Let’s define one in Bicep:
1
2
3
4
5
6
7
8
9
10
11
12
| param location string = 'northeurope'
targetScope = 'subscription'
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'bicep-vm-resource-group'
location: location
tags: {
environment: 'demo'
project: 'bicep-vm-tutorial'
}
}
|
🔹 Key Points:
✔ targetScope = ‘subscription’ allows us to create a Resource Group at the subscription level.
✔ We define tags for easier resource tracking and cost management.
Step 3: Define the Virtual Network and Subnet
A Virtual Network (VNet) is required for our VM to communicate securely.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| param location string
param resourceGroupName string
resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {
name: 'bicep-vnet'
location: location
resourceGroupName: resourceGroupName
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
subnets: [
{
name: 'frontendSubnet'
properties: {
addressPrefix: '10.0.1.0/24'
networkSecurityGroup: {
id: nsg.id
}
}
}
]
}
}
resource nsg 'Microsoft.Network/networkSecurityGroups@2021-02-01' = {
name: 'vm-nsg'
location: location
properties: {
securityRules: [
{
name: 'allow-rdp'
properties: {
priority: 1000
access: 'Allow'
direction: 'Inbound'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3389'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
description: 'Allow RDP connections'
}
}
]
}
}
output subnetId string = vnet.properties.subnets[0].id
|
🔹 Key Points:
✔ Network Security Group (NSG) is added for security.
✔ RDP (port 3389) is allowed, but in a real-world scenario, restrict source IPs.
✔ We define a subnet inside the VNet.
Step 4: Define a Public IP Address
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| param location string
resource publicIP 'Microsoft.Network/publicIPAddresses@2021-02-01' = {
name: 'bicep-public-ip'
location: location
properties: {
publicIPAllocationMethod: 'Static'
dnsSettings: {
domainNameLabel: 'bicep-vm-${uniqueString(resourceGroup().id)}'
}
}
sku: {
name: 'Standard'
}
}
output publicIPId string = publicIP.id
output fqdn string = publicIP.properties.dnsSettings.fqdn
|
🔹 Key Points:
✔ Static IP Allocation for better predictability.
✔ DNS Label automatically generates a user-friendly domain name.
Step 5: Define a Network Interface (NIC)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| param location string
param subnetId string
param publicIPId string
resource networkInterface 'Microsoft.Network/networkInterfaces@2021-02-01' = {
name: 'bicep-vm-nic'
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: subnetId
}
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: publicIPId
}
}
}
]
}
}
output nicId string = networkInterface.id
|
Step 6: Define the Virtual Machine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| param location string
param nicId string
param adminUsername string
@secure()
param adminPassword string
resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = {
name: 'bicep-vm'
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_B2s'
}
osProfile: {
computerName: 'bicep-vm'
adminUsername: adminUsername
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: '2019-Datacenter'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Premium_LRS'
}
}
}
networkProfile: {
networkInterfaces: [{ id: nicId }]
}
}
}
output vmName string = vm.name
|
🔹 Key Points:
✔ Uses Windows Server 2019 as the OS.
✔ Secure Parameter (@secure()) for admin password.
✔ Uses Premium SSDs (Premium_LRS) for performance.
Step 7: Deploy the Bicep Templates
Login to Azure:
Deploy the main.bicep
template:
1
| az deployment sub create --location northeurope --template-file main.bicep --parameters adminPassword="YourSecurePassword"
|
To check deployment progress:
1
| az deployment sub list --query "[?name=='main'].properties.provisioningState" -o tsv
|
Conclusion
In this guide, we automated Azure Virtual Machine deployment using Bicep. This approach provides:
✅ Improved Readability compared to raw ARM templates.
✅ Modularity by breaking infrastructure into reusable Bicep modules.
✅ Repeatability & Automation for consistent deployments.
✅ Better Security with encrypted passwords and NSG rules.
Next Steps:
🔹 Integrate this with Azure DevOps or GitHub Actions for CI/CD.
🔹 Use Azure Key Vault for secure password storage.
🔹 Add monitoring using Azure Monitor & Log Analytics.
🔥 Have you tried Bicep? Drop your thoughts in the comments! 🚀