Create an Azure Virtual Machine with Bicep: A Step-by-Step Guide.

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:

1
touch 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:

1
az login

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! 🚀